Por que essa instrução MERGE faz com que a sessão seja encerrada?


23

Eu tenho a MERGEinstruçã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 OUTPUTcláusula, o erro não ocorrerá. Além disso, se eu remover a deletedreferência, o erro não ocorrerá. Então, olhei os documentos do MSDN para a OUTPUTclá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 OUTPUTmesma 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:

Plano de Execução Estimado


1
A consulta pode gerar um plano estimado? (Além disso, isso não vai chocar muitas pessoas, mas eu recomendo a antiga metodologia upsert - de qualquer forma, a sua MERGEnão possui HOLDLOCK, 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.)
Aaron Bertrand

1
Ele fornece um despejo de pilha com uma violação de acesso. Até onde eu posso ver ao desenrolar a pilha aqui i.stack.imgur.com/f9aWa.png Você deve aumentar isso com o Microsoft PSS se isso estiver causando grandes problemas para você. Especificamente, parece deleted.ObjectIdque está causando o problema. OUTPUT $action, inserted.*, deleted.Name, deleted.LocationId, deleted.Regionfunciona bem.
Martin Smith

1
Concordo com Martin. Enquanto isso, verifique se você pode evitar o problema não usando o MySchema.PointTabletipo e apenas usando uma VALUES()cláusula simples , ou #temp table ou variável de tabela, dentro do USING. Pode ajudar a isolar fatores contribuintes.
Aaron Bertrand

Obrigado pela ajuda pessoal, tentei usar uma tabela temporária e ocorreu o mesmo erro. Eu o aumentarei com o suporte ao produto - enquanto isso, reescrevi a consulta para não usar a mesclagem, para que pudéssemos manter o produto em execução.
Mr.Brownstone

Respostas:


20

Isso é um bug.

Ele está relacionado às MERGEotimizaçõ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 :

Fragmento de plano

Soluções alternativas

Existem várias maneiras de derrotar essa otimização e, assim, evitar o bug.

  1. Use um sinalizador de rastreamento não documentado para forçar a proteção explícita do Halloween:

    OPTION (QUERYTRACEON 8692);
  2. Mude a ONcláusula para:

    ON s."ObjectId" = t."ObjectId" + 0
  3. Altere o tipo de tabela PointTablepara substituir a chave primária por:

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

    A CHECKparte 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 MERGEadiciona 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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.