¿Por qué esta declaración MERGE hace que la sesión se cierre?


23

Tengo la siguiente MERGEdeclaración que se emite contra la base de datos:

MERGE "MySchema"."Point" AS t
USING (
       SELECT "ObjectId", "PointName", z."Id" AS "LocationId", i."Id" AS "Region"
         FROM @p1 AS d
         JOIN "MySchema"."Region" AS i ON i."Name" = d."Region"
    LEFT JOIN "MySchema"."Location" AS z ON z."Name" = d."Location" AND z."Region" = i."Id"
       ) AS s
   ON s."ObjectId" = t."ObjectId"
 WHEN NOT MATCHED BY TARGET 
    THEN INSERT ("ObjectId", "Name", "LocationId", "Region") VALUES (s."ObjectId", s."PointName", s."LocationId", s."Region")
 WHEN MATCHED 
    THEN UPDATE 
     SET "Name" = s."PointName"
       , "LocationId" = s."LocationId"
       , "Region" = s."Region"
OUTPUT $action, inserted.*, deleted.*;

Sin embargo, esto hace que la sesión finalice con el siguiente error:

Mensaje 0, Nivel 11, Estado 0, Línea 67 Se produjo un error grave en el comando actual. Los resultados, si los hay, deben descartarse.

Mensaje 0, Nivel 20, Estado 0, Línea 67 Se produjo un error grave en el comando actual. Los resultados, si los hay, deben descartarse.

He creado un breve script de prueba que produce el error:

USE master;
GO
IF DB_ID('TEST') IS NOT NULL
DROP DATABASE "TEST";
GO
CREATE DATABASE "TEST";
GO
USE "TEST";
GO

SET NOCOUNT ON;

IF SCHEMA_ID('MySchema') IS NULL
EXECUTE('CREATE SCHEMA "MySchema"');
GO

IF OBJECT_ID('MySchema.Region', 'U') IS NULL
CREATE TABLE "MySchema"."Region" (
"Id" TINYINT IDENTITY NOT NULL CONSTRAINT "PK_MySchema_Region" PRIMARY KEY,
"Name" VARCHAR(8) NOT NULL CONSTRAINT "UK_MySchema_Region" UNIQUE
);
GO

INSERT [MySchema].[Region] ([Name]) 
VALUES (N'A'), (N'B'), (N'C'), (N'D'), (N'E'), ( N'F'), (N'G');

IF OBJECT_ID('MySchema.Location', 'U') IS NULL
CREATE TABLE "MySchema"."Location" (
"Id" SMALLINT IDENTITY NOT NULL CONSTRAINT "PK_MySchema_Location" PRIMARY KEY,
"Region" TINYINT NOT NULL CONSTRAINT "FK_MySchema_Location_Region" FOREIGN KEY REFERENCES "MySchema"."Region" ("Id"),
"Name" VARCHAR(128) NOT NULL,
CONSTRAINT "UK_MySchema_Location" UNIQUE ("Region", "Name") 
);
GO

IF OBJECT_ID('MySchema.Point', 'U') IS NULL
CREATE TABLE "MySchema"."Point" (
"ObjectId" BIGINT NOT NULL CONSTRAINT "PK_MySchema_Point" PRIMARY KEY,
"Name" VARCHAR(64) NOT NULL,
"LocationId" SMALLINT NULL CONSTRAINT "FK_MySchema_Point_Location" FOREIGN KEY REFERENCES "MySchema"."Location"("Id"),
"Region" TINYINT NOT NULL CONSTRAINT "FK_MySchema_Point_Region" FOREIGN KEY REFERENCES "MySchema"."Region" ("Id"),
CONSTRAINT "UK_MySchema_Point" UNIQUE ("Name", "Region", "LocationId")
);
GO

-- CONTAINS HISTORIC Point DATA
IF OBJECT_ID('MySchema.PointHistory', 'U') IS NULL
CREATE TABLE "MySchema"."PointHistory" (
"Id" BIGINT IDENTITY NOT NULL CONSTRAINT "PK_MySchema_PointHistory" PRIMARY KEY,
"ObjectId" BIGINT NOT NULL,
"Name" VARCHAR(64) NOT NULL,
"LocationId" SMALLINT NULL,
"Region" TINYINT NOT NULL
);
GO

CREATE TYPE "MySchema"."PointTable" AS TABLE (
"ObjectId"      BIGINT          NOT NULL PRIMARY KEY,
"PointName"     VARCHAR(64)     NOT NULL,
"Location"      VARCHAR(16)     NULL,
"Region"        VARCHAR(8)      NOT NULL,
UNIQUE ("PointName", "Region", "Location")
);
GO

DECLARE @p1 "MySchema"."PointTable";

insert into @p1 values(10001769996,N'ABCDEFGH',N'N/A',N'E')

