Insert / Update / Delete / Merge (ModifyTable)
Appears in EXPLAIN asInsert · Update · Delete · Merge
Applies INSERT / UPDATE / DELETE / MERGE to a table.
What it is
The ModifyTable node carries out data changes. EXPLAIN labels it by operation — Insert, Update, Delete, or Merge — and it sits at the top of the plan, consuming rows from the child that identifies what to change.
When the planner picks it
For any INSERT, UPDATE, DELETE, or MERGE statement.
Is it good or bad?
Necessary for writes. The performance story is in the child plan (how target rows are found) plus side effects: indexes to maintain, triggers to fire, and foreign keys to check. A Seq Scan feeding an UPDATE/DELETE on a big table usually wants an index.
In depth
The plan's child is what you actually tune
ModifyTable is the top of the plan for any INSERT, UPDATE, DELETE, or
MERGE — the EXPLAIN label is the operation name ("Update on …" etc.). Its job
is to take rows from its child and apply the change, plus fire triggers,
maintain indexes, and check constraints.
The performance story is mostly in the child: how target rows are found. A Seq Scan feeding a big UPDATE/DELETE almost always wants an index on the WHERE predicate, exactly as for a slow SELECT.
The hidden costs
Beyond finding the rows, each modification pays:
- Index maintenance — every non-HOT update touches every index that references a changed column (and every index for INSERT/DELETE). A wide table with many indexes can spend more time on indexes than on the heap.
- Triggers — row-level triggers fire per modified row; statement-level
triggers fire once. Watch
Trigger … time=…lines in EXPLAIN. - Foreign keys — referential checks add lookups against the referenced
table; the planner shows them as
SubPlanorInitplanblocks.
HOT updates
A HOT (Heap-Only Tuple) update is one where no indexed column changed and
the new row fits on the same page — the indexes are skipped entirely. Two
practical levers:
- Tables with rapidly changing non-indexed columns benefit from a lower
fillfactor(e.g. 70–90) so there's headroom on each page for HOT updates. - An index that includes the changed column makes HOT impossible — keep indexes narrow.
RETURNING is free-ish
UPDATE … RETURNING … doesn't change the plan shape; it just exposes the
rows the ModifyTable already produced. There's no extra scan.
What the analyzer flags here
- Seq Scan hot spot — finding rows to modify without an index
Settings that influence it
work_memsynchronous_commit
FAQ
- Why is my UPDATE doing a sequential scan?
- The WHERE clause that selects rows to update has no supporting index, so PostgreSQL scans the whole table to find them. Index the predicate columns — the same advice as for a slow SELECT with that WHERE.