Database
 sql >> Base de Dados >  >> RDS >> Database

Estimativa de Cardinalidade:Combinando Estatísticas de Densidade


Este artigo mostra como o SQL Server combina informações de densidade de várias estatísticas de coluna única para produzir uma estimativa de cardinalidade para uma agregação em várias colunas. Espera-se que os detalhes sejam interessantes por si mesmos. Eles também fornecem uma visão sobre algumas das abordagens gerais e algoritmos usados ​​pelo estimador de cardinalidade.

Considere a seguinte consulta de banco de dados de exemplo do AdventureWorks, que lista o número de itens de inventário de produtos em cada compartimento em cada prateleira do depósito:
SELECT 
    INV.Shelf, 
    INV.Bin, 
    COUNT_BIG(*)
FROM Production.ProductInventory AS INV
GROUP BY 
    INV.Shelf,
    INV.Bin
ORDER BY 
    INV.Shelf,
    INV.Bin;

O plano de execução estimado mostra 1.069 linhas sendo lidas da tabela, classificadas em Shelf e Bin order e, em seguida, agregados usando um operador Stream Aggregate:

Plano de Execução Estimado

A questão é:como o otimizador de consultas do SQL Server chegou à estimativa final de 744 linhas?

Estatísticas disponíveis


Ao compilar a consulta acima, o otimizador de consulta criará estatísticas de coluna única na Shelf e Bin colunas, se as estatísticas adequadas ainda não existirem. Entre outras coisas, essas estatísticas fornecem informações sobre o número de valores de coluna distintos (no vetor de densidade):
DBCC SHOW_STATISTICS 
(
    [Production.ProductInventory], 
    [Shelf]
)
WITH DENSITY_VECTOR;
 
DBCC SHOW_STATISTICS 
(
    [Production.ProductInventory], 
    [Bin]
)
WITH DENSITY_VECTOR;

Os resultados estão resumidos na tabela abaixo (a terceira coluna é calculada a partir da densidade):
Coluna Densidade 1 / Densidade
Prateleira 0,04761905 21
Caixa 0,01612903 62

Informações do vetor de densidade de prateleira e caixa

Como observa a documentação, o recíproco da densidade é o número de valores distintos na coluna. A partir das informações estatísticas mostradas acima, o SQL Server sabe que havia 21 Shelf distintos valores e 62 Bin distintos valores na tabela, quando as estatísticas foram coletadas.

A tarefa de estimar o número de linhas produzidas por um GROUP BY cláusula é trivial quando apenas uma única coluna está envolvida (supondo que não haja outros predicados). Por exemplo, é fácil ver que GROUP BY Shelf produzirá 21 linhas; GROUP BY Bin produzirá 62.

No entanto, não está imediatamente claro como o SQL Server pode estimar o número de (Shelf, Bin) distintos combinações para nosso GROUP BY Shelf, Bin inquerir. Colocando a questão de uma maneira um pouco diferente:Dadas 21 prateleiras e 62 caixas, quantas combinações únicas de prateleiras e caixas haverá? Deixando de lado os aspectos físicos e outros conhecimentos humanos do domínio do problema, a resposta pode estar em qualquer lugar de max(21, 62) =62 a (21 * 62) =1.302. Sem mais informações, não há uma maneira óbvia de saber onde lançar uma estimativa nesse intervalo.

No entanto, para nossa consulta de exemplo, o SQL Server estima 744.312 linhas (arredondadas para 744 na visualização do Plan Explorer), mas em que base?

Evento Estendido de Estimativa de Cardinalidade


A maneira documentada de analisar o processo de estimativa de cardinalidade é usar o evento estendido query_optimizer_estimate_cardinality (apesar de estar no canal "debug"). Enquanto uma sessão que coleta esse evento está em execução, os operadores do plano de execução obtêm uma propriedade adicional StatsCollectionId que liga as estimativas de operadores individuais aos cálculos que as produziram. Para nossa consulta de exemplo, o ID de coleta de estatísticas 2 está vinculado à estimativa de cardinalidade do grupo por operador agregado.

A saída relevante do Extended Event para nossa consulta de teste é:
<data name="calculator">
    <type name="xml" package="package0"></type>
    <value>
    <CalculatorList>
        <DistinctCountCalculator CalculatorName="CDVCPlanLeaf" SingleColumnStat="Shelf,Bin" />
    </CalculatorList>
    </value>
</data>
<data name="stats_collection">
    <type name="xml" package="package0"></type>
    <value>
    <StatsCollection Name="CStCollGroupBy" Id="2" Card="744.31">
        <LoadedStats>
        <StatsInfo DbId="6" ObjectId="258099960" StatsId="3" />
        <StatsInfo DbId="6" ObjectId="258099960" StatsId="4" />
        </LoadedStats>
    </StatsCollection>
    </value>
