Database
 sql >> база данни >  >> RDS >> Database

Изненади и предположения при представянето:DATEADD

Още през 2013 г. писах за грешка в оптимизатора, където 2-ри и 3-ти аргументи на DATEDIFF() могат да бъдат разменени – което може да доведе до неправилни оценки за броя на редовете и от своя страна до лош избор на план за изпълнение:

  • Изненади и предположения за ефективността:DATEDIFF

Миналия уикенд научих за подобна ситуация и веднага предположих, че това е същият проблем. В крайна сметка симптомите изглеждаха почти идентични:

  1. Имаше функция за дата/час в WHERE клауза.
    • Този път беше DATEADD() вместо DATEDIFF() .
  2. Имаше очевидно неправилна оценка на броя на редовете от 1 в сравнение с действителен брой редове от над 3 милиона.
    • Това всъщност беше приблизителна оценка от 0, но SQL Server винаги закръгля тези оценки до 1.
  3. Беше направен лош избор на план (в този случай беше избрано свързване на цикъл) поради ниската оценка.

Нарушителният модел изглеждаше така:

WHERE [datetime2(7) column] >= DATEADD(DAY, -365, SYSUTCDATETIME());

Потребителят опита няколко варианта, но нищо не се промени; те в крайна сметка успяха да заобиколят проблема, като промениха предиката на:

WHERE DATEDIFF(DAY, [column], SYSUTCDATETIME()) <= 365;

Това получи по-добра оценка (типичното предположение за 30% неравенство); така че не е съвсем правилно. И докато елиминира присъединяването на цикъл, има два основни проблема с този предикат:

  1. Това не същата заявка, тъй като сега се търси преминаване на границите от 365 дни, за разлика от това, че са по-големи от определен момент от време преди 365 дни. Статистически значим? Може би не. Но прагът, технически, не е същият.
  2. Прилагането на функцията спрямо колоната прави целия израз неподлежащ на саргиране – което води до пълно сканиране. Когато таблицата съдържа данни само за малко повече от година, това не е голяма работа, но когато таблицата стане по-голяма или предикатът стане по-тесен, това ще се превърне в проблем.

Отново стигнах до заключението, че DATEADD() операцията беше проблемът и препоръча подход, който не разчита на DATEADD() – изграждане на datetime от всички части на текущото време, което ми позволява да извадя година без да използвам DATEADD() :

WHERE [column] >= DATETIMEFROMPARTS(
      DATEPART(YEAR,   SYSUTCDATETIME())-1, 
      DATEPART(MONTH,  SYSUTCDATETIME()),
      DATEPART(DAY,    SYSUTCDATETIME()),
      DATEPART(HOUR,   SYSUTCDATETIME()), 
      DATEPART(MINUTE, SYSUTCDATETIME()),
      DATEPART(SECOND, SYSUTCDATETIME()), 0);

Освен че беше обемист, това имаше някои собствени проблеми, а именно, че трябваше да се добави куп логика, за да се отчетат правилно високосните години. Първо, за да не се провали, ако се случи да работи на 29 февруари, и второ, да включва точно 365 дни във всички случаи (вместо 366 през годината след високосна). Лесни поправки, разбира се, но правят логиката много по-грозна – особено защото заявката трябваше да съществува в изглед, където междинни променливи и множество стъпки не са възможни.

Междувременно ОП подаде елемент на Connect, уплашен от оценката за 1 ред:

  • Свързване #2567628 :Ограничение с DateAdd() не дава добри прогнози

Тогава се появи Пол Уайт (@SQL_Kiwi) и, както много пъти преди, хвърли допълнителна светлина върху проблема. Той сподели свързан елемент за Connect, подаден от Erland Sommarskog през 2011 г.:

  • Свързване #685903 :Неправилна оценка, когато sysdatetime се появи в израз dateadd()

По същество проблемът е, че лоша оценка може да се направи не просто когато SYSDATETIME() (или SYSUTCDATETIME() ) се появява, както Erland първоначално съобщи, но когато произтича datetime2 изразът участва в предиката (и може би само когато DATEADD() също се използва). И може да върви и в двете посоки – ако сменим >= за <= , оценката става цялата таблица, така че изглежда, че оптимизаторът гледа SYSDATETIME() стойност като константа и напълно игнорира всякакви операции като DATEADD() които се извършват срещу него.

