Ao ingressar em uma tabela mestre em uma tabela de detalhes, como incentivar o SQL Server 2014 a usar a estimativa de cardinalidade da tabela maior (detalhes) como a estimativa de cardinalidade da saída da associação?
Por exemplo, ao associar 10 mil linhas mestras a 100 mil linhas de detalhes, desejo que o SQL Server estime a associação em 100 mil linhas - o mesmo que o número estimado de linhas de detalhes. Como devo estruturar minhas consultas e / ou tabelas e / ou índices para ajudar o estimador do SQL Server a aproveitar o fato de que cada linha de detalhes sempre tem uma linha principal correspondente? (Significando que uma união entre eles nunca deve reduzir a estimativa de cardinalidade.)
Aqui estão mais detalhes. Nosso banco de dados possui um par de tabelas mestre / detalhes: VisitTarget
possui uma linha para cada transação de vendas e VisitSale
uma linha para cada produto em cada transação. É um relacionamento um para muitos: uma linha VisitTarget para uma média de 10 linhas VisitSale.
As tabelas ficam assim: (Estou simplificando apenas as colunas relevantes para esta pergunta)
-- "master" table
CREATE TABLE VisitTarget
(
VisitTargetId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
SaleDate date NOT NULL,
StoreId int NOT NULL
-- other columns omitted for clarity
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitTarget_SaleDate
ON VisitTarget (SaleDate) INCLUDE (StoreId /*, ...more columns */);
-- "detail" table
CREATE TABLE VisitSale
(
VisitSaleId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
VisitTargetId int NOT NULL,
SaleDate date NOT NULL, -- denormalized; copied from VisitTarget
StoreId int NOT NULL, -- denormalized; copied from VisitTarget
ItemId int NOT NULL,
SaleQty int NOT NULL,
SalePrice decimal(9,2) NOT NULL
-- other columns omitted for clarity
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitSale_SaleDate
ON VisitSale (SaleDate)
INCLUDE (VisitTargetId, StoreId, ItemId, SaleQty, TotalSalePrice decimal(9,2) /*, ...more columns */
);
ALTER TABLE VisitSale
WITH CHECK ADD CONSTRAINT FK_VisitSale_VisitTargetId
FOREIGN KEY (VisitTargetId)
REFERENCES VisitTarget (VisitTargetId);
ALTER TABLE VisitSale
CHECK CONSTRAINT FK_VisitSale_VisitTargetId;
Por motivos de desempenho, desnormalizamos parcialmente copiando as colunas de filtragem mais comuns (por exemplo SaleDate
) da tabela mestre nas linhas de cada tabela de detalhes e adicionamos índices de cobertura em ambas as tabelas para melhor suportar as consultas filtradas por data. Isso funciona muito bem para reduzir a E / S ao executar consultas filtradas por data, mas acho que essa abordagem está causando problemas de estimativa de cardinalidade ao unir as tabelas mestre e de detalhes.
Quando juntamos essas duas tabelas, as consultas ficam assim:
SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitTarget vt
JOIN VisitSale vs on vt.VisitTargetId = vs.VisitTargetId
WHERE
vs.SaleDate BETWEEN '20170101' and '20171231'
and vt.SaleDate BETWEEN '20170101' and '20171231'
-- more filtering goes here, e.g. by store, by product, etc.
O filtro de data na tabela de detalhes ( VisitSale
) é redundante. Está lá para ativar a E / S sequencial (também conhecida como operador Index Seek) na tabela de detalhes para consultas filtradas por um período.
O plano para esses tipos de consultas é assim:
Um plano real de uma consulta com o mesmo problema pode ser encontrado aqui .
Como você pode ver, a estimativa de cardinalidade para a junção (a dica de ferramenta na parte inferior esquerda da imagem) é mais de 4x muito baixa: 2,1 milhões reais versus 0,5 milhões estimados. Isso causa problemas de desempenho (por exemplo, espalhar para tempdb), especialmente quando essa consulta é uma subconsulta usada em uma consulta mais complexa.
Mas as estimativas de contagem de linhas para cada ramificação da junção estão próximas das contagens de linhas reais. A metade superior da junção é 100K real versus 164K estimada. A metade inferior da junção tem 2,1 milhões de linhas reais versus 3,7 milhões estimados. A distribuição de hash bucket também parece boa. Essas observações sugerem que as estatísticas são válidas para cada tabela e que o problema é a estimativa da cardinalidade da junção.
No começo, pensei que o problema era o SQL Server esperando que as colunas SaleDate em cada tabela fossem independentes, enquanto na verdade elas eram idênticas. Então, tentei adicionar uma comparação de igualdade para as datas de venda na condição de junção ou na cláusula WHERE, por exemplo
ON vt.VisitTargetId = vs.VisitTargetId and vt.SaleDate = vs.SaleDate
ou
WHERE vt.SaleDate = vs.SaleDate
Isso não funcionou. Até piorou as estimativas de cardinalidade! Portanto, o SQL Server não está usando essa dica de igualdade ou outra coisa é a causa raiz do problema.
Tem alguma idéia de como solucionar problemas e, esperamos, corrigir esse problema de estimativa de cardinalidade? Meu objetivo é que a cardinalidade da junção mestre / detalhe seja estimada da mesma forma que a estimativa para a entrada maior ("tabela de detalhes") da junção.
Se isso importa, estamos executando o SQL Server 2014 Enterprise SP2 CU8 build 12.0.5557.0 no Windows Server. Não há sinalizadores de rastreamento ativados. O nível de compatibilidade do banco de dados é o SQL Server 2014. Vemos o mesmo comportamento em vários servidores SQL diferentes, portanto, parece improvável que seja um problema específico do servidor.
Há uma otimização no SQL Server 2014 Cardinality Estimator que é exatamente o comportamento que estou procurando:
O novo CE, no entanto, usa um algoritmo mais simples, que pressupõe que haja uma associação de junção um para muitos entre uma tabela grande e uma tabela pequena. Isso pressupõe que cada linha na tabela grande corresponde exatamente a uma linha na tabela pequena. Esse algoritmo retorna o tamanho estimado da entrada maior como a cardinalidade da junção.
Idealmente, eu poderia obter esse comportamento, onde a estimativa de cardinalidade para a junção seria a mesma da tabela grande, mesmo que minha tabela "pequena" ainda retorne mais de 100 mil linhas!