Pourquoi cette instruction MERGE entraîne-t-elle la fin de la session?

23

J'ai la MERGEdéclaration ci-dessous qui est émise contre la base de données:

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

Toutefois, cela entraîne la fin de la session avec l'erreur suivante:

Msg 0, niveau 11, état 0, ligne 67 Une erreur grave s'est produite sur la commande actuelle. Le cas échéant, les résultats doivent être ignorés.

Msg 0, niveau 20, état 0, ligne 67 Une erreur grave s'est produite sur la commande actuelle. Le cas échéant, les résultats doivent être ignorés.

J'ai mis un court script de test ensemble qui produit l'erreur:

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 je supprime la OUTPUTclause, l'erreur ne se produit pas. De plus, si je supprime la deletedréférence, l'erreur ne se produit pas. J'ai donc regardé les documents MSDN pour la OUTPUTclause qui stipule:

DELETED ne peut pas être utilisé avec la clause OUTPUT dans l'instruction INSERT.

Ce qui est logique pour moi, mais le fait MERGEest que vous ne le savez peut-être pas à l'avance.

De plus, le script ci-dessous fonctionne parfaitement bien quelle que soit l'action entreprise:

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 

De plus, j'ai d'autres requêtes qui utilisent la OUTPUTmême manière que celle qui génère une erreur et elles fonctionnent parfaitement bien - la seule différence entre elles est les tables qui participent à la MERGE.

Cela nous cause de gros problèmes de production. J'ai reproduit cette erreur dans SQL2014 et SQL2016 sur la machine virtuelle et physique avec 128 Go de RAM, 12 cœurs 2,2 GHz, Windows Server 2012 R2.

Le plan d'exécution estimé généré à partir de la requête peut être trouvé ici:

Plan d'exécution estimé

Mr.Brownstone
la source
1
La requête peut-elle générer un plan estimé? (En outre, cela ne choquera pas beaucoup de gens, mais je recommande de toute façon l'ancienne méthode upsert - la vôtre MERGEn'en a pas HOLDLOCK, d'une part, elle n'est donc pas à l'abri des conditions de concurrence , et il y a encore d'autres bogues à considérer même après avoir résolu - ou signalé - tout ce qui cause ce problème.)
Aaron Bertrand
1
Il donne un vidage de pile avec une violation d'accès. Autant que je puisse voir lors du déroulement de la pile ici i.stack.imgur.com/f9aWa.png Vous devez le signaler à Microsoft PSS si cela vous pose des problèmes majeurs. Plus précisément, il semble deleted.ObjectIdque ce soit la cause du problème. OUTPUT $action, inserted.*, deleted.Name, deleted.LocationId, deleted.Regionfonctionne bien.
Martin Smith
1
Soyez d'accord avec Martin. En attendant, voyez si vous pouvez éviter le problème en n'utilisant pas le MySchema.PointTabletype et en utilisant simplement une VALUES()clause nue , une table #temp ou une variable de table à l'intérieur de USING. Pourrait aider à isoler les facteurs contributifs.
Aaron Bertrand
Merci pour votre aide, j'ai essayé d'utiliser une table temporaire et la même erreur s'est produite. Je vais le soulever avec le support produit - en attendant, j'ai réécrit la requête pour ne pas utiliser la fusion afin que nous puissions continuer à produire.
Mr.Brownstone

Réponses:

20

Ceci est un bug.

Elle est liée aux MERGEoptimisations spécifiques de remplissage de trous utilisées pour éviter la protection explicite d'Halloween et pour éliminer une jointure, et comment celles-ci interagissent avec d'autres fonctionnalités du plan de mise à jour.

Il y a des détails sur ces optimisations dans mon article, Le problème d'Halloween - Partie 3 .

Le cadeau est l'insertion suivie d'une fusion sur la même table :

Fragment de plan

Solutions de contournement

Il existe plusieurs façons de contourner cette optimisation, et donc d'éviter le bogue.

  1. Utilisez un indicateur de trace non documenté pour forcer une protection Halloween explicite:

    OPTION (QUERYTRACEON 8692);
  2. Remplacez la ONclause par:

    ON s."ObjectId" = t."ObjectId" + 0
  3. Modifiez le type de table PointTablepour remplacer la clé primaire par:

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

    La CHECKpartie contrainte est facultative, incluse pour préserver la propriété de rejet nul d'origine d'une clé primaire.

Le traitement «simple» des requêtes de mise à jour (vérifications de clés étrangères, maintenance d'index unique et colonnes de sortie) est suffisamment complexe pour commencer. L'utilisation MERGEajoute plusieurs couches supplémentaires à cela. Combinez cela avec l'optimisation spécifique mentionnée ci-dessus, et vous avez un excellent moyen de rencontrer des bogues de bord comme celui-ci.

Un de plus à ajouter à la longue liste de bogues signalés MERGE.

Paul White dit GoFundMonica
la source