¿Cómo evitar el uso de la consulta de combinación al insertar múltiples datos usando el parámetro xml?


9

Estoy tratando de actualizar una tabla con una matriz de valores. Cada elemento de la matriz contiene información que coincide con una fila en una tabla en la base de datos de SQL Server. Si la fila ya existe en la tabla, actualizamos esa fila con la información en la matriz dada. De lo contrario, insertamos una nueva fila en la tabla. Básicamente he descrito upsert.

Ahora, estoy tratando de lograr esto en un procedimiento almacenado que toma un parámetro XML. La razón por la que estoy usando XML y no un parámetro con valores de tabla es porque, al hacer esto último, tendré que crear un tipo personalizado en SQL y asociar este tipo al procedimiento almacenado. Si alguna vez cambiara algo en mi procedimiento almacenado o mi esquema db en el futuro, tendría que rehacer tanto el procedimiento almacenado como el tipo personalizado. Quiero evitar esta situación. Además, la superioridad que tiene TVP sobre XML no es útil para mi situación porque mi tamaño de matriz de datos nunca excederá 1000. Esto significa que no puedo usar la solución propuesta aquí: Cómo insertar múltiples registros usando XML en SQL Server 2008

Además, una discusión similar aquí ( UPSERT - ¿Hay una mejor alternativa para MERGE o @@ rowcount? ) Es diferente de lo que estoy preguntando porque estoy tratando de insertar varias filas en una tabla.

Tenía la esperanza de que simplemente usaría el siguiente conjunto de consultas para insertar los valores del xml. Pero esto no va a funcionar. Se supone que este enfoque solo funciona cuando la entrada es una sola fila.

begin tran
   update table with (serializable) set select * from xml_param
   where key = @key

   if @@rowcount = 0
   begin
      insert table (key, ...) values (@key,..)
   end
commit tran

La siguiente alternativa es usar un SI exhaustivo EXISTE o una de sus variaciones de la siguiente forma. Pero, rechazo esto por ser de eficiencia subóptima:

IF (SELECT COUNT ... ) > 0
    UPDATE
ELSE
    INSERT

La siguiente opción fue usar la instrucción Merge como se describe aquí: http://www.databasejournal.com/features/mssql/using-the-merge-statement-to-perform-an-upsert.html . Pero, luego leí sobre problemas con la consulta de fusión aquí: http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/ . Por esta razón, estoy tratando de evitar la fusión.

Entonces, mi pregunta es: ¿hay alguna otra opción o una mejor manera de lograr una respuesta múltiple múltiple usando el parámetro XML en el procedimiento almacenado de SQL Server 2008?

Tenga en cuenta que los datos en el parámetro XML pueden contener algunos registros que no se deben UPSERT debido a que son más antiguos que el registro actual. Hay un ModifiedDatecampo tanto en el XML como en la tabla de destino que debe compararse para determinar si el registro debe actualizarse o descartarse.


Intentar evitar hacer cambios en el proceso en el futuro no es realmente una buena razón para no usar un TVP. si los datos pasaron en los cambios, terminarás haciendo cambios en el código de cualquier manera.
Max Vernon

1
@MaxVernon Al principio tuve el mismo pensamiento y casi hice un comentario muy similar porque eso solo no es una razón para evitar TVP. Pero sí requieren un poco más de esfuerzo, y con la advertencia de "nunca más de 1000 filas" (¿implicado a veces, o tal vez incluso a menudo?) Es un poco difícil. Sin embargo, supongo que debería calificar mi respuesta para afirmar que <1000 filas a la vez no es muy diferente de XML siempre que no se llame 10k veces seguidas. Entonces, las diferencias de rendimiento menores ciertamente se suman.
Solomon Rutzky

Los problemas con los MERGEque Bertrand señala son en su mayoría casos extremos e ineficiencias, no muestran obstáculos: MS no lo habría lanzado si fuera un verdadero campo minado. ¿Estás seguro de que las convoluciones por las que estás pasando para evitar MERGEno están creando más errores potenciales de los que están guardando?
Jon of All Trades

