sqlpostgresqlctedelete

CTE + DELETE ... RETURNING: mover filas en una sola sentencia

Archiva y mueve filas de forma atomica con CTE que modifican datos, sin ventana de carrera entre DELETE e INSERT.

3 min de lecturaReferencesql · postgresql · cte · delete · returning

Mover filas de una tabla a otra parece trivial: INSERT ... SELECT y luego DELETE. Pero entre esas dos sentencias hay una ventana en la que los datos se duplican o se pierden. Los CTE que modifican datos en PostgreSQL reducen todo a una sola sentencia atomica.

El problema de las dos sentencias

El tipico fragmento de "archivar y luego purgar" es asi:

INSERT INTO orders_archive
SELECT * FROM orders WHERE status = 'cancelled';

DELETE FROM orders WHERE status = 'cancelled';

Dos cosas pueden salir mal. Primero, entre el INSERT y el DELETE otra sesion puede modificar o borrar una fila, asi que archivas una cosa y borras otra. Segundo, si ocurre un fallo entre ambas sentencias, las filas quedan en las dos tablas a la vez. Envolverlo en una transaccion resuelve el fallo, pero la logica sigue partida en dos sentencias y recorre la tabla dos veces.

DELETE ... RETURNING dentro de WITH

PostgreSQL permite poner INSERT, UPDATE y DELETE directamente en una clausula WITH. Las filas borradas vuelven mediante RETURNING y alimentan directamente el siguiente paso:

WITH moved AS (
    DELETE FROM orders
    WHERE status = 'cancelled'
    RETURNING *
)
INSERT INTO orders_archive
SELECT * FROM moved;

Es una unica sentencia. Las filas se borran y se archivan en la misma transaccion, sin ventana intermedia y sin un segundo recorrido de la tabla. Enumera las columnas explicitamente cuando los esquemas difieren:

WITH moved AS (
    DELETE FROM orders
    WHERE created_at < now() - interval '1 year'
    RETURNING id, user_id, amount, status, created_at
)
INSERT INTO orders_archive (id, user_id, amount, status, archived_at)
SELECT id, user_id, amount, status, now()
FROM moved;

La regla de la instantanea unica

El matiz clave: cada subsentencia de un CTE que modifica datos ve la misma instantanea, tomada al iniciar la sentencia. Los cambios de un CTE no son visibles para los demas mediante un SELECT normal; solo son visibles a traves de RETURNING.

  • Si users referencia los orders que borras, un SELECT FROM orders en la misma sentencia todavia "ve" las filas que borra un CTE hermano.
  • El orden de ejecucion de las subsentencias no esta definido; confia solo en el flujo de datos a traves de RETURNING.
  • Intentar modificar la misma fila dos veces en una sentencia (dos UPDATE, por ejemplo) da un resultado indefinido y no se recomienda.

Esta regla es justo lo que hace correcto el traslado: operas sobre un conjunto fijo de filas borradas, no sobre una tabla viva y cambiante.

Particionar filas en una sola pasada

El mismo truco reparte filas hacia varios destinos. Movemos a los empleados dados de baja y contamos cuantos salieron de cada departamento al mismo tiempo:

WITH removed AS (
    DELETE FROM employees
    WHERE salary IS NULL
    RETURNING id, name, manager_id, dept, salary
), archived AS (
    INSERT INTO employees_archive
    SELECT * FROM removed
    RETURNING dept
)
SELECT dept, count(*) AS moved
FROM archived
GROUP BY dept;

El SELECT final devuelve un informe del trabajo realizado, comodo para registros y comprobaciones.

Trampas y diferencias entre motores

  • Trampa: los disparadores AFTER DELETE de la tabla origen si se ejecutan, pero los CTE que modifican datos no garantizan el orden respecto a ellos; no metas los efectos secundarios de disparadores en tu logica de traslado.
  • RETURNING * arrastra todas las columnas de las filas borradas; si cambia el esquema origen se desplaza en silencio, asi que enumera las columnas en el codigo de produccion.
  • MySQL no tiene CTE que modifiquen datos: un DELETE dentro de WITH se rechaza. Alli mueves filas con una transaccion que contiene INSERT ... SELECT mas DELETE, o INSERT ... SELECT ... FOR UPDATE para bloquear las filas.
  • ClickHouse es un motor analitico: alli DELETE es una mutacion pesada y RETURNING no existe. Los traslados se hacen con INSERT INTO ... SELECT mas una mutacion aparte o manipulacion de particiones.

Los CTE que modifican datos son la forma idiomatica de PostgreSQL de expresar "mueve estas filas" como una intencion unica, atomica y legible.

Practica con ejercicios reales

Resuelve ejercicios en el entrenador de SQL con corrección instantánea y pistas.

Abrir el entrenador