web-dev-qa-db-fra.com

MISE À JOUR des performances là où aucune donnée ne change

Si j'ai une instruction UPDATE qui ne modifie en fait aucune donnée (car les données sont déjà à l'état mis à jour). Y a-t-il un avantage en termes de performances à mettre une vérification dans la clause WHERE pour empêcher la mise à jour?

Par exemple, y aurait-il une différence de vitesse d'exécution entre UPDATE 1 et UPDATE 2 dans les cas suivants:

CREATE TABLE MyTable (ID int PRIMARY KEY, Value int);
INSERT INTO MyTable (ID, Value)
VALUES
    (1, 1),
    (2, 2),
    (3, 3);

-- UPDATE 1
UPDATE MyTable
SET
    Value = 2
WHERE
    ID = 2
    AND Value <> 2;
SELECT @@ROWCOUNT;

-- UPDATE 2
UPDATE MyTable
SET
    Value = 2
WHERE
    ID = 2;
SELECT @@ROWCOUNT;

DROP TABLE MyTable;

La raison pour laquelle je demande, c'est que j'ai besoin du nombre de lignes pour inclure la ligne inchangée, donc je sais si je dois faire une insertion si l'ID n'existe pas. En tant que tel, j'ai utilisé le formulaire UPDATE 2. S'il y a un avantage de performance à utiliser le formulaire UPDATE 1, est-il possible d'obtenir le nombre de lignes dont j'ai besoin?

32
Martin Brown

Si j'ai une instruction UPDATE qui ne modifie en fait aucune donnée (car les données sont déjà à l'état mis à jour), y a-t-il un avantage en termes de performances à mettre une vérification dans la clause where pour empêcher la mise à jour?

Il pourrait certainement y en avoir car il y a une légère différence de performances due à UPDATE 1:

  • ne pas réellement mettre à jour de lignes (donc rien à écrire sur le disque, pas même une activité de journal minimale), et
  • supprimer les verrous moins restrictifs que ce qui est nécessaire pour effectuer la mise à jour réelle (donc mieux pour la concurrence) ( Veuillez consulter la section Mise à jour vers la fin)

Cependant, vous devez mesurer la différence qu'il y a sur votre système avec votre schéma, vos données et votre charge système. Plusieurs facteurs jouent sur l'impact d'une MISE À JOUR sans mise à jour:

  • la quantité de conflits sur la table en cours de mise à jour
  • le nombre de lignes mises à jour
  • s'il y a des déclencheurs UPDATE sur la table en cours de mise à jour (comme l'a noté Mark dans un commentaire sur la question). Si vous exécutez UPDATE TableName SET Field1 = Field1, un déclencheur de mise à jour se déclenchera et indiquera que le champ a été mis à jour (si vous vérifiez à l'aide des fonctions PDATE () ou COLUMNS_UPDATED ), et que le champ dans les tables INSERTED et DELETED ont la même valeur.

En outre, la section récapitulative suivante se trouve dans l'article de Paul White, The Impact of Non-Updating Updates (comme l'a noté @spaghettidba dans un commentaire sur sa réponse):

SQL Server contient un certain nombre d'optimisations pour éviter la journalisation inutile ou le vidage de page lors du traitement d'une opération UPDATE qui n'entraînera aucune modification de la base de données persistante.

  • Les mises à jour sans mise à jour d'une table en cluster évitent généralement la journalisation supplémentaire et le vidage de page, sauf si une colonne qui forme (une partie de) la clé de cluster est affectée par l'opération de mise à jour.
  • Si une partie de la clé de cluster est "mise à jour" à la même valeur, l'opération est journalisée comme si les données avaient changé et les pages affectées sont marquées comme sales dans le pool de mémoire tampon. Ceci est une conséquence de la conversion de la MISE À JOUR en une opération de suppression puis d'insertion.
  • Les tables de tas se comportent de la même manière que les tables en cluster, sauf qu'elles n'ont pas de clé de cluster pour provoquer une journalisation supplémentaire ou un vidage de page. Cela reste le cas même lorsqu'une clé primaire non clusterisée existe sur le tas. Les mises à jour non mises à jour d'un tas évitent donc généralement la journalisation et le vidage supplémentaires (mais voir ci-dessous).
  • Les tas et les tables en cluster subiront la journalisation et le vidage supplémentaires pour toute ligne où une colonne LOB contenant plus de 8000 octets de données est mise à jour à la même valeur en utilisant une syntaxe autre que "SET nom_colonne = nom_colonne".
  • L'activation simple de l'un ou l'autre type de niveau d'isolation de version de ligne sur une base de données entraîne toujours la journalisation et le vidage supplémentaires. Cela se produit quel que soit le niveau d'isolement en vigueur pour la transaction de mise à jour.

