Ist es sicher, sich auf die Reihenfolge der OUTPUT-Klausel eines INSERT zu verlassen?

19

Angesichts dieser Tabelle:

CREATE TABLE dbo.Target (
   TargetId int identity(1, 1) NOT NULL,
   Color varchar(20) NOT NULL,
   Action varchar(10) NOT NULL, -- of course this should be normalized
   Code int NOT NULL,
   CONSTRAINT PK_Target PRIMARY KEY CLUSTERED (TargetId)
);

In zwei leicht unterschiedlichen Szenarien möchte ich Zeilen einfügen und die Werte aus der Identitätsspalte zurückgeben.

Szenario 1

INSERT dbo.Target (Color, Action, Code)
OUTPUT inserted.TargetId
SELECT t.Color, t.Action, t.Code
FROM
   (VALUES
      ('Blue', 'New', 1234),
      ('Blue', 'Cancel', 4567),
      ('Red', 'New', 5678)
   ) t (Color, Action, Code)
;

Szenario 2

CREATE TABLE #Target (
   Color varchar(20) NOT NULL,
   Action varchar(10) NOT NULL,
   Code int NOT NULL,
   PRIMARY KEY CLUSTERED (Color, Action)
);

-- Bulk insert to the table the same three rows as above by any means

INSERT dbo.Target (Color, Action, Code)
OUTPUT inserted.TargetId
SELECT t.Color, t.Action, t.Code
FROM #Target
;

Frage

Kann ich mich darauf verlassen, dass die zurückgegebenen Identitätswerte aus der dbo.TargetTabelleneinfügung in der Reihenfolge zurückgegeben werden, in der sie in der 1) VALUESKlausel und 2) #TargetTabelle vorhanden waren, damit ich sie anhand ihrer Position im Ausgabe-Rowset wieder mit der ursprünglichen Eingabe korrelieren kann?

Als Referenz

Hier ist ein kleinerer C # -Code, der zeigt, was in der Anwendung geschieht (Szenario 1, das bald zur Verwendung konvertiert wird SqlBulkCopy):

public IReadOnlyCollection<Target> InsertTargets(IEnumerable<Target> targets) {
   var targetList = targets.ToList();
   const string insertSql = @"
      INSERT dbo.Target (
         CoreItemId,
         TargetDateTimeUtc,
         TargetTypeId,
      )
      OUTPUT
         Inserted.TargetId
      SELECT
         input.CoreItemId,
         input.TargetDateTimeUtc,
         input.TargetTypeId,
      FROM
         (VALUES
            {0}
         ) input (
            CoreItemId,
            TargetDateTimeUtc,
            TargetTypeId
         );";
   var results = Connection.Query<DbTargetInsertResult>(
      string.Format(
         insertSql,
         string.Join(
            ", ",
            targetList
               .Select(target => $@"({target.CoreItemId
                  }, '{target.TargetDateTimeUtc:yyyy-MM-ddTHH:mm:ss.fff
                  }', {(byte) target.TargetType
                  })";
               )
         )
      )
      .ToList();
   return targetList
      .Zip( // The correlation that relies on the order of the two inputs being the same
         results,
         (inputTarget, insertResult) => new Target(
            insertResult.TargetId, // with the new TargetId to replace null.
            inputTarget.TargetDateTimeUtc,
            inputTarget.CoreItemId,
            inputTarget.TargetType
         )
      )
      .ToList()
      .AsReadOnly();
}
ErikE
quelle

Antworten:

22

Kann ich mich darauf verlassen, dass die zurückgegebenen Identitätswerte aus der dbo.Target-Tabelleneinfügung in der Reihenfolge zurückgegeben werden, in der sie in der 1) VALUES-Klausel und 2) #Target-Tabelle vorhanden sind, damit ich sie anhand ihrer Position im zurückgegebenen Rowset korrelieren kann? zur ursprünglichen Eingabe?

Nein, Sie können sich nicht darauf verlassen, dass eine Garantie ohne eine tatsächlich dokumentierte Garantie gegeben ist. In der Dokumentation wird ausdrücklich darauf hingewiesen, dass eine solche Garantie nicht besteht.

SQL Server garantiert nicht die Reihenfolge, in der Zeilen von DML-Anweisungen unter Verwendung der OUTPUT-Klausel verarbeitet und zurückgegeben werden. Es ist Aufgabe der Anwendung, eine geeignete WHERE-Klausel aufzunehmen, die die gewünschte Semantik garantiert, oder zu verstehen, dass es keine garantierte Reihenfolge gibt, wenn sich mehrere Zeilen für die DML-Operation qualifizieren können.

Dies würde auf vielen undokumentierten Annahmen beruhen

  1. Die Reihenfolge, in der die Zeilen vom konstanten Scan ausgegeben werden, entspricht der Reihenfolge der Werte-Klausel (ich habe noch nie gesehen, dass sie sich unterscheiden, aber AFAIK garantiert dies nicht).
  2. Die Reihenfolge, in der die Zeilen eingefügt werden, entspricht der Reihenfolge, in der sie beim konstanten Scan ausgegeben werden (dies ist definitiv nicht immer der Fall).
  3. Bei Verwendung eines Ausführungsplans "wide" (pro Index) werden die Werte aus der Ausgabeklausel vom Clustered-Index-Aktualisierungsoperator und nicht von den Werten der Sekundärindizes abgerufen.
  4. Dass die Reihenfolge danach garantiert erhalten bleibt - z. B. beim Packen von Zeilen für die Übertragung über das Netzwerk .
  5. Dies gilt auch dann, wenn die Reihenfolge jetzt als vorhersehbar erscheint, wenn Änderungen an der Implementierung von Features wie dem parallelen Einfügen die Reihenfolge in Zukunft nicht ändern (derzeit gelten parallele Pläne, wenn die OUTPUT-Klausel in der INSERT… SELECT-Anweisung angegeben ist, um Ergebnisse an den Client zurückzugeben.) im Allgemeinen deaktiviert, einschließlich INSERTs )

(Color, Action)Wenn Sie der VALUESKlausel 600 Zeilen hinzufügen, kann ein Beispiel für einen Fehler bei Punkt zwei (unter der Annahme, dass der Cluster-PK von ) angezeigt werden. Dann hat der Plan einen Sortieroperator vor dem Einfügen, wodurch Ihre ursprüngliche Reihenfolge in der VALUESKlausel verloren geht.

Es gibt jedoch einen dokumentierten Weg, um Ihr Ziel zu erreichen, und dieser besteht darin, der Quelle eine Nummerierung hinzuzufügen und MERGEstattdessen zu verwendenINSERT

MERGE dbo.Target
USING (VALUES (1, 'Blue', 'New', 1234),
              (2, 'Blue', 'Cancel', 4567),
              (3, 'Red', 'New', 5678) ) t (SourceId, Color, Action, Code)
ON 1 = 0
WHEN NOT MATCHED THEN
  INSERT (Color,
          Action,
          Code)
  VALUES (Color,
          Action,
          Code)
OUTPUT t.SourceId,
       inserted.TargetId; 

Bildbeschreibung hier eingeben

@ein Pferd ohne Name

Ist die Zusammenführung wirklich notwendig? Könntest du nicht einfach eine machen insert into ... select ... from (values (..)) t (...) order by sourceid?

Ja du könntest. Bestellgarantien in SQL Server… besagen dies

INSERT-Abfragen, die SELECT mit ORDER BY zum Auffüllen von Zeilen verwenden, gewährleisten, dass die Identitätswerte berechnet werden, nicht jedoch die Reihenfolge, in der die Zeilen eingefügt werden

Du könntest es also gebrauchen

INSERT dbo.Target (Color, Action, Code)
OUTPUT inserted.TargetId
SELECT t.Color, t.Action, t.Code
FROM
(VALUES (1, 'Blue', 'New', 1234),
        (2, 'Blue', 'Cancel', 4567),
        (3, 'Red', 'New', 5678) ) t (SourceId, Color, Action, Code)
ORDER BY t.SourceId

Bildbeschreibung hier eingeben

Dies würde sicherstellen, dass die Identitätswerte in der Reihenfolge zugewiesen werden, t.SourceIdaber nicht in einer bestimmten Reihenfolge ausgegeben werden oder dass die zugewiesenen Identitätsspaltenwerte keine Lücken aufweisen (z. B. wenn versucht wird, gleichzeitig einzufügen).

Martin Smith
quelle
2
Das letzte bisschen über das Potenzial für Lücken und die Ausgabe in einer bestimmten Reihenfolge macht die Dinge ein bisschen interessanter für den Versuch, wieder mit der Eingabe zu korrelieren. Ich nehme an, eine Bestellung in der Anwendung würde die Arbeit erledigen, aber es scheint sicherer und klarer, nur die zu verwenden MERGE.
ErikE
Verwenden Sie die OUTPUT ... INTO [#temp]Syntax, SELECT ... FROM [#temp] ORDER BYum die Ausgabereihenfolge zu gewährleisten.
Max Vernon,
TL; DR-Version: Bei SQL Server und meiner Meinung nach bei SQL-Implementierungen im Allgemeinen kann die Reihenfolge nicht garantiert werden, es sei denn, es gibt eine ORDER BY-Klausel.
Nateirvin
Re. Lücken: Würde das Einschließen der InsertErklärung in eine TransactionLücke verhindern?
Tom