</data>

Há algumas informações úteis lá, com certeza.

Podemos ver que a classe de calculadora de valores distintos da folha do plano (CDVCPlanLeaf ) foi empregado, usando estatísticas de coluna única em Shelf e Bin como entradas. O elemento de coleta de estatísticas corresponde a este fragmento ao id (2) mostrado no plano de execução, que mostra a estimativa de cardinalidade de 744,31 , e mais informações sobre os IDs de objetos de estatísticas usados ​​também.

Infelizmente, não há nada na saída do evento que diga exatamente como a calculadora chegou ao valor final, que é o que realmente nos interessa.

Combinando contagens distintas


Seguindo uma rota menos documentada, podemos solicitar um plano estimado para a consulta com sinalizadores de rastreamento 2363 e 3604 habilitado:
SELECT 
    INV.Shelf, 
    INV.Bin, 
    COUNT_BIG(*)
FROM Production.ProductInventory AS INV
GROUP BY 
    INV.Shelf,
    INV.Bin
ORDER BY 
    INV.Shelf,
    INV.Bin
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363);

Isso retorna informações de depuração para a guia Mensagens no SQL Server Management Studio. A parte interessante é reproduzida abaixo:
Begin distinct values computation
Input tree:
  LogOp_GbAgg OUT(QCOL: [INV].Shelf,QCOL: [INV].Bin,COL: Expr1001 ,) BY(QCOL: [INV].Shelf,QCOL: [INV].Bin,)
      CStCollBaseTable(ID=1, CARD=1069 TBL: Production.ProductInventory AS TBL: INV)
      AncOp_PrjList 
          AncOp_PrjEl COL: Expr1001 
              ScaOp_AggFunc stopCountBig
                  ScaOp_Const TI(int,ML=4) XVAR(int,Not Owned,Value=0)

Plan for computation:
  CDVCPlanLeaf
      0 Multi-Column Stats, 2 Single-Column Stats, 0 Guesses

Loaded histogram for column QCOL: [INV].Shelf from stats with id 3
Loaded histogram for column QCOL: [INV].Bin from stats with id 4

Using ambient cardinality 1069 to combine distinct counts:
  21
  62

Combined distinct count: 744.312
Result of computation: 744.312

Stats collection generated: 
  CStCollGroupBy(ID=2, CARD=744.312)
      CStCollBaseTable(ID=1, CARD=1069 TBL: Production.ProductInventory AS TBL: INV)

End distinct values computation

Isso mostra as mesmas informações que o Extended Event mostrou em um formato (sem dúvida) mais fácil de consumir:
  • O operador relacional de entrada para calcular uma estimativa de cardinalidade para (LogOp_GbAgg – grupo lógico por agregado)
  • A calculadora usada (CDVCPlanLeaf ) e estatísticas de entrada
  • Os detalhes da coleta de estatísticas resultantes

A nova informação interessante é a parte sobre como usar a cardinalidade do ambiente para combinar contagens distintas .
Isso mostra claramente que os valores 21, 62 e 1069 foram usados, mas (frustrantemente) ainda não exatamente quais cálculos foram realizados para chegar ao 744.312 resultado.

Para o depurador!


Anexar um depurador e usar símbolos públicos nos permite explorar em detalhes o caminho do código seguido durante a compilação da consulta de exemplo.
O instantâneo abaixo mostra a parte superior da pilha de chamadas em um ponto representativo do processo:
MSVCR120!log
sqllang!OdblNHlogN
sqllang!CCardUtilSQL12::ProbSampleWithoutReplacement
sqllang!CCardUtilSQL12::CardDistinctMunged
sqllang!CCardUtilSQL12::CardDistinctCombined
sqllang!CStCollAbstractLeaf::CardDistinctImpl
sqllang!IStatsCollection::CardDistinct
sqllang!CCardUtilSQL12::CardGroupByHelperCore
sqllang!CCardUtilSQL12::PstcollGroupByHelper
sqllang!CLogOp_GbAgg::PstcollDeriveCardinality
sqllang!CCardFrameworkSQL12::DeriveCardinalityProperties

Há alguns detalhes interessantes aqui. Trabalhando de baixo para cima, vemos que a cardinalidade está sendo derivada usando o CE atualizado (CCardFrameworkSQL12 ) disponível no SQL Server 2014 e posterior (o CE original é CCardFrameworkSQL7 ), para o grupo por operador lógico agregado (CLogOp_GbAgg ).

