Forçando Fluxo Distinto

19

Eu tenho uma tabela como esta:

CREATE TABLE Updates
(
    UpdateId INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
    ObjectId INT NOT NULL
)

Rastreando essencialmente as atualizações de objetos com um ID crescente.

O consumidor desta tabela selecionará um pedaço de 100 IDs de objetos distintos, ordenados UpdateIde iniciados a partir de um específico UpdateId. Essencialmente, mantenha o controle de onde parou e, em seguida, consulte as atualizações.

Eu descobri que isso seja um problema de otimização interessante porque eu só fui capaz de gerar um plano de consulta máximo ideal escrevendo consultas que acontecem a fazer o que eu quero devido a índices, mas não garantir o que eu quero:

SELECT DISTINCT TOP 100 ObjectId
FROM Updates
WHERE UpdateId > @fromUpdateId

Onde @fromUpdateIdé um parâmetro de procedimento armazenado.

Com um plano de:

SELECT <- TOP <- Hash match (flow distinct, 100 rows touched) <- Index seek

Devido à busca no UpdateIdíndice que está sendo usado, os resultados já são bons e ordenados do menor para o maior ID de atualização, como eu quero. E isso gera um plano distinto de fluxo , que é o que eu quero. Mas o pedido obviamente não é um comportamento garantido, então não quero usá-lo.

Esse truque também resulta no mesmo plano de consulta (embora com um TOP redundante):

WITH ids AS
(
    SELECT ObjectId
    FROM Updates
    WHERE UpdateId > @fromUpdateId
    ORDER BY UpdateId OFFSET 0 ROWS
)
SELECT DISTINCT TOP 100 ObjectId FROM ids

No entanto, não tenho certeza (e suspeito que não) se isso realmente garante pedidos.

Uma consulta que eu esperava que o SQL Server fosse inteligente o suficiente para simplificar foi essa, mas acaba gerando um plano de consulta muito ruim:

SELECT TOP 100 ObjectId
FROM Updates
WHERE UpdateId > @fromUpdateId
GROUP BY ObjectId
ORDER BY MIN(UpdateId)

Com um plano de:

SELECT <- Top N Sort <- Hash Match aggregate (50,000+ rows touched) <- Index Seek

Estou tentando encontrar uma maneira de gerar um plano ideal com uma busca de índice UpdateIde um fluxo distinto para remover ObjectIds duplicados . Alguma ideia?

Amostra de dados, se desejar. Os objetos raramente têm mais de uma atualização e quase nunca devem ter mais de uma em um conjunto de 100 linhas, e é por isso que estou buscando um fluxo distinto , a menos que haja algo melhor que eu não conheça? No entanto, não há garantia de que um único ObjectIdnão tenha mais de 100 linhas na tabela. A tabela possui mais de 1.000.000 de linhas e deve crescer rapidamente.

Suponha que o usuário tenha outra maneira de encontrar o próximo apropriado @fromUpdateId. Não há necessidade de retorná-lo nesta consulta.

Cory Nelson
fonte

Respostas:

15

O otimizador do SQL Server não pode produzir o plano de execução que você procura com a garantia necessária, porque o operador Hash Match Flow Distinct não preserva a ordem.

No entanto, não tenho certeza (e suspeito que não) se isso realmente garante pedidos.

Você pode observar a preservação do pedido em muitos casos, mas este é um detalhe da implementação; não há garantia, então você não pode confiar nela. Como sempre, a ordem de apresentação só pode ser garantida por uma ORDER BYcláusula de nível superior .

Exemplo

O script abaixo mostra que o Hash Match Flow Distinct não preserva a ordem. Ele configura a tabela em questão com os números correspondentes de 1 a 50.000 nas duas colunas:

IF OBJECT_ID(N'dbo.Updates', N'U') IS NOT NULL
    DROP TABLE dbo.Updates;
GO
CREATE TABLE Updates
(
    UpdateId INT NOT NULL IDENTITY(1,1),
    ObjectId INT NOT NULL,

    CONSTRAINT PK_Updates_UpdateId PRIMARY KEY (UpdateId)
);
GO
INSERT dbo.Updates (ObjectId)
SELECT TOP (50000)
    ObjectId =
        ROW_NUMBER() OVER (
            ORDER BY C1.[object_id]) 
FROM sys.columns AS C1
CROSS JOIN sys.columns AS C2
ORDER BY
    ObjectId;

A consulta de teste é:

DECLARE @Rows bigint = 50000;

-- Optimized for 1 row, but will be 50,000 when executed
SELECT DISTINCT TOP (@Rows)
    U.ObjectId 
FROM dbo.Updates AS U
WHERE 
    U.UpdateId > 0
OPTION (OPTIMIZE FOR (@Rows = 1));

O plano estimado mostra uma busca e fluxo de índice distintos:

Plano estimado

A saída certamente parece ordenada para começar com:

Início dos resultados

... mas valores mais baixos começam a desaparecer:

Padrão quebrando

...e eventualmente:

O caos irrompe

A explicação nesse caso específico é que o operador de hash se espalha:

