Как получить вставленное значение NewSequentialId при выполнении нескольких вставок? - PullRequest
3 голосов
/ 16 мая 2019

Гарантирует ли SQL Server, что NewSequentialId () будет вызываться для каждой строки в порядке, указанном в предложении ORDER BY инструкции INSERT?

Цель состоит в том, чтобы взять список объектов в C #, каждый из которых представляет строку, которая должна быть вставлена ​​в таблицу, и довольно быстро вставить их в таблицу.

То, что я пытаюсь сделать, это вставить строки во временную таблицу с помощью SqlBulkCopy, затем вставить строки из временной таблицы в таблицу, которая использует NewSequentialId (), а затем извлечь новые идентификаторы так, чтобы они могли быть отсортированы в том же порядке, что и список объектов в C #, так что идентификаторы могут быть прикреплены к каждому соответствующему объекту в C #.

Я использую SQL Server 2016, и это целевая таблица:

CREATE TABLE dbo.MyTable
(
    Id UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
    SomeNonUniqueValue NVARCHAR(50) NOT NULL
)

Сначала я использую SqlBulkCopy, чтобы вставить строки в эту временную таблицу. Столбцы RowOrder содержат целое число, сгенерированное в приложении. RowOrder - это порядок, в котором нужно вернуть сгенерированные идентификаторы. В приложении RowOrder - это индекс каждого объекта C # в списке.

CREATE TABLE #MyTableStaging
(
    RowOrder INT NOT NULL,
    SomeNonUniqueValue NVARCHAR(50) NOT NULL
)

Затем я запускаю этот SQL, чтобы взять строки из #MyTableStaging, вставить их в MyTable и получить вставленные идентификаторы.

DECLARE @MyTableOutput TABLE
(
    Id UNIQUEIDENTIFIER NOT NULL
)

INSERT INTO dbo.MyTable (SomeNonUniqueValue)
OUTPUT Inserted.Id INTO @MyTableOutput(Id)
SELECT SomeNonUniqueValue 
FROM #MyTableStaging
ORDER BY RowOrder

SELECT Id FROM @MyTableOutput ORDER BY Id

