Database
 sql >> Base de données >  >> RDS >> Database

Pour la dernière fois, NON, vous ne pouvez pas faire confiance à IDENT_CURRENT()

J'ai eu une discussion hier avec Kendal Van Dyke (@SQLDBA) à propos de IDENT_CURRENT(). Fondamentalement, Kendal disposait de ce code, qu'il avait testé et approuvé par lui-même, et voulait savoir s'il pouvait compter sur la précision de IDENT_CURRENT() dans un environnement simultané à grande échelle :

BEGIN TRANSACTION;
INSERT dbo.TableName(ColumnName) VALUES('Value');
SELECT IDENT_CURRENT('dbo.TableName');
COMMIT TRANSACTION;

La raison pour laquelle il a dû le faire est qu'il doit renvoyer la valeur IDENTITY générée au client. Voici comment nous procédons généralement :

  • SCOPE_IDENTITY()
  • Clause OUTPUT
  • @@IDENTITY
  • IDENT_CURRENT()

Certains d'entre eux sont meilleurs que d'autres, mais cela a été fait à mort, et je ne vais pas m'y attarder ici. Dans le cas de Kendal, IDENT_CURRENT était son dernier et unique recours, car :

  • TableName avait un déclencheur INSTEAD OF INSERT, rendant SCOPE_IDENTITY() et la clause OUTPUT inutiles pour l'appelant, car :
    • SCOPE_IDENTITY() renvoie NULL, car l'insertion s'est réellement produite dans une portée différente
    • la clause OUTPUT génère l'erreur Msg 334 à cause du déclencheur
  • Il a éliminé @@IDENTITY ; considérez que le déclencheur INSTEAD OF INSERT pourrait maintenant (ou pourrait être modifié ultérieurement) s'insérer dans d'autres tables qui ont leurs propres colonnes IDENTITY, ce qui fausserait la valeur renvoyée. Cela contrecarrerait également SCOPE_IDENTITY(), si c'était possible.
  • Et enfin, il ne pouvait pas utiliser la clause OUTPUT (ou un jeu de résultats d'une deuxième requête de la pseudo-table insérée après l'insertion éventuelle) dans le déclencheur, car cette fonctionnalité nécessite un paramètre global et est obsolète depuis SQL Server 2005. Naturellement, le code de Kendal doit être compatible avec les versions ultérieures et, dans la mesure du possible, ne pas dépendre entièrement de certains paramètres de base de données ou de serveur.

