Insertar, actualizar y eliminar datos mediante MERGE

En SQL Server 2008, se pueden realizar operaciones de inserción, actualización o eliminación en una sola instrucción utilizando la instrucción MERGE. La instrucción MERGE le permite combinar un origen de datos con una tabla o vista de destino y, a continuación, realizar varias acciones con el destino según los resultados de esa combinación. Por ejemplo, puede utilizar la instrucción MERGE para realizar las operaciones siguientes:

  • Condicionalmente insertar o actualizar filas en una tabla de destino.

    Si la fila existe en la tabla de destino, actualizar una o varias columnas; de lo contrario, insertar los datos en una fila nueva.

  • Sincronizar dos tablas.

    Insertar, actualizar o eliminar filas en una tabla de destino según las diferencias con los datos de origen.

La sintaxis de MERGE está compuesta de cinco cláusulas principales:

  • La cláusula MERGE especifica la tabla o vista que es el destino de las operaciones de inserción, actualización o eliminación.

  • La cláusula USING especifica el origen de datos que va a combinarse con el destino.

  • La cláusula ON especifica las condiciones de combinación que determinan las coincidencias entre el destino y el origen.

  • Las cláusulas WHEN (WHEN MATCHED, WHEN NOT MATCHED BY TARGET y WHEN NOT MATCHED BY SOURCE) especifican las acciones que se van a llevar a cabo según los resultados de la cláusula ON y cualquier criterio de búsqueda adicional especificado en las cláusulas WHEN.

  • La cláusula OUTPUT devuelve una fila por cada fila del destino que se inserta, actualiza o elimina.

Para obtener detalles completos de la sintaxis y las reglas, vea MERGE (Transact-SQL).

Especificar las condiciones de búsqueda de origen y destino

Es importante comprender cómo se combinan los datos de destino y origen en un flujo de entrada único y cómo se pueden usar criterios de búsqueda adicionales para filtrar correctamente las filas innecesarias. De lo contrario, podría especificar un criterio de búsqueda adicional de manera que genere los resultados incorrectos.

Las filas en el origen coinciden con las filas en el destino según en el predicado de combinación especificado en la cláusula ON. El resultado es un flujo de entrada combinado. Se realiza una operación de inserción, actualización o de eliminación por cada fila de entrada. Dependiendo de las cláusulas WHEN especificadas en la instrucción, la fila de entrada podría ser una de las siguientes:

  • Un par coincidente compuesto de una fila del destino y una del origen. Éste es el resultado de la cláusula WHEN MATCHED.

  • Una fila del origen que no tiene ninguna fila correspondiente en el destino. Éste es el resultado de la cláusula WHEN NOT MATCHED BY TARGET.

  • Una fila del destino que no tiene ninguna fila correspondiente en el origen. Éste es el resultado de la cláusula WHEN NOT MATCHED BY SOURCE.

La combinación de las cláusulas WHEN especificadas en la instrucción MERGE determina el tipo de combinación que implementa el procesador de consultas y que afecta al flujo de entrada resultante. Como ejemplo, tenga en cuenta las siguientes tablas de origen y de destino de ejemplo, así como los datos.

USE tempdb;
GO
CREATE TABLE dbo.Target(EmployeeID int, EmployeeName varchar(10), 
     CONSTRAINT Target_PK PRIMARY KEY(EmployeeID));
CREATE TABLE dbo.Source(EmployeeID int, EmployeeName varchar(10), 
     CONSTRAINT Source_PK PRIMARY KEY(EmployeeID));
GO
INSERT dbo.Target(EmployeeID, EmployeeName) VALUES(100, 'Mary');
INSERT dbo.Target(EmployeeID, EmployeeName) VALUES(101, 'Sara');
INSERT dbo.Target(EmployeeID, EmployeeName) VALUES(102, 'Stefano');

GO
INSERT dbo.Source(EmployeeID, EmployeeName) Values(103, 'Bob');
INSERT dbo.Source(EmployeeID, EmployeeName) Values(104, 'Steve');
GO

La tabla siguiente enumera los posibles tipos de combinación e indica cuando el optimizador de consultas implementa cada tipo. La tabla también muestra el flujo de entrada resultante para las tablas de origen y de destino de ejemplo cuando el criterio de búsqueda para que coincidan los datos de origen y de destino es Source.EmployeeID = Target.EmployeeID.

