Comment éviter d'utiliser la requête de fusion lors de la migration de plusieurs données à l'aide du paramètre xml?

9

J'essaie de mettre à jour une table avec un tableau de valeurs. Chaque élément du tableau contient des informations qui correspondent à une ligne d'une table dans la base de données SQL Server. Si la ligne existe déjà dans la table, nous mettons à jour cette ligne avec les informations dans le tableau donné. Sinon, nous insérons une nouvelle ligne dans le tableau. J'ai essentiellement décrit upsert.

Maintenant, j'essaie d'y parvenir dans une procédure stockée qui prend un paramètre XML. La raison pour laquelle j'utilise XML et non un paramètre table est que, ce faisant, je devrai créer un type personnalisé en SQL et associer ce type à la procédure stockée. Si jamais je modifiais quelque chose dans ma procédure stockée ou mon schéma db sur la route, je devrais refaire à la fois la procédure stockée et le type personnalisé. Je veux éviter cette situation. En outre, la supériorité de TVP sur XML n'est pas utile dans ma situation car, la taille de mon tableau de données ne dépassera jamais 1000. Cela signifie que je ne peux pas utiliser la solution proposée ici: Comment insérer plusieurs enregistrements à l'aide de XML dans SQL Server 2008

En outre, une discussion similaire ici ( UPSERT - Existe-t-il une meilleure alternative à MERGE ou @@ rowcount? ) Est différente de ce que je demande parce que j'essaie d'insérer plusieurs lignes dans une table.

J'espérais que j'utiliserais simplement l'ensemble de requêtes suivant pour inverser les valeurs du xml. Mais cela ne fonctionnera pas. Cette approche est censée fonctionner uniquement lorsque l'entrée est une seule ligne.

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 prochaine alternative consiste à utiliser un IF EXISTS exhaustif ou l'une de ses variantes de la forme suivante. Mais, je rejette cela au motif d'être d'une efficacité sous-optimale:

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

L'option suivante utilisait l'instruction Merge comme décrit ici: http://www.databasejournal.com/features/mssql/using-the-merge-statement-to-perform-an-upsert.html . Mais, j'ai lu des informations sur les problèmes de fusion ici: http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/ . Pour cette raison, j'essaie d'éviter la fusion.

Donc, maintenant ma question est: existe-t-il une autre option ou une meilleure façon d'obtenir plusieurs upsert en utilisant le paramètre XML dans la procédure stockée SQL Server 2008?

Veuillez noter que les données du paramètre XML peuvent contenir certains enregistrements qui ne doivent pas être supprimés en raison de leur ancienneté par rapport à l'enregistrement actuel. Il existe un ModifiedDatechamp dans le XML et dans la table de destination qui doit être comparé afin de déterminer si l'enregistrement doit être mis à jour ou supprimé.

GMalla
la source
Essayer d'éviter de modifier le proc à l'avenir n'est pas vraiment une bonne raison de ne pas utiliser de TVP. si les données sont passées dans les modifications, vous finirez par apporter des modifications au code dans les deux cas.
Max Vernon
1
@MaxVernon J'ai eu la même pensée au début et j'ai presque fait un commentaire très similaire car cela seul n'est pas une raison pour éviter le TVP. Mais ils prennent un peu plus d'effort, et avec la mise en garde de "jamais plus de 1000 lignes" (implicite parfois, ou peut-être même souvent?), C'est un peu compliqué. Cependant, je suppose que je devrais qualifier ma réponse pour déclarer que <1 000 lignes à la fois n'est pas trop différent de XML tant qu'il n'est pas appelé 10 000 fois de suite. Ensuite, des différences de performances mineures s'ajoutent certainement.
Solomon Rutzky
Les problèmes avec MERGElesquels Bertrand souligne sont principalement des cas marginaux et des inefficacités, pas des bouchons - MS ne l'aurait pas libéré s'il s'agissait d'un véritable champ de mines. Êtes-vous sûr que les circonvolutions que vous devez éviter MERGEne créent pas plus d'erreurs potentielles qu'elles n'en économisent?
Jon of All Trades
@JonofAllTrades Pour être juste, ce que j'ai proposé n'est pas vraiment compliqué par rapport à MERGE. Les étapes INSERT et UPDATE de MERGE sont toujours traitées séparément. La principale différence dans mon approche est la variable de table qui contient les ID d'enregistrement mis à jour et la requête DELETE qui utilise cette variable de table pour supprimer ces enregistrements de la table temporaire des données entrantes. Et je suppose que la SOURCE pourrait être directement de @ XMLparam.nodes () au lieu de se déverser dans une table temporaire, mais ce n'est pas beaucoup de choses supplémentaires pour ne pas avoir à vous soucier de vous retrouver dans l'un de ces cas extrêmes; - ).
Solomon Rutzky

