Eu tenho um CTE recursivo dentro de uma função com valor de tabela embutida. O ITVF está retornando uma lista de IDs contendo uma longa sequência de ancestrais de uma pessoa, geralmente retorna cerca de 12 a 18 vezes antes de chegar ao fim. É bastante rápido, mas há um erro nas estimativas que se acumula quando usado por muitas pessoas, tornando-se extremamente lento.
O CTE fica assim
WITH ancestors AS (
SELECT
IndID,
AncestorID
FROM
dbo.persons
UNION ALL
SELECT
IndID,
AncestorID
FROM
ancestors a
INNER JOIN dbo.persons p ON p.IndID = a.AncestorID
)
SELECT IndID, AncestorID FROM ancestors
Eu tenho uma dúzia de milhões de linhas, então é uma tabela bem grande. Quando solicito um IndID, o plano de execução diz que estimou 7 linhas, mas obteve 1.300 linhas reais. Para uma única solicitação, é aceitável (é executado em menos de um segundo), mas se eu me juntar a ela em outra solicitação para que ela seja chamada, digamos 100 vezes, a velocidade cai para um rastreamento, pois a estimativa está ficando cada vez pior.
Só para ficar claro, o erro de estimativa está presente mesmo fora do IVTF. Especifiquei apenas para deixar claro que não posso usar apenas uma tabela temporária. Ele precisa ficar em um IVTF para que eu possa integrá-lo em solicitações maiores e mais complexas e permanecer paralelizado. O que posso fazer para estimar melhor as linhas?
Atualização: cole o plano
Atualização 2: Menos simplificado
Estou meio preso entre dois problemas. Ou eu uso um MSTVF e todas as minhas consultas não podem ser paralelizadas, ou uso um ITVF e espero que os deuses do SQL sejam generosos e não subestimem terrivelmente a contagem de linhas, então tudo agora é trocado no disco rígido em vez de na RAM. Espero que seja apenas porque sou burro e que seja uma solução estúpida e fácil em algum lugar.
Atualização 3 Para responder, com o melhor de meu conhecimento, às perguntas feitas.
uno) Atualizado para a atualização cumulativa mais recente. Não mudou nada como esperado, mas é bom estar atualizado como você disse :)
dos) Estamos na edição Standard, mas tenho uma Column Store e não me lembro por que fiz isso. Está em IndID, FirstNameID, LastNameID. Vou tentar abandoná-lo, somos apenas 2 usuários no banco de dados hoje, podemos gerenciar os tempos de inatividade se ele travar alguma outra coisa.
Depois de remover o ColumnStore, ele economizou cerca de 30 segundos! Ainda lento, mas está melhor. Terei que verificar minhas anotações para descobrir por que fiz aquele ColumnStore.
dos: parte 2) A sensação de "caixa com pouca potência" que você tem é exatamente o que me pegou até agora. Achei que nossa máquina não tinha potência suficiente, mas depois de conversar com a TI aqui, eles disseram que não estávamos usando mais de 25% dos recursos disponíveis, então o gargalo estava definitivamente no nível SQL. Então, pedi uma atualização do SQL 2017 para 2022 no mês passado e então, agora que vi que a maioria das minhas consultas pesadas estavam sempre rodando serializadas, comecei a otimizar até chegar a esta. Eu tentei OPTION(USE HINT('DISALLOW_BATCH_MODE'), MAXDOP 8);
e não vejo nenhuma mudança na velocidade.
tres) Essa solicitação deve retornar cerca de 14 milhões de linhas, então não se preocupe desse lado. Mas o fato de apenas 8 linhas terem sido estimadas na reserva de recursos não é uma razão pela qual ela é muito mais lenta do que deveria?
mais contexto) Eu estava usando um MSTVF antes de todo o meu trabalho este mês, quando mudei para um IVTF ele é mais rápido, mas a curva de tempo gasto versus linhas solicitadas é exponencial em vez de linear, se é que você me entende. Estou aberto para repensar como tudo isso é feito.
Trabalho para um grupo de pesquisa e parte do meu trabalho é extrair conjuntos de dados para pesquisadores. Sou praticamente o único usuário pesado do banco de dados, meus colegas estão mais envolvidos na parte do trabalho "inserir e limpar os dados". Portanto, posso fazer praticamente o que quiser com os índices, funções, etc., desde que a estrutura da tabela em si não seja muito alterada.
Atualização 4 - O quê? Não entendi, estava tentando fazer um belo gráfico para mostrar a curva exponencial "tempo gasto versus linhas solicitadas", então mudei minha consulta para obter bons números quadrados.
select
count(*)
FROM
(SELECT TOP 10000 * FROM individus.Individus WHERE AnneeNaissance > 1901 AND AnneeDeces < 1911) i CROSS APPLY
individus.GetAscendanceSimple(i.IndID) a
E isso funcionou em 10 segundos... Até tentei o TOP 10.000.000 e ainda rápido, então só tenho que colocar um número arbitrário grande para que todos os meus casos sejam cobertos e ele rode tão rápido quanto eu esperava (O TOP é importante). Antes de colocar isso como solução, devo estar errado, não? Essa é uma solução realmente estúpida se for tudo o que precisamos fazer para corrigir o planejamento.
Quanto mais olho para isso, mais dúvidas tenho em relação ao plano de consulta. Provavelmente isso não responderá à sua pergunta na primeira tentativa, mas vamos começar.
um
Primeiro, o XML do plano indica que você está no SQL Server 2022 RTM (16.0.1000.6), o que coloca 11 atualizações cumulativas atrás das atuais. Não estou culpando seus problemas, mas não custa nada estar atualizado.
dos
Em segundo lugar, e mais intrigante: você está na edição Standard ou Enterprise do SQL Server ou
Individus
uma coluna armazena índice em algum lugar?A razão pela qual pergunto é porque o otimizador decidiu que um operador em seu plano deveria ser executado em modo lote:
Na ausência de um índice de armazenamento de coluna, isso só acontece na Enterprise Edition com o recurso Intelligent Query Processing, Batch Mode On Row Store.
O que torna isso ainda mais confuso é que seu plano só é executado com um Grau de Paralelismo (DOP) de 2. Limitar as operações do modo em lote a um DOP de 2 é algo que apenas a Standard Edition faz.
Normalmente eu diria que você está em uma caixa com pouca potência, mas o plano de consulta também indica que você tem 12 threads disponíveis para uma consulta paralela. Talvez o DOP esteja limitado a dois lugares que o otimizador desconhece.
Isso se torna lamentável no seu caso no Hash Match Aggregate mencionado acima, porque um thread fica parado por cerca de 22 segundos aguardando no HTBUILD.
Aqui estão as estatísticas de tempo real para o agregado de correspondência de hash. Observe a alta duração (tempo de relógio) versus tempo de CPU muito baixo.
Uma coisa que você deve tentar é encerrar sua consulta com
OPTION(USE HINT('DISALLOW_BATCH_MODE'), MAXDOP 8);
.tres
Deixando tudo isso de lado, sua consulta tem outras esperas bem estranhas.
PAGELATCH_SH e PAGELATCH_EX combinados representam aproximadamente 49 segundos de tempo de espera enquanto sua consulta é executada. Aqui estão a maioria dos horários do operador em seu plano antes do agregado de correspondência de hash do modo em lote:
Coloquei setas apontando para dois operadores que usam tempdb para armazenamento, porque às vezes (mas nem sempre, claro) aquelas esperas PAGELATCH_XX de que acabei de falar estão associadas à atividade lá. Não há como contornar esses spools em um plano de execução de expressão de tabela comum recursiva que eu conheça.
Ler os tempos dos operadores é um pouco peculiar. No Modo Linha, os operadores pais mostram não apenas seu tempo, mas também o tempo gasto nos operadores filhos. No Modo Lote cada operador mostra apenas o tempo gasto.
Um dos motivos pelos quais mencionei a possível contenção de tempdb aqui é que seu Lazy Index Spool fica bastante grande, com cerca de 14 milhões de linhas, e como carregá-lo com dados é a única coisa que geraria esperas de PAGELATCH_EX, vale a pena verificar.
está bem então
Dito isso, a estimativa de cardinalidade não é a raiz do seu problema aqui, e usar um MSTVF, que usaria uma variável de tabela, provavelmente não ajudaria na sua situação de tempdb ou na sua situação de estimativa de cardinalidade.
Embora as variáveis de tabela nas versões mais recentes do SQL Server possam obter estimativas de cardinalidade no nível da tabela, elas ainda não obtêm nenhum histograma no nível da coluna criado para elas, mesmo se você adicionar um índice. Eles também causarão uma zona serial no seu plano, sempre que os dados forem modificados neles (no seu caso, na inserção) e quando a variável da tabela retornada for lida pela consulta externa.
Edite sua pergunta para adicionar detalhes sobre a leitura. Tentarei acompanhar conforme o tempo permitir para editar minha resposta de acordo.
quatro
Não tenho seu banco de dados para experimentar, mas posso fazer uma tentativa razoável com minha cópia do banco de dados StackOverflow.
Para começar, adicionei alguns índices:
E uma função semelhante à sua:
And then a starting query that results in a Lazy Spool that gets about 14 million rows and change in it. I have some hints here to better match your environment.
That query takes about 1 minute for me. Part of the problem is that recursive common table expressions don't generally qualify for parallel plans, and that parallel plans involving Nested Loops joins really just run DOP copies of the serial plan. Because of that, they're really sensitive to uneven row distributions on parallel threads.
The reason why the query with a TOP in it runs so much faster is because the Top operator introduces a serial zone in the query plan, which requires a Distribute Streams Exchange operator to move the rows back into a parallel portion of the plan.
I can duplicate the behavior by copying your queries:
Há apenas uma diferença de tempo de cerca de 30 segundos para mim, mas o motivo está nos seus planos de consulta e nos meus. Aqui estão algumas capturas de tela da distribuição de linhas paralelas em seus dois novos exemplos de planos de consulta:
É claro que esse problema não era óbvio nos planos de consulta originais que você compartilhou, porque o modo em lote na Standard Edition estava limitando você a um DOP de 2. Em um plano de modo totalmente em linha, onde você pode usar um DOP muito mais alto , a questão fica clara.
Você pode aprender muito mais sobre isso na sessão de Adam Machanic sobre metas de linha .
Como você governou tudo além dos ITVFs, nunca obterá uma boa estimativa de cardinalidade com isso. Simplesmente não há como saber em tempo de compilação quantas linhas você obterá. As restrições do ITVF excluem quaisquer truques úteis, como
OPTION (RECOMPILE)
tabelas temporárias. Dito isto, não vejo nenhuma razão para você ter descartado funções com valor de tabela com múltiplas instruções. Você já pensou em usar variáveis de tabela eOPTION (RECOMPILE)
?Desafio do quadro: pare de usar CTEs recursivos para esta tarefa. hierarquiaid é a solução moderna. Melhor ainda, evite completamente o SQL, se puder. Este é um trabalho para um banco de dados gráfico.