Cuando insertas una fila y enseguida quieres su id generado sin lanzar una segunda consulta, la clausula RETURNING es la respuesta. Esta extension de PostgreSQL convierte INSERT, UPDATE y DELETE en sentencias que devuelven filas — igual que un SELECT, pero sobre los datos que acabas de escribir.
Por que importa RETURNING
El caso clasico: insertas un usuario y necesitas su id para filas relacionadas. Sin RETURNING ejecutas el insert y luego un SELECT aparte (o llamas a currval), perdiendo un viaje a la base de datos y arriesgando una condicion de carrera. RETURNING devuelve los valores directamente desde la misma orden.
INSERT INTO users (email, name, country)
VALUES ('a@example.com', 'Ann', 'DE')
RETURNING id, created_at;
Propiedades clave:
- Funciona en
INSERT, UPDATE y DELETE.
- Devuelve una fila por cada fila afectada, no solo la primera.
- La lista admite cualquier columna, expresiones sobre ellas y alias.
RETURNING * y columnas calculadas
Cuando quieres todas las columnas de la fila resultante — incluidos los valores por defecto y lo que rellenaron los triggers — usa RETURNING *. Es muy comodo cuando algunos campos se completan del lado de la base de datos.
INSERT INTO orders (user_id, amount, status)
VALUES (42, 99.50, 'pending')
RETURNING *;
La lista de RETURNING puede contener expresiones, no solo columnas sueltas:
UPDATE orders
SET amount = amount * 1.10
WHERE status = 'pending'
RETURNING id, amount AS new_amount, round(amount / 1.10, 2) AS old_amount;
DELETE tambien puede devolver lo que elimino — una auditoria lista de lo que desaparecio exactamente:
DELETE FROM orders
WHERE status = 'cancelled'
RETURNING id, user_id, amount;
RETURNING dentro de un CTE
El uso mas potente es pasar el resultado de RETURNING a una expresion de tabla comun (CTE) y usarlo en el paso siguiente. Asi se arma una cadena atomica "inserta el padre, toma su id, inserta los hijos" en una sola consulta.
WITH new_user AS (
INSERT INTO users (email, name, country)
VALUES ('team@example.com', 'Team', 'ES')
RETURNING id
)
INSERT INTO orders (user_id, amount, status)
SELECT id, 0, 'pending'
FROM new_user
RETURNING id AS order_id, user_id;
El mismo truco resuelve "actualiza y registra el historial": captura los valores viejos con una subconsulta y guardalos junto a los nuevos.
WITH bumped AS (
UPDATE employees
SET salary = salary * 1.05
WHERE dept = 'eng'
RETURNING id, salary AS new_salary
)
SELECT id, new_salary FROM bumped ORDER BY id;
Patrones utiles:
- Mover filas:
DELETE ... RETURNING * dentro de un CTE y luego INSERT del resultado en una tabla de archivo.
- Devolver a la vez un
id y un conteo de filas afectadas con count(*) sobre el CTE.
- Capturar el valor viejo y el nuevo de una columna en una sola pasada.
Gotcha: RETURNING ve lo nuevo, y MySQL no lo tiene
Algunas trampas a vigilar:
- En
UPDATE ... RETURNING, las columnas ya son los valores nuevos. Para capturar el viejo necesitas una subconsulta o un CTE contra la tabla original; la sentencia por si sola no lo mostrara.
- El orden de las filas en
RETURNING no esta garantizado. Si necesitas orden, envuelvelo en un CTE y ordena con un SELECT ... ORDER BY externo.
RETURNING devuelve solo las filas realmente escritas. Con INSERT ... ON CONFLICT DO NOTHING, una fila omitida por conflicto no se devuelve en absoluto.
- MySQL no soporta la clausula
RETURNING (MariaDB 10.5+ si, para INSERT y DELETE). En MySQL, lee el autoincremento tras el insert con LAST_INSERT_ID():
INSERT INTO users (email, name, country)
VALUES ('a@example.com', 'Ann', 'DE');
SELECT LAST_INSERT_ID();
ClickHouse tampoco tiene RETURNING: alli los inserts son asincronos y por lotes, asi que genera las claves en el lado de la aplicacion. En resumen: en PostgreSQL, RETURNING ahorra un viaje y cierra carreras; en MySQL, ten a mano LAST_INSERT_ID().
Cuando insertas una fila y enseguida quieres su
idgenerado sin lanzar una segunda consulta, la clausulaRETURNINGes la respuesta. Esta extension de PostgreSQL convierteINSERT,UPDATEyDELETEen sentencias que devuelven filas — igual que unSELECT, pero sobre los datos que acabas de escribir.Por que importa RETURNING
El caso clasico: insertas un usuario y necesitas su
idpara filas relacionadas. SinRETURNINGejecutas el insert y luego unSELECTaparte (o llamas acurrval), perdiendo un viaje a la base de datos y arriesgando una condicion de carrera.RETURNINGdevuelve los valores directamente desde la misma orden.INSERT INTO users (email, name, country) VALUES ('a@example.com', 'Ann', 'DE') RETURNING id, created_at;Propiedades clave:
INSERT,UPDATEyDELETE.RETURNING * y columnas calculadas
Cuando quieres todas las columnas de la fila resultante — incluidos los valores por defecto y lo que rellenaron los triggers — usa
RETURNING *. Es muy comodo cuando algunos campos se completan del lado de la base de datos.INSERT INTO orders (user_id, amount, status) VALUES (42, 99.50, 'pending') RETURNING *;La lista de
RETURNINGpuede contener expresiones, no solo columnas sueltas:UPDATE orders SET amount = amount * 1.10 WHERE status = 'pending' RETURNING id, amount AS new_amount, round(amount / 1.10, 2) AS old_amount;DELETEtambien puede devolver lo que elimino — una auditoria lista de lo que desaparecio exactamente:DELETE FROM orders WHERE status = 'cancelled' RETURNING id, user_id, amount;RETURNING dentro de un CTE
El uso mas potente es pasar el resultado de
RETURNINGa una expresion de tabla comun (CTE) y usarlo en el paso siguiente. Asi se arma una cadena atomica "inserta el padre, toma suid, inserta los hijos" en una sola consulta.WITH new_user AS ( INSERT INTO users (email, name, country) VALUES ('team@example.com', 'Team', 'ES') RETURNING id ) INSERT INTO orders (user_id, amount, status) SELECT id, 0, 'pending' FROM new_user RETURNING id AS order_id, user_id;El mismo truco resuelve "actualiza y registra el historial": captura los valores viejos con una subconsulta y guardalos junto a los nuevos.
WITH bumped AS ( UPDATE employees SET salary = salary * 1.05 WHERE dept = 'eng' RETURNING id, salary AS new_salary ) SELECT id, new_salary FROM bumped ORDER BY id;Patrones utiles:
DELETE ... RETURNING *dentro de un CTE y luegoINSERTdel resultado en una tabla de archivo.idy un conteo de filas afectadas concount(*)sobre el CTE.Gotcha: RETURNING ve lo nuevo, y MySQL no lo tiene
Algunas trampas a vigilar:
UPDATE ... RETURNING, las columnas ya son los valores nuevos. Para capturar el viejo necesitas una subconsulta o un CTE contra la tabla original; la sentencia por si sola no lo mostrara.RETURNINGno esta garantizado. Si necesitas orden, envuelvelo en un CTE y ordena con unSELECT ... ORDER BYexterno.RETURNINGdevuelve solo las filas realmente escritas. ConINSERT ... ON CONFLICT DO NOTHING, una fila omitida por conflicto no se devuelve en absoluto.RETURNING(MariaDB 10.5+ si, paraINSERTyDELETE). En MySQL, lee el autoincremento tras el insert conLAST_INSERT_ID():-- MySQL: no RETURNING, read the generated id separately INSERT INTO users (email, name, country) VALUES ('a@example.com', 'Ann', 'DE'); SELECT LAST_INSERT_ID();ClickHouse tampoco tiene
RETURNING: alli los inserts son asincronos y por lotes, asi que genera las claves en el lado de la aplicacion. En resumen: en PostgreSQL,RETURNINGahorra un viaje y cierra carreras; en MySQL, ten a manoLAST_INSERT_ID().