Во всех моих тестах это работает. Однако недавно я обнаружил, что порядок, в котором строки вставляются в таблицу, указанную в предложении OUTPUT, не всегда совпадает с порядком, заданным в ORDER BY в операторе INSERT (я обнаружил это, потому что оригинальный дизайн этой системы должен был использовать идентификатор в #MyTableStaging, а не упорядочивать по # MyTableStaging. Я упорядочивал по столбцу идентификаторов).

Я знаю, что SQL Server гарантирует, что значения идентификаторов генерируются в порядке, указанном в предложении ORDER BY инструкции INSERT (с https://docs.microsoft.com/en-us/sql/t-sql/statements/insert-transact-sql?view=sql-server-2017#limitations-and-restrictions):

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

1 Ответ

2 голосов
/ 16 мая 2019

Самый простой (и, вероятно, самый эффективный) способ - вставить непосредственно в целевую таблицу MyTable без промежуточной промежуточной таблицы. Я бы использовал параметр табличного значения, чтобы передать таблицу значений в вашу хранимую процедуру.

https://docs.microsoft.com/en-us/sql/relational-databases/tables/use-table-valued-parameters-database-engine?view=sql-server-2017

https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/table-valued-parameters


Если вы действительно хотите использовать промежуточную таблицу, вы не можете полагаться на порядок строк, возвращаемых предложением OUTPUT. Вам необходимо сохранить явное отображение между #MyTableStaging.RowOrder и сгенерированным MyTable.Id. Когда вы используете выражение OUTPUT в простом операторе INSERT, вы не можете включать столбцы из исходной таблицы в вывод. Есть обходной путь. Вы можете использовать MERGE вместо предложений INSERT и OUTPUT оператора MERGE, разрешающих столбцы из исходной таблицы.

См. Очень похожий вопрос Объединить OUTPUT вставки.id со значением из выбранной строки

MERGE может INSERT, UPDATE и DELETE строк. В нашем случае нам нужно всего лишь INSERT. 1=0 всегда ложно, поэтому часть NOT MATCHED BY TARGET всегда выполняется. В общем, могут быть и другие ветки, см. Документы. WHEN MATCHED обычно используется для UPDATE; WHEN NOT MATCHED BY SOURCE обычно используется для DELETE, но они нам здесь не нужны.

Эта запутанная форма MERGE эквивалентна простой INSERT, но в отличие от простого INSERT его предложение OUTPUT позволяет ссылаться на нужные нам столбцы. Он позволяет извлекать столбцы из исходной и целевой таблиц, сохраняя соответствие между старым и новым идентификаторами.

DECLARE @MyTableOutput TABLE
(
    OldRowOrder int NOT NULL
    ,NewID UNIQUEIDENTIFIER NOT NULL
);


MERGE INTO dbo.MyTable
USING
(
    SELECT RowOrder, SomeNonUniqueValue
    FROM #MyTableStaging
) AS Src
ON 1 = 0
WHEN NOT MATCHED BY TARGET THEN
INSERT (SomeNonUniqueValue)
VALUES (Src.SomeNonUniqueValue)
OUTPUT Src.RowOrder AS OldRowOrder, inserted.ID AS NewID
INTO @MyTableOutput(OldRowOrder, NewID)
;

Если ваш администратор БД так боится MERGE, вам не нужно его использовать. Хотя это будет менее эффективно.

Просто вставьте все строки.

INSERT INTO dbo.MyTable (SomeNonUniqueValue)
SELECT SomeNonUniqueValue 
FROM #MyTableStaging
;

Нам нет дела до заказа.

Если бы SomeNonUniqueValue были уникальными, вы можете просто присоединиться к этому столбцу, чтобы сопоставить RowOrder с Id. Поскольку эти значения не являются уникальными, нам потребуется дополнительный шаг и сгенерировать уникальные номера строк для объединения.

WITH
CTE_Dst
AS
(

    SELECT
        Id
        ,SomeNonUniqueValue
        ,ROW_NUMBER() OVER (ORDER BY SomeNonUniqueValue) AS rn
    FROM dbo.MyTable
)
,CTE_Src
AS
(

    SELECT
        RowOrder
        ,SomeNonUniqueValue
        ,ROW_NUMBER() OVER (ORDER BY SomeNonUniqueValue) AS rn
    FROM #MyTableStaging
)
SELECT
    CTE_Dst.Id
    ,CTE_Src.RowOrder
FROM
    CTE_Dst
    INNER JOIN CTE_Src ON CTE_Src.rn = CTE_Dst.rn
;

Если у вас есть, скажем, три строки с одинаковым SomeNonUniqueValue, на самом деле не имеет значения, как вы отображаете эти строки вместе, потому что SomeNonUniqueValue - это то же самое.

Пример:

#MyTableStaging
+----------+--------------------+
| RowOrder | SomeNonUniqueValue |
+----------+--------------------+
|        1 | qwerty             |
|        2 | qwerty             |
|        3 | qwerty             |
|        4 | asdf               |
|        5 | asdf               |
+----------+--------------------+

MyTable
+----+--------------------+
| ID | SomeNonUniqueValue |
+----+--------------------+
| A  | qwerty             |
| B  | qwerty             |
| C  | qwerty             |
| D  | asdf               |
| E  | asdf               |
+----+--------------------+

Вы можете отобразить их так:

+----------+----+--------------------+
| RowOrder | ID | SomeNonUniqueValue |
+----------+----+--------------------+
|        1 | A  | qwerty             |
|        2 | B  | qwerty             |
|        3 | C  | qwerty             |
|        4 | D  | asdf               |
|        5 | E  | asdf               |
+----------+----+--------------------+

Или вы можете отобразить их так:

+----------+----+--------------------+
| RowOrder | ID | SomeNonUniqueValue |
+----------+----+--------------------+
|        1 | B  | qwerty             |
|        2 | C  | qwerty             |
|        3 | A  | qwerty             |
|        4 | E  | asdf               |
|        5 | D  | asdf               |
+----------+----+--------------------+

Это все еще допустимое отображение, потому что все три значения qwerty одинаковы. Ни одно из этих сопоставлений не является «более правильным», чем другое.

Очевидно, что если ваш MyTable не был пустым до INSERT, вам нужно выбрать только новые строки.

...