Nous rencontrons souvent la situation "S'il n'existe pas, insérez". Le blog de Dan Guzman a une excellente enquête sur la façon de rendre ce processus threadsafe.
J'ai une table de base qui catalogue simplement une chaîne en un entier à partir de a SEQUENCE
. Dans une procédure stockée, je dois soit obtenir la clé entière de la valeur si elle existe, soit la INSERT
récupérer, puis obtenir la valeur résultante. Il y a une contrainte d'unicité sur la dbo.NameLookup.ItemName
colonne afin que l'intégrité des données ne soit pas en danger mais je ne veux pas rencontrer les exceptions.
Ce n'est pas un IDENTITY
donc je ne peux pas obtenir SCOPE_IDENTITY
et la valeur pourrait être NULL
dans certains cas.
Dans ma situation, je n'ai qu'à gérer la INSERT
sécurité sur la table, donc j'essaie de décider s'il est préférable de l'utiliser MERGE
comme ceci:
SET NOCOUNT, XACT_ABORT ON;
DECLARE @vValueId INT
DECLARE @inserted AS TABLE (Id INT NOT NULL)
MERGE
dbo.NameLookup WITH (HOLDLOCK) AS f
USING
(SELECT @vName AS val WHERE @vName IS NOT NULL AND LEN(@vName) > 0) AS new_item
ON f.ItemName= new_item.val
WHEN MATCHED THEN
UPDATE SET @vValueId = f.Id
WHEN NOT MATCHED BY TARGET THEN
INSERT
(ItemName)
VALUES
(@vName)
OUTPUT inserted.Id AS Id INTO @inserted;
SELECT @vValueId = s.Id FROM @inserted AS s
Je pourrais le faire sans utiliser MERGE
avec juste un conditionnel INSERT
suivi d'un SELECT
Je pense que cette deuxième approche est plus claire pour le lecteur, mais je ne suis pas convaincu que c'est une "meilleure" pratique
SET NOCOUNT, XACT_ABORT ON;
INSERT INTO
dbo.NameLookup (ItemName)
SELECT
@vName
WHERE
NOT EXISTS (SELECT * FROM dbo.NameLookup AS t WHERE @vName IS NOT NULL AND LEN(@vName) > 0 AND t.ItemName = @vName)
DECLARE @vValueId int;
SELECT @vValueId = i.Id FROM dbo.NameLookup AS i WHERE i.ItemName = @vName
Ou peut-être y a-t-il une autre meilleure façon que je n'ai pas envisagée
J'ai cherché et référencé d'autres questions. Celui-ci: /programming/5288283/sql-server-insert-if-not-exists-best-practice est le plus approprié que j'ai pu trouver mais ne semble pas très applicable à mon cas d'utilisation. D'autres questions sur l' IF NOT EXISTS() THEN
approche que je ne pense pas acceptables.
la source
Réponses:
Étant donné que vous utilisez une séquence, vous pouvez utiliser la même fonction NEXT VALUE FOR - que vous avez déjà dans une contrainte par défaut sur le
Id
champ Clé primaire - pour générer une nouvelleId
valeur à l'avance. Générer la valeur signifie d'abord que vous n'avez pas à vous soucier de ne pas l'avoirSCOPE_IDENTITY
, ce qui signifie ensuite que vous n'avez pas besoin de laOUTPUT
clause ou de faire un complémentSELECT
pour obtenir la nouvelle valeur; vous aurez la valeur avant de le faireINSERT
, et vous n'avez même pas besoin de jouer avecSET IDENTITY INSERT ON / OFF
:-)Cela prend donc en charge une partie de la situation globale. L'autre partie traite le problème de simultanéité de deux processus, en même temps, ne trouve pas de ligne existante pour la même chaîne exacte et continue avec
INSERT
. Le souci est d'éviter la violation de contrainte unique qui se produirait.Une façon de gérer ces types de problèmes de concurrence consiste à forcer cette opération particulière à être à thread unique. Pour ce faire, utilisez des verrous d'application (qui fonctionnent sur plusieurs sessions). Bien qu'efficaces, ils peuvent être un peu lourds pour une situation comme celle-ci où la fréquence des collisions est probablement assez faible.
L'autre façon de gérer les collisions est d'accepter qu'elles se produisent parfois et de les gérer plutôt que d'essayer de les éviter. En utilisant la
TRY...CATCH
construction, vous pouvez effectivement intercepter une erreur spécifique (dans ce cas: "violation de contrainte unique", Msg 2601) et réexécuterSELECT
pour obtenir laId
valeur car nous savons qu'elle existe maintenant en raison du fait qu'elle se trouve dans leCATCH
bloc avec cette donnée particulière Erreur. D'autres erreurs peuvent être traitées de manière typiqueRAISERROR
/RETURN
ouTHROW
.Configuration du test: séquence, table et index unique
Configuration du test: procédure stockée
Le test
Question de OP
MERGE
a divers "problèmes" (plusieurs références sont liées dans la réponse de @ SqlZim donc pas besoin de dupliquer cette information ici). Et, il n'y a pas de verrouillage supplémentaire dans cette approche (moins de conflits), il devrait donc être préférable en concurrence. Dans cette approche, vous n'obtiendrez jamais de violation de contrainte unique, sans aucuneHOLDLOCK
, etc. Il est pratiquement garanti de fonctionner.Le raisonnement derrière cette approche est:
CATCH
bloc en premier lieu sera assez faible. Il est plus logique d'optimiser le code qui s'exécutera 99% du temps au lieu du code qui s'exécutera 1% du temps (à moins qu'il n'y ait aucun coût pour optimiser les deux, mais ce n'est pas le cas ici).Commentaire de la réponse de @ SqlZim (non souligné dans l'original)
Je serais d'accord avec cette première phrase si elle était modifiée pour indiquer "et _quand prudent". Ce n'est pas parce que quelque chose est techniquement possible que la situation (c'est-à-dire le cas d'utilisation prévu) en bénéficierait.
Le problème que je vois avec cette approche est qu'elle se verrouille plus que ce qui est suggéré. Il est important de relire la documentation citée sur "sérialisable", en particulier les suivantes (soulignement ajouté):
Maintenant, voici le commentaire dans l'exemple de code:
Le mot clé est "plage". Le verrou pris n'est pas seulement sur la valeur en
@vName
, mais plus précisément une plage commençant àl'emplacement où cette nouvelle valeur doit aller (c'est-à-dire entre les valeurs de clé existantes de chaque côté de l'endroit où la nouvelle valeur tient), mais pas la valeur elle-même. Cela signifie que d'autres processus ne pourront pas insérer de nouvelles valeurs, selon la ou les valeurs actuellement recherchées. Si la recherche est effectuée en haut de la plage, l'insertion de tout ce qui pourrait occuper cette même position sera bloquée. Par exemple, si les valeurs "a", "b" et "d" existent, alors si un processus fait le SELECT sur "f", alors il ne sera pas possible d'insérer les valeurs "g" ou même "e" ( car l'un d'eux viendra immédiatement après "d"). Mais, l'insertion d'une valeur de "c" sera possible car elle ne sera pas placée dans la plage "réservée".L'exemple suivant doit illustrer ce comportement:
(Dans l'onglet de requête (ie Session) # 1)
(Dans l'onglet de requête (ie Session) # 2)
De même, si la valeur "C" existe et que la valeur "A" est sélectionnée (et donc verrouillée), vous pouvez insérer une valeur de "D", mais pas une valeur de "B":
(Dans l'onglet de requête (ie Session) # 1)
(Dans l'onglet de requête (ie Session) # 2)
Pour être juste, dans mon approche suggérée, lorsqu'il y a une exception, il y aura 4 entrées dans le journal des transactions qui ne se produiront pas dans cette approche de "transaction sérialisable". MAIS, comme je l'ai dit ci-dessus, si l'exception se produit 1% (ou même 5%) du temps, cela a beaucoup moins d'impact que le cas beaucoup plus probable du SELECT initial bloquant temporairement les opérations INSERT.
Un autre problème, bien que mineur, avec cette approche "transaction sérialisable + clause OUTPUT" est que la
OUTPUT
clause (dans son utilisation actuelle) renvoie les données sous la forme d'un ensemble de résultats. Un jeu de résultats nécessite plus de surcharge (probablement des deux côtés: dans SQL Server pour gérer le curseur interne et dans la couche d'application pour gérer l'objet DataReader) qu'un simpleOUTPUT
paramètre. Étant donné que nous n'avons affaire qu'à une seule valeur scalaire et que l'hypothèse est une fréquence élevée d'exécutions, cette surcharge supplémentaire de l'ensemble de résultats s'additionne probablement.Bien que la
OUTPUT
clause puisse être utilisée de manière à renvoyer unOUTPUT
paramètre, cela nécessiterait des étapes supplémentaires pour créer une table ou une variable de table temporaire, puis pour sélectionner la valeur de cette variable table / table temporaire dans leOUTPUT
paramètre.Précision supplémentaire: réponse à la réponse de @ SqlZim (réponse mise à jour) à ma réponse à la réponse de @ SqlZim (dans la réponse d'origine) à ma déclaration concernant la concurrence et les performances ;-)
Désolé si cette partie est un peu longue, mais à ce stade, nous n'en sommes qu'aux nuances des deux approches.
Oui, je dois admettre que je suis partial, mais pour être juste:
INSERT
échec en raison d'une violation de contrainte unique. Je n'ai vu cela mentionné dans aucune des autres réponses / messages.Concernant l'approche "JFDI" de @ gbn, le post "Ugly Pragmatism For The Win" de Michael J. Swart, et le commentaire d'Aaron Bertrand sur le post de Michael (concernant ses tests montrant quels scénarios ont diminué les performances), et votre commentaire sur votre "adaptation de Michael J" . L'adaptation par Stewart de la procédure Try Catch JFDI de @ gbn "indiquant:
En ce qui concerne cette discussion gbn / Michael / Aaron relative à l'approche "JFDI", il serait incorrect d'assimiler ma suggestion à l'approche "JFDI" de gbn. En raison de la nature de l'opération "Get or Insert", il est explicitement nécessaire de faire le
SELECT
pour obtenir laID
valeur des enregistrements existants. Ce SELECT agit comme uneIF EXISTS
vérification, ce qui rend cette approche plus équivalente à la variation "CheckTryCatch" des tests d'Aaron. Le code réécrit de Michael (et votre adaptation finale de l'adaptation de Michael) comprend également unWHERE NOT EXISTS
pour faire cette même vérification en premier. Par conséquent, ma suggestion (avec le code final de Michael et votre adaptation de son code final) ne frappera pas leCATCH
bloc si souvent. Ce ne peut être que des situations où deux sessions,ItemName
INSERT...SELECT
au même moment exact de telle sorte que les deux sessions reçoivent un "vrai" pour leWHERE NOT EXISTS
même moment exact et donc toutes deux tentent de le faireINSERT
au même moment exact. Ce scénario très spécifique se produit beaucoup moins souvent que la sélection d'un existantItemName
ou l'insertion d'un nouveauItemName
lorsqu'aucun autre processus ne tente de le faire au même moment .AVEC TOUT CE QUI PRÉCÈDE DANS L'ESPRIT: Pourquoi est-ce que je préfère mon approche?
Voyons d'abord ce qui se produit dans l'approche "sérialisable". Comme mentionné ci-dessus, la "plage" qui est verrouillée dépend des valeurs de clé existantes de chaque côté de l'endroit où la nouvelle valeur de clé s'insérerait. Le début ou la fin de la plage peut également être le début ou la fin de l'index, respectivement, s'il n'y a pas de valeur clé existante dans cette direction. Supposons que nous ayons l'index et les clés suivants (
^
représente le début de l'index tandis que$
représente la fin de celui-ci):Si la session 55 tente d'insérer une valeur clé de:
A
, alors la plage # 1 (de^
àC
) est verrouillée: la session 56 ne peut pas insérer une valeur deB
, même si elle est unique et valide (encore). Mais la session 56 peut insérer des valeurs deD
,G
etM
.D
, alors la plage # 2 (deC
àF
) est verrouillée: la session 56 ne peut pas insérer une valeur deE
(encore). Mais la session 56 peut insérer des valeurs deA
,G
etM
.M
, alors la plage # 4 (deJ
à$
) est verrouillée: la session 56 ne peut pas insérer une valeur deX
(encore). Mais la session 56 peut insérer des valeurs deA
,D
etG
.Au fur et à mesure que davantage de valeurs clés sont ajoutées, les plages entre les valeurs clés deviennent plus étroites, réduisant ainsi la probabilité / fréquence d'insertion de plusieurs valeurs en même temps en se battant sur la même plage. Certes, ce n'est pas un problème majeur , et heureusement, il semble que ce soit un problème qui diminue avec le temps.
Le problème avec mon approche a été décrit ci-dessus: cela ne se produit que lorsque deux sessions tentent d'insérer la même valeur de clé en même temps. À cet égard, cela revient à ce qui a la plus forte probabilité de se produire: deux valeurs clés différentes, mais proches, sont tentées en même temps, ou la même valeur clé est tentée en même temps? Je suppose que la réponse réside dans la structure de l'application qui effectue les insertions, mais de manière générale, je suppose qu'il est plus probable que deux valeurs différentes qui se trouvent partager la même plage soient insérées. Mais la seule façon de vraiment savoir serait de tester les deux sur le système OP.
Ensuite, considérons deux scénarios et comment chaque approche les gère:
Toutes les demandes concernent des valeurs clés uniques:
Dans ce cas, le
CATCH
bloc dans ma suggestion n'est jamais entré, donc pas de "problème" (c'est-à-dire 4 entrées de journal de transfert et le temps qu'il faut pour le faire). Mais, dans l'approche "sérialisable", même si tous les inserts sont uniques, il y aura toujours un certain potentiel de blocage d'autres inserts dans la même plage (quoique pas pour très longtemps).Fréquence élevée de demandes de la même valeur de clé en même temps:
Dans ce cas - un très faible degré d'unicité en termes de demandes entrantes pour des valeurs de clés inexistantes - le
CATCH
bloc de ma suggestion sera régulièrement entré. Cela aura pour effet que chaque insertion échouée devra effectuer une restauration automatique et écrire les 4 entrées dans le journal des transactions, ce qui représente une légère baisse des performances à chaque fois. Mais l'opération globale ne devrait jamais échouer (du moins pas à cause de cela).(Il y avait un problème avec la version précédente de l'approche "mise à jour" qui lui permettait de souffrir de blocages. Un
updlock
indice a été ajouté pour résoudre ce problème et il ne reçoit plus de blocages.)MAIS, dans l'approche "sérialisable" (même la version mise à jour et optimisée), l'opération se bloquera. Pourquoi? Parce que leserializable
comportement empêche uniquement lesINSERT
opérations dans la plage qui a été lue et donc verrouillée; cela n'empêche pas lesSELECT
opérations sur cette plage.L'
serializable
approche, dans ce cas, ne semblerait pas avoir de frais généraux supplémentaires et pourrait fonctionner légèrement mieux que ce que je suggère.Comme pour beaucoup / la plupart des discussions concernant les performances, en raison de la multiplicité des facteurs susceptibles d'affecter le résultat, la seule façon de vraiment avoir une idée de la façon dont quelque chose va fonctionner est de l'essayer dans l'environnement cible où il s'exécutera. À ce stade, ce ne sera plus une question d'opinion :).
la source
Réponse mise à jour
Réponse à @srutzky
Je suis d'accord, et pour ces mêmes raisons, j'utilise des paramètres de sortie lorsque prudent . C'était mon erreur de ne pas utiliser de paramètre de sortie sur ma réponse initiale, j'étais paresseux.
Voici une procédure révisée utilisant un paramètre de sortie, des optimisations supplémentaires, ainsi
next value for
que @srutzky explique dans sa réponse :mise à jour note : Y compris
updlock
avec la sélection saisira les verrous appropriés dans ce scénario. Merci à @srutzky, qui a souligné que cela pouvait provoquer des blocages lors de l'utilisation uniquementserializable
sur leselect
.Remarque: Ce n'est peut-être pas le cas, mais si cela est possible, la procédure sera appelée avec une valeur pour
@vValueId
, inclureset @vValueId = null;
aprèsset xact_abort on;
, sinon elle peut être supprimée.Concernant les exemples @ srutzky de comportement de verrouillage de plage de clés:
@srutzky n'utilise qu'une seule valeur dans sa table et verrouille la clé "suivant" / "infini" pour ses tests pour illustrer le verrouillage de la plage de clés. Bien que ses tests illustrent ce qui se passe dans ces situations, je pense que la façon dont les informations sont présentées pourrait conduire à de fausses hypothèses sur la quantité de verrouillage que l'on peut s'attendre à rencontrer lors de l'utilisation
serializable
dans le scénario présenté dans la question d'origine.Même si je perçois un biais (peut-être à tort) dans la façon dont il présente ses explications et ses exemples de verrouillage de plage de touches, ils sont toujours corrects.
Après plus de recherches, j'ai trouvé un article de blog particulièrement pertinent de 2011 par Michael J. Swart: Mythbusting: Concurrent Update / Insert Solutions . Dans ce document, il teste plusieurs méthodes pour la précision et la simultanéité. Méthode 4: Isolation accrue + verrous de réglage fin est basé sur le modèle d'insertion ou de mise à jour de Sam Saffron pour SQL Server , et la seule méthode du test d'origine pour répondre à ses attentes (rejointe plus tard par
merge with (holdlock)
).En février 2016, Michael J. Swart a publié Ugly Pragmatism For The Win . Dans ce post, il couvre certains ajustements supplémentaires qu'il a faits à ses procédures upsert Saffron pour réduire le verrouillage (que j'ai inclus dans la procédure ci-dessus).
Après avoir apporté ces modifications, Michael n'était pas content que sa procédure commence à paraître plus compliquée et a consulté un collègue nommé Chris. Chris a lu tous les articles Mythbusters originaux et a lu tous les commentaires et a posé des questions sur le modèle TRY CATCH JFDI de @ gbn . Ce modèle est similaire à la réponse de @ srutzky et est la solution que Michael a fini par utiliser dans ce cas.
Michael J Swart:
À mon avis, les deux solutions sont viables. Bien que je préfère toujours augmenter le niveau d'isolement et les verrous de réglage fin, la réponse de @ srutzky est également valide et peut ou non être plus performante dans votre situation spécifique.
Peut-être qu'à l'avenir j'arriverai moi aussi à la même conclusion que Michael J. Swart, mais je n'y suis tout simplement pas encore.
Ce n'est pas ma préférence, mais voici à quoi ressemblerait mon adaptation de l'adaptation de Michael J. Stewart à la procédure Try Catch JFDI de @ gbn :
Si vous insérez de nouvelles valeurs plus souvent que la sélection de valeurs existantes, cela peut être plus performant que la version de @ srutzky . Sinon, je préférerais la version de @ srutzky à celle-ci.
Les commentaires d'Aaron Bertrand sur le post de Michael J Swart renvoient aux tests pertinents qu'il a effectués et ont conduit à cet échange. Extrait de la section des commentaires sur le pragmatisme laid pour la victoire :
et la réponse de:
Nouveaux liens:
Réponse originale
Je préfère toujours l' approche upsert de Sam Saffron par rapport à l'utilisation
merge
, en particulier lorsqu'il s'agit d'une seule ligne.J'adapterais cette méthode upsert à cette situation comme ceci:
Je serais cohérent avec votre nom, et comme
serializable
c'est la même chose queholdlock
, choisissez-en un et soyez cohérent dans son utilisation. J'ai tendance à utiliserserializable
car c'est le même nom que celui utilisé lors de la spécificationset transaction isolation level serializable
.En utilisant
serializable
ouholdlock
un verrou de plage est pris en fonction de la valeur@vName
qui fait attendre toute autre opération si elle sélectionne ou insère des valeursdbo.NameLookup
qui incluent la valeur dans lawhere
clause.Pour que le verrouillage de plage fonctionne correctement, il doit y avoir un index sur la
ItemName
colonne qui s'applique également lors de l'utilisationmerge
.Voici ce que la procédure ressemblerait la plupart du temps suivant les livres blancs de Erland Sommarskog pour le traitement des erreurs , en utilisant
throw
. Si cethrow
n'est pas la façon dont vous générez vos erreurs, modifiez-la pour qu'elle soit cohérente avec le reste de vos procédures:Pour résumer ce qui se passe dans la procédure ci-dessus:
set nocount on; set xact_abort on;
comme vous le faites toujours , alors si notre variable d'entréeis null
ou vide,select id = cast(null as int)
comme résultat. S'il n'est pas nul ou vide, récupérez laId
pour notre variable tout en maintenant cet endroit au cas où il ne serait pas là. Si leId
est là, envoyez-le. S'il n'est pas là, insérez-le et envoyez ce nouveauId
.Pendant ce temps, les autres appels à cette procédure essayant de trouver l'ID pour la même valeur attendront que la première transaction soit effectuée, puis la sélectionne et la renvoie. D'autres appels à cette procédure ou d'autres instructions à la recherche d'autres valeurs continueront car celle-ci n'est pas gênante.
Bien que je convienne avec @srutzky que vous pouvez gérer les collisions et avaler les exceptions pour ce type de problème, je préfère personnellement essayer de personnaliser une solution pour éviter de le faire lorsque cela est possible. Dans ce cas, je ne pense pas que l'utilisation des verrous
serializable
soit une approche lourde, et je serais convaincu qu'il gérerait bien la concurrence élevée.Citation de la documentation du serveur SQL sur les conseils de table
serializable
/holdlock
:Citation de la documentation du serveur SQL sur le niveau d'isolement des transactions
serializable
Liens relatifs à la solution ci-dessus:
Insérer ou mettre à jour un modèle pour Sql Server - Sam Saffron
Documentation sur les indices de table sérialisables et autres - MSDN
Gestion des erreurs et des transactions dans SQL Server, première partie - Gestion des erreurs Jumpstart - Erland Sommarskog
Les conseils d' Erland Sommarskog concernant @@ rowcount (que je n'ai pas suivi dans ce cas).
MERGE
a une histoire inégale, et il semble prendre plus de temps pour s'assurer que le code se comporte comme vous le souhaitez dans toute cette syntaxe.merge
Articles pertinents :Un insecte MERGE intéressant - Paul White
UPSERT Race Condition With Merge - sqlteam
Soyez prudent avec l'instruction MERGE de SQL Server - Aaron Bertrand
Puis-je optimiser cette déclaration de fusion - Aaron Bertrand
Si vous utilisez des vues indexées et MERGE, veuillez lire ceci! - Aaron Bertrand
Un dernier lien, Kendra Little a fait une comparaison approximative de
merge
vsinsert with left join
, avec la mise en garde où elle dit "Je n'ai pas fait de test de charge approfondi sur cela", mais c'est toujours une bonne lecture.la source