Compte tenu de ce tableau:
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)
);
Dans deux scénarios légèrement différents, je veux insérer des lignes et renvoyer les valeurs de la colonne d'identité.
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)
;
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
;
Puis-je compter sur les valeurs d'identité renvoyées par le dbo.Target
insertion de table à renvoyer dans l'ordre dans lequel elles existaient dans la clause 1) VALUES
et 2) #Target
table, afin que je puisse les corréler par leur position dans l'ensemble de lignes de sortie par rapport à l'entrée d'origine?
Voici un code C # réduit qui montre ce qui se passe dans l'application (scénario 1, qui sera bientôt converti pour utiliser 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();
}
Puis-je compter sur les valeurs d'identité renvoyées par l'insertion de table dbo.Target à renvoyer dans l'ordre dans lequel elles existaient dans la clause 1) VALUES et 2) la table #Target, afin de pouvoir les corréler par leur position dans l'ensemble de lignes de sortie à l'entrée d'origine?
Non, vous ne pouvez pas compter sur quoi que ce soit pour être garanti sans une véritable garantie documentée. La documentation indique explicitement il n'y a pas une telle garantie.
SQL Server ne garantit pas l'ordre dans lequel les lignes sont traitées et renvoyées par les instructions DML à l'aide de la clause OUTPUT. Il appartient à l'application d'inclure une clause WHERE appropriée qui peut garantir la sémantique souhaitée, ou de comprendre que lorsque plusieurs lignes peuvent se qualifier pour l'opération DML, il n'y a pas d'ordre garanti.
Cela reposerait sur un grand nombre d'hypothèses non documentées
Un exemple d'échec du point deux (en supposant un PK en cluster de (Color, Action)
) Peut être vu si vous ajoutez 600 lignes à la clause VALUES
. Ensuite, le plan a un opérateur de tri avant l'insertion, ce qui vous fait perdre votre commande d'origine dans la clause VALUES
.
Il existe cependant un moyen documenté d'atteindre votre objectif, qui consiste à ajouter une numérotation à la source et à utiliser MERGE
au lieu de 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;
La fusion est-elle vraiment nécessaire? Ne pourriez-vous pas simplement faire une
insert into ... select ... from (values (..)) t (...) order by sourceid
?
Oui vous pourriez. Les garanties de commande dans SQL Server… indique que
INSÉRER les requêtes qui utilisent SELECT avec ORDER BY pour remplir les lignes garantissent la façon dont les valeurs d'identité sont calculées, mais pas l'ordre dans lequel les lignes sont insérées
Vous pouvez donc utiliser
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
Cela garantirait que les valeurs d'identité sont attribuées dans l'ordre de t.SourceId
Mais pas qu'elles sont sorties dans un ordre particulier ou que les valeurs de colonne d'identité attribuées n'ont pas d'espaces (par exemple si une insertion simultanée est tentée).