RETURNING Clause
RETURNING Clause
Level 3 — CRUD Operations (The Four Pillars of SQL) A PostgreSQL-specific SQL clause appended to
INSERT,UPDATE, orDELETEstatements that immediately returns values from the affected rows, eliminating the need for a separateSELECTquery.
1. Prerequisites
INSERT INTO— Sourcing new rows.UPDATE— Modifying rows.DELETE— Removing rows.
2. Term Category
SQL Command / Clause (Mutation Result Projection Clause): RETURNING returns modified or generated column values directly from INSERT, UPDATE, or DELETE statements.
3. Explanation
Environment Context
- PostgreSQL Specific (A highly popular extension to standard SQL. Supported natively in PostgreSQL and CockroachDB, but absent or implemented differently in other database systems like MySQL or SQL Server).
(1) Design Motivation — "Why did we design this?"
In database operations, write queries often generate values dynamically on the server:
- An
INSERTstatement triggers an auto-incrementing identity sequence to generate a new primary keyid. - An
INSERTstatement triggers aDEFAULT NOW()constraint to generate a timestamp. - An
UPDATEquery decrements a user's wallet balance.
If your backend application code needs to know these new values (for example, displaying the newly created user ID on a website), standard SQL forces you to make a second network trip:
-- Step 1: Insert data
INSERT INTO users (username) VALUES ('alice');
-- Step 2: Query database again to find what ID was just created!
SELECT id FROM users WHERE username = 'alice';
This two-step process has major drawbacks:
- Network Latency: You waste time running two separate database connections.
- Race Conditions: If two users name
'alice'register at the same second, your secondarySELECTmight retrieve the wrong user's ID.
PostgreSQL designed the RETURNING clause to solve this.
By appending RETURNING to the end of a write statement, you instruct the server: "Perform this write, and in the same transaction return the resulting values back to me."
It acts as a hybrid write-read query, executing in a single network round-trip.
(2) Returning Deleted Rows
One of the most powerful uses of RETURNING is with DELETE queries.
It allows you to wipe data while returning exactly what was deleted, which is highly useful for audit logging or client notifications:
DELETE FROM sessions
WHERE expires_at < NOW()
RETURNING username;
-- Wipes expired sessions and returns a list of usernames who were logged out!
(3) Reality Metaphor
Imagine a restaurant coat check:
- Without RETURNING (Standard SQL): You hand the coat to the check clerk (Insert). You then have to ask the clerk: "Which hanger number did you put my coat on?" (Select). The clerk checks and tells you
42. - With RETURNING: You hand the coat to the clerk. In the same motion, the clerk takes the coat and hands you a plastic ticket printed with the number
42. You get the validation key immediately.
(4) Code Examples
Returning Generated IDs on Insert
CREATE TABLE staff (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name VARCHAR(100) NOT NULL,
joined_at TIMESTAMPTZ DEFAULT NOW()
);
-- Insert and fetch the auto-generated ID and timestamp instantly
INSERT INTO staff (name)
VALUES ('Franklin')
RETURNING id, joined_at;
Fetching New Balances on Update
CREATE TABLE wallets (
user_id INT PRIMARY KEY,
balance NUMERIC(10,2)
);
-- Subtract cost and verify the new balance in one query
UPDATE wallets
SET balance = balance - 15.50
WHERE user_id = 101
RETURNING balance;
4. Common Mistakes & Pitfalls
Mistake 1: Assuming RETURNING works identically in all SQL databases
The mistake: Writing application code that uses RETURNING and expecting it to deploy cleanly on a MySQL or SQL Server database.
Why it's wrong: RETURNING is a PostgreSQL extension. If you run a query ending with RETURNING on MySQL, the query engine will crash with a syntax error. MySQL uses separate API hooks (like LAST_INSERT_ID()), while SQL Server uses a custom OUTPUT clause syntax.
Fix: Only use RETURNING if your stack is committed to PostgreSQL (or compatible engines like CockroachDB/YugabyteDB). If write portability is critical, use an Object-Relational Mapper (ORM) library that abstracts write-return syntax variations for you.
Mistake 2: Executing INSERT Followed by SELECT to Retrieve Auto-Increment Primary Keys
The mistake: Executing INSERT INTO users (name) VALUES ('Alice'); followed by SELECT id FROM users WHERE name = 'Alice';.
Why it's wrong: Executing a secondary query creates race conditions and adds network RPC overhead. Use RETURNING id on the INSERT statement.
Incorrect:
INSERT INTO users (name) VALUES ('Alice');
SELECT max(id) FROM users; -- ❌ Race condition risk!
Fix:
INSERT INTO users (name) VALUES ('Alice') RETURNING id; -- Atomic key return
Mistake 3: Expecting RETURNING Output from Bulk Updates Without Capturing Result Sets
The mistake: Executing UPDATE users SET active = true RETURNING *; without consuming returned rows in client driver.
Why it's wrong: In client drivers, RETURNING statements return result streams (like SELECT). Ensure application code consumes the returned row cursor.
Incorrect:
// Running UPDATE ... RETURNING * without reading result stream
Fix:
const res = await client.query('UPDATE users SET active = true RETURNING *'); console.log(res.rows);
5. Practice Exercises
Exercise 1: Returning Auto-Generated Primary Keys on Insert
Scenario:
Insert a new customer and return generated id and created_at values instantly.
Requirements:
- Append
RETURNING id, created_attoINSERT INTO.
Answer
Implementation
INSERT INTO customers (company_name)
VALUES ('Acme Corp')
RETURNING id, created_at;
Technical Explanation
RETURNINGprojects modified or generated row attributes directly from the write statement.- Eliminates issuing a secondary
SELECTquery to fetch sequence values. - Single network roundtrip optimization.
Exercise 2: Capturing Pre-Update State in UPDATE Statements
Scenario:
Deactivate a user and return their previous email address in the response payload.
Requirements:
- Execute
UPDATE users SET is_active = FALSE WHERE id = 10 RETURNING email.
Answer
Exercise 3: Returning Deleted Rows for Audit Logging
Scenario: Delete expired sessions and return the deleted session tokens for audit archiving.
Requirements:
- Append
RETURNING token, user_idtoDELETE FROM.
Answer
Implementation
DELETE FROM user_sessions
WHERE expires_at < CURRENT_TIMESTAMP
RETURNING token, user_id, expires_at;
Technical Explanation
RETURNINGonDELETEprojects the data content of deleted rows before they are purged.- Allows application code to log or archive deleted row payloads.
- Powerful PostgreSQL extension.
6. Related Terms
INSERT INTO— The parent write statement.UPDATE— The parent edit statement.DELETE— The parent delete statement.
7. Key Takeaways
RETURNINGis a PostgreSQL clause that returns values from modified rows.- Eliminates the need to run a secondary
SELECTquery, reducing network latency. - Supported on
INSERT,UPDATE, andDELETEqueries. - Often used to retrieve auto-generated primary IDs and default timestamps.
- Running
DELETE ... RETURNINGyields a list of the data that was wiped. - It is a Postgres-specific feature; avoid it if database porting is required.