Question

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

Quel est le moyen le plus performant d’écrire ceci?

Était-ce utile?

La solution

N'oubliez pas les transactions. Les performances sont bonnes mais l’approche simple (SI EXISTE ..) est très dangereuse.
Lorsque plusieurs threads vont essayer d’insérer ou de mettre à jour, vous pouvez facilement obtenir une violation de clé primaire.

Solutions fournies par @Beau Crawford & amp; @Esteban montre 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

Autres conseils

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

@Beau Crawford est un bon moyen d'utiliser SQL 2005 et les versions antérieures. Toutefois, si vous accordez un représentant à votre entreprise, cela devrait aller. sur le premier homme à le mettre sous SO . Le seul problème est que pour les insertions, il reste encore deux opérations IO.

MS Sql2008 introduit la fusion à 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: - (

Faites un UPSERT:

UPDATE MyTable SET FieldA=@FieldA WHERE Key=@Key

IF @@ROWCOUNT = 0
   INSERT INTO MyTable (FieldA) VALUES (@FieldA)

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

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 / statement-caution-with-sql-servers-merge-statement /

Même avec ce "plus simple" syntaxe disponible, je préfère toujours cette approche (traitement des erreurs omis 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 ceci:

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 ceci:

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

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

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

Modifier:

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

Si vous souhaitez UPSERT plusieurs enregistrements à 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écouvrez Reproduction de l'instruction MERGE dans SQL Server 2005 .

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

Ces instructions Insert + Update sont généralement appelées "Upsert". et peut être implémenté à 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-Condition-Race-Avec-MERGE.aspx

Ce qui précède explique également les scénarios de verrouillage et d'accès simultané.

Je citerai la même chose 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;
/*
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. Veillez à ce que utilise la condition ON . Définissez ensuite la valeur (et le type) appropriés pour les variables de la ligne DECLARE.

A bientôt.

Vous pouvez utiliser l'instruction MERGE . Cette instruction est utilisée pour insérer des données si elles n'existent pas ou mettre à jour si elles existent.

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

Si vous passez à la valeur UPDATE si-pas-lignes-mises à jour, alors INSÉREZ, envisagez de faire l’opération INSERT en premier pour éviter une situation de concurrence critique (en supposant qu’aucune suppression n’intervienne)

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

Mis à part le fait d'éviter une condition de concurrence critique, si dans la plupart des cas l'enregistrement existe déjà, cela provoquera l'échec de l'INSERT et le gaspillage de la CPU.

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

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

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

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. L’instruction MERGE sera très appréciée lorsqu’il s’agit de grands ensembles de données.

Avant que tout le monde ne passe à HOLDLOCK-s par peur de ces nafares utilisateurs exécutant directement vos sprocs :-), laissez-moi vous dire clés, générateurs de séquence 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 avant d'utiliser update).

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). Ce qui 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.

Ceci dit, utiliser MERGE ou UPDATE, puis 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. Si c’est juste une collection d’enregistrements non liés, alors des "enveloppes" & autres; transaction sera préjudiciable.

Les conditions de course importent-elles vraiment si vous essayez d’abord une mise à jour suivie d’un insert? Supposons que deux threads souhaitent définir une valeur pour la clé clé :

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

Exemple de scénario de condition de concurrence

  1. la clé n'est pas définie
  2. Le fil 1 échoue avec la mise à jour
  3. La discussion 2 échoue avec la mise à jour
  4. Un des threads 1 ou 2 réussit avec l'insertion. Par exemple. le fil 1
  5. L'autre thread échoue avec l'insertion (avec la clé de duplication d'erreur) - thread 2.

    • Résultat: Le " premier " des deux marches à insérer, décide de la valeur.
    • Résultat recherché: le dernier des 2 threads pour écrire des données (update ou insert) doit 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 " le 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é dupliquée", cela doit être traité comme une réussite.

En outre, il ne faut bien entendu jamais présumer que la valeur de la base de données est identique à la dernière valeur que vous avez écrite.

J'avais essayé la solution ci-dessous et cela fonctionnait pour moi lorsqu’une demande concurrente d’instruction d’insertion se produisait.

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

Vous pouvez utiliser cette requête. Travailler dans toutes les éditions de SQL Server. C'est simple et clair. Mais vous devez utiliser 2 requêtes. Vous pouvez utiliser si vous ne pouvez pas utiliser MERGE

    BEGIN TRAN

    UPDATE table
    SET Id = @ID, Description = @Description
    WHERE Id = @Id

    INSERT INTO table(Id, Description)
    SELECT @Id, @Description
    WHERE NOT EXISTS (SELECT NULL FROM table WHERE Id = @Id)

    COMMIT TRAN

REMARQUE: veuillez expliquer les réponses négatives

Si vous utilisez ADO.NET, le DataAdapter le gère.

Si vous voulez vous en occuper vous-même, voici le chemin:

Assurez-vous qu'il existe une contrainte de clé primaire sur votre colonne de clé.

Ensuite, vous:

  1. faire la mise à jour
  2. Si la mise à jour échoue car un enregistrement avec la clé existe déjà, effectuez l'insertion. Si la mise à jour n'échoue pas, vous avez terminé.

Vous pouvez également le faire dans l’inverse, c’est-à-dire effectuer l’insertion en premier et effectuer la mise à jour si l’insertion échoue. Normalement, le premier moyen est préférable, car les mises à jour sont effectuées plus souvent que les insertions.

Faire un if existe ... sinon ... implique de faire deux demandes minimum (une pour vérifier, une pour prendre des mesures). L’approche suivante n’exige que l’enregistrement qui existe et deux si un insert est requis:

DECLARE @RowExists bit
SET @RowExists = 0
UPDATE MyTable SET DataField1 = 'xxx', @RowExists = 1 WHERE Key = 123
IF @RowExists = 0
  INSERT INTO MyTable (Key, DataField1) VALUES (123, 'xxx')

Je fais habituellement ce que plusieurs autres affiches ont dit en vérifiant qu’il existe en premier, puis en faisant quel que soit le chemin correct. Une chose à ne pas oublier lors de cette opération est que le plan d'exécution mis en cache par SQL peut ne pas être optimal pour un chemin ou pour un autre. Je pense que la meilleure façon de procéder consiste à appeler deux procédures stockées différentes.

FirstSP:
If Exists
   Call SecondSP (UpdateProc)
Else
   Call ThirdSP (InsertProc)

Maintenant, je ne suis pas mon propre conseil très souvent, alors prenez-le avec un grain de sel.

Faites une sélection, si vous obtenez un résultat, mettez-le à jour, sinon, créez-le.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top