Розділ відповідей
Існують різні способи переписати це за допомогою різних конструкцій T-SQL. Ми розглянемо плюси і мінуси і зробимо загальне порівняння нижче.
Перший вгору : ВикористанняOR
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;
Використання OR
дає нам більш ефективний план пошуку, який читає потрібну кількість рядків, однак він додає те, що технічний світ викликає a whole mess of malarkey
у плані запитів.
Також зауважте, що Seek тут виконується двічі, що дійсно повинно бути очевиднішим із графічного оператора:
Table 'Users'. Scan count 2, logical reads 8233, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 469 ms, elapsed time = 473 ms.
Друге вгору : Використання похідних таблиць з UNION ALL
нашим запитом також можна переписати так
SELECT SUM(Records)
FROM
(
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records);
Це дає однотипний план із набагато меншим рівнем макіяжу та більш очевидним ступенем чесності щодо того, скільки разів шукали (шукали?) Індекс.
Він виконує таку ж кількість читань (8233), що і OR
запит, але створює близько 100 мс відключеного процесора.
CPU time = 313 ms, elapsed time = 315 ms.
Однак ви повинні бути дуже обережними, тому що якщо цей план спробує йти паралельно, дві окремі COUNT
операції будуть серіалізовані, оскільки кожна з них вважається глобальною скалярною сукупністю. Якщо ми змусимо паралельний план за допомогою Trace Flag 8649, проблема стає очевидною.
SELECT SUM(Records)
FROM
(
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
Цього можна уникнути, трохи змінивши наш запит.
SELECT SUM(Records)
FROM
(
SELECT 1
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
Тепер обидва вузли, що виконують Seek, повністю паралельні, поки ми не потрапимо в оператор конкатенації.
Що варто, цілком паралельна версія має певну користь. Ціною приблизно ще 100 читань та приблизно 90мм додаткового часу процесора час, що минає, скорочується до 93мс.
Table 'Users'. Scan count 12, logical reads 8317, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 500 ms, elapsed time = 93 ms.
Що з CROSS APPLY?
Жодна відповідь не завершена без магії CROSS APPLY
!
На жаль, у нас виникає більше проблем COUNT
.
SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT COUNT(Id)
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
Цей план жахливий. Такий план ви закінчуєте, коли з’являєтесь останнім днем Святого Патріка. Хоча це паралельно, чомусь сканування PK / CX. Ew План має вартість 2198 баксів.
Table 'Users'. Scan count 7, logical reads 31676233, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 29532 ms, elapsed time = 5828 ms.
Що є дивним вибором, адже якщо ми змусимо його використовувати некластеризований індекс, ціна значно знижується до 1798 доларів.
SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT COUNT(Id)
FROM dbo.Users AS u2 WITH (INDEX(ix_Id_Age))
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u2 WITH (INDEX(ix_Id_Age))
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
Гей, шукає! Перевірте вас там. Також зауважте, що з магією CROSS APPLY
нам не потрібно нічого робити, щоб мати переважно повністю паралельний план.
Table 'Users'. Scan count 5277838, logical reads 31685303, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 27625 ms, elapsed time = 4909 ms.
Хрестовий набір в кінцевому підсумку набагато краще без COUNT
речей там.
SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT 1
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
План виглядає добре, але читання та процесор не є покращенням.
Table 'Users'. Scan count 20, logical reads 17564, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 4844 ms, elapsed time = 863 ms.
Переписування хреста застосовуватиметься для отриманих результатів приєднання в абсолютно все. Я не збираюсь повторно публікувати план запитів та інформацію про статистику - вони насправді не змінилися.
SELECT COUNT(u.Id)
FROM dbo.Users AS u
JOIN
(
SELECT u.Id
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT u.Id
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x ON x.Id = u.Id;
Реляційна алгебра : Щоб бути ретельним, і щоб Джо Челко не переслідував мої мрії, нам потрібно хоча б спробувати деякі дивні реляційні речі. Ось нічого не йде!
Спроба с INTERSECT
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18
INTERSECT
SELECT u.Age WHERE u.Age IS NOT NULL );
Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 1094 ms, elapsed time = 1090 ms.
І ось спроба с EXCEPT
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18
EXCEPT
SELECT u.Age WHERE u.Age IS NULL);
Table 'Users'. Scan count 7, logical reads 9247, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 2126 ms, elapsed time = 376 ms.
Можливо, є й інші способи їх написання, але я залишу це людям, які, можливо, використовують EXCEPT
і INTERSECT
частіше, ніж я.
Якщо вам дійсно потрібен підрахунок, який
я використовую COUNT
в своїх запитах як скорочення (читайте: я занадто лінивий, щоб іноді придумати більше залучених сценаріїв). Якщо вам просто потрібен підрахунок, ви можете використовувати CASE
вираз, щоб зробити приблизно те ж саме.
SELECT SUM(CASE WHEN u.Age < 18 THEN 1
WHEN u.Age IS NULL THEN 1
ELSE 0 END)
FROM dbo.Users AS u
SELECT SUM(CASE WHEN u.Age < 18 OR u.Age IS NULL THEN 1
ELSE 0 END)
FROM dbo.Users AS u
Вони обидва отримують однаковий план і мають однакові характеристики процесора та зчитування.
Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 719 ms, elapsed time = 719 ms.
Переможець?
У моїх тестах форсований паралельний план із SUM над отриманою таблицею найкращим чином. І так, багатьом із цих запитів можна було допомогти, додавши пару відфільтрованих індексів для обліку обох предикатів, але я хотів залишити експерименти іншим.
SELECT SUM(Records)
FROM
(
SELECT 1
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
Дякую!
NOT EXISTS ( INTERSECT / EXCEPT )
запити можуть працювати безINTERSECT / EXCEPT
частин:WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18 );
Інший спосіб - який використовуєEXCEPT
:SELECT COUNT(*) FROM (SELECT UserID FROM dbo.Users EXCEPT SELECT UserID FROM dbo.Users WHERE u.Age >= 18) AS u ;
(де Ідентіфікатор_пользователя є PK або будь-який унікальний не нульовий стовпець (s)).