28 RETURNING
RETURNING is a PostgreSQL extension that turns an INSERT, UPDATE, or DELETE into a query: the statement returns the rows it just touched. This eliminates the second round trip you’d otherwise need to fetch the auto-generated id, the updated timestamp, or the values you just deleted. Most professional codebases lean on RETURNING heavily.
What you’ll learn
RETURNINGwithINSERT,UPDATE,DELETE- Returning specific columns vs
RETURNING * - Capturing generated values (identity, defaults) in one call
- Using
RETURNINGinside a CTE (WITH ... AS (...)) to chain operations - Why it almost always replaces “INSERT then SELECT last_insert_id()”
Example
RETURNING accepts the same expressions you would put in a SELECT list — column references, expressions, function calls, even *. For an INSERT, the columns reflect the post-insert values, including defaults and generated identities. For an UPDATE, they reflect the new values; in PostgreSQL 17, OLD.col is available in trigger functions, not RETURNING. PostgreSQL 18 adds explicit old/new values in RETURNING. For a DELETE, they reflect the row as it was.
The chaining pattern (WITH del AS (DELETE ... RETURNING) INSERT INTO archive SELECT * FROM del) is one of PostgreSQL’s superpowers. The two statements run as a single, atomic operation in one round trip — much faster and simpler than doing it from application code.
Run the example
To run:
Common pitfalls
- Forgetting
RETURNINGon anINSERTand then issuing aSELECT max(id) FROM t;— that is a race condition. Always useRETURNING idfor the new row’s id. - Multi-row
INSERT ... RETURNINGreturns rows in the order they were inserted, but only because of how the executor processes them. If you need a guaranteed mapping, include a stable key in theVALUESand theRETURNING. RETURNINGinsideINSERT ... ON CONFLICT DO NOTHINGdoes not return rows that were skipped due to conflict. UseDO UPDATE(covered in the next lesson) to make sure something is returned.- Drivers sometimes expose
INSERT ... RETURNINGas a query (with results) rather than as a statement (with row count). Check the language API.
INSERT INTO t (...) VALUES (...) RETURNING id is the canonical pattern for “create a row and tell me its id.” It works with bigint GENERATED ALWAYS AS IDENTITY, serial, UUID defaults, or anything else the database supplies. Use it everywhere.
Insert three rows with one statement and capture all three ids via RETURNING id. Then run UPDATE logs_example SET msg = msg WHERE id > 0 RETURNING id, msg; to see every row that was touched (a “no-op” update is still a row touch). Then use the CTE chaining pattern to move rows from logs_example to log_archive.
Downloads
SQL file: returning.sql · All lesson files · Setup guide