web-dev-qa-db-fra.com

Solutions pour INSERT OR UPDATE sur SQL Server

Supposons une structure de table de MyTable(KEY, datafield1, datafield2...).

Souvent, je souhaite mettre à jour un enregistrement existant ou insérer un nouvel enregistrement s'il n'existe pas.

Essentiellement:

IF (key exists)
  run update command
ELSE
  run insert command

Quelle est la manière la plus performante d’écrire cela?

547
Chris Cudmore

n'oubliez pas les transactions. La performance est bonne, mais l’approche simple (SI EXISTE ..) est très dangereuse.
Lorsque plusieurs threads essaient d'exécuter une insertion ou une mise à jour, vous pouvez facilement obtenir une violation de clé primaire.

Les solutions fournies par @Beau Crawford & @Esteban montrent une idée générale mais sujette aux erreurs.

Pour éviter les blocages et les violations de PK, vous pouvez utiliser quelque chose comme ceci:

begin tran
if exists (select * from table with (updlock,serializable) where key = @key)
begin
   update table set ...
   where key = @key
end
else
begin
   insert into table (key, ...)
   values (@key, ...)
end
commit tran

ou

begin tran
   update table with (serializable) set ...
   where key = @key

   if @@rowcount = 0
   begin
      insert into table (key, ...) values (@key,..)
   end
commit tran
346
aku

Voir mon réponse détaillée à une question précédente très similaire

@ Beau Crawford's est un bon moyen dans SQL 2005 et inférieur, bien que si vous accordez un représentant, il devrait aller à le premier type à SO it . Le seul problème est que, pour les insertions, il reste encore deux IO opérations.

MS Sql2008 introduit merge à partir du standard SQL: 2003:

merge tablename with(HOLDLOCK) as target
using (values ('new value', 'different value'))
    as source (field1, field2)
    on target.idfield = 7
when matched then
    update
    set field1 = source.field1,
        field2 = source.field2,
        ...
when not matched then
    insert ( idfield, field1, field2, ... )
    values ( 7,  source.field1, source.field2, ... )

