Eu tenho uma tabela de relatórios (cerca de 1 bilhão de linhas) e uma pequena tabela de dimensões:
CREATE TABLE dbo.Sales_unpartitioned (
BusinessUnit int NOT NULL,
[Date] date NOT NULL,
SKU varchar(8) NOT NULL,
Quantity numeric(10, 2) NOT NULL,
Amount numeric(10, 2) NOT NULL,
CONSTRAINT PK_Sales_unpartitioned PRIMARY KEY CLUSTERED (BusinessUnit, [Date], SKU)
);
--- Demo data:
INSERT INTO dbo.Sales_unpartitioned
SELECT severity AS BusinessUnit,
DATEADD(day, message_id, '2000-01-01') AS [Date],
LEFT([text], 3) AS SKU,
1000.*RAND(CHECKSUM(NEWID())) AS Quantity,
10000.*RAND(CHECKSUM(NEWID())) AS Amount
FROM sys.messages
WHERE [language_id]=1033;
--- Artificially inflate statistics of demo data:
UPDATE STATISTICS dbo.Sales_unpartitioned WITH ROWCOUNT=1000000000;
--- Dimension table:
CREATE TABLE dbo.BusinessUnits (
BusinessUnit int NOT NULL,
SalesManager nvarchar(250) NULL,
PRIMARY KEY CLUSTERED (BusinessUnit)
);
INSERT INTO dbo.BusinessUnits (BusinessUnit)
SELECT DISTINCT BusinessUnit FROM dbo.Sales;
... ao qual adicionei uma exibição de relatórios usada por um aplicativo para relatórios no estilo OLTP.
CREATE OR ALTER VIEW dbo.SalesReport_unpartitioned
AS
SELECT bu.BusinessUnit,
s.[Date],
s.SKU,
s.Quantity,
s.Amount
FROM dbo.BusinessUnits AS bu
CROSS APPLY (
--- Regular sales
SELECT t.BusinessUnit, t.[Date], t.SKU, t.Quantity, t.Amount
FROM dbo.Sales_unpartitioned AS t
WHERE t.BusinessUnit=bu.BusinessUnit
AND t.SKU LIKE 'T%'
UNION ALL
--- This is a special reporting entry. We only
--- want to see today's row. In case of duplicates,
--- get the row with the first "SKU".
SELECT TOP (1) s.BusinessUnit, s.[Date], s.SKU, s.Quantity, s.Amount
FROM dbo.Sales_unpartitioned AS s
WHERE s.BusinessUnit=bu.BusinessUnit
AND s.[Date]=CAST(SYSDATETIME() AS date)
AND s.SKU LIKE 'S%'
ORDER BY s.BusinessUnit, s.[Date], s.SKU
) AS s
A ideia é que o aplicativo do usuário consulte essa exibição com uma consulta SELECT que filtra um intervalo de datas e uma ou mais Unidades de Negócios. Para isso, escolhi um CROSS APPLY
padrão, para que a consulta possa fazer um "loop" em cada Unidade de Negócios, buscar um intervalo de Datas e aplicar um filtro residual no SKU.
Exemplo de consulta de aplicativo:
DECLARE @from date='2021-01-01', @to date='2021-12-31';
SELECT *
FROM dbo.SalesReport_unpartitioned
WHERE BusinessUnit=16
AND [Date] BETWEEN @from AND @to
ORDER BY BusinessUnit, [Date], SKU;
Eu esperaria um plano de consulta parecido com este: Plano desejado
No entanto, o plano fica assim: Plano real
Eu esperava que o SQL Server fizesse um "empilhamento de predicado" na coluna Data, permitindo que o Clustered Index Seek procurasse uma única unidade de negócios e um intervalo de datas e, em seguida, aplicasse um predicado residual no SKU. Isso funciona no Seek na ramificação "s" (aquele com TOP
) - provavelmente porque tem um predicado Date codificado na consulta - mas não na ramificação "t".
No entanto, na ramificação "t" o SQL Server busca apenas a BusinessUnit específica com um predicado residual no SKU, recuperando efetivamente todas as datas . Somente no final do plano é aplicado um operador Filtro que filtra na coluna Data.
Em uma tabela grande, isso tem uma penalidade de desempenho muito significativa - você pode acabar lendo 20 anos de dados do disco quando tudo o que você procura é uma semana.
Coisas que eu tentei
Soluções alternativas:
- Converter a exibição em uma função com valor de tabela embutida com os parâmetros @fromDate e @toDate que filtram as consultas "s" e "t" habilitará uma Busca em (BusinessUnit, Date) conforme desejado, mas requer a reescrita do código do aplicativo.
- Mover a
UNION ALL
saída deCROSS APPLY
(deCROSS APPLY (UNION)
paraCROSS APPLY() UNION CROSS APPLY()
) habilitará o pushdown de predicado. Faz mais uma busca na mesa da BusinessUnit, o que é perfeitamente aceitável.
Corrige o Seek, mas altera os resultados:
- Surpreendentemente, remover o
TOP (1)
andORDER BY
para a consulta "s" faz o empilhamento de predicado funcionar em "t", mas pode retornar muitas linhas de "s". - A eliminação
UNION ALL
removendo a consulta "s" ou "t" habilitará o empilhamento de predicado, mas gerará resultados incorretos.
Sem alteração ou inviável:
- Substituir
TOP (1)
por umROW_NUMBER()
padrão não altera a Busca. - Alterar o
CROSS APPLY
para uma correção forçadaINNER LOOP JOIN
do Seek em "t", mas na verdade altera "s" para um Scan, o que é ainda pior. - Adicionar o sinalizador de rastreamento 8780 para permitir que o otimizador trabalhe em um plano por mais tempo não altera nada. O plano já está otimizado FULL sem rescisão antecipada.
Um tópico comum parece ser que alterar/simplificar a consulta "s" (remover TOP
, ORDER BY
) corrige o problema na consulta "t", o que parece contra-intuitivo para mim.
O que estou olhando
Estou tentando entender se isso é uma falha do otimizador, se é o resultado de um mecanismo de otimização/custo deliberado, ou se eu simplesmente ignorei algo.