Чому ця заява 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 на VM та Physical із 128 ГБ оперативної пам’яті, ядрами 12 x 2.2 ГГц, Windows Server 2012 R2.

Орієнтовний план виконання, сформований із запиту, можна знайти тут:

Розрахунковий план виконання


1
Чи може запит генерувати орієнтовний план? (Крім того, це не приголомшить багатьох людей, але я все-таки рекомендую стару методологію прискорення - у вас такої 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. Може допомогти виділити сприяючі фактори.
Аарон Бертран

Дякую за вашу допомогу, хлопці, я спробував скористатися таблицею темпів, і сталася та сама помилка. Я підніму його з підтримкою продуктів - тим часом я переписав запит, щоб не використовувати злиття, щоб ми могли продовжувати працювати.
Містер Броунстоун,

Відповіді:


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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.