Est-il sûr de se fier à l'ordre d'une clause INSERT's OUTPUT?

19

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é.

Scénario 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)
;

Scénario 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
;

Question

Puis-je compter sur les valeurs d'identité renvoyées par l' dbo.Targetinsertion de table à renvoyer dans l'ordre dans lequel elles existaient dans la VALUESclause 1) et 2)#Target lequel table , afin de pouvoir les corréler par leur position dans l'ensemble de lignes de sortie avec l'entrée d'origine?

Pour référence

Voici un code C # réduit qui montre ce qui se passe dans l'application (scénario 1, qui sera bientôt converti pour être utilisé 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
la source

Réponses:

22

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 qu'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

  1. L'ordre dans lequel les lignes sont sorties du scan constant est dans le même ordre que la clause values ​​(je ne les ai jamais vues différer mais AFAIK ce n'est pas garanti).
  2. L'ordre dans lequel les lignes sont insérées sera le même que l'ordre de sortie du scan constant (ce n'est certainement pas toujours le cas).
  3. Si vous utilisez un plan d'exécution "large" (par index), les valeurs de la clause de sortie seront extraites de l'opérateur de mise à jour d'index cluster et non celles des index secondaires.
  4. Que l'ordre est garanti pour être préservé par la suite - par exemple lors du conditionnement des lignes pour la transmission sur le réseau .
  5. Même si l'ordre semble prévisible, les modifications d'implémentation de fonctionnalités telles que l'insertion parallèle ne changeront pas l'ordre à l'avenir (actuellement, si la clause OUTPUT est spécifiée dans l'instruction INSERT… SELECT pour renvoyer les résultats au client, les plans parallèles sont handicapés en général, y compris les INSERT )

Un exemple d'échec du point deux (en supposant PK en cluster de (Color, Action)) peut être vu si vous ajoutez 600 lignes à la VALUESclause. Ensuite, le plan a un opérateur de tri avant l'insertion, ce qui vous fait perdre votre commande d'origine dans la VALUESclause.

Il existe cependant un moyen documenté d'atteindre votre objectif, qui consiste à ajouter une numérotation à la source et à utiliser MERGEau lieu deINSERT

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; 

entrez la description de l'image ici

@un cheval sans nom

La fusion est-elle vraiment nécessaire? Ne pourriez-vous pas simplement faire un insert into ... select ... from (values (..)) t (...) order by sourceid?

Oui vous pourriez. Commander des garanties dans SQL Server… indique que

Les requêtes INSERT 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

entrez la description de l'image ici

Cela garantirait que les valeurs d'identité sont attribuées dans l'ordre t.SourceIdmais 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).

Martin Smith
la source
2
Ce dernier bit sur le potentiel des lacunes et la sortie n'étant pas dans un ordre particulier rend les choses un peu plus intéressantes pour essayer de corréler à l'entrée. Je suppose qu'une commande dans l'application ferait l'affaire, mais il semble plus sûr et plus clair d'utiliser simplement le MERGE.
ErikE
Utilisez la OUTPUT ... INTO [#temp]syntaxe, puis SELECT ... FROM [#temp] ORDER BYpour garantir l'ordre de sortie.
Max Vernon