Veuillez garder à l'esprit (surtout si vous ne suivez pas le lien pour voir l'article complet de Paul), les deux éléments suivants:

  1. Les mises à jour non mises à jour ont toujours certaines activité de journal, indiquant qu'une transaction commence et se termine. C'est juste qu'aucune modification des données ne se produit (ce qui est toujours une bonne économie).

  2. Comme je l'ai indiqué ci-dessus, vous devez tester sur votre système. Utilisez les mêmes requêtes de recherche que Paul utilise et voyez si vous obtenez les mêmes résultats. Je vois des résultats légèrement différents sur mon système que ce qui est indiqué dans l'article. Toujours pas de pages sales à écrire, mais un peu plus d'activité de journal.


... J'ai besoin du nombre de lignes pour inclure la ligne inchangée, donc je sais s'il faut faire une insertion si l'ID n'existe pas. ... est-il possible d'obtenir le nombre de lignes dont j'ai besoin?

Simplement, si vous ne traitez qu'avec une seule ligne, vous pouvez effectuer les opérations suivantes:

UPDATE MyTable
SET    Value = 2
WHERE  ID = 2
AND Value <> 2;

IF (@@ROWCOUNT = 0)
BEGIN
  IF (NOT EXISTS(
                 SELECT *
                 FROM   MyTable
                 WHERE  ID = 2 -- or Value = 2 depending on the scenario
                )
     )
  BEGIN
     INSERT INTO MyTable (ID, Value) -- or leave out ID if it is an IDENTITY
     VALUES (2, 2);
  END;
END;

Pour plusieurs lignes, vous pouvez obtenir les informations nécessaires pour prendre cette décision en utilisant la clause OUTPUT. En capturant exactement les lignes qui ont été mises à jour, vous pouvez affiner les éléments à rechercher pour connaître la différence entre ne pas mettre à jour les lignes qui n'existent pas et ne pas mettre à jour les lignes qui existent mais n'ont pas besoin de la mise à jour.

Je montre l'implémentation de base dans la réponse suivante:

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

La méthode indiquée dans cette réponse ne filtre pas les lignes qui existent mais n'ont pas besoin d'être mises à jour. Cette partie pourrait être ajoutée, mais vous devez d'abord montrer exactement où vous obtenez votre jeu de données que vous fusionnez dans MyTable. Viennent-ils d'une table temporaire? Un paramètre table (TVP)?


MISE À JOUR 1:

J'ai finalement pu faire quelques tests et voici ce que j'ai trouvé concernant le journal des transactions et le verrouillage. Tout d'abord, le schéma de la table:

CREATE TABLE [dbo].[Test]
(
  [ID] [int] NOT NULL CONSTRAINT [PK_Test] PRIMARY KEY CLUSTERED,
  [StringField] [varchar](500) NULL
);

Ensuite, le test met à jour le champ à la valeur qu'il a déjà:

UPDATE rt
SET    rt.StringField = '04CF508B-B78E-4264-B9EE-E87DC4AD237A'
FROM   dbo.Test rt
WHERE  rt.ID = 4082117

Résultats:

-- Transaction Log (2 entries):
Operation
----------------------------
LOP_BEGIN_XACT
LOP_COMMIT_XACT


