Раздел ответов
Есть несколько способов переписать это, используя разные конструкции 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
планом запроса.
Также обратите внимание, что поиск выполняется здесь дважды, что на самом деле должно быть более очевидно из графического оператора:
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);
Теперь оба узла, выполняющих поиск, полностью распараллелены, пока мы не коснемся оператора конкатенации.
Для чего это стоит, полностью параллельная версия имеет некоторое хорошее преимущество. При стоимости около 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. Еа. Стоимость плана составляет 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)).