Coluna computada persistente causando varredura

9

A conversão de uma coluna regular em uma coluna computada persistente está fazendo com que essa consulta não consiga realizar buscas de índice. Por quê?

Testado em várias versões do SQL Server, incluindo 2016 SP1 CU1 2016.

Repros

O problema é com table1, col7.

As tabelas e a consulta são uma versão parcial (e simplificada) dos originais. Estou ciente de que a consulta pode ser reescrita de maneira diferente e, por algum motivo, evitar o problema, mas precisamos evitar tocar no código, e a pergunta de por table1que não pode ser procurada ainda permanece.

Como Paul White mostrou (obrigado!), A busca está disponível se forçada, então a pergunta é: por que a busca não é escolhida pelo otimizador e se podemos fazer algo diferente para que a busca aconteça como deveria, sem alterar o código?

Para esclarecer a parte problemática, aqui está a verificação relevante no plano de execução incorreta:

plano

Alex Friedman
fonte

Respostas:

12

Por que a busca não é escolhida pelo otimizador


TL: DR A definição expandida da coluna computada interfere na capacidade do otimizador de reordenar uniões inicialmente. Com um ponto de partida diferente, a otimização baseada em custos segue um caminho diferente através do otimizador e termina com uma escolha de plano final diferente.


Detalhes

Para todas as consultas, exceto a mais simples, o otimizador não tenta explorar nada como todo o espaço possível de planos. Em vez disso, ele escolhe um ponto de partida de aparência razoável e gasta uma quantidade orçada de esforço explorando variações lógicas e físicas, em uma ou mais fases de pesquisa, até encontrar um plano razoável.

A principal razão pela qual você obtém planos diferentes (com diferentes estimativas de custo final) para os dois casos é que existem pontos de partida diferentes . A partir de um local diferente, a otimização termina em um local diferente (após seu número limitado de iterações de exploração e implementação). Espero que isso seja razoavelmente intuitivo.

O ponto de partida que mencionei é um pouco baseado na representação textual da consulta, mas são feitas alterações na representação interna da árvore conforme ela passa pelos estágios de análise, ligação, normalização e simplificação da compilação da consulta.

É importante ressaltar que o ponto de partida exato depende muito da ordem de junção inicial selecionada pelo otimizador. Essa escolha é feita antes do carregamento das estatísticas e antes de quaisquer estimativas de cardinalidade serem derivadas. A cardinalidade total (número de linhas) em cada tabela é no entanto conhecida, tendo sido obtida dos metadados do sistema.

A ordem de junção inicial é, portanto, baseada em heurísticas . Por exemplo, o otimizador tenta reescrever a árvore para que as tabelas menores sejam unidas antes das maiores e as junções internas antes das junções externas (e junções cruzadas).

A presença da coluna computada interfere nesse processo, mais especificamente na capacidade do otimizador de empurrar as junções externas para baixo na árvore de consultas. Isso ocorre porque a coluna computada é expandida em sua expressão subjacente antes que ocorra a reordenação da junção, e mover uma junção após uma expressão complexa é muito mais difícil do que movê-la para uma referência de coluna simples.

As árvores envolvidas são muito grandes, mas para ilustrar, a árvore de consulta inicial da coluna não computada começa com: (observe as duas junções externas na parte superior)

