Por que minha consulta SELECT DISTINCT TOP N varre a tabela inteira?

28

Encontrei algumas SELECT DISTINCT TOP Nconsultas que parecem pouco otimizadas pelo otimizador de consultas do SQL Server. Vamos começar considerando um exemplo trivial: uma tabela de milhões de linhas com dois valores alternados. Vou usar a função GetNums para gerar os dados:

DROP TABLE IF EXISTS X_2_DISTINCT_VALUES;

CREATE TABLE X_2_DISTINCT_VALUES (PK INT IDENTITY (1, 1), VAL INT NOT NULL);

INSERT INTO X_2_DISTINCT_VALUES WITH (TABLOCK) (VAL)
SELECT N % 2
FROM dbo.GetNums(1000000);

UPDATE STATISTICS X_2_DISTINCT_VALUES WITH FULLSCAN;

Para a seguinte consulta:

SELECT DISTINCT TOP 2 VAL
FROM X_2_DISTINCT_VALUES
OPTION (MAXDOP 1);

O SQL Server pode encontrar dois valores distintos apenas verificando a primeira página de dados da tabela, mas, em vez disso, verifica todos os dados . Por que o SQL Server não verifica apenas até encontrar o número solicitado de valores distintos?

Para esta pergunta, use os seguintes dados de teste que contêm 10 milhões de linhas com 10 valores distintos gerados em blocos:

DROP TABLE IF EXISTS X_10_DISTINCT_HEAP;

CREATE TABLE X_10_DISTINCT_HEAP (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_10_DISTINCT_HEAP WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_HEAP WITH FULLSCAN;

As respostas para uma tabela com um índice clusterizado também são aceitáveis:

DROP TABLE IF EXISTS X_10_DISTINCT_CI;

CREATE TABLE X_10_DISTINCT_CI (PK INT IDENTITY (1, 1), VAL VARCHAR(10) NOT NULL, PRIMARY KEY (PK));

INSERT INTO X_10_DISTINCT_CI WITH (TABLOCK) (VAL)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_CI WITH FULLSCAN;

A consulta a seguir verifica todos os 10 milhões de linhas da tabela . Como posso obter algo que não varre a tabela inteira? Estou usando o SQL Server 2016 SP1.

SELECT DISTINCT TOP 10 VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);
Joe Obbish
fonte

Respostas:

30

Parece haver três regras diferentes do otimizador que podem executar a DISTINCToperação na consulta acima. A consulta a seguir gera um erro que sugere que a lista é completa:

SELECT DISTINCT TOP 10 ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, QUERYRULEOFF GbAggToSort, QUERYRULEOFF GbAggToHS, QUERYRULEOFF GbAggToStrm);

Msg 8622, Nível 16, Estado 1, Linha 1

O processador de consultas não pôde produzir um plano de consulta devido às dicas definidas nesta consulta. Submeta novamente a consulta sem especificar nenhuma dica e sem usar SET FORCEPLAN.

GbAggToSortimplementa o agregado agrupar por (distinto) como uma classificação distinta. Este é um operador de bloqueio que lerá todos os dados da entrada antes de produzir qualquer linha. GbAggToStrmimplementa o agregado agrupar por como agregado de fluxo (que também requer uma classificação de entrada nessa instância). Este também é um operador de bloqueio. GbAggToHSimplementa como uma combinação de hash, que é o que vimos no plano ruim da pergunta, mas pode ser implementada como combinação de hash (agregada) ou combinação de hash (fluxo distinto).

O operador de combinação de hash ( fluxo distinto ) é uma maneira de resolver esse problema porque não está bloqueando. O SQL Server deve poder interromper a verificação quando encontrar valores distintos suficientes.

O operador lógico Flow Distinct varre a entrada, removendo duplicatas. Enquanto o operador Distinct consome toda a entrada antes de produzir qualquer saída, o operador Flow Distinct retorna cada linha conforme é obtida a partir da entrada (a menos que essa linha seja uma duplicata, caso em que é descartada).

Por que a consulta na pergunta usa correspondência de hash (agregada) em vez de correspondência de hash (fluxo distinto)? À medida que o número de valores distintos muda na tabela, eu esperaria que o custo da consulta de correspondência de hash (fluxo distinto) diminuísse porque a estimativa do número de linhas que ele precisa digitalizar para a tabela deve diminuir. Eu esperaria que o custo do plano de correspondência de hash (agregado) aumente porque a tabela de hash que ele precisa criar ficará maior. Uma maneira de investigar isso é criando um guia de plano . Se eu criar duas cópias dos dados, mas aplicar um guia de plano a uma delas, devo poder comparar a combinação de hash (agregada) e a combinação de hash (distinta) lado a lado com os mesmos dados. Observe que não posso fazer isso desativando as regras do otimizador de consultas, porque a mesma regra se aplica aos dois planos ( GbAggToHS).