Réponses:

11

Que la source soit XML ou TVP ne fait pas une énorme différence. L'opération globale est essentiellement:

  1. METTRE À JOUR les lignes existantes
  2. INSÉRER les lignes manquantes

Vous le faites dans cet ordre car si vous insérez d'abord, toutes les lignes existent pour obtenir la MISE À JOUR et vous effectuerez des travaux répétés pour toutes les lignes qui viennent d'être insérées.

Au-delà de cela, il existe différentes façons d'accomplir cela et diverses façons d'en modifier l'efficacité supplémentaire.

Commençons par le strict minimum. Étant donné que l'extraction du XML est susceptible d'être l'une des parties les plus coûteuses de cette opération (sinon la plus coûteuse), nous ne voulons pas avoir à le faire deux fois (car nous avons deux opérations à effectuer). Ainsi, nous créons une table temporaire et extrayons les données du XML dedans:

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

De là, nous faisons la MISE À JOUR, puis l'INSÉRER:

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

Maintenant que nous avons arrêté l'opération de base, nous pouvons faire quelques choses pour optimiser:

  1. capturer @@ ROWCOUNT de l'insertion dans la table temporaire et comparer à @@ ROWCOUNT de la MISE À JOUR. S'ils sont identiques, nous pouvons sauter l'INSERT

  2. capturer les valeurs d'ID mises à jour via la clause OUTPUT et SUPPRIMER celles de la table temporaire. L'INSERT n'a alors pas besoin duWHERE NOT EXISTS(...)

  3. S'il y a des lignes dans les données entrantes qui ne doivent pas être synchronisées (c'est-à-dire ni insérées ni mises à jour), alors ces enregistrements doivent être supprimés avant d'effectuer la MISE À JOUR

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;

J'ai utilisé ce modèle plusieurs fois sur les importations / ETL qui ont bien plus de 1000 lignes ou peut-être 500 dans un lot sur un ensemble total de 20k - plus d'un million de lignes. Cependant, je n'ai pas testé la différence de performances entre la suppression des lignes mises à jour de la table temporaire et la simple mise à jour du champ [IsUpdate].


Veuillez noter la décision d'utiliser XML sur TVP car il y a au plus 1000 lignes à importer à la fois (mentionnées dans la question):

Si cela est appelé plusieurs fois ici et là, alors le gain de performance mineur dans TVP pourrait ne pas valoir le coût de maintenance supplémentaire (il faut abandonner le proc avant de changer le type de table défini par l'utilisateur, les changements de code d'application, etc.) . Mais si vous importez 4 millions de lignes, en envoyant 1000 à la fois, soit 4000 exécutions (et 4 millions de lignes de XML à analyser, quelle que soit la façon dont elles sont fractionnées), et même une différence de performance mineure lorsqu'elle est exécutée à quelques reprises seulement ajouter à une différence notable.

Cela étant dit, la méthode que j'ai décrite ne change pas en dehors du remplacement de SELECT FROM @XmlInputParam pour être SELECT FROM @TVP. Étant donné que les TVP sont en lecture seule, vous ne pourrez pas les supprimer. Je suppose que vous pouvez simplement ajouter un WHERE NOT EXISTS(SELECT * FROM @UpdateIDs ids WHERE ids.IDField = tmp.IDField)à ce SELECT final (lié à l'INSERT) au lieu du simple WHERE IsUpdate = 0. Si vous deviez utiliser la @UpdateIDsvariable de table de cette manière, vous pourriez même vous en sortir sans vider les lignes entrantes dans la table temporaire.

Solomon Rutzky
la source