La sentencia MERGE INTO: el upsert de Oracle
Un solo pase sobre la tabla para insertar lo que falta y actualizar lo que cambia: MERGE es la sentencia que los demás dialectos acabaron copiando.
Sincronizar dos tablas —insertar las filas nuevas, actualizar las que cambiaron— era antes un baile de dos sentencias con condiciones en la aplicación y carreras de concurrencia. Oracle introdujo MERGE en 9i y lo completó en 10g, y desde entonces una sola sentencia atómica hace el trabajo completo. PostgreSQL la incorporó como INSERT ... ON CONFLICT, SQL Server copió la sintaxis casi literal, y el estándar SQL:2003 la bendijo con el alias UPSERT. Este artículo la recorre de la forma mínima a las variantes que sorprenden hasta en producción.
La forma mínima
MERGE INTO precios d
USING precios_nuevos s
ON (d.articulo = s.articulo)
WHEN MATCHED THEN
UPDATE SET d.precio = s.precio
WHEN NOT MATCHED THEN
INSERT (articulo, precio)
VALUES (s.articulo, s.precio);La tabla d (destino) es la que se modifica; s (fuente) puede ser una tabla, una vista o una subconsulta. La condición ON decide el destino de cada fila de la fuente: si casa, WHEN MATCHED la actualiza; si no, WHEN NOT MATCHED la inserta. Todo en un pase, de forma atómica, sin ventanas entre SELECT e INSERT donde otro proceso pueda colarse.
Filtrar la actualización y borrar en el mismo pase
MERGE INTO precios d
USING precios_nuevos s
ON (d.articulo = s.articulo)
WHEN MATCHED THEN
UPDATE SET d.precio = s.precio
WHERE d.precio <> s.precio -- solo si cambia
DELETE WHERE s.activo = 0 -- baja logica del catalogo
WHEN NOT MATCHED THEN
INSERT (articulo, precio, activo)
VALUES (s.articulo, s.precio, s.activo)
WHERE s.activo = 1; -- no insertar dados de bajaDos joyas de la sintaxis: la cláusula WHERE tras el UPDATE evita reescribir filas idénticas —menos redo, menos índices tocados—, y el DELETE WHERE secundario elimina filas dentro del mismo MERGE, pero solo filas que ya han sido actualizadas. Es perfecto para sincronizar catálogos con bajas lógicas: un artículo que llega marcado como inactivo actualiza su fila y sale de la tabla en la misma sentencia.
La fuente puede ser una consulta
Nada obliga a materializar la fuente. Un MERGE con la fuente calculada al vuelo resuelve el clásico «sumar el stock del albarán al inventario»:
MERGE INTO stock d
USING (SELECT sku, SUM(cantidad) AS unidades
FROM albaranes
WHERE fecha = TRUNC(SYSDATE)
GROUP BY sku) s
ON (d.sku = s.sku)
WHEN MATCHED THEN
UPDATE SET d.unidades = d.unidades + s.unidades
WHEN NOT MATCHED THEN
INSERT (sku, unidades) VALUES (s.sku, s.unidades);Errores que se repiten desde 2011
- ORA-00904 en la cláusula ON: solo se pueden referenciar columnas de las dos tablas ya presentes; una función sobre la columna en el
ONpuede desactivar el índice y convertir el MERGE en un escaneo completo. - «Unable to get a stable set of rows»: la fuente devuelve dos filas para la misma clave del destino. Oracle no sabe cuál aplicar. Cura:
DISTINCToGROUP BYen la fuente, como en el ejemplo del stock. - Actualizar columnas de la clave: las columnas del
ONno pueden ir en elSET.
Rendimiento: el índice correcto
El motor busca cada fila de la fuente en el destino por la clave del ON. Si esa clave no tiene índice único en el destino, cada fila provoca un escaneo y un MERGE de un millón de filas se convierte en la consulta de la semana. La receta es corta: índice único en la columna de enlace, estadísticas al día, y ROWS procesados visibles en el plan para confirmar que cada fila de la fuente toca una sola del destino. Para volúmenes enormes, la vía alternativa es una tabla particionada por fecha y MERGE por partición con PARTITION (p202608), que reduce el conjunto recorrido.
El upsert conecta con todo el ecosistema de carga: secuencias para las claves nuevas, XMLTABLE cuando la fuente llega en XML, y las utilidades Data Pump cuando lo que se mueve es el esquema completo.