Почему этот оператор MERGE вызывает прерывание сеанса?

23

У меня есть следующее MERGEзаявление, которое выдается против базы данных:

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

Однако это приводит к завершению сеанса со следующей ошибкой:

Сообщение 0, уровень 11, состояние 0, строка 67 Произошла серьезная ошибка в текущей команде. Результаты, если таковые имеются, должны быть отброшены.

Сообщение 0, уровень 20, состояние 0, строка 67 Произошла серьезная ошибка в текущей команде. Результаты, если таковые имеются, должны быть отброшены.

Я собрал короткий тестовый скрипт, который выдает ошибку:

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

Если я удалю OUTPUTпредложение, то ошибка не произойдет. Кроме того, если я удаляю deletedссылку, то ошибка не возникает. Поэтому я посмотрел на документы MSDN для OUTPUTпредложения, в котором говорится:

DELETED нельзя использовать с предложением OUTPUT в операторе INSERT.

Что имеет смысл для меня, однако весь смысл в MERGEтом, что вы можете не знать заранее.

Кроме того, приведенный ниже скрипт работает отлично, независимо от того, какое действие предпринято:

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 

Кроме того, у меня есть другие запросы, которые используют тот OUTPUTже способ, что и тот, который выдает ошибку, и они работают отлично - единственное различие между ними - таблицы, которые принимают участие в MERGE.

Это вызывает у нас серьезные проблемы в производстве. Я воспроизвел эту ошибку в SQL2014 и SQL2016 как на виртуальной машине, так и на физической с 128 ГБ ОЗУ, 12 x 2,2 ГГц ядрами, Windows Server 2012 R2.

Примерный план выполнения, сгенерированный по запросу, можно найти здесь:

Предполагаемый план выполнения

Mr.Brownstone
источник
1
Может ли запрос сгенерировать примерный план? (Кроме того, это не будет шокировать многих, но я все равно рекомендую старую методологию upsert - MERGEу вас HOLDLOCK, к примеру, нет, поэтому она не застрахована от условий гонки, но есть и другие ошибки, которые следует учитывать даже после того, как вы решите - или сообщите - что бы ни вызывало эту проблему.)
Аарон Бертран
1
Выдает дамп стека с нарушением прав доступа. Насколько я вижу при размотке стека здесь i.stack.imgur.com/f9aWa.png Вы должны поднять это с Microsoft PSS, если это вызывает у вас серьезные проблемы. В частности, кажется, deleted.ObjectIdчто это является причиной проблемы. OUTPUT $action, inserted.*, deleted.Name, deleted.LocationId, deleted.Regionработает отлично.
Мартин Смит
1
Согласен с Мартином. Тем временем, посмотрите, сможете ли вы избежать этой проблемы, не используя MySchema.PointTableтип, а просто используя голое VALUES()предложение, или таблицу #temp, или переменную таблицы, внутри USING. Может помочь выделить способствующие факторы.
Аарон Бертран
Спасибо за вашу помощь, ребята, я попытался использовать временную таблицу, и произошла та же ошибка. Я поднимет его с поддержкой продукта - тем временем я переписал запрос, чтобы не использовать слияние, чтобы мы могли продолжать работу Prod.
Браунстоун

Ответы:

20

Это ошибка.

Это связано с MERGEконкретными оптимизациями заполнения отверстий, которые используются для того, чтобы избежать явной защиты от Хэллоуина и устранения объединения, а также с тем, как они взаимодействуют с другими функциями плана обновления.

Подробности об этих оптимизациях есть в моей статье «Проблема Хэллоуина - часть 3» .

Дешевая распродажа - Вставка, сопровождаемая Объединением на той же самой таблице :

Фрагмент плана

обходные

Есть несколько способов победить эту оптимизацию и избежать ошибки.

  1. Используйте недокументированный флаг трассировки, чтобы активировать явную защиту Хэллоуина:

    OPTION (QUERYTRACEON 8692);
  2. Изменить ONпредложение на:

    ON s."ObjectId" = t."ObjectId" + 0
  3. Измените тип таблицы, PointTableчтобы заменить первичный ключ:

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

    Часть CHECKограничения является необязательной, она включена для сохранения исходного свойства отклонения нулевого значения первичного ключа.

«Простая» обработка запроса на обновление (проверка внешнего ключа, ведение уникального индекса и столбцы вывода) достаточно сложна для начала. Использование MERGEдобавляет к этому несколько дополнительных слоев. Объедините это с определенной оптимизацией, упомянутой выше, и у вас есть отличный способ встретить подобные ошибки.

Еще один, чтобы добавить к длинной строке ошибок, о которых сообщалось MERGE.

Пол Уайт говорит, что GoFundMonica
источник