Calcular a cardinalidade distinta envolve combinar (munging) múltiplas entradas, usando amostragem sem reposição.

A referência a H e um logaritmo (natural) no segundo método a partir do topo mostra o uso de Shannon Entropy no cálculo:

Entropia Shannon

A entropia pode ser usada para estimar a correlação informacional (informações mútuas) entre duas estatísticas:

Informações mútuas

Juntando tudo isso, podemos construir um script de cálculo T-SQL combinando a maneira como o SQL Server usa amostragem sem substituição, Shannon Entropy e informações mútuas para produzir a estimativa final de cardinalidade.

Começamos com os números de entrada (cardinalidade do ambiente e o número de valores distintos em cada coluna):
DECLARE 
    @Card float = 1069,
    @Distinct1 float = 21,
    @Distinct2 float = 62;

A frequência de cada coluna é o número médio de linhas por valor distinto:
DECLARE
    @Frequency1 float = @Card / @Distinct1,
    @Frequency2 float = @Card / @Distinct2;

Amostragem sem reposição (SWR) é uma simples questão de subtrair o número médio de linhas por valor distinto (frequência) do número total de linhas:
DECLARE
    @SWR1 float = @Card - @Frequency1,
    @SWR2 float = @Card - @Frequency2,
    @SWR3 float = @Card - @Frequency1 - @Frequency2;

Calcule as entropias (N log N) e as informações mútuas:
DECLARE
    @E1 float = (@SWR1 + 0.5) * LOG(@SWR1),
    @E2 float = (@SWR2 + 0.5) * LOG(@SWR2),
    @E3 float = (@SWR3 + 0.5) * LOG(@SWR3),
    @E4 float = (@Card + 0.5) * LOG(@Card);
 
-- Using logarithms allows us to express
-- multiplication as addition and division as subtraction
DECLARE
    @MI float = EXP(@E1 + @E2 - @E3 - @E4);

Agora que estimamos o quão correlacionados são os dois conjuntos de estatísticas, podemos calcular a estimativa final:
SELECT (1e0 - @MI) * @Distinct1 * @Distinct2;

O resultado do cálculo é 744,311823994677, que é 744,312 arredondado para três casas decimais.

Por conveniência, aqui está todo o código em um bloco:
DECLARE 
    @Card float = 1069,
    @Distinct1 float = 21,
    @Distinct2 float = 62;
 
DECLARE
    @Frequency1 float = @Card / @Distinct1,
    @Frequency2 float = @Card / @Distinct2;
 
-- Sample without replacement
DECLARE
    @SWR1 float = @Card - @Frequency1,
    @SWR2 float = @Card - @Frequency2,
    @SWR3 float = @Card - @Frequency1 - @Frequency2;
 
-- Entropy
DECLARE
    @E1 float = (@SWR1 + 0.5) * LOG(@SWR1),
    @E2 float = (@SWR2 + 0.5) * LOG(@SWR2),
    @E3 float = (@SWR3 + 0.5) * LOG(@SWR3),
    @E4 float = (@Card + 0.5) * LOG(@Card);
 
-- Mutual information
DECLARE
    @MI float = EXP(@E1 + @E2 - @E3 - @E4);
 
-- Final estimate
SELECT (1e0 - @MI) * @Distinct1 * @Distinct2;

Considerações finais


A estimativa final é imperfeita neste caso – a consulta de exemplo retorna 441 linhas.

Para obter uma estimativa melhorada, podemos fornecer ao otimizador melhores informações sobre a densidade do Bin e Shelf colunas usando uma estatística de várias colunas. Por exemplo:
CREATE STATISTICS stat_Shelf_Bin 
ON Production.ProductInventory (Shelf, Bin);

Com essa estatística em vigor (como fornecida ou como um efeito colateral da adição de um índice de várias colunas semelhante), a estimativa de cardinalidade para a consulta de exemplo está exatamente correta. No entanto, é raro calcular uma agregação tão simples. Com predicados adicionais, a estatística multicoluna pode ser menos eficaz. No entanto, é importante lembrar que as informações de densidade adicionais fornecidas pelas estatísticas de várias colunas podem ser úteis para agregações (assim como para comparações de igualdade).

Sem uma estatística de várias colunas, uma consulta agregada com predicados adicionais ainda poderá usar a lógica essencial mostrada neste artigo. Por exemplo, em vez de aplicar a fórmula à cardinalidade da tabela, ela pode ser aplicada aos histogramas de entrada passo a passo.

Conteúdo relacionado:Estimativa de cardinalidade para um predicado em uma expressão COUNT