Donc, revenons à la réalité de Kendal. Son code semble assez sûr – c'est dans une transaction, après tout; Qu'est-ce qui pourrait mal se passer? Eh bien, examinons quelques phrases importantes de la documentation IDENT_CURRENT (c'est moi qui souligne, car ces avertissements sont là pour une bonne raison) :

Renvoie la dernière valeur d'identité générée pour une table ou une vue spécifiée. La dernière valeur d'identité générée peut être pour n'importe quelle session et toute portée .

Soyez prudent lorsque vous utilisez IDENT_CURRENT pour prédire la prochaine valeur d'identité générée. La valeur réelle générée peut être différente de IDENT_CURRENT plus IDENT_INCR en raison des insertions effectuées par d'autres sessions .

Les transactions sont à peine mentionnées dans le corps du document (uniquement dans le contexte de l'échec, pas de la simultanéité), et aucune transaction n'est utilisée dans aucun des exemples. Alors, testons ce que faisait Kendal et voyons si nous pouvons le faire échouer lorsque plusieurs sessions s'exécutent simultanément. Je vais créer une table de journal pour garder une trace des valeurs générées par chaque session - à la fois la valeur d'identité qui a été réellement générée (à l'aide d'un déclencheur après) et la valeur déclarée être générée selon IDENT_CURRENT().

Tout d'abord, les tables et les déclencheurs :

-- the destination table:
 
CREATE TABLE dbo.TableName
(
  ID INT IDENTITY(1,1), 
  seq INT
);
 
-- the log table:
 
CREATE TABLE dbo.IdentityLog
(
  SPID INT, 
  seq INT, 
  src VARCHAR(20), -- trigger or ident_current 
  id INT
);
GO
 
-- the trigger, adding my logging:
 
CREATE TRIGGER dbo.InsteadOf_TableName
ON dbo.TableName
INSTEAD OF INSERT
AS
BEGIN
  INSERT dbo.TableName(seq) SELECT seq FROM inserted;
 
  -- this is just for our logging purposes here:
  INSERT dbo.IdentityLog(SPID,seq,src,id)
    SELECT @@SPID, seq, 'trigger', SCOPE_IDENTITY() 
    FROM inserted;
END
GO

Maintenant, ouvrez quelques fenêtres de requête et collez ce code, en les exécutant aussi près que possible pour assurer le plus de chevauchement :

SET NOCOUNT ON;
 
DECLARE @seq INT = 0;
 
WHILE @seq <= 100000
BEGIN
  BEGIN TRANSACTION;
 
  INSERT dbo.TableName(seq) SELECT @seq;
  INSERT dbo.IdentityLog(SPID,seq,src,id)
    SELECT @@SPID,@seq,'ident_current',IDENT_CURRENT('dbo.TableName');
 
  COMMIT TRANSACTION;
  SET @seq += 1;
END

Une fois toutes les fenêtres de requête terminées, exécutez cette requête pour voir quelques lignes aléatoires où IDENT_CURRENT a renvoyé la mauvaise valeur, et un décompte du nombre total de lignes affectées par ce nombre erroné :

SELECT TOP (10)
  id_cur.SPID,  
  [ident_current] = id_cur.id, 
  [actual id] = tr.id, 
  total_bad_results = COUNT(*) OVER()
FROM dbo.IdentityLog AS id_cur
INNER JOIN dbo.IdentityLog AS tr
   ON id_cur.SPID = tr.SPID 
   AND id_cur.seq = tr.seq 
   AND id_cur.id <> tr.id
WHERE id_cur.src = 'ident_current' 
   AND tr.src     = 'trigger'
ORDER BY NEWID();

Voici mes 10 lignes pour un test :

J'ai trouvé surprenant que près d'un tiers des rangées soient éteintes. Vos résultats varieront certainement et peuvent dépendre de la vitesse de vos lecteurs, du modèle de récupération, des paramètres du fichier journal ou d'autres facteurs. Sur deux machines différentes, j'avais des taux d'échec très différents - d'un facteur 10 (une machine plus lente n'avait qu'environ 10 000 échecs, soit environ 3 %).

Immédiatement, il est clair qu'une transaction n'est pas suffisante pour empêcher IDENT_CURRENT d'extraire les valeurs IDENTITY générées par d'autres sessions. Que diriez-vous d'une transaction SERIALIZABLE ? Effacez d'abord les deux tables :

TRUNCATE TABLE dbo.TableName;
TRUNCATE TABLE dbo.IdentityLog;

Ensuite, ajoutez ce code au début du script dans plusieurs fenêtres de requête et réexécutez-les aussi simultanément que possible :

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Cette fois, lorsque j'exécute la requête sur la table IdentityLog, cela montre que SERIALIZABLE a peut-être aidé un peu, mais cela n'a pas résolu le problème :

Et bien que ce soit faux, il semble d'après mes exemples de résultats que la valeur IDENT_CURRENT n'est généralement décalée que d'un ou deux. Cependant, cette requête devrait indiquer qu'elle peut être *way* off. Lors de mes tests, ce résultat atteignait 236 :

SELECT MAX(ABS(id_cur.id - tr.id))
FROM dbo.IdentityLog AS id_cur
INNER JOIN dbo.IdentityLog AS tr
  ON id_cur.SPID = tr.SPID 
  AND id_cur.seq = tr.seq 
  AND id_cur.id <> tr.id
WHERE id_cur.src = 'ident_current' 
  AND tr.src     = 'trigger';

Grâce à cette preuve, nous pouvons conclure que IDENT_CURRENT n'est pas sécurisé pour les transactions. Cela semble rappeler un problème similaire mais presque opposé, où les fonctions de métadonnées telles que OBJECT_NAME() sont bloquées - même lorsque le niveau d'isolation est READ UNCOMMITTED - parce qu'elles n'obéissent pas à la sémantique d'isolation environnante. (Voir Connect Item #432497 pour plus de détails.)

En surface, et sans en savoir beaucoup plus sur l'architecture et les applications, je n'ai pas vraiment de bonne suggestion pour Kendal ; Je sais juste que IDENT_CURRENT n'est *pas* la réponse. :-) Ne l'utilisez pas. Pour rien. Déjà. Au moment où vous lisez la valeur, elle peut déjà être erronée.