Como melhorar a estimativa de 1 linha em uma Visualização restrita por DateAdd () em relação a um índice

8

Usando o Microsoft SQL Server 2012 (SP3) (KB3072779) - 11.0.6020.0 (X64).

Dada uma tabela e um índice:

create table [User].[Session] 
(
  SessionId int identity(1, 1) not null primary key
  CreatedUtc datetime2(7) not null default sysutcdatetime())
)

create nonclustered index [IX_User_Session_CreatedUtc]
on [User].[Session]([CreatedUtc]) include (SessionId)

As linhas reais para cada uma das consultas a seguir são de 3,1 milhões; as linhas estimadas são mostradas como comentários.

Quando essas consultas alimentam outra consulta em uma Visualização , o otimizador escolhe uma junção de loop devido às estimativas de 1 linha. Como melhorar a estimativa nesse nível do solo para evitar substituir a dica de junção da consulta pai ou recorrer a um SP?

Usar uma data codificada funciona muito bem:

 select distinct SessionId from [User].Session -- 2.9M (great)
  where CreatedUtc > '04/08/2015'  -- but hardcoded

Essas consultas equivalentes são compatíveis com visualização, mas todas estimam uma linha:

select distinct SessionId from [User].Session -- 1
 where CreatedUtc > dateadd(day, -365, sysutcdatetime())         

select distinct SessionId from [User].Session  -- 1
 where dateadd(day, 365, CreatedUtc) > sysutcdatetime();          

