Database
 sql >> Base de Dados >  >> RDS >> Database

Algumas Transformações Agregadas de QUALQUER estão quebradas


O ANY agregado não é algo que podemos escrever diretamente no Transact SQL. É um recurso interno usado apenas pelo otimizador de consulta e mecanismo de execução.

Pessoalmente, gosto bastante do ANY agregado, então foi um pouco decepcionante saber que ele está quebrado de uma maneira bastante fundamental. O sabor particular de “quebrado” ao qual me refiro aqui é a variedade de resultados errados.

Neste post, dou uma olhada em dois lugares específicos onde o ANY agregado geralmente aparece, demonstra o problema de resultados errados e sugere soluções alternativas quando necessário.

Para obter informações sobre o ANY agregado, veja meu post anterior Planos de consulta não documentados:O ANY Aggregate.


1. Uma linha por consultas de grupo


Este deve ser um dos requisitos de consulta mais comuns do dia a dia, com uma solução muito conhecida. Você provavelmente escreve esse tipo de consulta todos os dias, seguindo automaticamente o padrão, sem realmente pensar nisso.

A ideia é numerar o conjunto de linhas de entrada usando o ROW_NUMBER função de janela, particionada pela coluna ou colunas de agrupamento. Isso é encapsulado em uma Expressão de tabela comum ou tabela derivada , e filtrado para linhas em que o número de linha calculado é igual a um. Desde o ROW_NUMBER reinicia em um para cada grupo, isso nos dá a linha necessária por grupo.

Não há problema com esse padrão geral. O tipo de consulta de uma linha por grupo que está sujeita ao ANY problema agregado é aquele em que não nos importamos com qual linha específica é selecionada de cada grupo.

Nesse caso, não está claro qual coluna deve ser usada no ORDER BY obrigatório cláusula do ROW_NUMBER função de janela. Afinal, explicitamente não nos importamos qual linha está selecionada. Uma abordagem comum é reutilizar o PARTITION BY coluna(s) no ORDER BY cláusula. É aqui que o problema pode ocorrer.

Exemplo


Vejamos um exemplo usando um conjunto de dados de brinquedo:
CREATE TABLE #Data
(
    c1 integer NULL,
    c2 integer NULL,
    c3 integer NULL
);
 
INSERT #Data
    (c1, c2, c3)
VALUES
    -- Group 1
    (1, NULL, 1),
    (1, 1, NULL),
    (1, 111, 111),
    -- Group 2
    (2, NULL, 2),
    (2, 2, NULL),
    (2, 222, 222);

O requisito é retornar qualquer linha completa de dados de cada grupo, onde a associação ao grupo é definida pelo valor na coluna c1 .



Seguindo o ROW_NUMBER padrão, podemos escrever uma consulta como a seguinte (observe o ORDER BY cláusula do ROW_NUMBER função de janela corresponde a PARTITION BY cláusula):
WITH 
    Numbered AS 
    (
        SELECT 
            D.*, 
            rn = ROW_NUMBER() OVER (
                PARTITION BY D.c1
                ORDER BY D.c1) 
        FROM #Data AS D
    )
SELECT
    N.c1, 
    N.c2, 
    N.c3
FROM Numbered AS N
WHERE
    N.rn = 1;

Conforme apresentado, esta consulta é executada com sucesso, com resultados corretos. Os resultados são tecnicamente não determinísticos já que o SQL Server pode retornar validamente qualquer uma das linhas em cada grupo. No entanto, se você executar essa consulta por conta própria, é bem provável que veja o mesmo resultado que eu:



O plano de execução depende da versão do SQL Server usada e não depende do nível de compatibilidade do banco de dados.

No SQL Server 2014 e anteriores, o plano é:



Para SQL Server 2016 ou posterior, você verá:



Ambos os planos são seguros, mas por motivos diferentes. A Classificação Distinta plano contém um ANY agregado, mas a Classificação Distinta implementação do operador não manifesta o bug.

O plano mais complexo do SQL Server 2016+ não usa o ANY agregado em tudo. A Classificação coloca as linhas na ordem necessária para a operação de numeração de linhas. O Segmento O operador define um sinalizador no início de cada novo grupo. O Projeto Sequência calcula o número da linha. Por fim, o Filtro operador passa apenas as linhas que têm um número de linha calculado de um.