Tipo de combinación

Implementación

Resultados del flujo de entrada de ejemplo

INNER JOIN

La cláusula WHEN MATCHED es la única cláusula WHEN especificada.

SrcEmpID SrcName TrgEmpID TrgName

-------- ------- -------- -------

NULL     NULL    NULL     NULL

LEFT OUTER JOIN

Se especifica la cláusula WHEN NOT MATCHED BY TARGET pero no se especifica la cláusula WHEN NOT MATCHED BY SOURCE. La cláusula WHEN MATCHED se podría o no especificar.

SrcEmpID SrcName TrgEmpID TrgName

-------- ------- -------- -------100      Mary    NULL     NULL

101      Sara    NULL     NULL

102      Stefano NULL     NULL

RIGHT OUTER JOIN

Se especifican las cláusulas WHEN MATCHED y WHEN NOT MATCHED BY SOURCE pero no es especifica la cláusula WHEN NOT MATCHED BY TARGET.

SrcEmpID SrcName TrgEmpID TrgName

-------- ------- -------- -------NULL     NULL    103      Bob

NULL     NULL    104      Steve

FULL OUTER JOIN

Se especifican las cláusulas WHEN NOT MATCHED BY TARGET y WHEN NOT MATCHED BY SOURCE. La cláusula WHEN MATCHED se podría o no especificar.

SrcEmpID SrcName TrgEmpID TrgName

-------- ------- -------- -------100      Mary    NULL     NULL

101      Sara    NULL     NULL

102      Stefano NULL     NULL

NULL     NULL    103      Bob

NULL     NULL    104      Steve

ANTI SEMI JOIN

La cláusula WHEN NOT MATCHED BY SOURCE es la única cláusula WHEN especificada.

TrgEmpID TrgName

-------- -------

100      Mary

101      Sara

102      Stefano

Los resultados del flujo de entrada de ejemplo muestran que los resultados del flujo de entrada dependen de la combinación de las cláusulas WHEN. Ahora suponga que desea realizar las acciones siguientes en la tabla de destino según ese flujo de entrada:

  • Insertar filas de la tabla de origen si el identificador del empleado no existe en la tabla de destino y el nombre del empleado de origen empieza con 'S'.

  • Eliminar filas en la tabla de destino si el nombre del empleado de destino empieza con 'S' y el identificador del empleado no existe en la tabla de origen.

Para realizar estas acciones, son necesarias las siguientes cláusulas WHEN:

  • WHEN NOT MATCHED BY TARGET THEN INSERT

  • WHEN NOT MATCHED BY SOURCE THEN DELETE

Tal y como se describe en la tabla anterior, cuando se especifican ambas cláusulas WHEN NOT MATCHED, el flujo de entrada resultante es una combinación externa completa de las tablas de destino y de origen. Ahora que se conocen los resultados del flujo de entrada, debe tener en cuenta cómo se aplicarán las acciones de inserción, actualización y eliminación en el flujo de entrada.

Como se ha indicado anteriormente, las cláusulas WHEN especifican las acciones que se deben realizar según los resultados de la cláusula ON y cualquier criterio de búsqueda adicional especificados en las cláusulas WHEN. En muchos casos, las condiciones de búsqueda especificadas en la cláusula ON generan el flujo de entrada necesario. Sin embargo, en el ejemplo, las acciones de inserción y eliminación exigen un filtrado adicional para restringir las filas afectadas a aquéllas con un nombre de empleado que empieza con 'S'. En el ejemplo siguiente, las condiciones de filtrado se aplican a WHEN NOT MATCHED BY TARGET y WHEN NOT MATCHED BY SOURCE. La salida de la instrucción muestra que las filas esperadas del flujo de entrada se corrigen, insertan o eliminan.

-- MERGE statement with the join conditions specified correctly.
USE tempdb;
GO
BEGIN TRAN;
MERGE Target AS T
USING Source AS S
ON (T.EmployeeID = S.EmployeeID) 
WHEN NOT MATCHED BY TARGET AND S.EmployeeName LIKE 'S%' 
    THEN INSERT(EmployeeID, EmployeeName) VALUES(S.EmployeeID, S.EmployeeName)