Maintenant, il ne s’agit que d’une opération IO, mais d’un code affreux :-(

368
Keith

Faites un UPSERT:

 UPDATE MyTable SET FieldA = @ FieldA WHERE Clé = @ Key 
 
 IF @@ ROWCOUNT = 0 
 INSÉRER DANS MyTable (FieldA) VALEURS (@FieldA) 

http://en.wikipedia.org/wiki/Upsert

159
Beau Crawford

Beaucoup de gens vous suggéreront d'utiliser MERGE, mais je vous le déconseille. Par défaut, cela ne vous protège pas plus que de multiples déclarations de la concurrence et de la concurrence, mais cela présente d'autres dangers:

http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/

Même avec cette syntaxe "plus simple" disponible, je préfère quand même cette approche (la gestion des erreurs est omise pour des raisons de brièveté):

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
UPDATE dbo.table SET ... WHERE PK = @PK;
IF @@ROWCOUNT = 0
BEGIN
  INSERT dbo.table(PK, ...) SELECT @PK, ...;
END
COMMIT TRANSACTION;

Beaucoup de gens suggéreront de cette façon:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
IF EXISTS (SELECT 1 FROM dbo.table WHERE PK = @PK)
BEGIN
  UPDATE ...
END
ELSE
  INSERT ...
END
COMMIT TRANSACTION;

Mais tout ce que cela fait, c’est que vous devrez peut-être lire le tableau deux fois pour localiser la ou les lignes à mettre à jour. Dans le premier exemple, il vous suffira de localiser une ou plusieurs lignes. (Dans les deux cas, si aucune ligne n'est trouvée lors de la lecture initiale, une insertion est effectuée.)

D'autres suggéreront de cette façon:

BEGIN TRY
  INSERT ...
END TRY
BEGIN CATCH
  IF ERROR_NUMBER() = 2627
    UPDATE ...
END CATCH

Toutefois, cela pose problème si, pour la seule raison qui précède, laisser les exceptions de capture SQL Server que vous auriez pu éviter en premier lieu est beaucoup plus coûteux, sauf dans le cas rare où presque toutes les insertions échouent. Je prouve autant ici:

82
Aaron Bertrand
IF EXISTS (SELECT * FROM [Table] WHERE ID = rowID)
UPDATE [Table] SET propertyOne = propOne, property2 . . .
ELSE
INSERT INTO [Table] (propOne, propTwo . . .)

Edit:

Hélas, même à mon propre détriment, je dois admettre que les solutions qui permettent de le faire sans choix semblent être meilleures puisqu'elles accomplissent la tâche avec un pas de moins.

54
Esteban Araya

Si vous souhaitez activer plus d'un enregistrement à la fois, vous pouvez utiliser l'instruction ANSI SQL: 2003 DML MERGE.

MERGE INTO table_name WITH (HOLDLOCK) USING table_name ON (condition)
WHEN MATCHED THEN UPDATE SET column1 = value1 [, column2 = value2 ...]
WHEN NOT MATCHED THEN INSERT (column1 [, column2 ...]) VALUES (value1 [, value2 ...])

Départ imitation de l'instruction MERGE dans SQL Server 2005 .

36
Eric Weilnau

Bien qu'il soit assez tard pour commenter, je souhaite ajouter un exemple plus complet utilisant MERGE.

Ces instructions Insert + Update sont généralement appelées instructions "Upsert" et peuvent être implémentées à l'aide de MERGE dans SQL Server.

Un très bon exemple est donné ici: http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx

Ce qui précède explique également les scénarios de verrouillage et de concurrence.

Je citerai le même pour référence:

ALTER PROCEDURE dbo.Merge_Foo2
      @ID int
AS

SET NOCOUNT, XACT_ABORT ON;

MERGE dbo.Foo2 WITH (HOLDLOCK) AS f
USING (SELECT @ID AS ID) AS new_foo
      ON f.ID = new_foo.ID
WHEN MATCHED THEN
    UPDATE
            SET f.UpdateSpid = @@SPID,
            UpdateTime = SYSDATETIME()
WHEN NOT MATCHED THEN
    INSERT
      (
            ID,
            InsertSpid,
            InsertTime
      )
    VALUES
      (
            new_foo.ID,
            @@SPID,
            SYSDATETIME()
      );

RETURN @@ERROR;
10
user243131
/*
CREATE TABLE ApplicationsDesSocietes (
   id                   INT IDENTITY(0,1)    NOT NULL,
   applicationId        INT                  NOT NULL,
   societeId            INT                  NOT NULL,
   suppression          BIT                  NULL,
   CONSTRAINT PK_APPLICATIONSDESSOCIETES PRIMARY KEY (id)
)
GO
--*/

DECLARE @applicationId INT = 81, @societeId INT = 43, @suppression BIT = 0

MERGE dbo.ApplicationsDesSocietes WITH (HOLDLOCK) AS target
--set the SOURCE table one row
USING (VALUES (@applicationId, @societeId, @suppression))
    AS source (applicationId, societeId, suppression)
    --here goes the ON join condition
    ON target.applicationId = source.applicationId and target.societeId = source.societeId
WHEN MATCHED THEN
    UPDATE
    --place your list of SET here
    SET target.suppression = source.suppression
WHEN NOT MATCHED THEN
    --insert a new line with the SOURCE table one row
    INSERT (applicationId, societeId, suppression)
    VALUES (source.applicationId, source.societeId, source.suppression);
GO

Remplacez les noms de table et de champ par tout ce dont vous avez besoin. Prenez soin de la condition en utilisant ON. Définissez ensuite la valeur (et le type) appropriés pour les variables de la ligne DECLARE.

À votre santé.

8
Denver

Vous pouvez utiliser l'instruction MERGE. Cette instruction permet d'insérer des données si elles n'existent pas ou de se mettre à jour si elles existent.

MERGE INTO Employee AS e
using EmployeeUpdate AS eu
ON e.EmployeeID = eu.EmployeeID`
7
Daniel Acosta

Si vous insérez la route UPDATE si-pas-lignes-mis à jour, puis INSERT, envisagez de commencer par la commande INSERT pour éviter une condition de concurrence (en supposant qu'il n'y ait pas de SUPPRESSION intervenant)

INSERT INTO MyTable (Key, FieldA)
   SELECT @Key, @FieldA
   WHERE NOT EXISTS
   (
       SELECT *
       FROM  MyTable
       WHERE Key = @Key
   )
IF @@ROWCOUNT = 0
BEGIN
   UPDATE MyTable
   SET FieldA=@FieldA
   WHERE Key=@Key
   IF @@ROWCOUNT = 0
   ... record was deleted, consider looping to re-run the INSERT, or RAISERROR ...
END

Outre le fait d'éviter une condition de concurrence critique, si dans la plupart des cas l'enregistrement existait déjà, cela entraînerait l'échec de l'INSERT, gaspillant le processeur.

L'utilisation de MERGE est probablement préférable pour SQL2008 et les versions ultérieures.

4
Kristen

Cela dépend du modèle d'utilisation. Il faut examiner la situation dans son ensemble sans se perdre dans les détails. Par exemple, si le modèle d'utilisation correspond à 99% de mises à jour après la création de l'enregistrement, l'option "UPSERT" constitue la meilleure solution.

Après la première insertion (hit), ce sera toutes les mises à jour de déclaration simples, pas de si. La condition "où" sur l'insert est nécessaire, sinon les doublons seront insérés et vous ne voudrez pas vous occuper du verrouillage.

UPDATE <tableName> SET <field>=@field WHERE key=@key;

IF @@ROWCOUNT = 0
BEGIN
   INSERT INTO <tableName> (field)
   SELECT @field
   WHERE NOT EXISTS (select * from tableName where key = @key);
END
3
Saleh Najar

Dans SQL Server 2008, vous pouvez utiliser l'instruction MERGE.

3
Bart

MS SQL Server 2008 introduit l'instruction MERGE, qui, je crois, fait partie de la norme SQL: 2003. Comme beaucoup l'ont montré, ce n'est pas un problème de gérer les cas d'une ligne, mais pour traiter de grands ensembles de données, il faut un curseur, avec tous les problèmes de performances qui se posent. La déclaration MERGE sera très appréciée lorsqu’il s’agit de grands ensembles de données.

2
bjorsig

Avant que tout le monde ne se lance dans HOLDLOCK-s par peur de ces nafariants utilisateurs exécutant directement vos sprocs :-), permettez-moi de souligner que vous devez garantir l'unicité du nouveau PK-s de par leur conception (clés d'identité, séquence générateurs dans Oracle, index uniques pour les ID externes, requêtes couvertes par des index). C'est l'alpha et l'oméga de la question. Si vous ne le possédez pas, aucun HOLDLOCK-s de l'univers ne vous sauvera et si vous le possédez, vous n'aurez besoin de rien au-delà de UPDLOCK lors de la première sélection (ou d'utiliser d'abord la mise à jour).

Les sprocs fonctionnent normalement dans des conditions très contrôlées et avec l'hypothèse d'un appelant de confiance (niveau intermédiaire). Cela signifie que si un simple motif upsert (mise à jour + insertion ou fusion) voit jamais un PK en double, cela signifie un bogue dans votre conception de niveau intermédiaire ou de table et il est bon que SQL crie une erreur dans ce cas et rejette l'enregistrement. Dans ce cas, placer un HOLDLOCK équivaut à manger des exceptions et à prendre des données potentiellement erronées, en plus de réduire vos performances.

Cela dit, utiliser MERGE ou UPDATE then INSERT est plus facile sur votre serveur et moins sujet aux erreurs car vous n’avez pas à vous rappeler d’ajouter (UPDLOCK) à la première sélection. De plus, si vous effectuez des insertions/mises à jour par petits lots, vous devez connaître vos données afin de décider si une transaction est appropriée ou non. S'il ne s'agit que d'une collection d'enregistrements non liés, une transaction "enveloppante" supplémentaire sera préjudiciable.

1
ZXX

Les conditions de course importent-elles vraiment si vous essayez d’abord une mise à jour suivie d’un insert? Disons que vous avez deux threads qui veulent définir une valeur pour key key:

Fil 1: valeur = 1
Fil 2: valeur = 2

Exemple de scénario de condition de concurrence

  1. clé n'est pas défini
  2. Le fil 1 échoue avec la mise à jour
  3. Le fil 2 échoue avec la mise à jour
  4. Un des threads 1 ou 2 réussit avec l'insertion. Par exemple. fil 1
  5. L'autre thread échoue avec insert (avec erreur duplicate key) - thread 2.

    • Résultat: la "première" des deux bandes de roulement à insérer décide de la valeur.
    • Résultat recherché: le dernier des 2 threads pour écrire des données (update ou insert) devrait décider de la valeur

Mais; Dans un environnement multithread, le planificateur de système d'exploitation décide de l'ordre d'exécution du thread. Dans le scénario ci-dessus, où nous avons cette condition de concurrence critique, c'est le système d'exploitation qui a décidé de la séquence d'exécution. C'est-à-dire qu'il est faux de dire que "thread 1" ou "thread 2" était "premier" du point de vue du système.

Lorsque l'heure d'exécution est si proche pour les threads 1 et 2, le résultat de la condition de concurrence critique n'a pas d'importance. La seule exigence devrait être qu'un des threads définisse la valeur résultante.

Pour l'implémentation: Si update suivi de insert entraîne l'erreur "clé en double", cela doit être traité comme une réussite.

En outre, il ne faut bien sûr jamais présumer que la valeur dans la base de données est la même que la valeur que vous avez écrite en dernier.

1
runec

J'avais essayé la solution ci-dessous et cela fonctionne pour moi, quand une demande concurrente d'instruction d'insertion se produit.

begin tran
if exists (select * from table with (updlock,serializable) where key = @key)
begin
   update table set ...
   where key = @key
end
else
begin
   insert table (key, ...)
   values (@key, ...)
end
commit tran
0
Dev