Sqlserver
 sql >> Base de données >  >> RDS >> Sqlserver

UPSERT atomique dans SQL Server 2005

INSERT INTO <table>
SELECT <natural keys>, <other stuff...>
FROM <table>
WHERE NOT EXISTS
   -- race condition risk here?
   ( SELECT 1 FROM <table> WHERE <natural keys> )

UPDATE ...
WHERE <natural keys>
  • il y a une condition de concurrence dans le premier INSERT. La clé peut ne pas exister lors de la requête interne SELECT, mais existe au moment de l'INSERT, ce qui entraîne une violation de clé.
  • il y a une condition de concurrence entre INSERT et UPDATE. La clé peut exister lorsqu'elle est cochée dans la requête interne de l'INSERT, mais elle a disparu au moment de l'exécution de UPDATE.

Pour la deuxième condition de concurrence, on pourrait dire que la clé aurait été supprimée de toute façon par le thread concurrent, donc ce n'est pas vraiment une mise à jour perdue.

La solution optimale consiste généralement à essayer le cas le plus probable et à gérer l'erreur en cas d'échec (dans le cadre d'une transaction, bien sûr) :

  • si la clé est probablement manquante, insérez-la toujours en premier. Gérer la violation de contrainte unique, revenir à la mise à jour.
  • si la clé est probablement présente, mettez-la toujours à jour en premier. Insérer si aucune ligne n'a été trouvée. Gérer une éventuelle violation de contrainte unique, revenir à la mise à jour.

Outre l'exactitude, ce modèle est également optimal pour la vitesse :il est plus efficace d'essayer d'insérer et de gérer l'exception que de faire de faux verrouillages. Les blocages signifient des lectures de pages logiques (ce qui peut signifier des lectures de pages physiques), et les E/S (même logiques) sont plus chères que SEH.

Mettre à jour @Pierre

Pourquoi une seule instruction n'est-elle pas "atomique" ? Disons que nous avons une table triviale :

create table Test (id int primary key);

Maintenant, si j'exécutais cette instruction unique à partir de deux threads, dans une boucle, ce serait "atomique", comme vous le dites, une condition de non concurrence peut exister :

  insert into Test (id)
    select top (1) id
    from Numbers n
    where not exists (select id from Test where id = n.id); 

Pourtant, en quelques secondes seulement, une violation de clé primaire se produit :

Msg 2627, Niveau 14, État 1, Ligne 4
Violation de la contrainte PRIMARY KEY 'PK__Test__24927208'. Impossible d'insérer une clé en double dans l'objet 'dbo.Test'.

Pourquoi donc? Vous avez raison de dire que le plan de requête SQL fera la "bonne chose" sur DELETE ... FROM ... JOIN , sur WITH cte AS (SELECT...FROM ) DELETE FROM cte et dans bien d'autres cas. Mais il y a une différence cruciale dans ces cas :la "sous-requête" fait référence à la cible d'une mise à jour ou supprimer opération. Dans de tels cas, le plan de requête utilisera en effet un verrou approprié, en fait, ce comportement est critique dans certains cas, comme lors de la mise en œuvre de files d'attente Utilisation de tables comme files d'attente.

Mais dans la question d'origine, ainsi que dans mon exemple, la sous-requête est vue par l'optimiseur de requête comme une sous-requête dans une requête, et non comme une requête spéciale de type "analyse pour mise à jour" qui nécessite une protection de verrouillage spéciale. Le résultat est que l'exécution de la recherche de sous-requête peut être observée comme une opération distincte par un observateur concurrent , rompant ainsi le comportement "atomique" de l'instruction. À moins que des précautions particulières ne soient prises, plusieurs threads peuvent tenter d'insérer la même valeur, tous deux convaincus qu'ils ont vérifié et que la valeur n'existe pas déjà. Un seul peut réussir, l'autre frappera la violation PK. CQFD.