Parece haver três regras diferentes do otimizador que podem executar a DISTINCT
operaçã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.
GbAggToSort
implementa 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. GbAggToStrm
implementa 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. GbAggToHS
implementa 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 FOR
dica 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_HEAP
e é 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 à LAG
expressã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_HEAP
e é 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 SUBSTRING
expressão que sempre retorna uma string vazia. O SQL Server não acredita que SUBSTRING
isso 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_CI
e é 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 N
consultas 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 FOR
dica ou aumentar o número estimado de linhas distintas usando LAG()
ou SUBSTRING
em uma coluna exclusiva.
Para completar, outra maneira de abordar esse problema é usar OUTER APPLY . Podemos adicionar um
OUTER APPLY
operador 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 usarTOP
as tabelas derivadas em vez daROW_NUMBER()
solução alternativa. Uma grande desvantagem é que o texto da consulta fica mais longo à medida queN
aumenta.Aqui está uma implementação para a consulta no heap:
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:
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:
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 FOR
consulta 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 desys.dm_exec_sessions
esys.dm_exec_session_wait_stats
. A sessão 56 foi aAPPLY
consulta e a sessão 63 foi aOPTIMIZE FOR
consulta.Saída de
sys.dm_exec_sessions
:Parece haver uma clara vantagem em cpu_time e decorrido_time para a
APPLY
consulta.Saída de
sys.dm_exec_session_wait_stats
:A
OPTIMIZE FOR
consulta 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.fonte
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.
fonte