skip to content

What does adding RETURNING order_id to an INSERT statement produce?

level: juniorimportance: must knowfreq 55%

answer

  1. the write hands something back
  2. no follow-up SELECT needed
  3. server-generated values travel back too
  4. one row per inserted row
  5. the INSERT produces a result set

basics

~10 s

RETURNING makes the INSERT also yield a result set: one row per inserted row, containing the listed columns with their final stored values — generated identity keys, applied defaults and computed columns included.

solid answer

~50 s

A `RETURNING` clause turns a write statement into something that behaves like a query as well. `INSERT INTO orders (customer_id, total) VALUES (42, 99.50) RETURNING order_id, created_at` inserts the row and hands back a result set with one row per row inserted, holding exactly the expressions you listed. The values are the ones actually stored, so anything the server produced — an identity/sequence key, a `DEFAULT CURRENT_TIMESTAMP`, a generated column — comes back without a follow-up `SELECT`. Because the statement now returns rows, the client has to run it through the query path (`executeQuery`, `fetchall`, …) rather than the update-count path; calling an update-only API typically loses the rows or errors. `RETURNING` accepts a full output list, including `*` and computed expressions with aliases. It is a widely implemented extension, not core ISO SQL, so not every engine has it.

go deeper

for a junior

Be ready to write an INSERT that hands back the generated key, and to say that the statement now returns a result set you must read like a query.

for a middle

Explain what the returned values actually are — the stored row after defaults, identity and triggers — and why the client must use the query path rather than the update-count path.

for a senior

Show the round-trip and correctness payoff in a write path, and flag that the clause is a vendor extension whose absence changes how you write the data-access layer.

for a principal

Own the portability call: whether the write path may assume result-returning DML at all, or must be written against the lowest common denominator across the engines you support.

## What the clause does Normally a DML statement is write-only from the client's point of view: `INSERT`, `UPDATE` and `DELETE` report how many rows they affected, and nothing else. If you need to know *what* the server stored — the key it generated, the timestamp its default filled in, the value a computed column derived — you have to ask again with a `SELECT`. A `RETURNING` clause removes that second step. You append it to the write statement with an output list shaped exactly like a `SELECT` list: ```sql INSERT INTO orders (customer_id, total) VALUES (42, 99.50) RETURNING order_id, created_at, total * 0.2 AS tax; ``` The statement performs the insert and, in the same execution, produces a result set. ## The shape of the result The result is a table, not a scalar. It has one row for every row the statement inserted (five rows for a five-row `VALUES` list, N rows for an `INSERT ... SELECT` that inserted N rows), and one column per expression in the output list. `RETURNING *` returns every column of the target table, which is handy for a round-trip of the complete stored row. Because of that, the statement must be executed as a query. In JDBC that means `executeQuery` (or `execute` plus `getResultSet`) rather than `executeUpdate`; in Python DB-API you call `cursor.fetchall()` after executing; in Go you use `Query` rather than `Exec`. Sending a `RETURNING` statement down an update-only API is the single most common first mistake: some drivers throw, others silently discard the rows. ## Which values you see The expressions are evaluated against the row **as stored**, not against the literals you supplied. That is the whole point. Concretely: - an identity or sequence-backed key that you did not supply comes back with its assigned value; - a column you omitted comes back with the default the server applied; - a generated/computed column comes back with its computed value; - if a BEFORE-style trigger rewrote a column on the way in, you see the rewritten value. So `INSERT INTO users (email) VALUES ('[email protected]') RETURNING user_id, created_at, status` tells you the key, the timestamp and the default status in one round trip. ## Why it matters Three practical wins. First, one network round trip instead of two, which matters a lot in a loop or a hot write path. Second, no ambiguity about *which* row you are reading back: the result belongs to the rows this statement wrote, so you do not have to invent a `WHERE` clause that re-identifies them (often impossible when the only unique value is the key the server just generated). Third, it composes: engines that allow a data-modifying statement inside a `WITH` clause let you feed the returned rows straight into another statement. ## Where it works, and how it is spelled `RETURNING` is not part of core ISO SQL — it is a vendor extension that several engines converged on. PostgreSQL and SQLite (from 3.35) support it on `INSERT`, `UPDATE` and `DELETE`; MariaDB supports it on some statement kinds depending on version. Oracle has `RETURNING ... INTO`, which writes into bind variables instead of producing a client result set. SQL Server spells the feature `OUTPUT`, using `inserted.` and `deleted.` pseudo-tables. MySQL has no `RETURNING` clause at all. Treat "my engine returns rows from an INSERT" as something to verify, not assume. ## Common mistakes Expecting `RETURNING` to echo back the values you sent — it echoes what was stored, which can differ. Expecting an update count as well as rows; you generally get the rows, and the row count is the number of rows in the result set. Assuming it works on any engine, then discovering the target deployment is MySQL. And putting business logic in the output list that would be clearer in the surrounding query — the clause is for reading back what the write produced, not a general-purpose reporting hook.

  • Can the RETURNING list hold anything other than plain column names?
    Yes — it is an output list like a `SELECT` list. You can use `*`, individual columns, computed expressions and column aliases, for example `RETURNING order_id, total * 1.2 AS gross`. The expressions are evaluated over the row as stored by this statement.
  • Why does calling executeUpdate on an INSERT ... RETURNING go wrong?
    Because the statement now produces a result set. The update-count path has nowhere to put those rows, so drivers either raise an error or discard them. Execute it as a query (`executeQuery`, `fetchall`, `Query`) and read the rows.
  • Do values written by a trigger or a generated column show up in RETURNING?
    Yes. The output list is evaluated against the row as finally stored, so a column rewritten by a before-insert trigger, a filled-in default, and a generated column all come back with their stored values — that is exactly why it beats echoing your own input.

Like a vending machine that prints a receipt with the item and its serial number as it dispenses, instead of making you go look the number up afterwards.

saying these in an interview costs you the question

  • Says you must always run a SELECT after inserting
  • Assumes every engine supports RETURNING, MySQL included
  • Executes it via executeUpdate and wonders where the rows went
  • Thinks RETURNING echoes the literals you supplied
  • Believes RETURNING can only return the primary key

context