Por que a função LEN () subestima a cardinalidade no SQL Server 2014?

26

Eu tenho uma tabela com uma coluna de seqüência de caracteres e um predicado que verifica se há linhas com um determinado comprimento. No SQL Server 2014, estou vendo uma estimativa de 1 linha, independentemente do comprimento que estou verificando. Isso está produzindo planos muito ruins, porque na verdade existem milhares ou até milhões de linhas e o SQL Server está optando por colocar essa tabela no lado externo de um loop aninhado.

Existe uma explicação para a estimativa de cardinalidade de 1.0003 para o SQL Server 2014 enquanto o SQL Server 2012 estima 31.622 linhas? Existe uma boa solução alternativa?

Aqui está uma breve reprodução do problema:

-- Create a table with 1MM rows of dummy data
CREATE TABLE #customers (cust_nbr VARCHAR(10) NOT NULL)
GO

INSERT INTO #customers WITH (TABLOCK) (cust_nbr)
    SELECT TOP 1000000 
        CONVERT(VARCHAR(10),
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))) AS cust_nbr
    FROM master..spt_values v1
    CROSS JOIN master..spt_values v2
GO

-- Looking for string of a certain length.
-- While both CEs yield fairly poor estimates, the 2012 CE is much
-- more conservative (higher estimate) and therefore much more likely
-- to yield an okay plan rather than a drastically understimated loop join.
-- 2012: 31,622 rows estimated, 900K rows actual
-- 2014: 1 row estimated, 900K rows actual
SELECT COUNT(*)
FROM #customers
WHERE LEN(cust_nbr) = 6
OPTION (QUERYTRACEON 9481) -- Optionally, use 2012 CE
GO

Aqui está um script mais completo mostrando testes adicionais

Também li o whitepaper no Estimador de cardinalidade do SQL Server 2014 , mas não encontrei nada lá que esclarecesse a situação.

Geoff Patterson
fonte

Respostas:

20

Para o CE herdado, vejo que a estimativa é de 3,16228% das linhas - e essa é uma heurística de "número mágico" usada para colunas = predicados literais (existem outras heurísticas baseadas na construção de predicados - mas estão LENagrupadas em torno da coluna para o resultados herdados da CE correspondem a essa estrutura de suposição). Você pode ver exemplos disso em uma postagem sobre seletividade palpites na ausência de Statistics, de Joe Sack, e Constant-Constant Comparison Estimation, de Ian Jose.

-- Legacy CE: 31622.8 rows
SELECT  COUNT(*)
FROM    #customers
WHERE   LEN(cust_nbr) = 6
OPTION  ( QUERYTRACEON 9481); -- Legacy CE
GO

Agora, quanto ao novo comportamento do CE, parece que isso agora está visível para o otimizador (o que significa que podemos usar estatísticas). Fiz o exercício de examinar a saída da calculadora abaixo e você pode ver a geração automática de estatísticas associada como um ponteiro:

-- New CE: 1.00007 rows
SELECT  COUNT(*)
FROM    #customers
WHERE   LEN(cust_nbr) = 6
OPTION  ( QUERYTRACEON 2312 ); -- New CE
GO

-- View New CE behavior with 2363 (for supported option use XEvents)
SELECT  COUNT(*)
FROM    #customers
WHERE   LEN(cust_nbr) = 6
OPTION  (QUERYTRACEON 2312, QUERYTRACEON 2363, QUERYTRACEON 3604, RECOMPILE); -- New CE
GO

/*
Loaded histogram for column QCOL:
[tempdb].[dbo].[#customers].cust_nbr from stats with id 2
Using ambient cardinality 1e+006 to combine distinct counts:
  999927

Combined distinct count: 999927
Selectivity: 1.00007e-006
Stats collection generated:
  CStCollFilter(ID=2, CARD=1.00007)
      CStCollBaseTable(ID=1, CARD=1e+006 TBL: #customers)

End selectivity computation
*/

EXEC tempdb..sp_helpstats '#customers';


--Check out AVG_RANGE_ROWS values (for example - plenty of ~ 1)
DBCC SHOW_STATISTICS('tempdb..#customers', '_WA_Sys_00000001_B0368087');
--That's my Stats name yours is subject to change

Infelizmente, a lógica se baseia em uma estimativa do número de valores distintos, que não são ajustados para o efeito da LENfunção.

Solução possível

Você pode obter uma estimativa baseada em três testes nos dois modelos de CE reescrevendo LENcomo LIKE:

SELECT COUNT_BIG(*)
FROM #customers AS C
WHERE C.cust_nbr LIKE REPLICATE('_', 6);

COMO plano


Informações sobre sinalizadores de rastreamento usados:

  • 2363: mostra muitas informações, incluindo estatísticas sendo carregadas.
  • 3604: imprime a saída dos comandos DBCC na guia de mensagens.
Zane
fonte
13

Existe uma explicação para a estimativa de cardinalidade de 1.0003 para o SQL 2014 enquanto o SQL 2012 estima 31.622 linhas?

Acho que a resposta de @ Zane cobre muito bem essa parte.

Existe uma boa solução alternativa?

Você pode tentar criar uma coluna computada não persistente para LEN(cust_nbr)(opcionalmente) criar um índice não agrupado nessa coluna computada. Isso deve fornecer estatísticas precisas.

Eu fiz alguns testes e aqui está o que eu encontrei:

  • As estatísticas foram criadas automaticamente na Coluna Computada Não Persistida, quando nenhum índice foi definido nela.
  • Adicionar o índice não clusterizado à coluna computada não apenas não ajudou, como na verdade afetou um pouco o desempenho. CPU ligeiramente mais alta e tempos decorridos. Custo estimado ligeiramente mais alto (o que valer a pena).
  • Tornar a coluna computada como PERSISTED(sem índice) foi melhor do que as outras duas variações. As linhas estimadas foram mais precisas. A CPU e o tempo decorrido foram melhores (como esperado, pois não era necessário calcular nada por linha).
  • Não foi possível criar um Índice Filtrado ou Estatísticas Filtradas na Coluna Computada (devido ao fato de estar sendo computada), mesmo que fosse PERSISTED:-(
Solomon Rutzky
fonte
11
Obrigado pela comparação completa entre persistente e não. É bom saber que, mesmo que a coluna computada persistente tenha suas vantagens, a não persistente pode ser uma vitória muito rápida com muito pouca sobrecarga em alguns casos em que as estatísticas de uma expressão são benéficas.
Geoff Patterson