Warum wird durch diese MERGE-Anweisung die Sitzung beendet?

23

Ich habe die folgende MERGEAnweisung, die gegen die Datenbank ausgegeben wird:

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.*;

Dies führt jedoch dazu, dass die Sitzung mit dem folgenden Fehler beendet wird:

Meldung 0, Ebene 11, Status 0, Zeile 67 Beim aktuellen Befehl ist ein schwerwiegender Fehler aufgetreten. Die Ergebnisse sollten, falls vorhanden, verworfen werden.

Meldung 0, Ebene 20, Status 0, Zeile 67 Beim aktuellen Befehl ist ein schwerwiegender Fehler aufgetreten. Die Ergebnisse sollten, falls vorhanden, verworfen werden.

Ich habe ein kurzes Testskript zusammengestellt, das den Fehler erzeugt:

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.*;

Wenn ich die OUTPUTKlausel entferne , tritt der Fehler nicht auf. Auch wenn ich den deletedVerweis entferne, tritt der Fehler nicht auf. Also habe ich in den MSDN-Dokumenten nach der OUTPUTKlausel gesucht, die besagt :

DELETED kann nicht mit der OUTPUT-Klausel in der INSERT-Anweisung verwendet werden.

Was für mich jedoch Sinn macht, MERGEist, dass Sie es möglicherweise nicht im Voraus wissen.

Darüber hinaus funktioniert das folgende Skript unabhängig von der ausgeführten Aktion einwandfrei:

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 

Ich habe auch andere Abfragen, die OUTPUTdie gleiche Methode verwenden wie die, die einen Fehler auslöst, und die einwandfrei funktionieren - der einzige Unterschied zwischen ihnen besteht in den Tabellen, die an der teilnehmen MERGE.

Dies führt bei uns zu großen Produktionsproblemen. Ich habe diesen Fehler in SQL2014 und SQL2016 sowohl auf virtuellen als auch auf physischen Computern mit 128 GB RAM, 12 x 2,2 GHz-Kernen und Windows Server 2012 R2 reproduziert.

Den geschätzten Ausführungsplan, der aus der Abfrage generiert wurde, finden Sie hier:

Geschätzter Ausführungsplan

Herr Brownstone
quelle
1
Kann die Abfrage einen geschätzten Plan generieren? (Auch dies wird nicht viele Menschen schockieren, aber ich die alte Upsert Methodik empfehlen ohnehin - Ihr MERGEnicht über HOLDLOCK, für ein, so dass es nicht immun gegen Rennbedingungen ist, und es gibt noch andere Fehler auch zu prüfen , Nachdem Sie das Problem behoben oder gemeldet haben (was auch immer das Problem verursacht)
Aaron Bertrand
1
Es gibt einen Stapelspeicherauszug mit einer Zugriffsverletzung. Soweit ich beim Abwickeln des Stapels hier sehen kann, sollten Sie dies mit Microsoft PSS besprechen , wenn dies für Sie schwerwiegende Probleme verursacht. Insbesondere scheint dies deleted.ObjectIddas Problem zu verursachen. OUTPUT $action, inserted.*, deleted.Name, deleted.LocationId, deleted.Regionfunktioniert gut.
Martin Smith
1
Stimme Martin zu. Überprüfen Sie in der Zwischenzeit, ob Sie das Problem vermeiden können, indem Sie den MySchema.PointTableTyp nicht verwenden und einfach eine Naked- VALUES()Klausel oder eine #temp-Tabelle oder eine Tabellenvariable innerhalb von verwenden USING. Könnte helfen, beitragende Faktoren zu isolieren.
Aaron Bertrand
Vielen Dank für Ihre Hilfe, ich habe versucht, eine temporäre Tabelle zu verwenden, und der gleiche Fehler ist aufgetreten. Ich werde es mit dem Produktsupport ansprechen - in der Zwischenzeit habe ich die Abfrage so umgeschrieben, dass keine Zusammenführung verwendet wird, damit das Produkt weiter ausgeführt werden kann.
Mr. Brownstone

Antworten:

20

Das ist ein Fehler.

Es bezieht sich auf bestimmte MERGEOptimierungen zum Füllen von Löchern, die verwendet werden, um expliziten Halloween-Schutz zu vermeiden und einen Join zu beseitigen, und wie diese mit anderen Funktionen des Aktualisierungsplans interagieren.

Details zu diesen Optimierungen finden Sie in meinem Artikel The Halloween Problem - Part 3 .

Das Werbegeschenk ist das Einfügen, gefolgt von einem Zusammenführen in derselben Tabelle :

Fragment planen

Problemumgehungen

Es gibt verschiedene Möglichkeiten, diese Optimierung zu umgehen und so den Fehler zu vermeiden.

  1. Verwenden Sie ein nicht dokumentiertes Ablaufverfolgungsflag, um den expliziten Halloween-Schutz zu erzwingen:

    OPTION (QUERYTRACEON 8692);
  2. Ändern Sie die ONKlausel in:

    ON s."ObjectId" = t."ObjectId" + 0
  3. Ändern Sie den Tabellentyp PointTable, um den Primärschlüssel durch Folgendes zu ersetzen:

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

    Der CHECKEinschränkungsteil ist optional und wird eingefügt, um die ursprüngliche Eigenschaft eines Primärschlüssels beizubehalten, bei der keine Zurückweisung erfolgt.

Die "einfache" Verarbeitung von Aktualisierungsabfragen (Fremdschlüsselprüfungen, eindeutige Indexpflege und Ausgabespalten) ist anfangs komplex genug. Durch Verwenden von werden dem MERGEmehrere zusätzliche Ebenen hinzugefügt. Kombinieren Sie dies mit der oben genannten speziellen Optimierung, und Sie haben eine großartige Möglichkeit, Fehler wie diese zu finden.

Eine weitere, um die lange Reihe von Fehlern zu ergänzen, die gemeldet wurden MERGE.

Paul White sagt GoFundMonica
quelle