Sentencia MERGE en PostgreSQL

Avanzado
SQL
SQL
Actualizado: 19/04/2026

La sincronización de datos entre tablas suele requerir lógica condicional: si la fila existe la actualizo, si no la inserto, y a veces necesito borrar las que ya no aparecen en la fuente. Antes de MERGE este patrón se resolvía con bloques procedurales o con INSERT ... ON CONFLICT, pero ninguno cubría el caso completo. PostgreSQL 15 incorporó la sentencia MERGE del estándar SQL:2003 y la versión 17 añadió RETURNING y la cláusula WHEN NOT MATCHED BY SOURCE.

Sintaxis general

La estructura básica combina una tabla destino, una fuente (que puede ser otra tabla, una subconsulta o VALUES) y una condición de emparejamiento:

MERGE INTO destino AS d
USING fuente AS f
   ON d.clave = f.clave
WHEN MATCHED THEN
    UPDATE SET columna = f.valor
WHEN NOT MATCHED THEN
    INSERT (clave, columna) VALUES (f.clave, f.valor);

Cada cláusula WHEN evalúa una sola fila a la vez. PostgreSQL ejecuta las acciones en orden de aparición, así que la primera condición que coincide gana.

A diferencia de INSERT ... ON CONFLICT, que solo actúa cuando la inserción colisiona con una restricción, MERGE parte de un emparejamiento explícito mediante ON. Esto resulta más natural cuando la fuente y el destino son tablas distintas y no comparten índice único.

Caso práctico: sincronizar precios desde un CSV

Imagina que cada noche llega un fichero con precios actualizados de proveedores. Cargas el CSV en una tabla temporal precios_staging y necesitas reflejar los cambios en la tabla productos:

CREATE TEMP TABLE precios_staging (
    sku TEXT PRIMARY KEY,
    precio NUMERIC(10,2) NOT NULL,
    activo BOOLEAN NOT NULL DEFAULT true
);

COPY precios_staging FROM '/tmp/precios_2026_03_15.csv' CSV HEADER;

El destino es la tabla productos con columnas sku, precio, activo y actualizado_en. La sentencia MERGE que actualiza precios cambiados, inserta SKUs nuevos y desactiva los que ya no aparecen como activos:

MERGE INTO productos AS p
USING precios_staging AS s
   ON p.sku = s.sku
WHEN MATCHED AND p.precio <> s.precio THEN
    UPDATE SET precio = s.precio,
               actualizado_en = NOW()
WHEN MATCHED AND s.activo = false THEN
    UPDATE SET activo = false,
               actualizado_en = NOW()
WHEN NOT MATCHED THEN
    INSERT (sku, precio, activo, actualizado_en)
    VALUES (s.sku, s.precio, s.activo, NOW());

El motor evalúa cada fila de la fuente una vez. La primera cláusula WHEN MATCHED que se cumpla aplica su acción y termina la evaluación de esa fila. Las condiciones adicionales tras AND permiten distinguir escenarios distintos sobre el mismo emparejamiento.

Borrar filas desde MERGE

PostgreSQL acepta DELETE como acción dentro de WHEN MATCHED. Útil para limpieza condicional sin necesidad de un DELETE separado:

MERGE INTO inventario AS i
USING ventas_dia AS v
   ON i.sku = v.sku
WHEN MATCHED AND i.stock - v.unidades_vendidas <= 0 THEN
    DELETE
WHEN MATCHED THEN
    UPDATE SET stock = i.stock - v.unidades_vendidas;

Esta consulta resta las unidades vendidas y elimina las filas cuyo stock final llega a cero, todo en la misma transacción atómica.

WHEN NOT MATCHED BY SOURCE (PostgreSQL 17)

La versión 17 añade WHEN NOT MATCHED BY SOURCE, que se dispara para filas del destino que no aparecen en la fuente. Es la pieza que faltaba para implementar full sync de tipo "lo que ya no existe en origen, lo elimino del destino":

MERGE INTO categorias AS c
USING categorias_origen AS s
   ON c.id = s.id
WHEN NOT MATCHED BY SOURCE THEN
    DELETE
WHEN NOT MATCHED THEN
    INSERT (id, nombre) VALUES (s.id, s.nombre)
WHEN MATCHED AND c.nombre <> s.nombre THEN
    UPDATE SET nombre = s.nombre;

