Insérer la procédure stockée de mise à jour sur SQL Server

104

J'ai écrit un proc stocké qui fera une mise à jour si un enregistrement existe, sinon il fera une insertion. Cela ressemble à quelque chose comme ceci:

update myTable set Col1=@col1, Col2=@col2 where ID=@ID
if @@rowcount = 0
insert into myTable (Col1, Col2) values (@col1, @col2)

Ma logique derrière l'écriture de cette manière est que la mise à jour effectuera une sélection implicite en utilisant la clause where et si cela renvoie 0, l'insertion aura lieu.

L'alternative à faire de cette façon serait de faire une sélection, puis en fonction du nombre de lignes renvoyées, faire une mise à jour ou une insertion. Cela a été jugé inefficace car si vous devez faire une mise à jour, cela entraînera 2 sélections (le premier appel de sélection explicite et le second implicite dans le lieu de la mise à jour). Si le proc faisait un insert, il n'y aurait aucune différence d'efficacité.

Ma logique est-elle valable ici? Est-ce ainsi que vous combineriez une insertion et une mise à jour dans un processus stocké?

Gars
la source

Réponses:

61

Votre hypothèse est juste, c'est la façon optimale de le faire et cela s'appelle upsert / merge .

Importance d'UPSERT - de sqlservercentral.com :

Pour chaque mise à jour dans le cas mentionné ci-dessus, nous supprimons une lecture supplémentaire du tableau si nous utilisons UPSERT au lieu de EXISTS. Malheureusement pour une insertion, les méthodes UPSERT et IF EXISTS utilisent le même nombre de lectures sur la table. Par conséquent, la vérification de l'existence ne doit être effectuée que lorsqu'il existe une raison très valable pour justifier les E / S supplémentaires. La manière optimisée de faire les choses est de s'assurer que vous avez le moins de lectures possible sur la base de données.

La meilleure stratégie consiste à tenter la mise à jour. Si aucune ligne n'est affectée par la mise à jour, insérez. Dans la plupart des cas, la ligne existera déjà et une seule E / S sera requise.

Modifier : veuillez consulter cette réponse et l'article de blog lié pour en savoir plus sur les problèmes liés à ce modèle et sur la manière de le faire fonctionner en toute sécurité.

binOr
la source
1
Eh bien, il a répondu au moins à une question, je pense. Et je n'ai pas ajouté de code parce que le code de la question me semblait déjà correct. Bien que je le mette dans une transaction, je n'ai pas pris en compte le niveau d'isolement pour la mise à jour. Merci de l'avoir signalé dans votre réponse!
binOr
54

Veuillez lire l' article sur mon blog pour un bon modèle sûr que vous pouvez utiliser. Il y a beaucoup de considérations et la réponse acceptée à cette question est loin d'être sûre.

Pour une réponse rapide, essayez le modèle suivant. Cela fonctionnera bien sur SQL 2000 et au-dessus. SQL 2005 vous donne une gestion des erreurs qui ouvre d'autres options et SQL 2008 vous donne une commande MERGE.

begin tran
   update t with (serializable)
   set hitCount = hitCount + 1
   where pk = @id
   if @@rowcount = 0
   begin
      insert t (pk, hitCount)
      values (@id,1)
   end
commit tran
Sam Safran
la source
1
Dans votre article de blog, vous concluez en utilisant l'indication WITH (updlock, serializable) dans le contrôle d'existence. Toutefois, la lecture de MSDN indique: "UPDLOCK - Spécifie que les verrous de mise à jour doivent être pris et maintenus jusqu'à ce que la transaction se termine." Cela signifie-t-il que l'indication sérialisable est superflue car le verrou de mise à jour sera de toute façon conservé pour le reste de la transaction, ou ai-je mal compris quelque chose?
Dan Def
10

S'il doit être utilisé avec SQL Server 2000/2005, le code d'origine doit être inclus dans la transaction pour s'assurer que les données restent cohérentes dans le scénario simultané.

