web-dev-qa-db-fra.com

SI EXISTE avant INSÉRER, METTRE À JOUR, SUPPRIMER pour l'optimisation

Il y a assez souvent une situation où vous devez exécuter l'instruction INSERT, UPDATE ou DELETE en fonction d'une condition. Et ma question est de savoir si l'effet sur les performances de la requête ajoute IF EXISTS avant la commande.

Exemple

IF EXISTS(SELECT 1 FROM Contacs WHERE [Type] = 1)
    UPDATE Contacs SET [Deleted] = 1 WHERE [Type] = 1

Qu'en est-il des insertions ou suppressions?

36
Ed Gomoliako

Je ne suis pas tout à fait sûr, mais j'ai l'impression que cette question concerne vraiment upsert, qui est l'opération atomique suivante:

  • Si la ligne existe à la fois dans la source et dans la cible, UPDATE la cible;
  • Si la ligne n'existe que dans la source, INSERT la ligne dans la cible;
  • (Facultatif) Si la ligne existe dans la cible mais pas la source, DELETE la ligne de la cible.

Les développeurs devenus DBA écrivent souvent naïvement ligne par ligne, comme ceci:

-- For each row in source
IF EXISTS(<target_expression>)
    IF @delete_flag = 1
        DELETE <target_expression>
    ELSE
        UPDATE target
        SET <target_columns> = <source_values>
        WHERE <target_expression>
ELSE
    INSERT target (<target_columns>)
    VALUES (<source_values>)

C'est à peu près la pire chose que vous puissiez faire, pour plusieurs raisons:

  • Il a une condition de course. La ligne peut disparaître entre IF EXISTS et les DELETE ou UPDATE suivants.

  • C'est du gaspillage. Pour chaque transaction, vous avez une opération supplémentaire en cours; c'est peut-être trivial, mais cela dépend entièrement de la qualité de votre indexation.

  • Le pire de tout - c'est de suivre un modèle itératif, en pensant à ces problèmes au niveau d'une seule rangée. Cela aura le plus (le pire) impact sur la performance globale.

