Раздел ответов
Есть несколько способов переписать это, используя разные конструкции 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)).SELECT result = (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age < 18) + (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age IS NULL) ;
Извините, если я пропустил миллион проверенных вами версий!UNION ALL
планами (360 мс ЦП, 11k считываний).Я не был игрой, чтобы восстановить 110 ГБ базы данных только для одной таблицы, поэтому я создал свои собственные данные . Распределение по возрасту должно совпадать с тем, что в переполнении стека, но, очевидно, сама таблица не будет соответствовать. Я не думаю, что это слишком большая проблема, потому что запросы все равно будут попадать в индексы. Я тестирую на 4-х процессорном компьютере с SQL Server 2016 SP1. Стоит отметить, что для запросов, которые завершают это быстро, важно не включать фактический план выполнения. Это может немного замедлить ход событий.
Я начал с рассмотрения некоторых решений в прекрасном ответе Эрика. Для этого:
Я получил следующие результаты из sys.dm_exec_sessions за 10 испытаний (для меня этот запрос проходил параллельно):
Запрос, который работал лучше для Эрика, на моем компьютере работал хуже:
Результаты 10 испытаний:
Я не сразу могу объяснить, почему это так плохо, но не ясно, почему мы хотим заставить почти каждый оператор в плане запроса идти параллельно. В первоначальном плане у нас есть последовательная зона, которая находит все строки с
AGE < 18
. Есть только несколько тысяч строк. На моей машине я получаю 9 логических чтений для этой части запроса и 9 мс сообщаемого времени ЦП и истекшего времени. Также есть последовательная зона для глобального агрегата для строк с,AGE IS NULL
но она обрабатывает только одну строку на DOP. На моей машине это всего четыре ряда.Мой вывод заключается в том, что наиболее важно оптимизировать часть запроса, которая находит строки с помощью
NULL
for,Age
потому что таких строк миллионы. Я не смог создать индекс с меньшим количеством страниц, которые покрывали данные, чем простой сжатый страницей столбец. Я предполагаю, что существует минимальный размер индекса на строку или что много пространства индекса нельзя избежать с помощью хитростей, которые я пробовал. Так что, если мы застряли с примерно одинаковым числом логических чтений для получения данных, тогда единственный способ ускорить его - сделать запрос более параллельным, но это нужно сделать иначе, чем запрос Эрика, который использовал TF 8649. В приведенном выше запросе у нас есть соотношение 3,62 для процессорного времени к истекшему времени, что довольно хорошо. Идеальным было бы соотношение 4,0 на моей машине.Одной из возможных областей улучшения является более равномерное распределение работы между потоками. На скриншоте ниже мы видим, что один из моих процессоров решил сделать небольшой перерыв:
Сканирование индекса является одним из немногих операторов, которые могут быть реализованы параллельно, и мы ничего не можем сделать с тем, как строки распределяются по потокам. В этом тоже есть элемент случайности, но довольно последовательно я видел одну недоделанную нить. Один из способов обойти это - сделать параллелизм сложным путем: во внутренней части соединения с вложенным циклом. Все, что находится во внутренней части вложенного цикла, будет реализовано последовательным способом, но многие последовательные потоки могут работать одновременно. Пока мы получаем подходящий метод параллельного распределения (например, циклический перебор), мы можем точно контролировать, сколько строк отправляется в каждый поток.
Я выполняю запросы с DOP 4, поэтому мне нужно равномерно разделить
NULL
строки в таблице на четыре сегмента. Один из способов сделать это - создать группу индексов для вычисляемых столбцов:Я не совсем уверен, почему четыре отдельных индекса немного быстрее одного индекса, но это то, что я нашел в своем тестировании.
Чтобы получить план параллельного вложенного цикла, я собираюсь использовать недокументированный флаг трассировки 8649 . Я также собираюсь написать немного странный код, чтобы оптимизатор не обрабатывал больше строк, чем необходимо. Ниже приведена одна реализация, которая работает хорошо:
Результаты десяти испытаний:
С этим запросом мы имеем отношение ЦП к истекшему времени 3.85! Мы сократили время выполнения на 17 мс, и для этого потребовалось всего 4 вычисляемых столбца и индекса! Каждый поток обрабатывает очень близко к одинаковому количеству строк в целом, потому что каждый индекс имеет очень близко к одному и тому же числу строк, и каждый поток сканирует только один индекс:
В заключение отметим, что мы также можем нажать легкую кнопку и добавить некластеризованный CCI в
Age
столбец:Следующий запрос завершается через 3 мс на моей машине:
Это будет сложно победить.
источник
Хотя у меня нет локальной копии базы данных Stack Overflow, я смог выполнить несколько запросов. Моя мысль заключалась в том, чтобы получить количество пользователей из представления системного каталога (в отличие от непосредственного получения количества строк из базовой таблицы). Затем получите количество строк, которые соответствуют (или, возможно, не соответствуют) критериям Эрика, и выполните простую математику.
Я использовал Stack Exchange Data Explorer (вместе с
SET STATISTICS TIME ON;
иSET STATISTICS IO ON;
) для проверки запросов. Для справки, вот некоторые запросы и статистика CPU / IO:QUERY 1
QUERY 2
QUERY 3
1-я попытка
Это было медленнее, чем все запросы Эрика, которые я перечислил здесь ... по крайней мере, с точки зрения прошедшего времени.
2-я попытка
Здесь я выбрал переменную для хранения общего числа пользователей (вместо подзапроса). Количество сканирований увеличилось с 1 до 17 по сравнению с 1-й попыткой. Логические чтения остались прежними. Однако прошедшее время значительно сократилось.
Другие примечания: DBCC TRACEON не разрешен в Stack Exchange Data Explorer, как указано ниже:
источник
SELECT SUM(p.Rows) - (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age >= 18 ) FROM sys.partitions p WHERE p.index_id < 2 AND p.object_id = OBJECT_ID('dbo.Users')
Использовать переменные?
По комментарию можно пропустить переменные
источник
SELECT (select count(*) from table_1 where bb <= 1) + (select count(*) from table_1 where bb is null);
Хорошо используя
SET ANSI_NULLS OFF;
Это что-то, что пришло мне в голову. Просто выполнил это на https://data.stackexchange.com.
Но не так эффективно, как @blitz_erik, хотя
источник
Тривиальным решением является подсчет количества (*) - количество (возраст> = 18):
Или же:
Результаты здесь
источник