Eu tenho a MERGE
instrução abaixo que é emitida no banco de dados:
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.*;
No entanto, isso faz com que a sessão seja encerrada com o seguinte erro:
Msg 0, Nível 11, Estado 0, Linha 67 Ocorreu um erro grave no comando atual. Os resultados, se existirem, deveriam ser descartados.
Msg 0, Nível 20, Estado 0, Linha 67 Ocorreu um erro grave no comando atual. Os resultados, se existirem, deveriam ser descartados.
Eu montei um script de teste curto que produz o erro:
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.*;
Se eu remover a OUTPUT
cláusula, o erro não ocorrerá. Além disso, se eu remover a deleted
referência, o erro não ocorrerá. Então, olhei os documentos do MSDN para a OUTPUT
cláusula que afirma:
DELETED não pode ser usado com a cláusula OUTPUT na instrução INSERT.
O que faz sentido para mim, no entanto, o ponto principal MERGE
é que você talvez não saiba com antecedência.
Além disso, o script abaixo funciona perfeitamente, independentemente da ação executada:
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
Além disso, tenho outras consultas que usam da OUTPUT
mesma maneira que a que está lançando um erro e funcionam perfeitamente bem - a única diferença entre elas são as tabelas que participam da MERGE
.
Isso está causando grandes problemas de produção para nós. Reproduzi esse erro no SQL2014 e SQL2016 na VM e no Physical com 128GB de RAM, núcleos de 12 x 2.2GHZ, Windows Server 2012 R2.
O plano de execução estimado gerado a partir da consulta pode ser encontrado aqui:
fonte
MERGE
não possuiHOLDLOCK
, por isso não é imune a condições de corrida e ainda existem outros erros a serem considerados. depois de resolver - ou relatar - o que está causando esse problema.)deleted.ObjectId
que está causando o problema.OUTPUT $action, inserted.*, deleted.Name, deleted.LocationId, deleted.Region
funciona bem.MySchema.PointTable
tipo e apenas usando umaVALUES()
cláusula simples , ou #temp table ou variável de tabela, dentro doUSING
. Pode ajudar a isolar fatores contribuintes.Respostas:
Isso é um bug.
Ele está relacionado às
MERGE
otimizações específicas de preenchimento de furos usadas para evitar a proteção explícita do Halloween e para eliminar uma junção, e como elas interagem com outros recursos do plano de atualização.Há detalhes sobre essas otimizações no meu artigo, The Halloween Problem - Part 3 .
A oferta é a inserção seguida por uma mesclagem na mesma tabela :
Soluções alternativas
Existem várias maneiras de derrotar essa otimização e, assim, evitar o bug.
Use um sinalizador de rastreamento não documentado para forçar a proteção explícita do Halloween:
Mude a
ON
cláusula para:Altere o tipo de tabela
PointTable
para substituir a chave primária por:A
CHECK
parte de restrição é opcional, incluída para preservar a propriedade original de rejeição nula de uma chave primária.O processamento de consultas de atualização 'simples' (verificações de chave estrangeira, manutenção de índice exclusiva e colunas de saída) é suficientemente complexo para começar. Usar
MERGE
adiciona várias camadas adicionais a isso. Combine isso com a otimização específica mencionada acima e você terá uma ótima maneira de encontrar erros de borda como este.Mais um a acrescentar à longa linha de erros relatados
MERGE
.fonte