@JonofAllTrades Para ser justos, lo que propuse no es realmente tan complicado en comparación con MERGE. Los pasos INSERTAR y ACTUALIZAR de MERGE aún se procesan por separado. La principal diferencia en mi enfoque es la variable de tabla que contiene los ID de registro actualizados y la consulta DELETE que usa esa variable de tabla para eliminar esos registros de la tabla temporal de los datos entrantes. Y supongo que la FUENTE podría ser directa desde @ XMLparam.nodes () en lugar de volcar a una tabla temporal, pero aún así, eso no es un montón de cosas adicionales para no tener que preocuparse de encontrarse en uno de esos casos extremos; )
Solomon Rutzky

Respuestas:


11

Si la fuente es XML o un TVP no hace una gran diferencia. La operación general es esencialmente:

  1. ACTUALIZAR filas existentes
  2. INSERTAR filas faltantes

Lo haces en ese orden porque si INSERTAS primero, entonces todas las filas existen para obtener la ACTUALIZACIÓN y harás un trabajo repetido para las filas que acabas de insertar.

Más allá de eso, hay diferentes maneras de lograr esto y varias formas de ajustar algo de eficiencia adicional.

Comencemos con lo mínimo. Dado que es probable que extraer el XML sea una de las partes más caras de esta operación (si no la más costosa), no queremos tener que hacerlo dos veces (ya que tenemos dos operaciones que realizar). Entonces, creamos una tabla temporal y extraemos los datos del XML en ella:

CREATE TABLE #TempImport
(
  Field1 DataType1,
  Field2 DataType2,
  ...
);

INSERT INTO #TempImport (Field1, Field2, ...)
  SELECT tab.col.value('XQueryForField1', 'DataType') AS [Field1],
         tab.col.value('XQueryForField2', 'DataType') AS [Field2],
         ...
  FROM   @XmlInputParam.nodes('XQuery') tab(col);

A partir de ahí hacemos la ACTUALIZACIÓN y luego la INSERCIÓN:

UPDATE tab
SET    tab.Field1 = tmp.Field1,
       tab.Field2 = tmp.Field2,
       ...
FROM   [SchemaName].[TableName] tab
INNER JOIN #TempImport tmp
        ON tmp.IDField = tab.IDField
        ... -- more fields if PK or alternate key is composite

INSERT INTO [SchemaName].[TableName]
  (Field1, Field2, ...)
  SELECT tmp.Field1, tmp.Field2, ...
  FROM   #TempImport tmp
  WHERE  NOT EXISTS (
                       SELECT  *
                       FROM    [SchemaName].[TableName] tab
                       WHERE   tab.IDField = tmp.IDField
                       ... -- more fields if PK or alternate key is composite
                     );

Ahora que tenemos la operación básica inactiva, podemos hacer algunas cosas para optimizar:

  1. capture @@ ROWCOUNT de inserción en la tabla temporal y compárelo con @@ ROWCOUNT de la ACTUALIZACIÓN. Si son iguales, entonces podemos omitir el INSERTAR

  2. capturar los valores de ID actualizados a través de la cláusula OUTPUT y ELIMINAR aquellos de la tabla temporal. Entonces el INSERT no necesita elWHERE NOT EXISTS(...)

  3. Si hay filas en los datos entrantes que no deberían sincronizarse (es decir, ni insertarse ni actualizarse), esos registros deberían eliminarse antes de realizar la ACTUALIZACIÓN

CREATE TABLE #TempImport
(
  Field1 DataType1,
  Field2 DataType2,
  ...
);

DECLARE @ImportRows INT;
DECLARE @UpdatedIDs TABLE ([IDField] INT NOT NULL);