El orden importa: las cláusulas se evalúan secuencialmente. Si pones WHEN NOT MATCHED BY SOURCE THEN DELETE antes que el INSERT, el efecto es exactamente el de un full sync unidireccional.

RETURNING para auditar el resultado

Desde PostgreSQL 17 la cláusula RETURNING admite merge_action(), una función que indica qué acción se ejecutó en cada fila ('INSERT', 'UPDATE' o 'DELETE'):

MERGE INTO productos AS p
USING precios_staging AS s
   ON p.sku = s.sku
WHEN MATCHED THEN
    UPDATE SET precio = s.precio
WHEN NOT MATCHED THEN
    INSERT (sku, precio) VALUES (s.sku, s.precio)
RETURNING merge_action(), p.sku, p.precio;

El resultado permite auditar o registrar las operaciones realizadas sin necesidad de triggers. Es la base para implementar logging de cambios en pipelines ETL.

Diferencias clave frente a INSERT ON CONFLICT

| Aspecto | MERGE | INSERT ... ON CONFLICT | |---------|---------|--------------------------| | Origen de los datos | Tabla, subconsulta o VALUES | Solo VALUES o SELECT | | Emparejamiento | ON arbitrario | Restricción única | | Acciones | INSERT, UPDATE, DELETE | INSERT y UPDATE | | Múltiples ramas | Sí, con condiciones | No, una sola | | Borrado | Sí | No | | Maduración | Desde PG 15 | Desde PG 9.5 |

Para upserts simples sobre una clave única, INSERT ... ON CONFLICT sigue siendo más conciso. Para sincronización compleja con múltiples tipos de acción, MERGE es la herramienta correcta.

Consideraciones de concurrencia

MERGE no garantiza la ausencia de race conditions. Si dos sesiones simultáneas ejecutan el mismo MERGE con datos solapados puede aparecer un error de "duplicate key" o un comportamiento de lost update. La defensa habitual consiste en:

  • Usar el nivel de aislamiento SERIALIZABLE y reintentar en caso de serialization_failure.
  • Combinar MERGE con SELECT ... FOR UPDATE previo sobre las claves implicadas.
  • Aceptar el posible error y reintentar la operación completa.
BEGIN ISOLATION LEVEL SERIALIZABLE;

MERGE INTO productos AS p
USING precios_staging AS s
   ON p.sku = s.sku
WHEN MATCHED THEN UPDATE SET precio = s.precio
WHEN NOT MATCHED THEN INSERT (sku, precio) VALUES (s.sku, s.precio);

COMMIT;

En entornos OLTP con alta concurrencia conviene mantener MERGE en transacciones cortas y procesar los lotes en bloques pequeños para reducir la ventana de conflicto.

Limitaciones a recordar

MERGE en PostgreSQL no admite RETURNING en versiones anteriores a la 17, no se puede ejecutar como subconsulta y no se puede usar dentro de funciones que devuelvan filas modificadas. Si tu cluster aún ejecuta PostgreSQL 14 o anterior, la alternativa es la combinación INSERT ... ON CONFLICT para upserts y CTE separadas para la lógica adicional.

flowchart LR
    A[Fuente: precios_staging] -->|ON sku| B{MATCH?}
    B -->|Si y precio cambia| C[UPDATE precio]
    B -->|Si y activo=false| D[UPDATE activo]
    B -->|No| E[INSERT nuevo SKU]
    B -->|Solo en destino| F[DELETE huerfano]
    C --> G[Tabla productos]
    D --> G
    E --> G
    F --> G

La instrucción MERGE se ha convertido en estándar para los pipelines de ingesta diaria, los sistemas de reconciliación contable y las operaciones de master data management. Dominarla evita decenas de líneas de lógica imperativa y reduce los errores derivados de transacciones múltiples mal coordinadas.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, SQL es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de SQL

Explora más contenido relacionado con SQL y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Comprender la sintaxis MERGE INTO, USING, ON y las cláusulas WHEN MATCHED y WHEN NOT MATCHED. Diferenciar MERGE frente a INSERT ON CONFLICT. Aplicar MERGE para sincronización ETL, upserts complejos y borrado condicional. Usar la cláusula RETURNING introducida en PostgreSQL 17.

Cursos que incluyen esta lección

Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje