Czy bezpiecznie jest polegać na kolejności klauzuli WYDAJNOŚCI INSERT?

19

Biorąc pod uwagę tę tabelę:

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)
);

W dwóch nieco odmiennych scenariuszach chcę wstawić wiersze i zwrócić wartości z kolumny tożsamości.

Scenariusz 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)
;

Scenariusz 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
;

Pytanie

Czy mogę polegać na zwracanych wartościach tożsamości z dbo.Targetwstawionej tabeli, które mają być zwrócone, w kolejności, w jakiej istniały w 1) VALUESklauzuli i 2) #Targettabeli, aby móc skorelować je według ich pozycji w wyjściowym zestawie wierszy z powrotem z oryginalnym wejściem?

Na przykład

Oto trochę skrócony kod C #, który pokazuje, co dzieje się w aplikacji (scenariusz 1, który wkrótce zostanie przekonwertowany do użycia 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
źródło

Odpowiedzi:

22

Czy mogę polegać na zwracanych wartościach tożsamości z dbo. Wstawianie tabeli celów do zwrotu w kolejności, w jakiej istniały w klauzuli 1) VALUES i 2) #Target table, dzięki czemu mogę skorelować je według ich pozycji w wyjściowym zestawie wierszy do oryginalnego wejścia?

Nie, nie można polegać na niczym, co można zagwarantować bez faktycznej udokumentowanej gwarancji. Dokumentacja wyraźnie stwierdza, że nie ma takiej gwarancji.

SQL Server nie gwarantuje kolejności przetwarzania i zwracania wierszy przez instrukcje DML za pomocą klauzuli OUTPUT. Do aplikacji należy dołączenie odpowiedniej klauzuli WHERE, która może zagwarantować pożądaną semantykę lub zrozumieć, że gdy wiele wierszy może kwalifikować się do operacji DML, nie ma gwarantowanej kolejności.

Opierałoby się to na wielu nieudokumentowanych założeniach

  1. Kolejność, w jakiej wiersze są wyprowadzane ze stałego skanowania, jest w tej samej kolejności co klauzula wartości (nigdy nie widziałem, aby się różniły, ale AFAIK nie jest to gwarantowane).
  2. Kolejność wstawiania wierszy będzie taka sama, jak kolejność, w jakiej są wyprowadzane ze stałego skanowania (zdecydowanie nie zawsze tak jest).
  3. Jeśli używany jest plan wykonania „szeroki” (według indeksu), wartości z klauzuli wyjściowej zostaną pobrane z operatora aktualizacji indeksu klastrowanego, a nie z indeksów wtórnych.
  4. Aby zagwarantować, że zamówienie zostanie później zachowane - np. Przy pakowaniu w rzędy w celu przesłania przez sieć .
  5. Że nawet jeśli kolejność wydaje się przewidywalna, teraz zmiany implementacji funkcji takich jak wstawianie równoległe nie zmienią kolejności w przyszłości (obecnie jeśli klauzula OUTPUT jest określona w instrukcji INSERT… SELECT w celu zwrócenia wyników do klienta, wówczas plany równoległe są ogólnie wyłączone, w tym INSERT )

Przykład niepowodzenia punktu drugiego (przy założeniu klastrowanego PK z (Color, Action)) można zobaczyć, jeśli dodasz 600 wierszy do VALUESklauzuli. Następnie plan ma operator sortowania przed wstawką, co powoduje utratę oryginalnego zamówienia w VALUESklauzuli.

Istnieje jednak udokumentowany sposób osiągnięcia twojego celu, a mianowicie dodanie numeracji do źródła i użycie MERGEzamiastINSERT

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; 

wprowadź opis zdjęcia tutaj

@koń bez imienia

Czy scalenie jest naprawdę konieczne? Nie możesz po prostu zrobić insert into ... select ... from (values (..)) t (...) order by sourceid?

Tak, mogłeś. Zamawianie gwarancji w SQL Server… stwierdza, że

WSTAW zapytania, które używają WYBIERZ z ZAMÓWIENIEM w celu wypełnienia wierszy, gwarantuje sposób obliczania wartości tożsamości, ale nie kolejność wstawiania wierszy

Więc możesz użyć

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

wprowadź opis zdjęcia tutaj

Zagwarantowałoby to, że wartości tożsamości są przypisywane w kolejności, t.SourceIdale nie są wyprowadzane w określonej kolejności lub że wartości przypisane do kolumn tożsamości nie mają przerw (np. Przy próbie jednoczesnego wstawienia).

Martin Smith
źródło
2
Ten ostatni fragment o potencjalnej luce i braku wyjścia w określonej kolejności sprawia, że ​​jest trochę bardziej interesująca próba skorelowania z powrotem z danymi wejściowymi. Przypuszczam, że zamówienie w aplikacji wykonałoby zadanie, ale wydaje się, że korzystanie z niego jest bezpieczniejsze i bardziej przejrzyste MERGE.
ErikE
Użyj OUTPUT ... INTO [#temp]składni, SELECT ... FROM [#temp] ORDER BYaby zagwarantować kolejność wyjściową.
Max Vernon,