LogOp_Select
    LogOp_Apply (x_jtLeftOuter) 
        LogOp_LeftOuterJoin
            LogOp_NAryJoin
                LogOp_LeftAntiSemiJoin
                    LogOp_NAryJoin
                        LogOp_Get TBL: dbo.table1 (também conhecido por TBL: a4)
                        LogOp_Select
                            LogOp_Get TBL: dbo.table6 (também conhecido por TBL: a3)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [a3] .col18
                                ScaOp_Const TI (agrupamento de varchar 53256, Var, Trim, ML = 16)
                        LogOp_Select
                            LogOp_Get TBL: dbo.table1 (também conhecido por TBL: a1)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [a1] .col2
                                ScaOp_Const TI (agrupamento de varchar 53256, Var, Trim, ML = 16)
                        LogOp_Select
                            LogOp_Get TBL: dbo.table5 (também conhecido por TBL: a2)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [a2] .col2
                                ScaOp_Const TI (agrupamento de varchar 53256, Var, Trim, ML = 16)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a4] .col2
                            ScaOp_Identifier QCOL: [a3] .col19
                    LogOp_Select
                        LogOp_Get TBL: dbo.table7 (também conhecido por TBL: a7)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a7] .col22
                            ScaOp_Const TI (agrupamento de varchar 53256, Var, Trim, ML = 16)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4] .col2
                        ScaOp_Identifier QCOL: [a7] .col23
                LogOp_Select
                    LogOp_Get TBL: tabela1 (também conhecido por TBL: cdc)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [cdc] .col6
                        ScaOp_Const TI (smallint, ML = 2) XVAR (smallint, Não Possuído, Valor = 4)
                LogOp_Get TBL: dbo.table5 (também conhecido por TBL: a5) 
                LogOp_Get TBL: tabela2 (também conhecido por TBL: cdt)  
                ScaOp_Logical x_lopAnd
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a5] .col2
                        ScaOp_Identifier QCOL: [cdc] .col2
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4] .col2
                        ScaOp_Identifier QCOL: [cdc] .col2
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [cdt] .col1
                        ScaOp_Identifier QCOL: [cdc] .col1
            LogOp_Get TBL: tabela3 (também conhecido por TBL: ahcr)
            ScaOp_Comp x_cmpEq
                ScaOp_Identifier QCOL: [ahcr] .col9
                ScaOp_Identifier QCOL: [cdt] .col1

O mesmo fragmento da consulta da coluna computada é: (observe a junção externa muito mais abaixo, a definição expandida da coluna computada e algumas outras diferenças sutis na ordem de junção (interna))

LogOp_Select
    LogOp_Apply (x_jtLeftOuter)
        LogOp_NAryJoin
            LogOp_LeftAntiSemiJoin
                LogOp_NAryJoin
                    LogOp_Get TBL: dbo.table1 (também conhecido por TBL: a4)
                    LogOp_Select
                        LogOp_Get TBL: dbo.table6 (também conhecido por TBL: a3)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a3] .col18
                            ScaOp_Const TI (agrupamento de varchar 53256, Var, Trim, ML = 16)
                    LogOp_Select
                        LogOp_Get TBL: dbo.table1 (também conhecido por TBL: a1
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a1] .col2
                            ScaOp_Const TI (agrupamento de varchar 53256, Var, Trim, ML = 16)
                    LogOp_Select
                        LogOp_Get TBL: dbo.table5 (também conhecido por TBL: a2)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a2] .col2
                            ScaOp_Const TI (agrupamento de varchar 53256, Var, Trim, ML = 16)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4] .col2
                        ScaOp_Identifier QCOL: [a3] .col19
                LogOp_Select
                    LogOp_Get TBL: dbo.table7 (também conhecido por TBL: a7) 
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a7] .col22
                        ScaOp_Const TI (agrupamento de varchar 53256, Var, Trim, ML = 16)
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL: [a4] .col2
                    ScaOp_Identifier QCOL: [a7] .col23
            LogOp_Project
                LogOp_LeftOuterJoin
                    LogOp_Join
                        LogOp_Select
                            LogOp_Get TBL: tabela1 (também conhecido por TBL: cdc) 
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [cdc] .col6
                                ScaOp_Const TI (smallint, ML = 2) XVAR (smallint, Não Possuído, Valor = 4)
                        LogOp_Get TBL: tabela2 (também conhecido por TBL: cdt) 
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [cdc] .col1
                            ScaOp_Identifier QCOL: [cdt] .col1
                    LogOp_Get TBL: table3 (também conhecido por TBL: ahcr) 
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [ahcr] .col9
                        ScaOp_Identifier QCOL: [cdt] .col1
                AncOp_PrjList 
                    QCOL AncOp_PrjEl: [cdc] .col7
                        ScaOp_Convert char collate 53256, Null, Trim, ML = 6
                            ScaOp_IIF varchar agrupar 53256, Nulo, Var, Trim, ML = 6
                                ScaOp_Comp x_cmpEq
                                    ScaOp_Intrinsic isnumeric
                                        ScaOp_Intrinsic right
                                            ScaOp_Identifier QCOL: [cdc] .col4
                                            ScaOp_Const TI (int, ML = 4) XVAR (int, Não possuído, Valor = 4)
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, Não possuído, Valor = 0)
                                ScaOp_Const TI (varchar agrupar 53256, Var, Trim, ML = 1) XVAR (varchar, Propriedade, Valor = Len, Dados = (0,))
                                ScaOp_Instrinsic substring
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, Não possuído, Valor = 6)
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, Não possuído, Valor = 1)
                                    ScaOp_Identifier QCOL: [cdc] .col4
            LogOp_Get TBL: dbo.table5 (também conhecido por TBL: a5)
            ScaOp_Logical x_lopAnd
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL: [a5] .col2
                    ScaOp_Identifier QCOL: [cdc] .col2
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL: [a4] .col2
                    ScaOp_Identifier QCOL: [cdc] .col2

