DELETE in PostgreSQL

Learn how to remove rows from a PostgreSQL table with DELETE, use WHERE to target specific rows, retrieve deleted data with RETURNING, and understand when to use TRUNCATE instead.

5 min read · Back to overview

Quick Answer

Use DELETE FROM table WHERE condition to remove specific rows. Without a WHERE clause every row is deleted. Add RETURNING to capture the deleted rows before they are removed. For removing all rows quickly, TRUNCATE is faster than an unfiltered DELETE.

Spin up a Postgres database in 20 seconds with Vela.

Try Vela Sandbox

The DELETE statement removes rows from a table. It operates on data only — it does not drop columns, remove constraints, or change the table structure. Each deleted row is individually logged, which means DELETE supports ROLLBACK and row-level audit logging with RETURNING. To remove all rows from a large table quickly without per-row logging, TRUNCATE is faster.

Syntax

DELETE FROM table_name
WHERE condition;

With RETURNING to capture deleted row values:

DELETE FROM table_name
WHERE condition
RETURNING column1, column2;

Practical Example

Create a line_items table for an order management system and insert sample data:

CREATE TABLE orders (
  id      SERIAL PRIMARY KEY,
  ref     VARCHAR(20) NOT NULL,
  status  VARCHAR(20) NOT NULL DEFAULT 'open'
);

CREATE TABLE line_items (
  id         SERIAL PRIMARY KEY,
  order_id   INT NOT NULL REFERENCES orders(id),
  product    VARCHAR(100) NOT NULL,
  qty        INT NOT NULL,
  unit_price NUMERIC(10, 2) NOT NULL,
  cancelled  BOOLEAN NOT NULL DEFAULT false
);

INSERT INTO orders (ref, status) VALUES ('ORD-0001', 'open'), ('ORD-0002', 'open');

INSERT INTO line_items (order_id, product, qty, unit_price, cancelled)
VALUES
  (1, 'Wireless Keyboard', 2, 59.99, false),
  (1, 'Mouse Pad XL',      1,  9.99, true),
  (1, 'HDMI Cable 2m',     3, 12.00, false),
  (2, 'USB Hub 7-Port',    1, 34.99, true);

Delete a single cancelled line item:

DELETE FROM line_items
WHERE id = 2;

Output:

DELETE 1

Delete all cancelled line items across all orders:

DELETE FROM line_items
WHERE cancelled = true;

Output:

DELETE 1

Use RETURNING to capture the removed rows for an audit trail:

DELETE FROM line_items
WHERE order_id = 1
  AND qty < 2
RETURNING id, product, qty, unit_price;
 id |   product    | qty | unit_price
----+--------------+-----+------------
  3 | HDMI Cable 2m |   3 |      12.00

Verify the remaining rows:

SELECT id, order_id, product, qty
FROM line_items
ORDER BY id;

RETURNING Clause

RETURNING returns the values of the deleted rows before they are removed. This is useful for:

  • Writing audit log entries without a second query.
  • Collecting deleted IDs to clean up related data in application code.
  • Confirming exactly which rows were removed in a conditional delete.
DELETE FROM line_items
WHERE order_id = 2
RETURNING id, product, unit_price AS removed_price;

The values in RETURNING reflect the row state at the moment of deletion — after any BEFORE DELETE trigger has run but before the row is physically removed.

Testing with Vela

Destructive operations like DELETE are irreversible once committed. Use Vela database branching to create a copy-on-write clone of your production database, run the DELETE on the branch, verify the affected row count and RETURNING output, then apply to production with confidence. This eliminates the risk of accidentally deleting the wrong rows in a live environment.

Production Tips

  • Always run a SELECT with the same WHERE clause before executing a DELETE to confirm the row count and data that will be removed.
  • Wrap deletes in an explicit transaction when they are part of a multi-step operation — issue ROLLBACK if a downstream step fails.
  • Use RETURNING to capture deleted rows for audit logging or to pass removed data to application code.
  • For large batch deletes, use a loop deleting in smaller chunks (e.g. LIMIT 1000) to avoid long lock contention on busy tables.
  • When removing all rows from a table, TRUNCATE is far faster than an unfiltered DELETE because it does not log individual row deletions.
  • Check foreign key constraints before deleting parent rows — use ON DELETE CASCADE or clean up child rows first to avoid constraint violations.

Continue in Modifying Data: DELETE Join.

Related in this section: INSERT · INSERT Multiple Rows · UPDATE

Frequently Asked Questions

What does DELETE do in PostgreSQL?
DELETE removes one or more rows from a table based on a WHERE condition. It does not change the table structure, indexes, or constraints. Each deleted row is logged individually, which enables row-level audit logging and supports ROLLBACK within a transaction.
Does DELETE lock the table?
DELETE acquires a ROW EXCLUSIVE lock on the table and row-level locks on each deleted row. Concurrent reads are not blocked. Concurrent writes to the same rows will wait. For large deletes, consider batching to limit lock duration.
What happens to dependent rows in child tables when I DELETE a parent row?
If the child table has ON DELETE CASCADE on its foreign key, those rows are automatically deleted. If the foreign key uses the default NO ACTION or RESTRICT behavior, PostgreSQL will return an error and prevent the delete if child rows exist.
Can I use IF EXISTS with DELETE?
There is no IF EXISTS clause for DELETE. If the WHERE condition matches no rows, the statement completes normally with DELETE 0. This is not an error.
What is the safest way to DELETE in production?
Always verify the WHERE clause with a SELECT first to confirm the exact rows affected. Wrap the DELETE in a transaction so you can ROLLBACK if the result is wrong. For large deletes, process in smaller batches to avoid long lock contention.