O erro


Para obter resultados incorretos com esse conjunto de dados, precisamos usar o SQL Server 2014 ou anterior e o ANY agregados precisam ser implementados em um Stream Aggregate ou Hash Aggregate operador (Flow Distinct Hash Match Aggregate não produz o bug).

Uma maneira de incentivar o otimizador a escolher um Stream Aggregate em vez de Classificação distinta é adicionar um índice clusterizado para fornecer ordenação por coluna c1 :
CREATE CLUSTERED INDEX c ON #Data (c1);

Após essa alteração, o plano de execução se torna:



O ANY agregados são visíveis nas Propriedades janela quando o Stream Aggregate operador é selecionado:



O resultado da consulta é:



Isso está errado . O SQL Server retornou linhas que não existem nos dados de origem. Não há linhas de origem em que c2 = 1 e c3 = 1 por exemplo. Como lembrete, os dados de origem são:



O plano de execução calcula erroneamente separado ANY agregados para o c2 e c3 colunas, ignorando nulos. Cada agregado independentemente retorna o primeiro não nulo valor que encontra, dando um resultado onde os valores para c2 e c3 vêm de diferentes linhas de origem . Isso não é o que a especificação de consulta SQL original solicitou.

O mesmo resultado errado pode ser produzido com ou sem o índice clusterizado adicionando uma OPTION (HASH GROUP) dica para produzir um plano com um Eager Hash Aggregate em vez de um Stream Aggregate .

Condições


Este problema só pode ocorrer quando vários ANY agregados estão presentes e os dados agregados contêm nulos. Conforme observado, o problema afeta apenas o Stream Aggregate e Ansioso Agregado de Hash operadores; Classificação distinta e Fluxo distinto não são afetados.

O SQL Server 2016 em diante faz um esforço para evitar a introdução de vários ANY agrega para qualquer padrão de consulta de numeração de linha por linha por grupo quando as colunas de origem são anuláveis. Quando isso acontecer, o plano de execução conterá Segmento , Projeto de sequência e Filtrar operadores em vez de um agregado. Esta forma de plano é sempre segura, pois não há ANY agregados são usados.

Reproduzindo o bug no SQL Server 2016+


O otimizador do SQL Server não é perfeito para detectar quando uma coluna originalmente restrita para ser NOT NULL ainda pode produzir um valor intermediário nulo por meio de manipulações de dados.

Para reproduzir isso, começaremos com uma tabela onde todas as colunas são declaradas como NOT NULL :
IF OBJECT_ID(N'tempdb..#Data', N'U') IS NOT NULL
BEGIN
    DROP TABLE #Data;
END;
 
CREATE TABLE #Data
(
    c1 integer NOT NULL,
    c2 integer NOT NULL,
    c3 integer NOT NULL
);
 
CREATE CLUSTERED INDEX c ON #Data (c1);
 
INSERT #Data
    (c1, c2, c3)
VALUES
    -- Group 1
    (1, 1, 1),
    (1, 2, 2),
    (1, 3, 3),
    -- Group 2
    (2, 1, 1),
    (2, 2, 2),
    (2, 3, 3);

Podemos produzir nulos a partir desse conjunto de dados de várias maneiras, a maioria das quais o otimizador pode detectar com sucesso e, portanto, evitar a introdução de ANY agregados durante a otimização.

Uma maneira de adicionar nulos que passam despercebidos é mostrada abaixo:
SELECT
    D.c1,
    OA1.c2,
    OA2.c3
FROM #Data AS D
OUTER APPLY (SELECT D.c2 WHERE D.c2 <> 1) AS OA1
OUTER APPLY (SELECT D.c3 WHERE D.c3 <> 2) AS OA2;

Essa consulta produz a seguinte saída:



A próxima etapa é usar essa especificação de consulta como os dados de origem para a consulta padrão “qualquer linha por grupo”:
WITH
    SneakyNulls AS 
    (
        -- Introduce nulls the optimizer can't see
        SELECT
            D.c1,
            OA1.c2,
            OA2.c3
        FROM #Data AS D
        OUTER APPLY (SELECT D.c2 WHERE D.c2 <> 1) AS OA1
        OUTER APPLY (SELECT D.c3 WHERE D.c3 <> 2) AS OA2
    ),
    Numbered AS 
    (
        SELECT
            D.c1,
            D.c2,
            D.c3,
            rn = ROW_NUMBER() OVER (
                PARTITION BY D.c1
                ORDER BY D.c1) 
        FROM SneakyNulls AS D
    )
