3.4 DML Operations, Transaction Control & MVCC Mechanics

Key Takeaways

  • The RETURNING clause on INSERT, UPDATE, and DELETE returns modified or generated tuple columns immediately without requiring secondary SELECT queries.
  • PostgreSQL supports UPSERT via INSERT ... ON CONFLICT DO NOTHING or DO UPDATE SET, utilizing the EXCLUDED pseudo-table to reference incoming values that conflicted with unique constraints.
  • TRUNCATE is a high-speed DDL-level operation that deallocates relation storage files under an ACCESS EXCLUSIVE lock, resets sequences with RESTART IDENTITY, and is fully transaction-safe in PostgreSQL.
  • In PostgreSQL's strict transaction model, any statement error shifts the transaction block into an ABORTED state (error 25P02), rejecting all subsequent commands until a ROLLBACK or rollback to a valid SAVEPOINT.
  • Multi-Version Concurrency Control (MVCC) uses hidden tuple headers xmin and xmax to manage row visibility without read locks; an UPDATE physically creates a new tuple version and sets the old tuple's xmax to the updating transaction ID.
Last updated: September 2026

3.4 DML Operations, Transaction Control & MVCC Mechanics

[!NOTE] Exam Blueprint Focus: DML and transaction management are critical operational domains on the PostgreSQL Associate exam. You must master the RETURNING clause, UPSERT syntax (ON CONFLICT DO UPDATE/NOTHING), multi-table UPDATE ... FROM, transactional TRUNCATE, transaction state management (BEGIN, COMMIT, ROLLBACK), partial rollbacks via SAVEPOINT, and PostgreSQL's strict aborted transaction behavior. Additionally, you must comprehend the foundational mechanics of Multi-Version Concurrency Control (MVCC), including tuple headers xmin and xmax, snapshot visibility, and dead tuple accumulation.

Data Manipulation Language (DML) enables applications to insert, modify, and delete data within PostgreSQL tables. In an enterprise relational database, DML operations do not occur in a vacuum—they operate under the strict guarantees of ACID (Atomicity, Consistency, Isolation, Durability) transactions and PostgreSQL's non-blocking Multi-Version Concurrency Control (MVCC) engine.


Advanced DML Operations

1. INSERT with Bulk Loading & RETURNING

PostgreSQL supports multi-row inserts in a single round-trip, dramatically reducing network and transaction logging overhead:

INSERT INTO products (sku, name, price) VALUES 
    ('WID-01', 'Standard Widget', 19.99),
    ('WID-02', 'Deluxe Widget', 29.99),
    ('WID-03', 'Industrial Widget', 89.99);

The PostgreSQL-specific RETURNING clause can be appended to any INSERT, UPDATE, or DELETE statement. It eliminates the need for an application to execute a follow-up SELECT query to retrieve auto-generated IDs, default timestamps, or calculated values:

-- Returns the newly generated identity ID and default created_at timestamp
INSERT INTO customers (name, email) 
VALUES ('Acme Corp', 'contact@acme.com')
RETURNING customer_id, created_at;

2. UPSERT: INSERT ... ON CONFLICT

Applications frequently need to insert a record, or update it if it already exists (atomic "UPSERT"). PostgreSQL handles this natively via the ON CONFLICT clause:

  • Target Conflict: You must specify the column or constraint that would trigger a unique violation. An underlying unique index or constraint must exist on this target.
  • DO NOTHING: Silently ignores the insert if a conflict occurs, avoiding an error.
  • DO UPDATE SET: Updates the existing conflicting row. Inside the SET expression, PostgreSQL provides the special EXCLUDED pseudo-table, which represents the values that were proposed for insertion.
-- Atomic UPSERT maintaining inventory counts
INSERT INTO inventory (product_sku, warehouse_id, quantity)
VALUES ('WID-01', 4, 100)
ON CONFLICT (product_sku, warehouse_id) 
DO UPDATE SET 
    quantity = inventory.quantity + EXCLUDED.quantity,
    last_restocked = clock_timestamp();

3. Multi-Table UPDATE ... FROM

PostgreSQL allows you to join other tables directly into an UPDATE statement using the FROM clause:

-- Adjust employee salaries based on their department's annual budget increase
UPDATE employees e
SET salary = e.salary * d.raise_multiplier
FROM department_budgets d
WHERE e.dept_id = d.dept_id 
  AND d.fiscal_year = 2026;

