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?
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
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 :-(
Faites un UPSERT:
UPDATE MyTable SET FieldA = @ FieldA WHERE Clé = @ Key IF @@ ROWCOUNT = 0 INSÉRER DANS MyTable (FieldA) VALEURS (@FieldA)
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:
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.
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 .
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;
/*
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é.
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`
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.
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
Dans SQL Server 2008, vous pouvez utiliser l'instruction MERGE.
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.
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.
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
L'autre thread échoue avec insert (avec erreur duplicate key) - thread 2.
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.
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