BEGIN TRY

  INSERT INTO #TempImport (Field1, Field2, ...)
    SELECT tab.col.value('XQueryForField1', 'DataType') AS [Field1],
           tab.col.value('XQueryForField2', 'DataType') AS [Field2],
           ...
    FROM   @XmlInputParam.nodes('XQuery') tab(col);

  SET @ImportRows = @@ROWCOUNT;

  IF (@ImportRows = 0)
  BEGIN
    RAISERROR('Seriously?', 16, 1); -- no rows to import
  END;

  -- optional: test to see if it helps or hurts
  -- ALTER TABLE #TempImport
  --   ADD CONSTRAINT [PK_#TempImport]
  --   PRIMARY KEY CLUSTERED (PKField ASC)
  --   WITH FILLFACTOR = 100;


  -- optional: remove any records that should not be synced
  DELETE tmp
  FROM   #TempImport tmp
  INNER JOIN [SchemaName].[TableName] tab
          ON tab.IDField = tmp.IDField
          ... -- more fields if PK or alternate key is composite
  WHERE  tmp.ModifiedDate < tab.ModifiedDate;

  BEGIN TRAN;

  UPDATE tab
  SET    tab.Field1 = tmp.Field1,
         tab.Field2 = tmp.Field2,
         ...
  OUTPUT INSERTED.IDField
  INTO   @UpdatedIDs ([IDField]) -- capture IDs that are updated
  FROM   [SchemaName].[TableName] tab
  INNER JOIN #TempImport tmp
          ON tmp.IDField = tab.IDField
          ... -- more fields if PK or alternate key is composite

  IF (@@ROWCOUNT < @ImportRows) -- if all rows were updates then skip, else insert remaining
  BEGIN
    -- get rid of rows that were updates, leaving only the ones to insert
    DELETE tmp
    FROM   #TempImport tmp
    INNER JOIN @UpdatedIDs del
            ON del.[IDField] = tmp.[IDField];

    -- OR, rather than the DELETE, maybe add a column to #TempImport for:
    -- [IsUpdate] BIT NOT NULL DEFAULT (0)
    -- Then UPDATE #TempImport SET [IsUpdate] = 1 JOIN @UpdatedIDs ON [IDField]
    -- Then, in below INSERT, add:  WHERE [IsUpdate] = 0

    INSERT INTO [SchemaName].[TableName]
      (Field1, Field2, ...)
      SELECT tmp.Field1, tmp.Field2, ...
      FROM   #TempImport tmp
  END;

  COMMIT TRAN;

END TRY
BEGIN CATCH
  IF (@@TRANCOUNT > 0)
  BEGIN
    ROLLBACK;
  END;

  -- THROW; -- if using SQL 2012 or newer, use this and remove the following 3 lines
  DECLARE @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();
  RAISERROR(@ErrorMessage, 16, 1);
  RETURN;
END CATCH;

He usado este modelo varias veces en Importaciones / ETL que tienen más de 1000 filas o quizás 500 en un lote de un conjunto total de 20k, más de un millón de filas. Sin embargo, no he probado la diferencia de rendimiento entre ELIMINAR las filas actualizadas fuera de la tabla temporal frente a simplemente actualizar el campo [IsUpdate].


Tenga en cuenta la decisión de usar XML sobre TVP debido a que hay, como máximo, 1000 filas para importar a la vez (mencionado en la pregunta):

Si esto se llama varias veces aquí y allá, entonces posiblemente la menor ganancia de rendimiento en TVP podría no valer el costo de mantenimiento adicional (la necesidad de abandonar el proceso antes de cambiar el Tipo de tabla definida por el usuario, los cambios en el código de la aplicación, etc.) . Pero si está importando 4 millones de filas, enviando 1000 a la vez, es decir, 4000 ejecuciones (y 4 millones de filas de XML para analizar, sin importar cómo se divida), e incluso una pequeña diferencia de rendimiento cuando se ejecuta solo unas pocas veces sumar a una diferencia notable.

Dicho esto, el método que he descrito no cambia fuera de reemplazar SELECT FROM @XmlInputParam para que sea SELECT FROM @TVP. Dado que los TVP son de solo lectura, no podrá eliminarlos. Supongo que simplemente podría agregar un WHERE NOT EXISTS(SELECT * FROM @UpdateIDs ids WHERE ids.IDField = tmp.IDField)a ese SELECT final (vinculado al INSERT) en lugar del simple WHERE IsUpdate = 0. Si usara la @UpdateIDsvariable de tabla de esta manera, incluso podría salirse con la suya sin tirar las filas entrantes en la tabla temporal.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.