As estatísticas são carregadas e uma estimativa de cardinalidade inicial é executada na árvore logo após a ordem de junção inicial ser definida. Ter as junções em ordens diferentes também afeta essas estimativas e, portanto, tem um efeito indireto durante a otimização posterior baseada em custos.

Finalmente, para esta seção, ter uma junção externa presa no meio da árvore pode impedir que outras regras de reordenação de junções sejam correspondidas durante a otimização baseada em custos.


O uso de um guia de plano (ou, equivalentemente, uma USE PLANdica - exemplo para sua consulta ) altera a estratégia de pesquisa para uma abordagem mais orientada a objetivos, guiada pela forma geral e pelos recursos do modelo fornecido. Isso explica por que o otimizador pode encontrar o mesmo table1plano de busca nos esquemas de colunas calculados e não calculados, quando um guia ou dica de plano é usada.

Se podemos fazer algo diferente para fazer a busca acontecer

Isso é algo com o qual você só precisa se preocupar se o otimizador não encontrar um plano com características de desempenho aceitáveis ​​por conta própria.

Todas as ferramentas normais de ajuste são potencialmente aplicáveis. Você pode, por exemplo, dividir a consulta em partes mais simples, revisar e melhorar a indexação disponível, atualizar ou criar novas estatísticas ... e assim por diante.

Todas essas coisas podem afetar as estimativas de cardinalidade, o caminho do código adotado pelo otimizador e influenciar as decisões baseadas em custos de maneiras sutis.

Você pode finalmente usar dicas (ou um guia de plano), mas essa não é geralmente a solução ideal.


Perguntas adicionais dos comentários

Concordo que é melhor simplificar a consulta etc., mas existe uma maneira (sinalizador de rastreamento) de fazer o otimizador continuar com a otimização e alcançar o mesmo resultado?

Não, não há sinalizador de rastreamento para executar uma pesquisa exaustiva e você não deseja um. O possível espaço de pesquisa é vasto e os tempos de compilação que excedem a idade do universo não seriam bem recebidos. Além disso, o otimizador não conhece todas as transformações lógicas possíveis (ninguém sabe).

Além disso, por que a expansão complexa é necessária, pois a coluna é mantida? Por que o otimizador não pode evitar expandi-lo, tratá-lo como uma coluna comum e alcançar o mesmo ponto de partida?

As colunas computadas são expandidas (como as visualizações) para permitir oportunidades adicionais de otimização. A expansão pode corresponder, por exemplo, a uma coluna ou índice persistente posteriormente no processo, mas isso ocorre depois que a ordem de junção inicial é corrigida.

Paul White 9
fonte