BEGIN TRANSACTION Upsert
update myTable set Col1=@col1, Col2=@col2 where ID=@ID
if @@rowcount = 0
insert into myTable (Col1, Col2) values (@col1, @col2)
COMMIT TRANSACTION Upsert

Cela entraînera des coûts de performance supplémentaires, mais garantira l'intégrité des données.

Ajouter, comme déjà suggéré, MERGE doit être utilisé le cas échéant.

Dima Malenko
la source
8

MERGE est d'ailleurs l'une des nouvelles fonctionnalités de SQL Server 2008.

Jon Galloway
la source
et vous devriez absolument l'utiliser plutôt que ce non-sens homebrew difficile à lire. Le bon exemple est ici - mssqltips.com/sqlservertip/1704/…
Rich Bryant
6

Vous devez non seulement l'exécuter en transaction, mais également un niveau d'isolation élevé. En fait, le niveau d'isolement par défaut est Read Commited et ce code doit être sérialisable.

SET transaction isolation level SERIALIZABLE
BEGIN TRANSACTION Upsert
UPDATE myTable set Col1=@col1, Col2=@col2 where ID=@ID
if @@rowcount = 0
  begin
    INSERT into myTable (ID, Col1, Col2) values (@ID @col1, @col2)
  end
COMMIT TRANSACTION Upsert

Peut-être que l'ajout de la vérification et de la restauration des erreurs @@ pourrait être une bonne idée.

Tomas Tintera
la source
@Munish Goyal Parce que dans la base de données, plusieurs commandes et précédures s'exécutent en parallèle. Ensuite, un autre thread peut insérer une ligne juste après l'exécution de la mise à jour et avant l'exécution de l'insertion.
Tomas Tintera
5

Si vous n'effectuez pas de fusion dans SQL 2008, vous devez le changer en:

si @@ rowcount = 0 et @@ error = 0

sinon, si la mise à jour échoue pour une raison quelconque, elle essaiera et une insertion par la suite car le nombre de lignes sur une instruction ayant échoué est 0

Simon Munro
la source
3

Grand fan de l'UPSERT, réduit vraiment le code à gérer. Voici une autre façon de le faire: l'un des paramètres d'entrée est ID, si l'ID est NULL ou 0, vous savez que c'est un INSERT, sinon c'est une mise à jour. Suppose que l'application sait s'il y a un ID, donc ne fonctionnera pas dans toutes les situations, mais coupera les exécutions de moitié si vous le faites.

Natron
la source
2

Message de Dima Malenko modifié:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 

BEGIN TRANSACTION UPSERT 

UPDATE MYTABLE 
SET    COL1 = @col1, 
       COL2 = @col2 
WHERE  ID = @ID 

IF @@rowcount = 0 
  BEGIN 
      INSERT INTO MYTABLE 
                  (ID, 
                   COL1, 
                   COL2) 
      VALUES      (@ID, 
                   @col1, 
                   @col2) 
  END 

IF @@Error > 0 
  BEGIN 
      INSERT INTO MYERRORTABLE 
                  (ID, 
                   COL1, 
                   COL2) 
      VALUES      (@ID, 
                   @col1, 
                   @col2) 
  END 

COMMIT TRANSACTION UPSERT 

Vous pouvez intercepter l'erreur et envoyer l'enregistrement vers une table d'insertion ayant échoué.
J'avais besoin de le faire parce que nous prenons toutes les données envoyées via WSDL et si possible les réparons en interne.

thughes78013
la source
1

Votre logique semble saine, mais vous voudrez peut-être envisager d'ajouter du code pour empêcher l'insertion si vous avez passé une clé primaire spécifique.

Sinon, si vous effectuez toujours une insertion si la mise à jour n'a affecté aucun enregistrement, que se passe-t-il lorsque quelqu'un supprime l'enregistrement avant l'exécution de "UPSERT"? Maintenant, l'enregistrement que vous essayez de mettre à jour n'existe pas, il va donc créer un enregistrement à la place. Ce n'est probablement pas le comportement que vous recherchiez.

Kevin Fairchild
la source