4. DELETE with RETURNING

Like inserts, deletions can return the deleted tuples for auditing or archiving:

-- Archive deleted expired tokens
WITH deleted_tokens AS (
    DELETE FROM session_tokens 
    WHERE expires_at < NOW() 
    RETURNING token_id, user_id
)
INSERT INTO expired_token_archive SELECT * FROM deleted_tokens;

5. High-Speed TRUNCATE

When an administrator needs to purge all data from a table, TRUNCATE is orders of magnitude faster than DELETE FROM table;:

  • How It Works: DELETE scans the table row-by-row, generating transaction log entries and leaving dead tuples for VACUUM to reclaim. TRUNCATE operates at the DDL storage level: it deallocates the table's underlying data files (storage forks) and creates brand new, empty relation files on disk.
  • Locks Required: TRUNCATE acquires an ACCESS EXCLUSIVE lock on the table, blocking all other connections.
  • Sequence Handling: By default, sequences are untouched (CONTINUE IDENTITY). Specifying RESTART IDENTITY automatically resets associated identity columns or sequences to their initial starting value.
  • Transaction Safety: In PostgreSQL, TRUNCATE is fully transaction-safe! If you execute TRUNCATE within a transaction block and issue a ROLLBACK, all truncated data is immediately and completely restored. (This contrasts sharply with Oracle or MySQL, where TRUNCATE performs an irreversible implicit commit).
TRUNCATE TABLE staging_orders RESTART IDENTITY CASCADE;

Transaction Control & PostgreSQL's Aborted State

A transaction is an atomic unit of work bounded by BEGIN (or START TRANSACTION) and COMMIT or ROLLBACK.

Savepoints: Partial Transaction Rollback

Inside long-running transactions or batch jobs, an error can invalidate hours of work. PostgreSQL provides Savepoints to create recovery checkpoints within an active transaction:

  • SAVEPOINT savepoint_name: Creates a named checkpoint.
  • ROLLBACK TO SAVEPOINT savepoint_name: Rewinds all changes made after the savepoint, while leaving earlier changes intact and keeping the overall transaction active.
  • RELEASE SAVEPOINT savepoint_name: Removes the savepoint checkpoint without committing or rolling back changes.
BEGIN;
  INSERT INTO orders (order_id, customer_id) VALUES (1, 100);
  
  SAVEPOINT order_payment;
  INSERT INTO payments (payment_id, order_id, amount) VALUES (501, 1, 99.99);
  -- Suppose this payment insert failed due to a transient network check
  ROLLBACK TO SAVEPOINT order_payment;
  
  -- The transaction is STILL VALID! We can log a fallback
  INSERT INTO payments_pending (order_id, amount) VALUES (1, 99.99);
COMMIT;

The Strict Aborted Transaction State (25P02)

A fundamental behavioral trait of PostgreSQL that frequently trips up newcomers and certification candidates is its strict aborted transaction model:

[ BEGIN ] ──> [ Valid Transaction State ]
                     │
                     │ (Any Statement Raises Error: syntax, constraint, div-by-zero)
                     ▼
              [ ABORTED TRANSACTION STATE ]
                     │
                     ├── Next SQL Statement ──> ERROR: current transaction is aborted,
                     │                         commands ignored until end of 
                     │                         transaction block (SQLSTATE 25P02)
                     ├── COMMIT             ──> Converted to ROLLBACK (Changes discarded)
                     ├── ROLLBACK           ──> Exits block, transaction terminated
                     └── ROLLBACK TO SP     ──> Restores transaction to pre-error savepoint!
  • Once any statement inside a transaction block raises an error (such as a foreign key violation, division by zero, or syntax error), the transaction is immediately marked as ABORTED.
  • PostgreSQL will refuse to execute any subsequent DML or DDL commands in that session, rejecting every query with: ERROR: current transaction is aborted, commands ignored until end of transaction block (SQLSTATE 25P02)
  • The only commands accepted in an aborted state are ROLLBACK (which aborts the entire transaction), COMMIT (which PostgreSQL automatically converts into a ROLLBACK), or ROLLBACK TO SAVEPOINT (which rewinds the transaction to a savepoint established prior to the error).
  • Unlike other database management systems that allow execution to continue after non-fatal statement errors, PostgreSQL strictly protects transaction consistency by halting further execution.

