Es würde kein Problem geben, wenn die Tabellenvariable immer nur einen Wert enthalten würde. Bei mehreren Zeilen gibt es eine neue Möglichkeit für Deadlocks. Angenommen, zwei gleichzeitige Prozesse (A & B) werden mit Tabellenvariablen ausgeführt, die (1, 2) und (2, 1) für dieselbe Firma enthalten.
Prozess A liest das Ziel, findet keine Zeile und fügt den Wert '1' ein. Es enthält eine exklusive Zeilensperre für den Wert '1'. Prozess B liest das Ziel, findet keine Zeile und fügt den Wert '2' ein. Es enthält eine exklusive Zeilensperre für den Wert '2'.
Jetzt muss Prozess A Zeile 2 verarbeiten, und Prozess B muss Zeile 1 verarbeiten. Keiner der Prozesse kann Fortschritte erzielen, da eine Sperre erforderlich ist, die nicht mit der exklusiven Sperre des anderen Prozesses kompatibel ist.
Um Deadlocks mit mehreren Zeilen zu vermeiden, müssen die Zeilen jedes Mal in derselben Reihenfolge verarbeitet (und auf Tabellen zugegriffen) werden . Die in der Frage gezeigte Tabellenvariable im Ausführungsplan ist ein Heap, daher haben die Zeilen keine intrinsische Reihenfolge (sie werden mit hoher Wahrscheinlichkeit in der Reihenfolge des Einfügens gelesen, obwohl dies nicht garantiert ist):
Das Fehlen einer konsistenten Zeilenverarbeitungsreihenfolge führt direkt zur Deadlock-Möglichkeit. Eine zweite Überlegung ist, dass das Fehlen einer Schlüssel-Eindeutigkeitsgarantie bedeutet, dass eine Tischspule erforderlich ist, um einen korrekten Halloween-Schutz zu gewährleisten. Der Spool ist ein eifriger Spool, dh alle Zeilen werden in eine Tempdb-Worktable geschrieben, bevor sie zurückgelesen und für den Einfügeoperator wiedergegeben werden.
Neudefinieren der TYPE
Variablen der Tabelle, um ein Cluster einzuschließen PRIMARY KEY
:
DROP TYPE dbo.CoUserData;
CREATE TYPE dbo.CoUserData
AS TABLE
(
MyKey integer NOT NULL PRIMARY KEY CLUSTERED,
MyValue integer NOT NULL
);
Der Ausführungsplan zeigt jetzt einen Scan des Clustered-Index und die Eindeutigkeitsgarantie bedeutet, dass das Optimierungsprogramm den Tabellenspool sicher entfernen kann:
In Tests mit 5000 Iterationen der MERGE
Anweisung in 128 Threads traten keine Deadlocks mit der gruppierten Tabellenvariablen auf. Ich möchte betonen, dass dies nur auf der Grundlage von Beobachtungen geschieht. Die gruppierte Tabellenvariable könnte ihre Zeilen auch ( technisch ) in einer Vielzahl von Reihenfolgen produzieren, aber die Chancen auf eine konsistente Reihenfolge sind sehr stark erhöht. Das beobachtete Verhalten muss natürlich für jedes neue kumulative Update, Service Pack oder jede neue Version von SQL Server erneut getestet werden.
Falls die Definition der Tabellenvariablen nicht geändert werden kann, gibt es eine andere Alternative:
MERGE dbo.CompanyUser AS R
USING
(SELECT DISTINCT MyKey, MyValue FROM @DataTable) AS NewData ON
R.CompanyId = @CompanyID
AND R.UserID = @UserID
AND R.MyKey = NewData.MyKey
WHEN NOT MATCHED THEN
INSERT
(CompanyID, UserID, MyKey, MyValue)
VALUES
(@CompanyID, @UserID, NewData.MyKey, NewData.MyValue)
OPTION (ORDER GROUP);
Dadurch wird auch die Beseitigung der Spool (und der Konsistenz der Zeilenreihenfolge) auf Kosten der Einführung einer expliziten Sortierung erreicht:
Dieser Plan erzeugte auch keine Deadlocks unter Verwendung des gleichen Tests. Reproduktionsskript unten:
CREATE TYPE dbo.CoUserData
AS TABLE
(
MyKey integer NOT NULL /* PRIMARY KEY */,
MyValue integer NOT NULL
);
GO
CREATE TABLE dbo.Company
(
CompanyID integer NOT NULL
CONSTRAINT PK_Company
PRIMARY KEY (CompanyID)
);
GO
CREATE TABLE dbo.CompanyUser
(
CompanyID integer NOT NULL,
UserID integer NOT NULL,
MyKey integer NOT NULL,
MyValue integer NOT NULL
CONSTRAINT PK_CompanyUser
PRIMARY KEY CLUSTERED
(CompanyID, UserID, MyKey),
FOREIGN KEY (CompanyID)
REFERENCES dbo.Company (CompanyID),
);
GO
CREATE NONCLUSTERED INDEX nc1
ON dbo.CompanyUser (CompanyID, UserID);
GO
INSERT dbo.Company (CompanyID) VALUES (1);
GO
DECLARE
@DataTable AS dbo.CoUserData,
@CompanyID integer = 1,
@UserID integer = 1;
INSERT @DataTable
SELECT TOP (10)
V.MyKey,
V.MyValue
FROM
(
VALUES
(1, 1),
(2, 2),
(3, 3),
(4, 4),
(5, 5),
(6, 6),
(7, 7),
(8, 8),
(9, 9)
) AS V (MyKey, MyValue)
ORDER BY NEWID();
BEGIN TRANSACTION;
-- Test MERGE statement here
ROLLBACK TRANSACTION;