web-dev-qa-db-fra.com

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

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 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?

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 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();
}
20
ErikE

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

  1. L'ordre dans lequel les lignes sont sorties du balayage 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 conservé par la suite - par exemple quand empaquetage des lignes pour transmission sur le résea .
  5. Que 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 désactivés en général, y compris les INSERTs )

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; 

enter image description here

@ a_horse_with_no_name

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

enter image description here

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

23
Martin Smith