Multi-Version Concurrency Control (MVCC) Mechanics

PostgreSQL uses Multi-Version Concurrency Control (MVCC) to deliver high-concurrency ACID guarantees without locking tables against reads:

[!IMPORTANT] The Golden Rule of MVCC: Readers never block writers, and writers never block readers.

When a query reads from a table, it does not acquire shared read locks on rows. When a transaction updates or deletes rows, it does not prevent concurrent queries from reading the previous, committed version of those rows.

Hidden System Tuple Headers: xmin and xmax

Every physical row stored in a PostgreSQL table (heap) is called a tuple. In addition to user-defined data columns, every tuple begins with a 23-byte header containing hidden system columns:

  1. xmin: The 32-bit Transaction ID (XID) of the transaction that inserted this tuple version into the table.
  2. xmax: The 32-bit Transaction ID (XID) of the transaction that deleted or updated this tuple version. For active, un-deleted rows, xmax is 0.
  3. t_ctid: A tuple identifier pointing to the physical location of the row on disk, formatted as (page_number, item_offset). When a row is updated, the old tuple's ctid is updated to point directly to the new tuple version!
-- Inspecting hidden MVCC header columns
SELECT ctid, xmin, xmax, employee_name, salary FROM employees LIMIT 3;

How PostgreSQL Implements UPDATE

In PostgreSQL, an UPDATE does not overwrite data in-place!

  1. When transaction 105 updates an employee's salary from $50,000 to $60,000, PostgreSQL leaves the old tuple in place and writes the updating transaction ID into its header: xmax = 105.
  2. PostgreSQL then physically inserts a brand-new tuple on an available page containing the new salary ($60,000), setting its header to xmin = 105 and xmax = 0.
  3. The old tuple's ctid pointer is updated to point forward to the new tuple's physical disk address.
OLD TUPLE VERSION (Disk Page 1, Offset 4):  [ctid: (1, 5)] [xmin: 90]  [xmax: 105] [Salary: $50,000]
                                                                 │
                                                                 ▼ (ctid points to new version)
NEW TUPLE VERSION (Disk Page 1, Offset 5):  [ctid: (1, 5)] [xmin: 105] [xmax: 0]   [Salary: $60,000]

Snapshot Isolation and Row Visibility

When a transaction or statement begins, PostgreSQL establishes a Transaction Snapshot consisting of:

  • xmin: The lowest XID that was still active (uncommitted) when the snapshot was taken.
  • xmax: The highest XID assigned so far plus one.
  • xip_list: The list of specific active XIDs between xmin and xmax.

A tuple is visible to a query if and only if:

  • The tuple's xmin is a committed transaction that completed before the snapshot was taken.
  • The tuple's xmax is either 0 (not deleted), belonged to an aborted transaction, or belonged to a transaction that committed after the snapshot was taken.

Dead Tuples and the Need for VACUUM

Because UPDATE creates new tuple versions and DELETE merely marks xmax, the old tuple versions remain physically stored on disk. Once all active transactions that could possibly see the old tuple versions have finished, those old versions are classified as Dead Tuples.

Dead tuples cause table bloat and consume disk space until the VACUUM process scans the pages and marks that space as reusable for future inserts. Understanding MVCC explains why PostgreSQL relies heavily on the autovacuum background daemon.

Loading diagram...
MVCC Tuple Versioning and Aborted Transaction State Machine
Test Your Knowledge

A developer runs a script containing the following commands inside a transaction block:

BEGIN;
INSERT INTO accounts (id, balance) VALUES (1, 100.00);
INSERT INTO accounts (id, balance) VALUES (1, 200.00); -- Fails: duplicate primary key
INSERT INTO audit_log (msg) VALUES ('Inserted account 1');
COMMIT;
What is the state of the database after executing this script?

A
B
C
D
Test Your Knowledge

An active row in a PostgreSQL table was created by transaction 200. Transaction 350 updates this row to change a status column. How does the PostgreSQL MVCC engine physically execute this update on disk?

A
B
C
D
Test Your Knowledge

What is a primary operational distinction between executing TRUNCATE TABLE logs; versus DELETE FROM logs; in PostgreSQL?

A
B
C
D