Безопасно ли полагаться на порядок предложения INSERT OUTPUT?

19

Учитывая эту таблицу:

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

В двух немного разных сценариях я хочу вставить строки и вернуть значения из столбца идентификаторов.

Сценарий 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)
;

Сценарий 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
;

Вопрос

Могу ли я рассчитывать, что возвращенные значения идентификаторов из dbo.Targetвставки таблицы будут возвращены в том порядке, в котором они существовали в 1) VALUESпредложении и 2) #Targetтаблице, чтобы я мог сопоставить их по их положению в выходном наборе строк обратно к исходному вводу?

Для справки

Вот некоторый урезанный код C #, который демонстрирует, что происходит в приложении (сценарий 1, который вскоре будет преобразован в использование 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
источник

Ответы:

22

Могу ли я рассчитывать на возвращенные значения идентификатора из вставки таблицы dbo.Target, которые будут возвращены в том порядке, в котором они существовали в 1) предложении VALUES и 2) таблице #Target, чтобы я мог сопоставить их по положению в выходном наборе строк назад к первоначальному вводу?

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

SQL Server не гарантирует порядок, в котором строки обрабатываются и возвращаются операторами DML с использованием предложения OUTPUT. Приложение должно включить соответствующее предложение WHERE, которое может гарантировать желаемую семантику, или понять, что когда несколько строк могут удовлетворять требованиям для операции DML, не существует гарантированного порядка.

Это будет опираться на множество недокументированных предположений

  1. Порядок вывода строк из постоянного сканирования в том же порядке, что и в разделе значений (я никогда не видел, чтобы они различались, но AFAIK это не гарантируется).
  2. Порядок вставки строк будет таким же, как и порядок их вывода при постоянном сканировании (определенно, это не всегда так).
  3. При использовании «широкого» (для индекса) плана выполнения значения из предложения вывода будут извлечены из оператора обновления кластеризованного индекса, а не из любых вторичных индексов.
  4. Что порядок гарантированно будет сохранен после этого - например, когда упаковка выстраивается в очередь для передачи по сети .
  5. Даже если порядок выглядит предсказуемым, теперь изменения в реализации таких функций, как параллельная вставка, не изменят порядок в будущем (в настоящее время, если в операторе INSERT… SELECT указано предложение OUTPUT для возврата результатов клиенту, параллельные планы отключено вообще, включая ВСТАВКИ )

Пример сбоя второго пункта (при условии, что кластеризованный PK (Color, Action)) можно увидеть, если добавить к VALUESпредложению 600 строк . Тогда план имеет оператор сортировки перед вставкой, поэтому теряет свой первоначальный порядок в VALUESпредложении.

Хотя есть документированный способ достижения вашей цели, это добавить нумерацию к источнику и использовать MERGEвместоINSERT

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; 

введите описание изображения здесь

@a_horse_with_no_name

Действительно ли слияние необходимо? Не могли бы вы просто сделать insert into ... select ... from (values (..)) t (...) order by sourceid?

Да, ты мог. Гарантии упорядочения в SQL Server ... утверждает, что

Запросы INSERT, которые используют SELECT с ORDER BY для заполнения строк, гарантируют, как вычисляются значения идентификаторов, но не порядок, в котором вставляются строки

Так что вы могли бы использовать

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

введите описание изображения здесь

Это будет гарантировать, что значения идентификаторов назначаются в порядке, t.SourceIdно не выводятся в каком-либо конкретном порядке, или что назначенные значения столбцов идентификаторов не имеют пропусков (например, при попытке одновременной вставки).

Мартин Смит
источник
2
Этот последний бит о возможном наличии пробелов и о том, что выходные данные не находятся в определенном порядке, делает вещи более интересными для попыток соотнести их с входными данными. Я полагаю, что порядок в приложении сделает эту работу, но кажется, что безопаснее и понятнее просто использовать MERGE.
ErikE
Используйте OUTPUT ... INTO [#temp]синтаксис, SELECT ... FROM [#temp] ORDER BYчтобы гарантировать порядок вывода.
Макс Вернон