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


31

Si j'ai une UPDATEdéclaration 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 à cocher la WHEREclause 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 en termes de performances à utiliser le formulaire UPDATE 1, est-il possible d'obtenir le nombre de lignes dont j'ai besoin?


Voir sqlperformance.com/2012/10/t-sql-queries/conditional-updates (bien que je n'aie pas décrit le cas où aucune valeur ne change).
Aaron Bertrand

Réponses:


24

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 à la MISE À JOUR 1 :

  • pas de mise à jour des lignes (donc rien à écrire sur le disque, pas même une activité minimale du journal), 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 UPDATE () ou COLUMNS_UPDATED ), et que le champ dans les deux INSERTEDet les DELETEDtables ont la même valeur.

En outre, la section récapitulative suivante se trouve dans l'article de Paul White, L'impact des mises à jour sans mise à jour (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 enregistré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 une certaine 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 OUTPUTclause. En capturant exactement quelles lignes ont été mises à jour, vous pouvez affiner les éléments à rechercher pour connaître la différence entre la non-mise à jour des lignes qui n'existent pas et la non-mise à jour des 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 migration 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 ensemble de données dans lequel vous fusionnez 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 affiché 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 serveur SQL. 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 SELECTpremier simple 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 IFet émettre le UPDATE seul si c'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é de verrouillage ). 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'OP), soit environ 66 fois par seconde. Ainsi, la proposition initiale équivaut à 396 opérations de verrouillage / déverrouillage par seconde, alors 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é :-).


14

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, OU vous devrez spécifier à chaque fois le contenu avant et après de chaque champ.

Si vous créez ces instructions de mise à jour de manière dynamique pour chaque table, en ne transmettant que les champs en cours de mise à jour, vous pouvez rapidement rencontrer un problème de pollution de cache de plan similaire au 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 judicieux de faire ce type 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.


1
Mon exemple dans le monde réel est aussi simple que cela, mais il est souvent appelé. Mon estimation est une fois toutes les 15 ms aux heures de pointe. Je me demandais si SQL Server est assez couperet pour ne pas écrire sur le disque quand il n'en a pas besoin.
Martin Brown

3

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 Mises à jour sans mise à jour par Paul White


3

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 fois la mise à jour et l'insérer si elle n'est pas trouvée. Pour MySQL, vous pouvez utiliser INSERT ON DUPLICATE KEY UPDATE .


1

Au lieu de vérifier les valeurs de tous les champs, ne pouvez-vous pas obtenir une valeur de hachage en utilisant les colonnes qui vous intéressent, puis comparer cela 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
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.