SELECT
    N.c1, 
    N.c2, 
    N.c3
FROM Numbered AS N
WHERE
    N.rn = 1;

Em qualquer versão do SQL Server, que produz o seguinte plano:



O Agregado de fluxo contém vários ANY agregados, e o resultado é errado . Nenhuma das linhas retornadas aparece no conjunto de dados de origem:



db<>demonstração online de violino

Solução


A única solução totalmente confiável até que esse bug seja corrigido é evitar o padrão em que o ROW_NUMBER tem a mesma coluna no ORDER BY cláusula como está no PARTITION BY cláusula.

Quando não nos importamos qual uma linha é selecionada de cada grupo, é lamentável que um ORDER BY cláusula é absolutamente necessária. Uma maneira de contornar o problema é usar uma constante de tempo de execução como ORDER BY @@SPID na função janela.

2. Atualização não determinística


O problema com vários ANY agregações em entradas anuláveis ​​não são restritas a qualquer linha por padrão de consulta de grupo. O otimizador de consulta pode introduzir um ANY interno agregado em várias circunstâncias. Um desses casos é uma atualização não determinística.

Um não determinístico update é onde a instrução não garante que cada linha de destino seja atualizada no máximo uma vez. Em outras palavras, há várias linhas de origem para pelo menos uma linha de destino. A documentação alerta explicitamente sobre isso:
Tenha cuidado ao especificar a cláusula FROM para fornecer os critérios para a operação de atualização.
Os resultados de uma instrução UPDATE são indefinidos se a instrução incluir uma cláusula FROM não especificada de forma que apenas um valor esteja disponível para cada ocorrência de coluna atualizada, que é se a instrução UPDATE não for determinística.
Para lidar com uma atualização não determinística, o otimizador agrupa as linhas por uma chave (índice ou RID) e aplica ANY agrega às colunas restantes. A ideia básica é escolher uma linha de vários candidatos e usar os valores dessa linha para realizar a atualização. Existem paralelos óbvios com o ROW_NUMBER anterior problema, portanto, não é surpresa que seja muito fácil demonstrar uma atualização incorreta.

Ao contrário da edição anterior, o SQL Server atualmente não executa nenhuma etapa especial para evitar vários ANY agrega em colunas anuláveis ​​ao executar uma atualização não determinística. Portanto, o seguinte se refere a todas as versões do SQL Server , incluindo SQL Server 2019 CTP 3.0.

Exemplo

DECLARE @Target table
(
    c1 integer PRIMARY KEY, 
    c2 integer NOT NULL, 
    c3 integer NOT NULL
);
 
DECLARE @Source table 
(
    c1 integer NULL, 
    c2 integer NULL, 
    c3 integer NULL, 
 
    INDEX c CLUSTERED (c1)
);
 
INSERT @Target 
    (c1, c2, c3) 
VALUES 
    (1, 0, 0);
 
INSERT @Source 
    (c1, c2, c3) 
VALUES 
    (1, 2, NULL),
    (1, NULL, 3);
 
UPDATE T
SET T.c2 = S.c2,
    T.c3 = S.c3
FROM @Target AS T
JOIN @Source AS S
    ON S.c1 = T.c1;
 
SELECT * FROM @Target AS T;

db<>demonstração online de violino

Logicamente, esta atualização deve sempre produzir um erro:A tabela de destino não permite nulos em nenhuma coluna. Qualquer que seja a linha correspondente escolhida na tabela de origem, uma tentativa de atualizar a coluna c2 ou c3 para nulo deve ocorrer.

Infelizmente, a atualização é bem-sucedida e o estado final da tabela de destino é inconsistente com os dados fornecidos:



Eu relatei isso como um bug. A solução é evitar escrever UPDATE não determinístico declarações, então ANY agregados não são necessários para resolver a ambiguidade.

Como mencionado, o SQL Server pode introduzir ANY agregados em mais circunstâncias do que os dois exemplos dados aqui. Se isso acontecer quando a coluna agregada contiver nulos, existe a possibilidade de resultados errados.