Как улучшить оценку 1 строки в представлении, ограниченном DateAdd () для индекса


8

Использование Microsoft SQL Server 2012 (SP3) (KB3072779) - 11.0.6020.0 (X64).

Учитывая таблицу и индекс:

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)

Фактические строки для каждого из следующих запросов составляют 3,1 млн., Предполагаемые строки отображаются в виде комментариев.

Когда эти запросы передают другой запрос в представлении , оптимизатор выбирает соединение цикла из-за оценки 1 строки. Как улучшить оценку на этом базовом уровне, чтобы избежать переопределения подсказки соединения родительского запроса или обращения к SP?

Использование жестко закодированной даты прекрасно работает:

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

Эти эквивалентные запросы совместимы с представлениями, но все оценивают 1 строку:

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)

Попробуйте несколько советов (но N / A для просмотра):

 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);

Попробуйте использовать Parameter / Hints (но N / A для просмотра):

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);

Оценка против фактического

Статистика актуальна.

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

Последние несколько строк гистограммы (всего 189 строк) показаны:

введите описание изображения здесь

Ответы:


6

Менее исчерпывающий ответ, чем у Аарона, но основная проблема заключается в ошибке оценки количества элементов DATEADDпри использовании типа datetime2 :

Connect: неверная оценка, когда sysdatetime появляется в выражении dateadd ()

Один обходной путь должен использовать GETUTCDATE(который возвращает дату и время):

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

Обратите внимание, что преобразование в datetime2 должно быть вне, DATEADDчтобы избежать ошибки.

Вопрос оценки мощности в 1 строку воспроизводится для меня во всех версиях SQL Server вплоть до 2016 года включительно RC0, где используется модель оценки мощности 70.

Аарон Бертран написал статью об этом для SQLPerformance.com:


6

В некоторых сценариях SQL Server может иметь действительно дикие оценки для DATEADD/ DATEDIFF, в зависимости от того, каковы аргументы и как выглядят ваши фактические данные. Я писал об этом, DATEDIFFкогда имел дело с началом месяца и некоторыми обходными путями, здесь:

Но мой типичный совет - просто прекратить использовать предложения DATEADD/ DATEDIFFin, где / join.

Следующий подход, хотя и не очень точный, когда високосный год находится в отфильтрованном диапазоне (в этом случае он будет включать дополнительный день), и, хотя округляется до дня, улучшит (но все же не очень!) Оценки, как вы не можете использовать sargable по DATEDIFFотношению к столбцовому подходу и все еще разрешаете использование поиска:

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

SELECT ... WHERE CreatedUtc >= @start;

Вы можете манипулировать входными данными, чтобы DATEFROMPARTSизбежать проблем в високосный день, использовать DATETIMEFROMPARTSдля получения большей точности вместо округления до дня и т. Д. Это просто для демонстрации того, что вы можете заполнить переменную датой в прошлом без использования DATEADD(это просто немного больше работы), и, таким образом, избежать более вредной части ошибки оценки (которая исправлена ​​в 2014+).

Чтобы избежать ошибок в високосный день, вы можете сделать это вместо этого, начиная с 28 февраля прошлого года вместо 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
);

Вы также можете сказать добавить день, проверив, не прошел ли мы високосный день в этом году, и если да, то добавьте день в начало (интересно, использование DATEADD здесь все же позволяет получить точные оценки):

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;

Если вам нужно быть более точным, чем день в полночь, тогда вы можете просто добавить больше манипуляций перед выбором:

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;

Теперь вы можете заклинить все это в представлении, и оно все равно будет использовать поиск и оценку 30%, не требуя никаких подсказок или флагов трассировки, но это не красиво. Вложенные CTE предназначены только для того, чтобы мне не приходилось вводить SYSUTCDATETIME()сто раз или повторять повторно используемые выражения - их все равно можно оценивать несколько раз.

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);

Это гораздо более многословно, чем ваш в DATEDIFFотношении столбца, но, как я уже упоминал в комментарии , такой подход не оправдан и, вероятно, будет работать конкурентоспособно, в то время как большая часть таблицы должна быть прочитана в любом случае, но я подозреваю, что это станет бременем так как «последний год» становится более низким процентом таблицы.

Также, просто для справки, вот некоторые метрики, которые я получил, когда пытался воспроизвести:

введите описание изображения здесь

Я не смог получить оценки в 1 строку, и я очень старался соответствовать вашему распределению (3,13 миллиона строк, 2,89 миллиона по сравнению с прошлым годом). Но вы можете увидеть:

  • оба наших решения выполняют примерно эквивалентные операции чтения.
  • Ваше решение немного менее точно, потому что оно учитывает только дневные границы (и это может быть хорошо, мой взгляд может быть сделан менее точным, чтобы соответствовать).
  • 4199+ перекомпиляция не очень изменила оценки (или планы).

Не рисуйте слишком много из цифр продолжительности - они сейчас близки, но могут не оставаться близко по мере роста таблицы (опять же, я полагаю, потому что даже поиск все еще должен читать большую часть таблицы).

Вот планы для v4 (ваш дата против столбца) и v5 (моя версия):

введите описание изображения здесь

введите описание изображения здесь


В итоге, как указано в вашем блоге . этот ответ дает полезный план оценки и поиска. Ответ @PaulWhite дает наилучшую оценку. Возможно, оценки из 1 строки, которые я получал (против 1500), могли быть связаны с тем, что в таблице не было строк за последние ~ 24 часа.
crokusek

@crokusek Если вы говорите, >= DATEADD(DAY, -365, SYSDATETIME())ошибка заключается в том, что оценка основана на >= SYSDATETIME(). Так что технически оценка основана на том, сколько строк в таблице будет CreatedUtcв будущем. Вероятно, это 0, но SQL Server всегда округляет 0 до 1 для оценочных строк.
Аарон Бертран

1

Замените dateadd () на datediff (), чтобы получить адекватную приблизительную величину (30% иша).

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

Это похоже на ошибку, похожую на MS Connect 630583 .

Вариант перекомпиляции не имеет значения.

План статистики


2
Обратите внимание, что применение datediff к столбцу делает выражение несаргетируемым, поэтому вам придется сканировать. Что, вероятно, хорошо, когда 90 +% таблицы нужно читать в любом случае, но по мере увеличения таблицы это будет обходиться дороже.
Аарон Бертран

Отличный момент. Я думал, что это может преобразовать это внутренне. Проверено, что выполняет сканирование.
crokusek
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.