Пол сподели, че решението е просто да се използва datetime еквивалентно при изчисляване на датата, преди да я преобразувате в правилния тип данни. В този случай можем да заменим SYSUTCDATETIME() и го променете на GETUTCDATE() :

WHERE [column] >= CONVERT(datetime2(7), DATEADD(DAY, -365, GETUTCDATE()));

Да, това води до малка загуба на прецизност, но също така може и частица прах да забави пръста ви по пътя към натискане на F5 ключ. Важното е, че търсенето все още може да се използва и оценките бяха правилни – всъщност почти перфектни:

Показанията са подобни, тъй като таблицата съдържа данни почти изключително от миналата година, така че дори търсенето се превръща в сканиране на диапазон на по-голямата част от таблицата. Броят на редовете не е идентичен, тъй като (а) втората заявка прекъсва в полунощ и (б) третата заявка включва допълнителен ден с данни поради високосния ден по-рано тази година. Във всеки случай, това все още показва как можем да се доближим до правилните оценки, като елиминираме DATEADD() , но правилното решение е да премахнете директната комбинация на DATEADD() и datetime2 .

За по-нататъшна илюстрация как оценките се бъркат, можете да видите, че ако предадем различни аргументи и насоки към оригиналната заявка и пренаписването на Пол, броят на приблизителните редове за първия винаги се основава на текущото време – те не не се променя с броя на изминалите дни (докато този на Пол е относително точен всеки път):

Действителните редове за първата заявка са малко по-ниски, тъй като това е изпълнено след продължителна дрямка

Оценките не винаги ще бъдат толкова добри; моята таблица просто има относително стабилно разпределение. Попълних го със следната заявка и след това актуализирах статистиката с fullscan, в случай че искате да изпробвате това сами:

-- OP's table definition:
CREATE TABLE dbo.DateaddRepro 
(
  SessionId  int IDENTITY(1, 1) NOT NULL PRIMARY KEY,
  CreatedUtc datetime2(7) NOT NULL DEFAULT SYSUTCDATETIME()
);
GO
 
CREATE NONCLUSTERED INDEX [IX_User_Session_CreatedUtc]
ON dbo.DateaddRepro(CreatedUtc) INCLUDE (SessionId);
GO
 
INSERT dbo.DateaddRepro(CreatedUtc)
SELECT dt FROM 
(
  SELECT TOP (3150000) dt = DATEADD(HOUR, (s1.[precision]-ROW_NUMBER()
    OVER (PARTITION BY s1.[object_id] ORDER BY s2.[object_id])) / 15, GETUTCDATE())
  FROM sys.all_columns AS s1 CROSS JOIN sys.all_objects AS s2
) AS x;
 
UPDATE STATISTICS dbo.DateaddRepro WITH FULLSCAN;
 
SELECT DISTINCT SessionId FROM dbo.DateaddRepro 
WHERE /* pick your WHERE clause to test */;

Коментирах новия елемент на Connect и вероятно ще се върна и ще коригирам отговора си на Stack Exchange.

Моралът на историята

Опитайте се да избягвате комбинирането на DATEADD() с изрази, които дават datetime2 , особено на по-стари версии на SQL Server (това беше на SQL Server 2012). Това също може да бъде проблем, дори в SQL Server 2016, когато се използва по-стар модел за оценка на мощността (поради по-ниско ниво на съвместимост или изрично използване на флаг за проследяване 9481). Проблеми като този са фини и не винаги са очевидни, така че да се надяваме, че това служи като напомняне (може би дори за мен следващия път, когато попадна на подобен сценарий). Както предложих в последната публикация, ако имате модели на заявки като този, проверете дали получавате правилни оценки и направете бележка някъде, за да ги проверите отново, когато има някакви сериозни промени в системата (като надстройка или сервизен пакет).


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Повече за въвеждането на часови зони в дълготраен проект

  2. Easysoft ODBC драйвери и библиотеката ODBCINST

  3. Използване на псевдоколони с свързан сървър

  4. Прикривайте чувствителни данни във вашите планове за изпълнение

  5. Множество начини за изтриване на дубликати от SQL таблици