Plano pós-execução

Depois que uma partição é derramada, todas as linhas com hash na mesma partição também são derramadas. Partições derramadas são processadas posteriormente, quebrando a expectativa de que valores distintos encontrados sejam emitidos imediatamente na sequência em que são recebidos.


Existem várias maneiras de escrever uma consulta eficiente para produzir o resultado ordenado desejado, como recursão ou uso de um cursor. No entanto, isso não pode ser feito usando o Hash Match Flow Distinct .

Paul White diz que a GoFundMonica
fonte
11

Estou insatisfeito com esta resposta porque não consegui obter um operador distinto de fluxo junto com os resultados que estavam garantidos como corretos. No entanto, tenho uma alternativa que deve obter um bom desempenho junto com os resultados corretos. Infelizmente, exige que um índice não clusterizado seja criado na tabela.

Abordei esse problema tentando pensar em uma combinação de colunas que consegui ORDER BYe obter os resultados corretos depois de aplicar DISTINCTa elas. O valor mínimo de UpdateIdpor ObjectIdjunto com ObjectIdé uma dessas combinações. No entanto, pedir diretamente o mínimo UpdateIdparece resultar na leitura de todas as linhas da tabela. Em vez disso, podemos pedir indiretamente o valor mínimo de UpdateIdcom outra associação à tabela. A idéia é varrer a Updatestabela em ordem, jogar fora as linhas cujo UpdateIdvalor não seja o mínimo para as linhas ObjectIde manter as 100 primeiras linhas. Com base na sua descrição da distribuição de dados, não precisamos jogar muitas linhas.

Para a preparação de dados, coloquei 1 milhão de linhas em uma tabela com 2 linhas para cada ObjectId distinto:

INSERT INTO Updates WITH (TABLOCK)
SELECT t.RN / 2
FROM 
(
    SELECT TOP 1000000 -1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) t;

CREATE INDEX IX On Updates (Objectid, UpdateId);

O índice não clusterizado Objectide UpdateIdé importante. Isso nos permite jogar eficientemente linhas que não têm o mínimo UpdateIdpor Objectid. Existem várias maneiras de escrever uma consulta que corresponda à descrição acima. Aqui está uma maneira de usar NOT EXISTS:

DECLARE @fromUpdateId INT = 9999;
SELECT ObjectId
FROM (
    SELECT DISTINCT TOP 100 u1.UpdateId, u1.ObjectId
    FROM Updates u1
    WHERE UpdateId > @fromUpdateId
    AND NOT EXISTS (
        SELECT 1
        FROM Updates u2
        WHERE u2.UpdateId > @fromUpdateId
        AND u1.ObjectId = u2.ObjectId
        AND u2.UpdateId < u1.UpdateId
    )
    ORDER BY u1.UpdateId, u1.ObjectId
) t;

Aqui está uma imagem do plano de consulta :

plano de consulta

Na melhor das hipóteses, o SQL Server fará apenas 100 buscas de índice no índice não clusterizado. Para simular o azar, mudei a consulta para retornar as primeiras 5000 linhas ao cliente. Isso resultou em buscas no índice 9999, por isso é como obter uma média de 100 linhas por distinto ObjectId. Aqui está a saída de SET STATISTICS IO, TIME ON:

Tabela 'Atualizações'. Contagem de varreduras 10000, leituras lógicas 31900, leituras físicas 0

Tempos de execução do SQL Server: tempo de CPU = 31 ms, tempo decorrido = 42 ms.

Joe Obbish
fonte
9

Adoro a pergunta - o Flow Distinct é um dos meus operadores favoritos.

Agora, a garantia é o problema. Quando você pensa sobre o operador FD puxando linhas do operador Seek de maneira ordenada, produzindo cada linha conforme determina que seja única, isso fornecerá as linhas na ordem correta. Mas é difícil saber se pode haver alguns cenários em que o FD não lida com uma única linha de cada vez.

Teoricamente, o FD poderia solicitar 100 linhas da Busca e produzi-las na ordem que fosse necessária.

As dicas de consulta OPTION (FAST 1, MAXDOP 1)podem ajudar, porque evitarão obter mais linhas do que o necessário do operador Seek. É uma garantia ? Nem tanto. Ainda poderia decidir puxar uma página de linhas por vez, ou algo assim.

Penso que a OPTION (FAST 1, MAXDOP 1)sua OFFSETversão lhe daria muita confiança no pedido, mas não é uma garantia.

Rob Farley
fonte
Pelo que entendi, o problema é que o operador Flow Distinct usa uma tabela de hash que pode se espalhar para o disco. Quando há um derramamento, as linhas que podem ser processadas usando a parte ainda na RAM são processadas imediatamente, mas as outras linhas não são processadas até que os dados derramados sejam lidos novamente a partir do disco. Pelo que sei, qualquer operador que utilize uma tabela de hash (como uma junção de hash) não garante a preservação da ordem devido ao seu comportamento de derramamento.
precisa
Corrigir. Veja a resposta de Paul White.
Rob Farley