¿Es seguro confiar en el orden de una cláusula OUTPUT de INSERT?

19

Dada esta tabla:

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

En dos escenarios ligeramente diferentes, quiero insertar filas y devolver los valores de la columna de identidad.

escenario 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)
;

Escenario 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
;

Pregunta

¿Puedo confiar en los valores de identidad devueltos de la dbo.Targetinserción de la tabla para que se devuelvan en el orden en que existían en la VALUEScláusula 1) y la #Targettabla 2) , de modo que pueda correlacionarlos por su posición en el conjunto de filas de salida a la entrada original?

Para referencia

Aquí hay un código C # recortado que demuestra lo que está sucediendo en la aplicación (escenario 1, que pronto se convertirá en uso 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
fuente

Respuestas:

22

¿Puedo confiar en los valores de identidad devueltos del dbo? Insertar tabla objetivo que se devolverá en el orden en que existían en la cláusula 1) VALORES y 2) tabla # Objetivo, para que pueda correlacionarlos por su posición en el conjunto de filas de salida a la entrada original?

No, no puede confiar en que se garantice nada sin una garantía real documentada. La documentación declara explícitamente que no existe tal garantía.

SQL Server no garantiza el orden en el que las declaraciones DML procesan y devuelven las filas mediante la cláusula OUTPUT. Depende de la aplicación incluir una cláusula WHERE apropiada que pueda garantizar la semántica deseada, o comprender que cuando varias filas pueden calificar para la operación DML, no hay un orden garantizado.

Esto dependería de muchos supuestos indocumentados

  1. El orden de salida de las filas del escaneo constante está en el mismo orden que la cláusula de valores (nunca he visto que difieran, pero AFAIK esto no está garantizado).
  2. El orden en que se insertan las filas será el mismo que el orden en que salen del escaneo constante (definitivamente no siempre es el caso).
  3. Si utiliza un plan de ejecución "amplio" (por índice), los valores de la cláusula de salida se extraerán del operador de actualización de índice agrupado y no el de ningún índice secundario.
  4. Que se garantiza que el pedido se conservará a partir de entonces, por ejemplo, cuando se empacan filas para su transmisión por la red .
  5. Incluso si el orden parece predecible, ahora la implementación de cambios en características como la inserción paralela no cambiará el orden en el futuro (actualmente si la cláusula OUTPUT se especifica en la instrucción INSERT ... SELECT para devolver resultados al cliente, entonces los planes paralelos son deshabilitado en general, incluidos los INSERT )

Se (Color, Action)puede ver un ejemplo de falla del punto dos (suponiendo una PK agrupada de ) si agrega 600 filas a la VALUEScláusula. Luego, el plan tiene un operador de clasificación antes de la inserción, por lo que pierde su orden original en la VALUEScláusula.

Sin embargo, hay una forma documentada de lograr su objetivo y es agregar una numeración a la fuente y usar en MERGElugar 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; 

ingrese la descripción de la imagen aquí

@un caballo sin nombre

¿Es realmente necesaria la fusión? ¿No podrías simplemente hacer un insert into ... select ... from (values (..)) t (...) order by sourceid?

Si, podrías. Ordenando garantías en SQL Server ... establece que

INSERTAR consultas que usan SELECT con ORDER BY para llenar filas garantiza cómo se calculan los valores de identidad pero no el orden en que se insertan las filas

Entonces podrías usar

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

ingrese la descripción de la imagen aquí

Esto garantizaría que los valores de identidad se asignen en orden t.SourceIdpero no que se emitan en un orden particular o que los valores de columna de identidad asignados no tengan espacios (por ejemplo, si se intenta una inserción concurrente).

Martin Smith
fuente
2
Este último bit sobre el potencial de brechas y la salida no está en un orden particular hace que las cosas sean un poco más interesantes para tratar de correlacionar de nuevo a la entrada. Supongo que un pedido en la aplicación haría el trabajo, pero parece más seguro y claro usar el MERGE.
ErikE
Use la OUTPUT ... INTO [#temp]sintaxis, luego SELECT ... FROM [#temp] ORDER BYpara garantizar el orden de salida.
Max Vernon
TL; Versión DR: con SQL Server, y creo que las implementaciones de SQL en general, a menos que haya una cláusula ORDER BY, el orden no está garantizado.
nateirvin
Re. vacíos: ¿incluiría la InsertDeclaración en una Transactionprevención de vacíos?
Tom