Aqui está uma maneira de obter o guia de plano que busco:

DROP TABLE IF EXISTS X_PLAN_GUIDE_TARGET;

CREATE TABLE X_PLAN_GUIDE_TARGET (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT CAST(N % 10000 AS VARCHAR(10))
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_PLAN_GUIDE_TARGET WITH FULLSCAN;

-- run this query
SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

Obtenha o identificador do plano e use-o para criar um guia de plano:

-- plan handle is 0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000
SELECT qs.plan_handle, st.text FROM 
sys.dm_exec_query_stats AS qs   
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st  
WHERE st.text LIKE '%X[_]PLAN[_]GUIDE[_]TARGET%'
ORDER BY last_execution_time DESC;

EXEC sp_create_plan_guide_from_handle 
'EVIL_PLAN_GUIDE', 
0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000;

Os guias de plano funcionam apenas no texto exato da consulta, portanto, copie-o novamente do guia de plano:

SELECT query_text
FROM sys.plan_guides
WHERE name = 'EVIL_PLAN_GUIDE';

Redefina os dados:

TRUNCATE TABLE X_PLAN_GUIDE_TARGET;

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

Obtenha um plano de consulta para a consulta com o guia de plano aplicado:

SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

Isso tem o operador de combinação de hash (fluxo distinto) que desejávamos com nossos dados de teste. Observe que o SQL Server espera ler todas as linhas da tabela e que o custo estimado é exatamente o mesmo do plano com a correspondência de hash (agregada). O teste que fiz sugeriu que os custos para os dois planos são idênticos quando a meta de linha do plano é maior ou igual ao número de valores distintos que o SQL Server espera da tabela, que nesse caso pode ser simplesmente derivado do Estatisticas. Infelizmente (para nossa consulta), o otimizador escolhe a correspondência de hash (agregada) sobre a correspondência de hash (fluxo distinto) quando os custos são os mesmos. Portanto, estamos 0,0000001 unidades de otimizador mágico longe do plano que queremos.

Uma maneira de atacar esse problema é diminuindo o objetivo da linha. Se a meta da linha do ponto de vista do otimizador for menor que a contagem distinta de linhas, provavelmente obteremos uma combinação de hash (fluxo distinto). Isso pode ser realizado com a OPTIMIZE FORdica de consulta:

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

Para esta consulta, o otimizador cria um plano como se a consulta precisasse apenas da primeira linha, mas quando a consulta é executada, ela volta as 10 primeiras linhas. Na minha máquina, essa consulta verifica 892800 linhas X_10_DISTINCT_HEAPe é concluída em 299 ms com 250 ms de tempo de CPU e 2537 leituras lógicas.

Observe que essa técnica não funcionará se as estatísticas reportarem apenas um valor distinto, o que pode ocorrer para estatísticas amostradas em relação a dados distorcidos. No entanto, nesse caso, é improvável que seus dados sejam compactados o suficiente para justificar o uso de técnicas como essa. Você não pode perder muito examinando todos os dados da tabela, especialmente se isso puder ser feito em paralelo.

Outra maneira de atacar esse problema é aumentar o número estimado de valores distintos que o SQL Server espera obter da tabela base. Isso foi mais difícil do que o esperado. A aplicação de uma função determinística não pode aumentar a contagem distinta de resultados. Se o otimizador de consulta estiver ciente desse fato matemático (alguns testes sugerem que é pelo menos para nossos propósitos), a aplicação de funções determinísticas (que inclui todas as funções de string ) não aumentará o número estimado de linhas distintas.

Muitas das funções não determinísticas também não funcionaram, incluindo as escolhas óbvias de NEWID()e RAND(). No entanto, LAG()faz o truque para esta consulta. O otimizador de consulta espera 10 milhões de valores distintos em relação à LAGexpressão, o que incentivará um plano de correspondência de hash (fluxo distinto) :

SELECT DISTINCT TOP 10 LAG(VAL, 0) OVER (ORDER BY (SELECT NULL)) AS ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

Na minha máquina, essa consulta varre 892800 linhas X_10_DISTINCT_HEAPe é concluída em 1165 ms com 1109 ms de tempo de CPU e 2537 leituras lógicas, portanto, isso LAG()adiciona um pouco de sobrecarga relativa. @Paul White sugeriu tentar o processamento em lote para esta consulta. No SQL Server 2016, podemos obter o processamento em modo de lote mesmo com MAXDOP 1. Uma maneira de obter o processamento em modo de lote para uma tabela rowstore é ingressar em uma CCI vazia da seguinte maneira:

CREATE TABLE #X_DUMMY_CCI (ID INT NOT NULL);

CREATE CLUSTERED COLUMNSTORE INDEX X_DUMMY_CCI ON #X_DUMMY_CCI;

SELECT DISTINCT TOP 10 VAL
FROM
(
    SELECT LAG(VAL, 1) OVER (ORDER BY (SELECT NULL)) AS VAL
    FROM X_10_DISTINCT_HEAP
    LEFT OUTER JOIN #X_DUMMY_CCI ON 1 = 0
) t
WHERE t.VAL IS NOT NULL
OPTION (MAXDOP 1);

Esse código resulta neste plano de consulta .

Paul apontou que eu tive que alterar a consulta para usar, LAG(..., 1)porque LAG(..., 0)não parece ser elegível para a otimização de agregação de janelas. Essa alteração reduziu o tempo decorrido para 520 ms e o tempo da CPU para 454 ms.

Observe que a LAG()abordagem não é a mais estável. Se a Microsoft alterar a suposição de exclusividade em relação à função, ela poderá não funcionar mais. Ele tem uma estimativa diferente com o CE herdado. Além disso, esse tipo de otimização contra um monte não é uma boa idéia. Se a tabela for reconstruída, é possível acabar no pior cenário possível, no qual quase todas as linhas precisam ser lidas na tabela.

Em uma tabela com uma coluna exclusiva (como o exemplo de índice clusterizado na pergunta), temos melhores opções. Por exemplo, podemos enganar o otimizador usando uma SUBSTRINGexpressão que sempre retorna uma string vazia. O SQL Server não acredita que SUBSTRINGisso alterará o número de valores distintos; portanto, se o aplicarmos a uma coluna exclusiva, como PK, o número estimado de linhas distintas será 10 milhões. Esta consulta a seguir obtém o operador de correspondência de hash (fluxo distinto):

SELECT DISTINCT TOP 10 VAL + SUBSTRING(CAST(PK AS VARCHAR(10)), 11, 1)
FROM X_10_DISTINCT_CI
OPTION (MAXDOP 1);

Na minha máquina, essa consulta varre 900000 linhas X_10_DISTINCT_CIe é concluída em 333 ms com 297 ms de tempo de CPU e 3011 leituras lógicas.

Em resumo, o otimizador de consulta parece assumir que todas as linhas serão lidas na tabela para SELECT DISTINCT TOP Nconsultas quando N> = o número estimado de linhas distintas da tabela. O operador de combinação de hash (agregado) pode ter o mesmo custo que o operador de combinação de hash (fluxo distinto), mas o otimizador sempre escolhe o operador agregado. Isso pode levar a leituras lógicas desnecessárias quando valores distintos suficientes estão localizados perto do início da varredura da tabela. Duas maneiras de induzir o otimizador a usar o operador de correspondência de hash (fluxo distinto) são diminuir a meta de linha usando a OPTIMIZE FORdica ou aumentar o número estimado de linhas distintas usando LAG()ou SUBSTRINGem uma coluna exclusiva.

Joe Obbish
fonte
12

Você já respondeu suas próprias perguntas corretamente.

Eu só quero adicionar uma observação de que a maneira mais eficiente é realmente verificar a tabela inteira - se ela puder ser organizada como um 'heap' de columnstore :

CREATE CLUSTERED COLUMNSTORE INDEX CCSI 
ON dbo.X_10_DISTINCT_HEAP;

A consulta simples:

SELECT DISTINCT TOP (10)
    XDH.VAL 
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (MAXDOP 1);

então dá:

Plano de execução

Tabela 'X_10_DISTINCT_HEAP'. Contagem de digitalizações 1,
 leituras lógicas 0, leituras físicas 0, leituras antecipadas 0, 
 lob lógico lê 66 , lob físico lê 0, lob read-ahead lê 0.
Tabela 'X_10_DISTINCT_HEAP'. O segmento lê 13, o segmento pulou 0.

 Tempos de execução do SQL Server:
   Tempo de CPU = 0 ms, tempo decorrido = 11 ms.

Atualmente, a correspondência de hash (fluxo distinto) não pode ser executada no modo em lote. Os métodos que usam isso são muito mais lentos devido à transição (invisível) cara do processamento em lote para a linha. Por exemplo:

SET ROWCOUNT 10;

SELECT DISTINCT 
    XDH.VAL
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (FAST 1);

SET ROWCOUNT 0;

Dá:

Plano de execução distinto de fluxo

Tabela 'X_10_DISTINCT_HEAP'. Contagem de digitalizações 1,
 leituras lógicas 0, leituras físicas 0, leituras antecipadas 0, 
 lob lógico lê 20 , lob físico lê 0, lob read-ahead lê 0.
Tabela 'X_10_DISTINCT_HEAP'. O segmento lê 4 , o segmento pulou 0.

 Tempos de execução do SQL Server:
   Tempo de CPU = 640 ms, tempo decorrido = 680 ms.

Isso é mais lento do que quando a tabela está organizada como um heap de armazenamento de linhas.

Paul White diz que a GoFundMonica
fonte
5

Aqui está uma tentativa de emular uma varredura parcial repetida (semelhante, mas não igual a uma ignorar varredura) usando uma CTE recursiva. O objetivo - como não temos nenhum índice (id)- é evitar classificações e varreduras múltiplas em cima da mesa.

Ele faz alguns truques para contornar algumas restrições CTE recursivas:

  • Não é TOPpermitido na parte recursiva. Usamos uma subconsulta e, em ROW_NUMBER()vez disso.
  • Não podemos ter várias referências à parte constante ou usar LEFT JOINou usar NOT IN (SELECT id FROM cte)a parte recursiva. Para ignorar, criamos uma VARCHARstring que acumula todos os idvalores, semelhantes a STRING_AGGou hierarchyID e, em seguida, comparamos com LIKE.

Para um Heap (assumindo que a coluna tenha o nome id) test-1 em rextester.com .

Isso - como os testes mostraram - não evita varreduras múltiplas, mas executa OK quando valores diferentes são encontrados nas primeiras páginas. Se, no entanto, os valores não forem distribuídos uniformemente, ele poderá fazer várias varreduras em grandes partes da tabela - o que, é claro, resulta em baixo desempenho.

WITH ct (id, found, list) AS
  ( SELECT TOP (1) id, 1, CAST('/' + id + '/' AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.ID, ct.found + 1, CAST(ct.list + y.id + '/' AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 3         -- the TOP (n) parameter here
      AND y.rn = 1
  )
SELECT id FROM ct ;

e quando a tabela estiver em cluster (IC ativadounique_key ), teste-2 em rextester.com .

Isso usa o índice clusterizado ( WHERE x.unique_key > ct.unique_key) para evitar várias verificações:

WITH ct (unique_key, id, found, list) AS
  ( SELECT TOP (1) unique_key, id, 1, CAST(CONCAT('/',id, '/') AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.unique_key, y.ID, ct.found + 1, 
        CAST(CONCAT(ct.list, y.id, '/') AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.unique_key, x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE x.unique_key > ct.unique_key
          AND ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 5       -- the TOP (n) parameter here
      AND y.rn = 1
  )
-- SELECT * FROM ct ;        -- for debugging
SELECT id FROM ct ;
ypercubeᵀᴹ
fonte
Há um problema de desempenho bastante sutil com esta solução. Ele acaba fazendo uma busca extra na tabela depois de encontrar o valor enésimo. Portanto, se houver 10 valores distintos para um top 10, ele procurará um 11º valor que não existe. Você acaba com uma verificação completa adicional e os 10 milhões de cálculos de ROW_NUMBER () realmente somam. Eu tenho uma solução alternativa aqui que acelera a consulta 20X na minha máquina. O que você acha? brentozar.com/pastetheplan/?id=SkDhAmFKe
Joe Obbish
2

Para completar, outra maneira de abordar esse problema é usar OUTER APPLY . Podemos adicionar um OUTER APPLYoperador para cada valor distinto que precisamos encontrar. Isso é semelhante em conceito à abordagem recursiva do ypercube, mas efetivamente tem a recursão escrita à mão. Uma vantagem é que podemos usar TOPas tabelas derivadas em vez da ROW_NUMBER()solução alternativa. Uma grande desvantagem é que o texto da consulta fica mais longo à medida que Naumenta.

Aqui está uma implementação para a consulta no heap:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t2 WHERE t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t3 WHERE t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t4 WHERE t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t5 WHERE t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t6 WHERE t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t7 WHERE t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t8 WHERE t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t9 WHERE t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t10 WHERE t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

Aqui está o plano de consulta real para a consulta acima. Na minha máquina, essa consulta é concluída em 713 ms com 625 ms de tempo de CPU e 12605 leituras lógicas. Obtemos um novo valor distinto a cada 100 mil linhas, portanto, espero que essa consulta verifique em torno de 900000 * 10 * 0,5 = 4500000 linhas. Em teoria, essa consulta deve fazer cinco vezes as leituras lógicas dessa consulta da outra resposta:

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

Essa consulta fez 2537 leituras lógicas. 2537 * 5 = 12685, que é bem próximo de 12605.

Para a tabela com o índice clusterizado, podemos fazer melhor. Isso ocorre porque podemos passar o último valor da chave em cluster para a tabela derivada para evitar a varredura das mesmas linhas duas vezes. Uma implementação:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t2 WHERE PK > t1.PK AND t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t3 WHERE PK > t2.PK AND t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t4 WHERE PK > t3.PK AND t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t5 WHERE PK > t4.PK AND t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t6 WHERE PK > t5.PK AND t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t7 WHERE PK > t6.PK AND t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t8 WHERE PK > t7.PK AND t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t9 WHERE PK > t8.PK AND t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t10 WHERE PK > t9.PK AND t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

Aqui está o plano de consulta real para a consulta acima. Na minha máquina, essa consulta é concluída em 154 ms com 140 ms de tempo de CPU e 3203 leituras lógicas. Isso pareceu executar um pouco mais rápido que a OPTIMIZE FORconsulta na tabela de índice em cluster. Eu não esperava isso, então tentei medir o desempenho com mais cuidado. Minha metodologia era executar cada consulta dez vezes sem conjuntos de resultados e examinar os números agregados de sys.dm_exec_sessionse sys.dm_exec_session_wait_stats. A sessão 56 foi a APPLYconsulta e a sessão 63 foi a OPTIMIZE FORconsulta.

Saída de sys.dm_exec_sessions:

╔════════════╦══════════╦════════════════════╦═══════════════╗
 session_id  cpu_time  total_elapsed_time  logical_reads 
╠════════════╬══════════╬════════════════════╬═══════════════╣
         56      1360                1373          32030 
         63      2094                2091          30400 
╚════════════╩══════════╩════════════════════╩═══════════════╝

Parece haver uma clara vantagem em cpu_time e decorrido_time para a APPLYconsulta.

Saída de sys.dm_exec_session_wait_stats:

╔════════════╦════════════════════════════════╦═════════════════════╦══════════════╦══════════════════╦═════════════════════╗
 session_id            wait_type             waiting_tasks_count  wait_time_ms  max_wait_time_ms  signal_wait_time_ms 
╠════════════╬════════════════════════════════╬═════════════════════╬══════════════╬══════════════════╬═════════════════════╣
         56  SOS_SCHEDULER_YIELD                             340             0                 0                    0 
         56  MEMORY_ALLOCATION_EXT                            38             0                 0                    0 
         63  SOS_SCHEDULER_YIELD                             518             0                 0                    0 
         63  MEMORY_ALLOCATION_EXT                            98             0                 0                    0 
         63  RESERVED_MEMORY_ALLOCATION_EXT                  400             0                 0                    0 
╚════════════╩════════════════════════════════╩═════════════════════╩══════════════╩══════════════════╩═════════════════════╝

A OPTIMIZE FORconsulta tem um tipo de espera adicional, RESERVED_MEMORY_ALLOCATION_EXT . Não sei exatamente o que isso significa. Pode ser apenas uma medida da sobrecarga no operador de combinação de hash (fluxo distinto). De qualquer forma, talvez não valha a pena se preocupar com uma diferença de 70 ms no tempo da CPU.

Joe Obbish
fonte
1

Eu acho que você tem uma resposta sobre por que
isso pode ser uma maneira de resolvê-lo.
Eu sei que parece confuso, mas o plano de execução disse que os 2 principais distintos representam 84% do custo.

SELECT distinct top (2)  [enumID]
FROM [ENRONbbb].[dbo].[docSVenum1]

declare @table table (enumID tinyint);
declare @enumID tinyint;
set @enumID = (select top (1) [enumID] from [docSVenum1]);
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
select enumID from @table;
paparazzo
fonte
Este código levou 5 segundos na minha máquina. Parece que as junções à variável da tabela adicionam um pouco de sobrecarga. Na consulta final, a variável da tabela foi varrida 892800 vezes. Essa consulta levou 1359 ms de tempo de CPU e 1374 ms de tempo decorrido. Definitivamente mais do que eu esperava. Adicionar uma chave primária à variável da tabela parece ajudar, embora não tenha certeza do motivo. Pode haver outras otimizações possíveis.
21417 Joe Obbish