27 DELETE
DELETE FROM table removes rows. Like UPDATE, the WHERE clause is the difference between “remove these specific rows” and “wipe the whole table.” Like UPDATE, DELETE is transactional — a ROLLBACK undoes it. DELETE can filter a subset and fires row-level delete triggers. Both commands enforce foreign-key restrictions; TRUNCATE supports its own statement-level triggers.
What you’ll learn
- Deleting rows with
DELETE FROM ... WHERE - Why
TRUNCATEexists alongsideDELETE DELETE ... USINGfor joining to a source table- Returning deleted rows with
RETURNING - Transactional safety, foreign-key cascades, and triggers
Example
DELETE marks row versions as deleted and runs applicable triggers and foreign-key checks. Changes to logged tables generate WAL for crash recovery; MVCC preserves the old versions while transactions may still need them.
TRUNCATE table removes all rows without scanning them and is usually much faster for large tables. It is transactional: ROLLBACK restores the data. It takes an ACCESS EXCLUSIVE lock, enforces foreign-key restrictions, and fires ON TRUNCATE statement triggers, never row-level ON DELETE triggers. It is not MVCC-safe for older snapshots; use it deliberately on scratch or staging data.
DELETE ... USING is the analog of UPDATE ... FROM: lets you express “delete rows in t that match rows in s” by joining to s.
Run the example
To run (requires fruits from earlier lessons):
Common pitfalls
- Forgetting
WHERE— same warning asUPDATE, even more painful. Always preview withSELECT count(*) FROM t WHERE same_condition;first. DELETEon a table withON DELETE CASCADEforeign keys can fan out across many child tables. Audit before running.DELETEdoes not reclaim disk space — the rows are marked dead and reclaimed byVACUUMlater. After a huge delete, runVACUUM (VERBOSE) t;to reclaim andANALYZEto refresh statistics.- Soft deletes (a
deleted_atcolumn instead of a realDELETE) are common in app design. Pick a strategy and stick to it.
For very large deletes, batch them: DELETE FROM logs WHERE created_at < now() - interval '30 days' AND id IN (SELECT id FROM logs WHERE created_at < ... LIMIT 10000);. This keeps each transaction small, autovacuum can keep up, and lock holds are short.
Add a row to a temp table and delete it. Then try BEGIN; DELETE FROM fruits; SELECT count(*) FROM fruits; ROLLBACK; SELECT count(*) FROM fruits; — the rollback should restore the rows. Then try TRUNCATE on a temp table.
Downloads
SQL file: delete.sql · All lesson files · Setup guide