select distinct SessionId from [User].Session s  -- 1
 inner loop join  (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
    on d.MinCreatedUtc < s.CreatedUtc    
    -- (also tried reversing join order, not shown, no change)

select distinct SessionId from [User].Session s -- 1
 cross apply (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
 where d.MinCreatedUtc < s.CreatedUtc
    -- (also tried reversing join order, not shown, no change)

Tente algumas dicas (mas N / D para exibir):

 select distinct SessionId from [User].Session -- 1
  where CreatedUtc > dateadd(day, -365, sysutcdatetime())
 option (recompile);

select distinct SessionId from [User].Session  -- 1
 where CreatedUtc > (select dateadd(day, -365, sysutcdatetime()))
 option (recompile, optimize for unknown);

select distinct SessionId                     -- 1
  from (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
 inner loop join [User].Session s    
    on s.CreatedUtc > d.MinCreatedUtc  
option (recompile);

Tente usar Parâmetro / Dicas (mas N / D para exibir):

declare
    @minDate datetime2(7) = dateadd(day, -365, sysutcdatetime());

select distinct SessionId from [User].Session  -- 1.2M (adequate)
 where CreatedUtc > @minDate;

select distinct SessionId from [User].Session  -- 2.96M (great)
 where CreatedUtc > @minDate
option (recompile);

select distinct SessionId from [User].Session  -- 1.2M (adequate)
 where CreatedUtc > @minDate
option (optimize for unknown);

Estimativa vs Real

As estatísticas estão atualizadas.

DBCC SHOW_STATISTICS('user.Session', 'IX_User_Session_CreatedUtc') with histogram;

As últimas linhas do histograma (total de 189 linhas) são mostradas:

insira a descrição da imagem aqui

crokusek
fonte

Respostas:

6

Uma resposta menos abrangente que a de Aaron, mas o principal problema é um erro de estimativa de cardinalidade DATEADDao usar o tipo datetime2 :

Connect: estimativa incorreta quando sysdatetime aparece em uma expressão dateadd ()

Uma solução alternativa é usar GETUTCDATE(que retorna data e hora):

WHERE CreatedUtc > CONVERT(datetime2(7), DATEADD(DAY, -365, GETUTCDATE()))

Observe que a conversão para datetime2 deve estar fora do DATEADDpara evitar o erro.

O problema da estimativa de cardinalidade de 1 linha é reproduzido para mim em todas as versões do SQL Server até o RC0 2016, incluindo 2016, onde o estimador de cardinalidade do modelo 70 é usado.

Aaron Bertrand escreveu um artigo sobre isso no SQLPerformance.com:

Paul White 9
fonte
6

Em alguns cenários, o SQL Server pode ter estimativas realmente curiosas para DATEADD/ DATEDIFF, dependendo dos argumentos e da aparência dos dados reais. Eu escrevi sobre isso para DATEDIFFquando lidar com o início do mês e algumas soluções alternativas, aqui:

Mas, o meu conselho típico é apenas para parar de usar DATEADD/ DATEDIFFno, onde / associar-se cláusulas.

A abordagem a seguir, embora não seja super precisa quando um ano bissexto estiver no intervalo filtrado (nesse caso, incluirá um dia extra) e, embora arredondada para o dia, obterá estimativas melhores (mas ainda não ótimas!), Assim como seu não-sargable DATEDIFFcontra a abordagem da coluna e ainda permite que uma busca seja usada:

DECLARE @start date = DATEFROMPARTS
(
  YEAR(GETUTCDATE())-1, 
  MONTH(GETUTCDATE()), 
  DAY(GETUTCDATE())
);

SELECT ... WHERE CreatedUtc >= @start;

Você pode manipular as entradas para DATEFROMPARTSevitar problemas no dia bissexto, usar DATETIMEFROMPARTSpara obter mais precisão em vez de arredondar para o dia etc. Isso é apenas para demonstrar que você pode preencher uma variável com uma data no passado sem usar DATEADD(é apenas uma pouco mais trabalho) e, portanto, evite a parte mais prejudicial do bug de estimativa (que é corrigido em 2014 ou mais).

Para evitar erros no dia bissexto, você pode fazer isso, a partir de 28 de fevereiro do ano passado, em vez de 29:

DECLARE @start date = DATEFROMPARTS
(
  YEAR(GETUTCDATE())-1, 
  MONTH(GETUTCDATE()), 
  CASE WHEN DAY(GETUTCDATE()) = 29 AND MONTH(GETUTCDATE()) = 2 
    THEN 28 ELSE DAY(GETUTCDATE()) END
);

Você também pode adicionar um dia verificando se passamos um dia bissexto este ano e, nesse caso, adicione um dia ao início (curiosamente, usar DATEADD aqui ainda permite estimativas precisas):

DECLARE @base date = GETUTCDATE();
IF GETUTCDATE() >= DATEFROMPARTS(YEAR(GETUTCDATE()),3,1) AND 
  TRY_CONVERT(datetime, DATEFROMPARTS(YEAR(GETUTCDATE()),2,29)) IS NOT NULL
BEGIN
  SET @base = DATEADD(DAY, 1, GETUTCDATE());
END

DECLARE @start date = DATEFROMPARTS
(
  YEAR(@base)-1, 
  MONTH(@base),
  CASE WHEN DAY(@base) = 29 AND MONTH(@base) = 2 
    THEN 28 ELSE DAY(@base) END
);

SELECT ... WHERE CreatedUtc >= @start;

Se você precisar ser mais preciso do que o dia à meia-noite, basta adicionar mais manipulação antes da seleção:

DECLARE @accurate_start datetime2(7) = DATETIME2FROMPARTS
(
  YEAR(@start), MONTH(@start), DAY(@start),
  DATEPART(HOUR,  SYSUTCDATETIME()), 
  DATEPART(MINUTE,SYSUTCDATETIME()),
  DATEPART(SECOND,SYSUTCDATETIME()), 
  0,0
);

SELECT ... WHERE CreatedUtc >= @accurate_start;

Agora, você pode bloquear tudo isso em uma exibição, e ele ainda usará uma pesquisa e a estimativa de 30% sem exigir dicas ou sinalizadores de rastreamento, mas não é bonito. CTEs aninhados são apenas para que eu não precise digitar SYSUTCDATETIME()cem vezes ou repetir expressões reutilizadas - elas ainda podem ser avaliadas várias vezes.

CREATE VIEW dbo.v5 
AS
  WITH d(d) AS ( SELECT SYSUTCDATETIME() ),
  base(d) AS
  (
    SELECT DATEADD(DAY,CASE WHEN d >= DATEFROMPARTS(YEAR(d),3,1) 
      AND TRY_CONVERT(datetime,RTRIM(YEAR(d))+RIGHT('0'+RTRIM(MONTH(d)),2)
      +RIGHT('0'+RTRIM(DAY(d)),2)) IS NOT NULL THEN 1 ELSE 0 END, d)
    FROM d
  ),
  src(d) AS
  (
    SELECT DATETIME2FROMPARTS
    (
      YEAR(d)-1, 
      MONTH(d),
      CASE WHEN MONTH(d) = 2 AND DAY(d) = 29
        THEN 28 ELSE DAY(d) END,
      DATEPART(HOUR,d), 
      DATEPART(MINUTE,d),
      DATEPART(SECOND,d),
      10*DATEPART(MICROSECOND,d),
      7
    ) FROM base
  )
  SELECT DISTINCT SessionId FROM [User].[Session]
    WHERE CreatedUtc >= (SELECT d FROM src);

Isso é muito mais detalhado do que o seu DATEDIFFna coluna, mas, como mencionei em um comentário , essa abordagem não é sargável e provavelmente terá um desempenho competitivo enquanto a maior parte da tabela precisa ser lida de qualquer maneira, mas suspeito que isso se tornará um fardo. como "o último ano" se torna uma porcentagem menor da tabela.

Além disso, apenas para referência, aqui estão algumas das métricas que recebi quando tentei reproduzir:

insira a descrição da imagem aqui

Não consegui obter estimativas de uma linha e tentei muito corresponder à sua distribuição (3,13 milhões de linhas, 2,89 milhões do ano passado). Mas você pode ver:

  • ambas as nossas soluções executam leituras aproximadamente equivalentes.
  • sua solução é um pouco menos precisa porque é responsável apenas pelos limites do dia (e isso pode ser bom, minha opinião pode ser menos precisa).
  • A recompilação do 4199 + não mudou realmente as estimativas (ou os planos).

Não use muito dos números de duração - eles estão próximos agora, mas podem não ficar próximos à medida que a mesa cresce (novamente, acredito, porque mesmo a busca ainda precisa ler a maior parte da tabela).

Aqui estão os planos para v4 (seu datado em relação à coluna) e v5 (minha versão):

insira a descrição da imagem aqui

insira a descrição da imagem aqui

Aaron Bertrand
fonte
Em resumo, conforme declarado no seu blog . esta resposta fornece uma estimativa utilizável e um plano baseado em busca. A resposta de @PaulWhite fornece a melhor estimativa. Talvez as estimativas de 1 linha que eu estava recebendo (vs 1500) possam ser devidas à tabela não ter nenhuma linha nas últimas ~ 24 horas.
crokusek
@crokusek Se você diz que >= DATEADD(DAY, -365, SYSDATETIME())o erro é que a estimativa é baseada >= SYSDATETIME(). Portanto, tecnicamente, a estimativa é baseada em quantas linhas na tabela existem CreatedUtcno futuro. Provavelmente, é 0, mas o SQL Server sempre arredonda de 0 a 1 para as linhas estimadas.
Aaron Bertrand
1

Substitua dateadd () por datediff () para obter uma aproximação adequada (30% ish).

 select distinct SessionId from [User].Session     -- 1.2M est, 3.0M act.
  where datediff(day, CreatedUtc, sysutcdatetime()) <= 365

Este parece ser um bug semelhante ao MS Connect 630583 .

A opção recompilar não faz diferença.

Estatísticas do plano

crokusek
fonte
2
Observe que a aplicação de datatediff na coluna torna a expressão não sargável, portanto você terá que digitalizar. O que provavelmente é bom quando mais de 90% da tabela precisa ser lida de qualquer maneira, mas, à medida que a tabela aumenta, isso se torna mais caro.
Aaron Bertrand
Ótimo ponto. Eu estava pensando que poderia convertê-lo internamente. Verificou que está executando uma verificação.
crokusek