WHEN MATCHED 
    THEN UPDATE SET T.EmployeeName = S.EmployeeName
WHEN NOT MATCHED BY SOURCE AND T.EmployeeName LIKE 'S%'
    THEN DELETE 
OUTPUT $action, inserted.*, deleted.*;
ROLLBACK TRAN;
GO 

A continuación se muestran los resultados de la cláusula OUTPUT.

$action   EmployeeID  EmployeeName EmployeeID  EmployeeName

--------- ----------- ------------ ----------- ------------

DELETE    NULL        NULL         101         Sara

DELETE    NULL        NULL         102         Stefano

INSERT    104         Steve        NULL        NULL

(3 filas afectadas)

Cuando se reduce el número de filas en el flujo de entrada al principio del proceso, especificando la condición de búsqueda adicional a la cláusula ON (por ejemplo, especificando ON Source.EmployeeID = Target.EmployeeID AND EmployeeName LIKE 'S%'), podría parecer que mejora el rendimiento de las consultas. Sin embargo, esto puede producir resultados inesperados e incorrectos. Dado que las condiciones de búsqueda adicionales especificadas en la cláusula ON no se usan para que coincidan los datos de destino y de origen, pueden aplicarse mal.

El ejemplo siguiente muestra cómo se pueden producir resultados incorrectos. La condición de búsqueda para que coincidan las tablas de destino y de origen, y la condición de búsqueda adicional para filtrar las filas, se especifican en la cláusula ON. Puesto que no se necesita la condición de búsqueda adicional para determinar la coincidencia de origen y de destino, se aplican las acciones de inserción y eliminación en todas las filas de entrada. De hecho, se omite la condición de filtrado EmployeeName LIKE 'S%'. Cuando se ejecuta la instrucción, la salida de las tablas inserted y deleted muestra que se han modificado dos filas de manera incorrecta: Mary incorrectamente se elimina de la tabla de destino y Bob incorrectamente se inserta.

-- MERGE statement with join conditions that produce unexpected results.
USE tempdb;
GO
BEGIN TRAN;
MERGE Target AS T
USING Source AS S
ON (T.EmployeeID = S.EmployeeID AND T.EmployeeName LIKE 'S%' 
    AND S.EmployeeName LIKE 'S%' )
WHEN NOT MATCHED BY TARGET
    THEN INSERT(EmployeeID, EmployeeName) VALUES(S.EmployeeID, S.EmployeeName)
WHEN MATCHED 
    THEN UPDATE SET T.EmployeeName = S.EmployeeName
WHEN NOT MATCHED BY SOURCE
    THEN DELETE
OUTPUT $action, Inserted.*, Deleted.*;
ROLLBACK TRAN;
GO

A continuación se muestran los resultados de la cláusula OUTPUT.

$action   EmployeeID  EmployeeName EmployeeID  EmployeeName

--------- ----------- ------------ ----------- ------------

DELETE    NULL        NULL         100         Mary

DELETE    NULL        NULL         101         Sara

DELETE    NULL        NULL         102         Stefano

INSERT    103         Bob          NULL        NULL

INSERT    104         Steve        NULL        NULL

(5 filas afectadas)

Instrucciones de condiciones de búsqueda

Las condiciones de búsqueda que se usan para que coincidan las filas de origen y de destino, así como las condiciones de búsqueda adicionales que se usan para filtrar las filas de origen o de destino, se deben especificar correctamente para asegurarse de que se obtienen resultados correctos. Se recomienda seguir estas instrucciones:

  • Especifique únicamente las condiciones de búsqueda en la cláusula ON <merge_search_condition> que determinan los criterios para que coincidan los datos en las tablas de origen y de destino. Es decir, especifique solo las columnas de la tabla de destino que se comparan con las correspondientes columnas de la tabla de origen.

  • No incluya comparaciones a otros valores como una constante.

