Does SQL Server guarantee that NewSequentialId() will be called for each row in the order specified by the ORDER BY clause of an INSERT statement?
The goal is to take a list of objects in C#, each of which represents a row to be inserted in a table, and insert them into a table fairly quickly.
What I'm trying to do is insert the rows into a temporary table using SqlBulkCopy, then insert the rows from the temp table into a table that uses NewSequentialId(), then retrieve the new IDs in a way that they can be sorted in the same order as the list of objects in C#, so that the IDs can be attached to each corresponding object in C#.
I'm using SQL Server 2016 and this is the target table:
CREATE TABLE dbo.MyTable
(
Id UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
SomeNonUniqueValue NVARCHAR(50) NOT NULL
)
First I use SqlBulkCopy to insert rows into this temp table. The RowOrder columns contains an integer generated in the application. RowOrder is the order I need the generated IDs to be returned in. In the application, RowOrder is the index of each C# object in the list.
CREATE TABLE #MyTableStaging
(
RowOrder INT NOT NULL,
SomeNonUniqueValue NVARCHAR(50) NOT NULL
)
Then I run this SQL to take the rows from #MyTableStaging, insert them into MyTable and retrieve the inserted IDs.
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
In all my testing this works. However, I recently found out that the order in which rows are inserted into the table specified in the OUTPUT clause is not always the same as the order specified by the ORDER BY in the INSERT statement (I found this because the original design of this system was to use an identity in #MyTableStaging, and rather than ordering by #MyTableStaging.Id I was ordering by the identity column).
I know that SQL Server guarantees that identity values are generated in the order specified in the ORDER BY clause of an INSERT statement (from https://learn.microsoft.com/en-us/sql/t-sql/statements/insert-transact-sql?view=sql-server-2017#limitations-and-restrictions):
INSERT queries that use SELECT with ORDER BY to populate rows guarantees how identity values are computed but not the order in which the rows are inserted.
The easiest (and likely most efficient) way would be to insert into the target
MyTabletable directly without the intermediate staging table. I'd use a table-valued-parameter to pass the table of values into your stored procedure.https://learn.microsoft.com/en-us/sql/relational-databases/tables/use-table-valued-parameters-database-engine?view=sql-server-2017
https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/sql/table-valued-parameters
If you really want to use the staging table, you can't rely on the order of rows returned by the
OUTPUTclause. You need to store the explicit mapping between the#MyTableStaging.RowOrderand the generatedMyTable.Id. When you useOUTPUTclause in a simpleINSERTstatement you can't include columns from the source table into the output. There is a workaround. You can useMERGEinstead ofINSERTandOUTPUTclause of theMERGEstatement allows columns from source table.See very similar question Combine OUTPUT inserted.id with value from selected row
MERGEcanINSERT,UPDATEandDELETErows. In our case we need only toINSERT.1=0is always false, so theNOT MATCHED BY TARGETpart is always executed. In general, there could be other branches, see docs.WHEN MATCHEDis usually used toUPDATE;WHEN NOT MATCHED BY SOURCEis usually used toDELETE, but we don't need them here.This convoluted form of
MERGEis equivalent to simpleINSERT, but unlike simpleINSERTitsOUTPUTclause allows to refer to the columns that we need. It allows to retrieve columns from both source and destination tables thus saving a mapping between old and new IDs.If your DBA is so afraid of
MERGEyou don't have to use it. It will be less efficient, though.Simply insert all rows.
We don't care about the order.
If
SomeNonUniqueValuewere unique, you could just join on this column to mapRowOrdertoId. Since these values are not unique, we'll need an extra step and generate unique row numbers for joining.If you have, say, three rows with the same
SomeNonUniqueValueit doesn't really matter how you map these rows together, becauseSomeNonUniqueValueis the same.Example:
You can map them like this:
Or, you can map them like this:
It is still a valid mapping, because all three values of
qwertyare the same. Neither of these mapping is "more correct" than another.Obviously, if your
MyTablewasn't empty before theINSERT, you need to select new rows only.