Solutions pour INSERT OU UPDATE sur SQL Server
-
01-07-2019 - |
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?
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)
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
- la clé n'est pas définie
- Le fil 1 échoue avec la mise à jour
- La discussion 2 échoue avec la mise à jour
- Un des threads 1 ou 2 réussit avec l'insertion. Par exemple. le fil 1
-
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:
- faire la mise à jour
- 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.