Para filtrar las filas de las tablas de origen o de destino, use uno de los métodos siguientes:

  • Especifique la condición de búsqueda para el filtrado de filas en cláusula WHEN adecuada. Por ejemplo, WHEN NOT MATCHED AND S.EmployeeName LIKE 'S%' THEN INSERT....

  • Defina una vista en el origen o destino que devuelva las filas filtradas y haga referencia a la vista como la tabla de origen o de destino. Si se define la vista en la tabla de destino, cualquier acción con ella debe satisfacer las condiciones para actualizar las vistas. Para obtener más información acerca de cómo actualizar datos mediante una vista, vea Modificar datos mediante una vista.

  • Use la cláusula WITH <expresión de tabla común> para filtrar filas de las tablas de origen o de destino. Este método es similar a especificar el criterio de búsqueda adicional en la cláusula ON y puede generar resultados incorrectos. Se recomienda evitar el uso de este método o prueba de manera exhaustiva antes de implementarlo.

Ejemplos

A. Usar una instrucción MERGE simple para realizar operaciones INSERT y UPDATE

Suponga que tiene una tabla FactBuyingHabits en una base de datos de almacenamiento de datos que realiza el seguimiento de la fecha más reciente en que cada cliente compró un determinado producto. Una segunda tabla, Purchases, que se hospeda en una base de datos OLTP, registra las compras realizadas durante una semana determinada. Desea agregar cada semana las filas de los productos que determinados clientes no han comprado nunca de la tabla Purchases a la tabla FactBuyingHabits. En el caso de las filas de clientes que compran productos que ya han comprado antes, simplemente desea actualizar la fecha de compra en la tabla FactBuyingHabits. Estas operaciones de inserción y actualización se pueden realizar en una sola instrucción utilizando MERGE.

El ejemplo siguiente crea en primer lugar las tablas Purchases y FactBuyingHabits y a continuación carga en ellas algunos datos de muestra. El rendimiento de las instrucciones MERGE mejora cuando se crean índices UNIQUE en la clave de combinación, de modo que los índices clúster se crean creando una restricción KEY PRIMARY en la columna ProductID en ambas tablas.

En este ejemplo, Purchases contiene las compras durante la semana del 21 de agosto de 2006. FactBuyingHabits contiene las compras correspondientes a la semana anterior; por lo general esta tabla se rellenaría con las filas de fechas muy anteriores.

USE AdventureWorks2008R2;
GO
IF OBJECT_ID (N'dbo.Purchases', N'U') IS NOT NULL 
    DROP TABLE dbo.Purchases;
GO
CREATE TABLE dbo.Purchases (
    ProductID int, CustomerID int, PurchaseDate datetime, 
    CONSTRAINT PK_PurchProdID PRIMARY KEY(ProductID,CustomerID));
GO
INSERT INTO dbo.Purchases VALUES(707, 11794, '20060821'),
(707, 15160, '20060825'),(708, 18529, '20060821'),
(711, 11794, '20060821'),(711, 19585, '20060822'),
(712, 14680, '20060825'),(712, 21524, '20060825'),
(712, 19072, '20060821'),(870, 15160, '20060823'),
(870, 11927, '20060824'),(870, 18749, '20060825');
GO
IF OBJECT_ID (N'dbo.FactBuyingHabits', N'U') IS NOT NULL 
    DROP TABLE dbo.FactBuyingHabits;
GO
CREATE TABLE dbo.FactBuyingHabits (
    ProductID int, CustomerID int, LastPurchaseDate datetime, 
    CONSTRAINT PK_FactProdID PRIMARY KEY(ProductID,CustomerID));
GO
INSERT INTO dbo.FactBuyingHabits VALUES(707, 11794, '20060814'),
(707, 18178, '20060818'),(864, 14114, '20060818'),
(866, 13350, '20060818'),(866, 20201, '20060815'),
(867, 20201, '20060814'),(869, 19893, '20060815'),
(870, 17151, '20060818'),(870, 15160, '20060817'),
(871, 21717, '20060817'),(871, 21163, '20060815'),
(871, 13350, '20060815'),(873, 23381, '20060815');
GO

Las tablas se rellenan ahora con los datos siguientes:

dbo.Purchases

ProductID   CustomerID  PurchaseDate

----------- ----------- -----------------------

707         11794       2006-08-20 00:00:00.000

707         15160       2006-08-25 00:00:00.000

708         18529       2006-08-21 00:00:00.000

711         11794       2006-08-20 00:00:00.000

711         19585       2006-08-22 00:00:00.000

712         14680       2006-08-26 00:00:00.000