Une optimisation très mineure (et j'insiste sur une mineure) consiste à essayer de toute façon le UPDATE; si la ligne n'existe pas, @@ROWCOUNT sera 0 et vous pourrez ensuite insérer "en toute sécurité":

-- For each row in source
BEGIN TRAN

UPDATE target
SET <target_columns> = <source_values>
WHERE <target_expression>

IF (@@ROWCOUNT = 0)
    INSERT target (<target_columns>)
    VALUES (<source_values>)

COMMIT

Le pire des cas, cela effectuera toujours deux opérations pour chaque transaction, mais au moins il y a chance d'en effectuer une seule, et cela élimine également la condition de concurrence (sorte de).

Mais le vrai problème est que cela se fait toujours pour chaque ligne de la source.

Avant SQL Server 2008, vous deviez utiliser un modèle maladroit en 3 étapes pour gérer cela au niveau défini (toujours mieux que ligne par ligne):

BEGIN TRAN

INSERT target (<target_columns>)
SELECT <source_columns> FROM source s
WHERE s.id NOT IN (SELECT id FROM target)

UPDATE t SET <target_columns> = <source_columns>
FROM target t
INNER JOIN source s ON t.d = s.id

DELETE t
FROM target t
WHERE t.id NOT IN (SELECT id FROM source)

COMMIT

Comme je l'ai dit, les performances étaient plutôt nulles à ce sujet, mais toujours bien meilleures que l'approche une ligne à la fois. Cependant, SQL Server 2008 a finalement introduit la syntaxe MERGE , alors maintenant tout ce que vous avez à faire est la suivante:

MERGE target
USING source ON target.id = source.id
WHEN MATCHED THEN UPDATE <target_columns> = <source_columns>
WHEN NOT MATCHED THEN INSERT (<target_columns>) VALUES (<source_columns>)
WHEN NOT MATCHED BY SOURCE THEN DELETE;

C'est tout. Une déclaration. Si vous utilisez SQL Server 2008 et devez effectuer n'importe quelle séquence de INSERT, UPDATE et DELETE selon que la ligne existe déjà ou non - même s'il ne s'agit que d'une seule ligne - il n'y a aucune excuse pour ne pas utiliser MERGE.

Vous pouvez même OUTPUT les lignes affectées par un MERGE dans une variable de table si vous avez besoin de découvrir par la suite ce qui a été fait. Simple, rapide et sans risque. Fais le.

72
Aaronaught

Cela n'est pas utile pour une seule mise à jour/suppression/insertion.
Ajoute éventuellement des performances si plusieurs opérateurs après la condition if.
Dans le dernier cas, mieux écrire

update a set .. where ..
if @@rowcount > 0 
begin
    ..
end
8
burnall

Ni

UPDATE … IF (@@ROWCOUNT = 0) INSERT

ni

IF EXISTS(...) UPDATE ELSE INSERT

les modèles fonctionnent comme prévu sous une concurrence élevée. Les deux peuvent échouer. Les deux peuvent échouer très fréquemment. MERGE est le roi - il résiste beaucoup mieux. Faisons des tests de résistance et voyons par nous-mêmes.

Voici le tableau que nous utiliserons:

CREATE TABLE dbo.TwoINTs
    (
      ID INT NOT NULL PRIMARY KEY,
      i1 INT NOT NULL ,
      i2 INT NOT NULL ,
      version ROWVERSION
    ) ;
GO

INSERT  INTO dbo.TwoINTs
        ( ID, i1, i2 )
VALUES  ( 1, 0, 0 ) ;    

SI EXISTE (…) ALORS le schéma échoue fréquemment sous une concurrence élevée.

Insérons ou mettons à jour des lignes dans une boucle en utilisant la logique simple suivante: si une ligne avec un ID donné existe, mettez-la à jour et sinon insérez-en une nouvelle. La boucle suivante implémente cette logique. Coupez-le et collez-le dans deux onglets, passez en mode texte dans les deux onglets et exécutez-les simultanément.

-- hit Ctrl+T to execute in text mode

SET NOCOUNT ON ;

DECLARE @ID INT ;

SET @ID = 0 ;
WHILE @ID > -100000
    BEGIN ;
        SET @ID = ( SELECT  MIN(ID)
                    FROM    dbo.TwoINTs
                  ) - 1 ;
        BEGIN TRY ;

            BEGIN TRANSACTION ;
            IF EXISTS ( SELECT  *
                        FROM    dbo.TwoINTs
                        WHERE   ID = @ID )
                BEGIN ;
                    UPDATE  dbo.TwoINTs
                    SET     i1 = 1
                    WHERE   ID = @ID ;
                END ;
            ELSE
                BEGIN ;
                    INSERT  INTO dbo.TwoINTs
                            ( ID, i1, i2 )
                    VALUES  ( @ID, 0, 0 ) ;
                END ;
            COMMIT ; 
        END TRY
        BEGIN CATCH ;
            ROLLBACK ; 
            SELECT  error_message() ;
        END CATCH ;
    END ; 

Lorsque nous exécutons ce script simultanément dans deux onglets, nous obtiendrons immédiatement une énorme quantité de violations de clé primaire dans les deux onglets. Cela montre à quel point le modèle IF EXISTS n'est pas fiable lorsqu'il s'exécute sous une concurrence élevée.

Remarque: cet exemple montre également qu'il n'est pas sûr d'utiliser SELECT MAX (ID) +1 ou SELECT MIN (ID) -1 comme prochaine valeur unique disponible si nous le faisons sous accès simultané.

4
A-K

Vous ne devriez pas le faire pour UPDATE et DELETE, comme s'il y avait impact sur les performances, c'est pas positif one.

Pour INSERT il peut y avoir des situations où votre INSERT lèvera une exception (UNIQUE CONSTRAINT violation etc), auquel cas vous souhaiterez peut-être l'empêcher avec le IF EXISTS et le gérer plus gracieusement.

4
van

Vous ne devriez pas faire cela dans la plupart des cas. Selon votre niveau de transaction, vous avez créé une condition de concurrence, maintenant dans votre exemple ici, cela n'aurait pas trop d'importance, mais les données peuvent être modifiées de la première sélection à la mise à jour. Et tout ce que vous avez fait est de forcer SQL à faire plus de travail

La meilleure façon de savoir avec certitude est de tester les deux différences et de voir laquelle vous donne les performances appropriées.

3
JoshBerke

IF EXISTS fera essentiellement un SELECT - le même que UPDATE.

En tant que tel, cela diminuera les performances - s'il n'y a rien à mettre à jour, vous avez fait le même travail (UPDATE aurait demandé le même manque de lignes que votre sélection) et s'il y a quelque chose à mettre à jour, vous juet a fait une sélection inutile.

3
DVK

La performance d'un IF EXISTS déclaration:

IF EXISTS(SELECT 1 FROM mytable WHERE someColumn = someValue)

dépend des index présents pour satisfaire la requête.

2
Mitch Wheat

Il y a un léger effet, puisque vous effectuez deux fois la même vérification, au moins dans votre exemple:

IF EXISTS(SELECT 1 FROM Contacs WHERE [Type] = 1)

Doit interroger, voir s'il y en a, si vrai alors:

UPDATE Contacs SET [Deleted] = 1 WHERE [Type] = 1

Doit interroger, voir lesquels ... même vérifier deux fois sans raison. Maintenant, si la condition que vous recherchez est indexée, cela devrait être rapide, mais pour les grandes tables, vous pouvez voir un certain retard simplement parce que vous exécutez la sélection.

2
Nick Craver

Cela répète largement les réponses précédentes (par le temps) à cinq (non, six) (non, sept) réponses, mais:

Oui, la structure IF EXISTS que vous avez dans l'ensemble doublera le travail effectué par la base de données. Bien que IF EXISTS "s'arrête" quand il trouve la première ligne correspondante (il n'a pas besoin de toutes les trouver), c'est toujours un effort supplémentaire et finalement inutile - pour les mises à jour et les suppressions.

  • Si aucune ligne de ce type n'existe, IF EXISTS effectuera une analyse complète (table ou index) pour le déterminer.
  • Si une ou plusieurs de ces lignes existent, IF EXISTS lira suffisamment de la table/de l'index pour trouver la première, puis UPDATE ou DELETE relira ensuite cette table pour la retrouver et la traiter - et elle lira "le reste" du tableau pour voir s'il y en a d'autres à traiter également. (Assez rapide si correctement indexé, mais quand même.)

Donc, dans tous les cas, vous finirez par lire la table ou l'index entier au moins une fois. Mais, pourquoi s'embêter avec les IF EXISTS en premier lieu?

UPDATE Contacs SET [Deleted] = 1 WHERE [Type] = 1 

ou le DELETE similaire fonctionnera correctement qu'il y ait ou non des lignes à traiter. Aucune ligne, table numérisée, rien de modifié, vous avez terminé; 1+ lignes, table numérisée, tout ce qui devrait être modifié, refait. Un passage, pas de bruit, pas de bruit, pas besoin de s'inquiéter "la base de données a-t-elle été modifiée par un autre utilisateur entre ma première requête et ma deuxième requête".

INSERT est la situation où cela peut être utile - vérifiez si la ligne est présente avant de l'ajouter, pour éviter les violations de clé primaire ou unique. Bien sûr, vous devez vous soucier de la concurrence - et si quelqu'un d'autre essaie d'ajouter cette ligne en même temps que vous? Envelopper tout cela dans un seul INSERT gérerait tout cela dans une transaction implicite (rappelez-vous vos propriétés ACID!):

INSERT Contacs (col1, col2, etc) values (val1, val2, etc) where not exists (select 1 from Contacs where col1 = val1)
IF @@rowcount = 0 then <didn't insert, process accordingly>
2
Philip Kelley

Oui, cela affectera les performances (le degré auquel les performances seront affectées sera affecté par un certain nombre de facteurs). En fait, vous effectuez la même requête "deux fois" (dans votre exemple). Demandez-vous si vous avez besoin d'être aussi défensif dans votre requête et dans quelles situations la ligne ne serait-elle pas là? De plus, avec une instruction de mise à jour, les lignes affectées sont probablement un meilleur moyen de déterminer si quelque chose a été mis à jour.

1
bleeeah

Si vous utilisez MySQL, vous pouvez utiliser insérer ... en double .

0
charles
IF EXISTS....UPDATE

Ne le fais pas. Il force deux scans/recherches au lieu d'un.

Si update ne trouve pas de correspondance sur la clause WHERE, le coût de l'instruction update est juste une recherche/scan.

S'il trouve une correspondance et si vous la faites précéder de SI EXISTE, il doit trouver la même correspondance deux fois. Et dans un environnement simultané, ce qui était vrai pour les EXISTS peut ne plus être vrai pour la MISE À JOUR.

C'est précisément pourquoi les instructions UPDATE/DELETE/INSERT autorisent une clause WHERE. Utilise le!

0
Peter Radocchia