-- SQL Profiler (3 Lock:Acquired events):
Mode            Type
--------------------------------------
8 - IX          5 - OBJECT
8 - IX          6 - PAGE
5 - X           7 - KEY

Enfin, le test qui filtre la mise à jour car la valeur ne change pas:

UPDATE rt
SET    rt.StringField = '04CF508B-B78E-4264-B9EE-E87DC4AD237A'
FROM   dbo.Test rt
WHERE  rt.ID = 4082117
AND    rt.StringField <> '04CF508B-B78E-4264-B9EE-E87DC4AD237A';

Résultats:

-- Transaction Log (0 entries):
Operation
----------------------------


-- SQL Profiler (3 Lock:Acquired events):
Mode            Type
--------------------------------------
8 - IX          5 - OBJECT
7 - IU          6 - PAGE
4 - U           7 - KEY

Comme vous pouvez le voir, rien n'est écrit dans le journal des transactions lors du filtrage de la ligne, contrairement aux deux entrées marquant le début et la fin de la transaction. Et s'il est vrai que ces deux entrées ne sont presque rien, elles sont toujours quelque chose.

De plus, le verrouillage des ressources PAGE et KEY est moins restrictif lors du filtrage des lignes qui n'ont pas changé. Si aucun autre processus n'interagit avec cette table, il s'agit probablement d'un problème (mais quelle est la probabilité, vraiment?). Gardez à l'esprit que ce test montré dans l'un des blogs liés (et même mes tests) suppose implicitement qu'il n'y a pas de conflit sur la table car il ne fait jamais partie des tests. Dire que les mises à jour non mises à jour sont si légères qu'elles ne paient pas pour faire le filtrage doit être pris avec un grain de sel puisque le test a été fait, plus ou moins, dans le vide. Mais en production, ce tableau n'est probablement pas isolé. Bien sûr, il se pourrait très bien que le peu de journalisation et les verrous plus restrictifs ne se traduisent pas par une efficacité moindre. Alors, la source d'information la plus fiable pour répondre à cette question? Serveur SQL. Plus précisément: votre SQL Server. Il vous montrera quelle méthode est la meilleure pour votre système :-).


MISE À JOUR 2:

Si les opérations dans lesquelles la nouvelle valeur est identique à la valeur actuelle (c.-à-d. Pas de mise à jour) sortent le nombre d'opérations dans lesquelles la nouvelle valeur est différente et la mise à jour est nécessaire, alors le modèle suivant pourrait s'avérer encore meilleur, surtout si il y a beaucoup de conflits sur la table. L'idée est de faire un simple SELECT d'abord pour obtenir la valeur actuelle. Si vous n'obtenez pas de valeur, vous avez votre réponse concernant le INSERT. Si vous avez une valeur, vous pouvez faire un simple IF et émettre le UPDATE seulement si cela est nécessaire.

DECLARE @CurrentValue VARCHAR(500) = NULL,
        @NewValue VARCHAR(500) = '04CF508B-B78E-4264-B9EE-E87DC4AD237A',
        @ID INT = 4082117;

SELECT @CurrentValue = rt.StringField
FROM   dbo.Test rt
WHERE  rt.ID = @ID;

IF (@CurrentValue IS NULL) -- if NULL is valid, use @@ROWCOUNT = 0
BEGIN
  -- row does not exist
  INSERT INTO dbo.Test (ID, StringField)
  VALUES (@ID, @NewValue);
END;
ELSE
BEGIN
  -- row exists, so check value to see if it is different
  IF (@CurrentValue <> @NewValue)
  BEGIN
    -- value is different, so do the update
    UPDATE rt
    SET    rt.StringField = @NewValue
    FROM   dbo.Test rt
    WHERE  rt.ID = @ID;
  END;
END;

Résultats:

-- Transaction Log (0 entries):
Operation
----------------------------


-- SQL Profiler (2 Lock:Acquired events):
Mode            Type
--------------------------------------
6 - IS          5 - OBJECT
6 - IS          6 - PAGE