712         21524       2006-08-26 00:00:00.000

712         19072       2006-08-20 00:00:00.000

870         15160       2006-08-23 00:00:00.000

870         11927       2006-08-24 00:00:00.000

870         18749       2006-08-25 00:00:00.000

dbo.FactBuyingHabits

ProductID   CustomerID  LastPurchaseDate

----------- ----------- -----------------------

707         11794       2006-08-14 00:00:00.000

707         18178       2006-08-18 00:00:00.000

864         14114       2006-08-18 00:00:00.000

866         13350       2006-08-18 00:00:00.000

866         20201       2006-08-15 00:00:00.000

867         20201       2006-08-14 00:00:00.000

869         19893       2006-08-15 00:00:00.000

870         17151       2006-08-18 00:00:00.000

870         15160       2006-08-17 00:00:00.000

871         21717       2006-08-17 00:00:00.000

871         21163       2006-08-15 00:00:00.000

871         13350       2006-08-15 00:00:00.000

873         23381       2006-08-15 00:00:00.000

Observe que hay dos filas Producto-Cliente en común las dos tablas: el cliente 11794 compró el producto 707 durante la semana actual y en la semana anterior, y lo mismo puede decirse del cliente 15160 respecto al producto 870. Para esas filas, actualizamos FactBuyingHabits con la fecha registrada para esas compras en Purchases utilizando la cláusula WHEN MATCHED THEN. Insertamos todas las demás filas en FactBuyingHabits utilizando la cláusula WHEN NOT MATCHED THEN.

MERGE dbo.FactBuyingHabits AS Target
USING (SELECT CustomerID, ProductID, PurchaseDate FROM dbo.Purchases) AS Source
ON (Target.ProductID = Source.ProductID AND Target.CustomerID = Source.CustomerID)
WHEN MATCHED THEN
    UPDATE SET Target.LastPurchaseDate = Source.PurchaseDate
WHEN NOT MATCHED BY TARGET THEN
    INSERT (CustomerID, ProductID, LastPurchaseDate)
    VALUES (Source.CustomerID, Source.ProductID, Source.PurchaseDate)
OUTPUT $action, Inserted.*, Deleted.*; 

B. Realizar las operaciones UPDATE y DELETE

El siguiente ejemplo usa MERGE para actualizar a diario la tabla ProductInventory de la base de datos de muestra AdventureWorks2008R2, en función de los pedidos procesados en la tabla SalesOrderDetail. Utilizando la siguiente instrucción MERGE, la columna Quantity de la tabla ProductInventory se actualiza restando el número de pedidos realizados cada día para cada producto. Si el número de pedidos para un producto hace que el inventario del producto llegue a 0 o menos, la fila correspondiente a ese producto se elimina de la tabla ProductInventory. Observe que la tabla de origen se agrega en la columna ProductID. Si no se hiciera, más de un ProductID de la tabla de origen podría coincidir con la tabla de destino y hacer que la instrucción MERGE devuelva un error.

USE AdventureWorks2008R2;
GO
IF OBJECT_ID (N'Production.usp_UpdateInventory', N'P') IS NOT NULL DROP PROCEDURE Production.usp_UpdateInventory;
GO
CREATE PROCEDURE Production.usp_UpdateInventory
    @OrderDate datetime
AS
MERGE Production.ProductInventory AS target
USING (SELECT ProductID, SUM(OrderQty) FROM Sales.SalesOrderDetail AS sod
    JOIN Sales.SalesOrderHeader AS soh
    ON sod.SalesOrderID = soh.SalesOrderID
    AND soh.OrderDate = @OrderDate
    GROUP BY ProductID) AS source (ProductID, OrderQty)
ON (target.ProductID = source.ProductID)
WHEN MATCHED AND target.Quantity - source.OrderQty <= 0
    THEN DELETE
WHEN MATCHED 
    THEN UPDATE SET target.Quantity = target.Quantity - source.OrderQty, 
                    target.ModifiedDate = GETDATE()
OUTPUT $action, Inserted.ProductID, Inserted.Quantity, Inserted.ModifiedDate, Deleted.ProductID,
    Deleted.Quantity, Deleted.ModifiedDate;
GO

EXECUTE Production.usp_UpdateInventory '20030501'

