Registros duplicados retornados da tabela sem duplicatas

8

Eu tenho um procedimento armazenado que consulta uma tabela de fila ocupada que é usada para distribuir o trabalho em nosso sistema. A tabela em questão possui uma chave primária no WorkID e nenhuma duplicata.

Uma versão simplificada da consulta é:

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)

A #Typestabela é preenchida anteriormente no procedimento.

Como eu disse, WorkTableestá ocupado e, às vezes, enquanto essa consulta está sendo executada, suspeito que um dos registros esteja se movendo de um conjunto de filtros WHEREpara outro. Especificamente, isso acontece quando alguém começa a trabalhar em um item e as W.InProgressalterações de 0 para 1. Quando isso acontece, recebo uma violação de chave duplicada quando tento adicionar uma chave primária à tabela temporária na qual esta consulta está inserida.

Confirmei no plano de consulta gerado quando ocorre o erro que não há paralelismo, o nível de isolamento é READ COMMITTEDe não há registros duplicados na tabela de origem. Você também pode ver que não há JOINoutra maneira de obter produtos cartesianos aqui.

Este é o plano de consulta anonimizado:

insira a descrição da imagem aqui

A questão é: o que está causando as duplicatas e como posso parar?

Eu acho que READ COMMITTEDdeveria funcionar aqui, preciso travar. Estou quase certo de que as bobagens ocorrem quando o InProgressbit em um registro muda enquanto estou consultando. Eu sei disso porque a tabela armazena o tempo dessa alteração e está dentro de milissegundos após a consulta e o erro.

JNK
fonte

Respostas:

9

Existem alguns cenários complicados que podem resultar na mesma linha sendo lida duas vezes em um índice, mesmo sob o READ COMMITTEDnível de isolamento .

Sua consulta não se qualifica para uma verificação de ordem de alocação; portanto , o mecanismo de armazenamento lerá os dados da tabela na ordem da chave em cluster.

Para sua tabela, você tem InProgressa primeira coluna da chave em cluster. É provável que você esteja obtendo bloqueios de linha ou de página enquanto percorre a tabela. Se você ler uma linha perto do início da varredura, solte o bloqueio nela, essa linha será atualizada de forma que InProgressmude de 0 para 1 e, em seguida, a linha seja lida novamente em uma página diferente. Você poderá ver WorkIDvalores duplicados da sua consulta .

Existem muitas soluções alternativas. Você pode inserir em uma pilha e simplesmente remover valores duplicados. Você pode adicionar um DISTINCTà consulta. Você também pode ativar um nível de isolamento de controle de versão de linha, para fornecer uma visão estável do estado confirmado do banco de dados, no início da transação ( isolamento de captura instantânea ) ou no início da instrução ( leia isolamento de captura instantânea confirmada )

Talvez seja apropriado adicionar dicas de bloqueio ou alterar a estrutura da tabela. Para uma solução bastante divertida (provavelmente não apropriada para produção), você pode tentar ler o índice ao contrário. Isso pode ser feito com um supérfluo TOPjunto com um ORDER BY. Abaixo está uma demonstração muito simples para ilustrar o ponto:

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);

A consulta a seguir possui a propriedade Ordered: false, mas continuará a ler os dados em ordem de chaves em cluster:

SELECT WorkId
FROM #WorkTable;

No entanto, a consulta a seguir lerá os dados em ordem de cluster reverso:

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

Podemos ver isso observando as propriedades da verificação:

varredura para trás

Para sua tabela, isso significa que, se uma linha for atualizada de forma que InProgressmude de 0 para 1, será muito menos provável que seja exibida duas vezes. Pode não aparecer, o que poderia ser um problema diferente.

Joe Obbish
fonte