Il n'y a donc que 2 verrous acquis au lieu de 3, et ces deux verrous sont partagés intentionnellement, pas Intention eXclusive ou mise à jour d'intention ( Compatibilité des verrous ). En gardant à l'esprit que chaque verrou acquis sera également libéré, chaque verrou est en réalité 2 opérations, donc cette nouvelle méthode est un total de 4 opérations au lieu des 6 opérations de la méthode initialement proposée. Considérant que cette opération s'exécute une fois toutes les 15 ms (environ, comme indiqué par l'O.P.), soit environ 66 fois par seconde. Ainsi, la proposition initiale équivaut à 396 opérations de verrouillage/déverrouillage par seconde, tandis que cette nouvelle méthode ne représente que 264 opérations de verrouillage/déverrouillage par seconde de verrous encore plus légers. Ce n'est pas une garantie de performances impressionnantes, mais cela vaut certainement la peine d'être testé :-).

24
Solomon Rutzky

Zoomez un peu et pensez à l'image plus grande. Dans le monde réel, votre déclaration de mise à jour ressemblera-t-elle vraiment à ceci:

UPDATE MyTable
  SET Value = 2
WHERE
     ID = 2
     AND Value <> 2;

Ou est-ce que ça va ressembler davantage à ceci:

UPDATE Customers
  SET AddressLine1 = '123 Main St',
      AddressLine2 = 'Apt 24',
      City = 'Chicago',
      State = 'IL',
      (and a couple dozen more fields)
WHERE
     ID = 2
     AND (AddressLine1 <> '123 Main St'
     OR AddressLine2 <> 'Apt 24'
     OR City <> 'Chicago'
     OR State <> 'IL'
      (and a couple dozen more fields))

Parce que dans le monde réel, les tableaux ont beaucoup de colonnes. Cela signifie que vous devrez générer beaucoup de logique d'application dynamique complexe pour créer des chaînes dynamiques, OR vous devrez spécifier le contenu avant et après de chaque champ, chaque temps.

Si vous créez ces instructions de mise à jour de manière dynamique pour chaque table, en ne passant que les champs en cours de mise à jour, vous pouvez rapidement rencontrer un problème de pollution de cache de plan similaire à le problème de taille des paramètres NHibernate il y a quelques années . Pire encore, si vous générez les instructions de mise à jour dans SQL Server (comme dans les procédures stockées), vous brûlerez de précieux cycles CPU car SQL Server n'est pas très efficace pour concaténer des chaînes ensemble à grande échelle.

En raison de ces complexités, il n'est généralement pas logique de faire ce genre de comparaison ligne par ligne, champ par champ pendant que vous effectuez les mises à jour. Pensez plutôt aux opérations basées sur des ensembles.

14
Brent Ozar

Vous pouvez constater un gain de performances en sautant des lignes qui n'ont pas besoin d'être mises à jour uniquement lorsque le nombre de lignes est important (moins de journalisation, moins de pages sales à écrire sur le disque).

Lorsque vous traitez des mises à jour sur une seule ligne comme dans votre cas, la différence de performances est complètement négligeable. Si, dans tous les cas, la mise à jour des lignes vous facilite la tâche, faites-le.

Pour plus d'informations sur le sujet, voir Non Updating Updates par Paul White

3
spaghettidba

Vous pouvez combiner la mise à jour et l'insérer dans une seule instruction. Sur SQL Server, vous pouvez utiliser une instruction MERGE pour effectuer la mise à jour et l'insérer si elle n'est pas trouvée. Pour MySQL, vous pouvez utiliser INSERT ON DUPLICATE KEY UPDATE .

3
Russell Harkins

Au lieu de vérifier les valeurs de tous les champs, vous ne pouvez pas obtenir une valeur de hachage en utilisant les colonnes qui vous intéressent, puis la comparer au hachage stocké par rapport à la ligne du tableau?

IF EXISTS (Select 1 from Table where ID =@ID AND HashValue=Sha256(column1+column2))
GOTO EXIT
ELSE
1
Ruchira Liyanagama