MERGE "MySchema"."Point" AS t
USING (
       SELECT "ObjectId", "PointName", z."Id" AS "LocationId", i."Id" AS "Region"
         FROM @p1 AS d
         JOIN "MySchema"."Region" AS i ON i."Name" = d."Region"
    LEFT JOIN "MySchema"."Location" AS z ON z."Name" = d."Location" AND z."Region" = i."Id"
       ) AS s
   ON s."ObjectId" = t."ObjectId"
 WHEN NOT MATCHED BY TARGET 
    THEN INSERT ("ObjectId", "Name", "LocationId", "Region") VALUES (s."ObjectId", s."PointName", s."LocationId", s."Region")
 WHEN MATCHED 
    THEN UPDATE 
     SET "Name" = s."PointName"
       , "LocationId" = s."LocationId"
       , "Region" = s."Region"
OUTPUT $action, inserted.*, deleted.*;

Si elimino la OUTPUTcláusula, no se produce el error. Además, si elimino la deletedreferencia, no se produce el error. Así que miré los documentos de MSDN para la OUTPUTcláusula que dice:

DELETED no se puede usar con la cláusula OUTPUT en la instrucción INSERT.

Lo que tiene sentido para mí, sin embargo, el punto MERGEes que quizás no lo sepas de antemano.

Además, el siguiente script funciona perfectamente bien independientemente de la acción que se realice:

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
-- 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 

Además, tengo otras consultas que usan la OUTPUTmisma manera que la que arroja un error y funcionan perfectamente bien: la única diferencia entre ellas son las tablas que participan en el MERGE.

Esto nos está causando problemas importantes en la producción. He reproducido este error en SQL2014 y SQL2016 en VM y física con 128 GB de RAM, 12 núcleos de 2,2 GHz, Windows Server 2012 R2.

El plan de ejecución estimado generado a partir de la consulta se puede encontrar aquí:

Plan de ejecución estimado


1
¿Puede la consulta generar un plan estimado? (Además, esto no sorprenderá a mucha gente, pero recomiendo la antigua metodología upsert de todos modos : por ejemplo , la tuya MERGEno es así HOLDLOCK, por lo que no es inmune a las condiciones de carrera, y aún hay otros errores que considerar incluso después de resolver - o el informe -. lo que está causando este problema)
Aaron Bertrand

1
Da un volcado de pila con una infracción de acceso. Por lo que puedo ver al desenrollar la pila aquí i.stack.imgur.com/f9aWa.png Debería plantear esto con Microsoft PSS si esto le está causando problemas importantes. Específicamente parece ser deleted.ObjectIdque está causando el problema. OUTPUT $action, inserted.*, deleted.Name, deleted.LocationId, deleted.Regionfunciona bien.
Martin Smith

1
Concurre con Martin. Mientras tanto, vea si puede evitar el problema al no usar el MySchema.PointTabletipo, y simplemente usando una VALUES()cláusula desnuda , o #temp table, o table variable, dentro de USING. Podría ayudar a aislar los factores contribuyentes.
Aaron Bertrand

Gracias por su ayuda chicos, intenté usar una tabla temporal y ocurrió el mismo error. Lo plantearé con soporte de producto; mientras tanto, reescribí la consulta para que no use la combinación y así podamos seguir ejecutando productos.
Mr.Brownstone

Respuestas:


20

Esto es un error

Está relacionado con MERGEoptimizaciones específicas para rellenar huecos que se utilizan para evitar la protección explícita de Halloween y eliminar una unión, y cómo interactúan con otras características del plan de actualización.

Hay detalles sobre esas optimizaciones en mi artículo, The Halloween Problem - Part 3 .

El sorteo es la inserción seguida de una combinación en la misma tabla :

Fragmento de plan

Soluciones alternativas

Hay varias formas de vencer esta optimización, y así evitar el error.

  1. Use una marca de rastreo indocumentada para forzar la protección explícita de Halloween:

    OPTION (QUERYTRACEON 8692);
  2. Cambie la ONcláusula a:

    ON s."ObjectId" = t."ObjectId" + 0
  3. Cambie el tipo de tabla PointTablepara reemplazar la clave primaria con:

    ObjectID bigint NULL UNIQUE CLUSTERED CHECK (ObjectId IS NOT NULL)

    La CHECKparte de restricción es opcional, incluida para preservar la propiedad original de rechazo nulo de una clave primaria.

El procesamiento de consultas de actualización 'simple' (comprobaciones de clave externa, mantenimiento de índice único y columnas de salida) es lo suficientemente complejo para empezar. El uso MERGEagrega varias capas adicionales a eso. Combine eso con la optimización específica mencionada anteriormente, y tiene una excelente manera de encontrar errores de borde de caso como este.

Uno más para agregar a la larga línea de errores con los que se ha informado MERGE.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.