C. Realizar las operaciones INSERT, UPDATE y DELETE

El ejemplo siguiente utiliza MERGE para insertar, actualizar o eliminar las filas de una tabla de destino según las diferencias con los datos de origen. Considere una pequeña compañía con cinco departamentos, cada uno con un gerente de departamento. La compañía decide reorganizar sus departamentos. Para implementar los resultados de la reorganización en la tabla de destino dbo.Departments, la instrucción MERGE debe implementar los cambios siguientes:

  • Algunos de los departamentos existentes no cambiarán.

  • Algunos departamentos existentes tendrán nuevos gerentes.

  • Se crearán algunos departamentos nuevos.

  • Algunos departamentos dejarán de existir tras la reorganización.

El código siguiente crea la tabla de destino dbo.Departments y la rellena con los gerentes.

USE AdventureWorks2008R2;
GO
IF OBJECT_ID (N'dbo.Departments', N'U') IS NOT NULL 
    DROP TABLE dbo.Departments;
GO
CREATE TABLE dbo.Departments (DeptID tinyint NOT NULL PRIMARY KEY, DeptName nvarchar(30), 
    Manager nvarchar(50));
GO
INSERT INTO dbo.Departments 
    VALUES (1, 'Human Resources', 'Margheim'),(2, 'Sales', 'Byham'), 
           (3, 'Finance', 'Gill'),(4, 'Purchasing', 'Barber'),
           (5, 'Manufacturing', 'Brewer');

Los cambios de la organización que se van a realizar en los departamentos están almacenados en la tabla de origen dbo.Departments_delta. El código siguiente crea y rellena esta tabla:

USE AdventureWorks2008R2;
GO
IF OBJECT_ID (N'dbo.Departments_delta', N'U') IS NOT NULL 
    DROP TABLE dbo.Departments_delta;
GO
CREATE TABLE dbo.Departments_delta (DeptID tinyint NOT NULL PRIMARY KEY, DeptName nvarchar(30), 
    Manager nvarchar(50));
GO
INSERT INTO dbo.Departments_delta VALUES 
    (1, 'Human Resources', 'Margheim'), (2, 'Sales', 'Erickson'),
    (3 , 'Accounting', 'Varkey'),(4, 'Purchasing', 'Barber'), 
    (6, 'Production', 'Jones'), (7, 'Customer Relations', 'Smith');
GO

Finalmente, para reflejar la reorganización de la compañía en la tabla de destino, el código siguiente utiliza la instrucción MERGE para comparar la tabla de origen, dbo.Departments_delta, con la tabla de destino dbo.Departments. La condición de búsqueda para esta comparación se define en la cláusula ON de la instrucción. Según los resultados de la comparación, se toman las medidas siguientes.

  • Los departamentos que existan en ambas tablas se actualizarán en la tabla de destino con los nuevos nombres, los nuevos gerentes o ambos en la tabla Departments. Si no hay ningún cambio, no se realizará ninguna actualización. Esto se logra en la cláusula WHEN MATCHED THEN.

  • Cualquier departamento de Departments_delta que no exista en Departments se inserta en Departments. Esto se lleva a cabo en la cláusula WHEN NOT MATCHED THEN.

  • Cualquier departamento de Departments que no exista en la tabla de origen Departments_delta se elimina de Departments. Esto se lleva a cabo en la cláusula WHEN NOT MATCHED BY SOURCE THEN.

MERGE dbo.Departments AS d
USING dbo.Departments_delta AS dd
ON (d.DeptID = dd.DeptID)
WHEN MATCHED AND d.Manager <> dd.Manager OR d.DeptName <> dd.DeptName
    THEN UPDATE SET d.Manager = dd.Manager, d.DeptName = dd.DeptName
WHEN NOT MATCHED THEN
    INSERT (DeptID, DeptName, Manager)
        VALUES (dd.DeptID, dd.DeptName, dd.Manager)
WHEN NOT MATCHED BY SOURCE THEN
    DELETE
OUTPUT $action, 
       inserted.DeptID AS SourceDeptID, inserted.DeptName AS SourceDeptName, 
       inserted.Manager AS SourceManager, 
       deleted.DeptID AS TargetDeptID, deleted.DeptName AS TargetDeptName, 
       deleted.Manager AS TargetManager;