Enregistrements en double renvoyés de la table sans doublons

8

J'ai une procédure stockée qui interroge une table de file d'attente occupée qui est utilisée pour distribuer le travail dans notre système. Le tableau en question a une clé primaire sur WorkID et aucun doublon.

Une version simplifiée de la requête est:

INSERT INTO #TempWorkIDs (WorkID)
SELECT
        W.WorkID

    FROM
        dbo.WorkTable W

    WHERE
        (@bool_param = 0 AND
        ((W.InProgress = 0
         AND ISNULL(W.UserID, -1) != @userid_param
         AND (@bool_filtered = 0
              OR W.TypeID IN (SELECT TypeID FROM #Types AS t)))
         OR 
         (@bool_param = 1
          AND W.InProgress = 1
          AND W.UserID != @userid_param)
        OR
        (@Auto_Param = 0
         AND W.UserID = @userid_param)))
         OR
         (@bool_param = 1 AND W.UserID = @userid_param)
    OPTION
        (RECOMPILE)

Le #Typestableau est rempli plus tôt dans la procédure.

Comme je l'ai dit, WorkTableest occupé, et parfois pendant que cette requête est en cours d'exécution, je SUSPECTE l' un des enregistrements se déplace d'un ensemble de filtres dans WHEREun autre. Plus précisément, cela se produit lorsque quelqu'un commence à travailler sur un élément et les W.InProgressmodifications de 0 à 1. Lorsque cela se produit, j'obtiens une violation de clé en double lorsque j'essaie d'ajouter une clé primaire à la table temporaire dans laquelle cette requête est insérée.

J'ai confirmé dans le plan de requête généré lorsque l'erreur se produit qu'il n'y a pas de parallélisme, que le niveau d'isolement est READ COMMITTEDet qu'il n'y a pas d'enregistrements en double dans la table source. Vous pouvez également voir qu'il n'y a pas de JOINs ou autre moyen d'obtenir des produits cartésiens ici.

Voici le plan de requête anonyme:

entrez la description de l'image ici

La question est, qu'est-ce qui cause les doublons et comment puis-je l'arrêter?

Je pense que READ COMMITTEDdevrait fonctionner ici, j'ai besoin de verrouillage. Je suis presque InProgresscertain que les dupes se produisent lorsque le bit sur un enregistrement change pendant que j'interroge. Je le sais car la table stocke l'heure de ce changement et elle se situe en quelques millisecondes lorsque je demande et obtient l'erreur.

JNK
la source

Réponses:

9

Il existe des scénarios délicats qui peuvent entraîner la lecture de la même ligne deux fois à partir d'un index, même sous le READ COMMITTEDniveau d'isolement .

Votre requête ne se qualifie pas pour une analyse d'ordre d'allocation, le moteur de stockage lira donc les données de la table dans l'ordre de la clé en cluster.

Pour votre table, vous disposez InProgressde la première colonne de la clé en cluster. Il est probable que vous obteniez des verrous de ligne ou de page lorsque vous parcourez le tableau. Si vous lisez une ligne près du début de l'analyse, relâchez le verrou, cette ligne est mise à jour de sorte que la valeur InProgresspasse de 0 à 1, puis la ligne est relue dans une page différente, vous pouvez voir des WorkIDvaleurs en double de votre requête .

Il existe de nombreuses solutions de contournement. Vous pouvez insérer dans un tas et supprimer simplement les valeurs en double. Vous pouvez ajouter un DISTINCTà la requête. Vous pouvez également activer un niveau d'isolement de versionnement des lignes, pour fournir une vue stable de l'état validé de la base de données, soit au début de la transaction ( isolement de cliché ), soit au début de l'instruction ( lire l'isolement de cliché validé ).

Il est peut-être approprié d'ajouter des conseils de verrouillage ou de modifier la structure de la table. Pour une solution plutôt amusante (probablement pas appropriée pour la production), vous pouvez essayer de lire l'index à l'envers. Cela peut être fait avec un superflu TOPavec un ORDER BY. Voici une démonstration très simple pour illustrer le point:

CREATE TABLE #WorkTable (
    InProgress TINYINT NOT NULL,
    WorkID INT NOT NULL
    , PRIMARY KEY (InProgress, WorkID)
);

INSERT INTO #WorkTable WITH (TABLOCK)
SELECT (RN - 1) / 5000, RN
FROM
(
    SELECT TOP (10000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) t
OPTION (MAXDOP 1);

La requête suivante a la propriété Ordered: false mais elle lira toujours les données dans l'ordre des clés en cluster:

SELECT WorkId
FROM #WorkTable;

Cependant, la requête suivante lira les données dans l'ordre cluster inversé:

SELECT TOP (9223372036854775807) WorkId
FROM #WorkTable
ORDER BY InProgress DESC, WorkId DESC;

Nous pouvons voir cela en regardant les propriétés de scan:

balayage vers l'arrière

Pour votre tableau, cela signifie que si une ligne est mise à jour de telle sorte que la valeur InProgresspasse de 0 à 1, il est beaucoup moins probable qu'elle s'affiche deux fois. Il peut ne pas apparaître du tout, ce qui pourrait être un problème différent.

Joe Obbish
la source