Как можно заменить ISNULL () в предложении WHERE, в котором используются только литеральные значения?

55

Что это не о:

Это не вопрос всеобъемлющих запросов, которые принимают пользовательский ввод или используют переменные.

Речь идет строго о запросах, которые ISNULL()используются в WHEREпредложении для замены NULLзначений канареечным значением для сравнения с предикатом, а также о различных способах переписывания этих запросов в SARGable в SQL Server.

Почему у вас нет места там?

Наш пример запроса относится к локальной копии базы данных Stack Overflow в SQL Server 2016 и ищет пользователей с NULLвозрастом или возрастом <18.

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE ISNULL(u.Age, 17) < 18;

План запроса показывает сканирование довольно вдумчивого некластеризованного индекса.

орешки

Оператор сканирования показывает (благодаря дополнениям к фактическому плану выполнения XML в более поздних версиях SQL Server), что мы читаем каждую вонючую строку.

орешки

В целом, мы делаем 9157 операций чтения и используем около половины секунды процессорного времени:

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 = 485 ms,  elapsed time = 483 ms.

Вопрос: как можно переписать этот запрос, чтобы сделать его более эффективным и, возможно, даже SARGable?

Не стесняйтесь предлагать другие предложения. Я не думаю, что мой ответ обязательно является ответом, и есть достаточно умных людей, чтобы придумать альтернативы, которые могут быть лучше.

Если вы хотите подыграть на своем компьютере, зайдите сюда, чтобы загрузить базу данных SO .

Спасибо!

Эрик Дарлинг
источник

Ответы:

57

Раздел ответов

Есть несколько способов переписать это, используя разные конструкции 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);

Спасибо!

Эрик Дарлинг
источник
1
Эти 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)).
ypercubeᵀᴹ
Это было проверено? 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) ;Извините, если я пропустил миллион проверенных вами версий!
ypercubeᵀᴹ
@ ypercubeᵀᴹ вот план для этого. Это немного отличается, но имеет сходные характеристики с UNION ALLпланами (360 мс ЦП, 11k считываний).
Эрик Дарлинг
Эй, Эрик, просто бродил по миру sql и заглянул, чтобы сказать «вычисляемая колонка», просто чтобы раздражать тебя. <3
тигель
17

Я не был игрой, чтобы восстановить 110 ГБ базы данных только для одной таблицы, поэтому я создал свои собственные данные . Распределение по возрасту должно совпадать с тем, что в переполнении стека, но, очевидно, сама таблица не будет соответствовать. Я не думаю, что это слишком большая проблема, потому что запросы все равно будут попадать в индексы. Я тестирую на 4-х процессорном компьютере с SQL Server 2016 SP1. Стоит отметить, что для запросов, которые завершают это быстро, важно не включать фактический план выполнения. Это может немного замедлить ход событий.

Я начал с рассмотрения некоторых решений в прекрасном ответе Эрика. Для этого:

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

Я получил следующие результаты из sys.dm_exec_sessions за 10 испытаний (для меня этот запрос проходил параллельно):

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     3532                 975          60830 
╚══════════╩════════════════════╩═══════════════╝

Запрос, который работал лучше для Эрика, на моем компьютере работал хуже:

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

Результаты 10 испытаний:

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     5704                1636          60850 
╚══════════╩════════════════════╩═══════════════╝

Я не сразу могу объяснить, почему это так плохо, но не ясно, почему мы хотим заставить почти каждый оператор в плане запроса идти параллельно. В первоначальном плане у нас есть последовательная зона, которая находит все строки с AGE < 18. Есть только несколько тысяч строк. На моей машине я получаю 9 логических чтений для этой части запроса и 9 мс сообщаемого времени ЦП и истекшего времени. Также есть последовательная зона для глобального агрегата для строк с, AGE IS NULLно она обрабатывает только одну строку на DOP. На моей машине это всего четыре ряда.

Мой вывод заключается в том, что наиболее важно оптимизировать часть запроса, которая находит строки с помощью NULLfor, Ageпотому что таких строк миллионы. Я не смог создать индекс с меньшим количеством страниц, которые покрывали данные, чем простой сжатый страницей столбец. Я предполагаю, что существует минимальный размер индекса на строку или что много пространства индекса нельзя избежать с помощью хитростей, которые я пробовал. Так что, если мы застряли с примерно одинаковым числом логических чтений для получения данных, тогда единственный способ ускорить его - сделать запрос более параллельным, но это нужно сделать иначе, чем запрос Эрика, который использовал TF 8649. В приведенном выше запросе у нас есть соотношение 3,62 для процессорного времени к истекшему времени, что довольно хорошо. Идеальным было бы соотношение 4,0 на моей машине.

Одной из возможных областей улучшения является более равномерное распределение работы между потоками. На скриншоте ниже мы видим, что один из моих процессоров решил сделать небольшой перерыв:

ленивая нить

Сканирование индекса является одним из немногих операторов, которые могут быть реализованы параллельно, и мы ничего не можем сделать с тем, как строки распределяются по потокам. В этом тоже есть элемент случайности, но довольно последовательно я видел одну недоделанную нить. Один из способов обойти это - сделать параллелизм сложным путем: во внутренней части соединения с вложенным циклом. Все, что находится во внутренней части вложенного цикла, будет реализовано последовательным способом, но многие последовательные потоки могут работать одновременно. Пока мы получаем подходящий метод параллельного распределения (например, циклический перебор), мы можем точно контролировать, сколько строк отправляется в каждый поток.

Я выполняю запросы с DOP 4, поэтому мне нужно равномерно разделить NULLстроки в таблице на четыре сегмента. Один из способов сделать это - создать группу индексов для вычисляемых столбцов:

ALTER TABLE dbo.Users
ADD Compute_bucket_0 AS (CASE WHEN Age IS NULL AND Id % 4 = 0 THEN 1 ELSE NULL END),
Compute_bucket_1 AS (CASE WHEN Age IS NULL AND Id % 4 = 1 THEN 1 ELSE NULL END),
Compute_bucket_2 AS (CASE WHEN Age IS NULL AND Id % 4 = 2 THEN 1 ELSE NULL END),
Compute_bucket_3 AS (CASE WHEN Age IS NULL AND Id % 4 = 3 THEN 1 ELSE NULL END);

CREATE INDEX IX_Compute_bucket_0 ON dbo.Users (Compute_bucket_0) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_1 ON dbo.Users (Compute_bucket_1) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_2 ON dbo.Users (Compute_bucket_2) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_3 ON dbo.Users (Compute_bucket_3) WITH (DATA_COMPRESSION = PAGE);

Я не совсем уверен, почему четыре отдельных индекса немного быстрее одного индекса, но это то, что я нашел в своем тестировании.

Чтобы получить план параллельного вложенного цикла, я собираюсь использовать недокументированный флаг трассировки 8649 . Я также собираюсь написать немного странный код, чтобы оптимизатор не обрабатывал больше строк, чем необходимо. Ниже приведена одна реализация, которая работает хорошо:

SELECT SUM(t.cnt) + (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age < 18)
FROM 
(VALUES (0), (1), (2), (3)) v(x)
CROSS APPLY 
(
    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_0 = CASE WHEN v.x = 0 THEN 1 ELSE NULL END

    UNION ALL

    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_1 = CASE WHEN v.x = 1 THEN 1 ELSE NULL END

    UNION ALL

    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_2 = CASE WHEN v.x = 2 THEN 1 ELSE NULL END

    UNION ALL

    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_3 = CASE WHEN v.x = 3 THEN 1 ELSE NULL END
) t
OPTION (QUERYTRACEON 8649);

Результаты десяти испытаний:

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     3093                 803          62008 
╚══════════╩════════════════════╩═══════════════╝

С этим запросом мы имеем отношение ЦП к истекшему времени 3.85! Мы сократили время выполнения на 17 мс, и для этого потребовалось всего 4 вычисляемых столбца и индекса! Каждый поток обрабатывает очень близко к одинаковому количеству строк в целом, потому что каждый индекс имеет очень близко к одному и тому же числу строк, и каждый поток сканирует только один индекс:

хорошо разделенная работа

В заключение отметим, что мы также можем нажать легкую кнопку и добавить некластеризованный CCI в Ageстолбец:

CREATE NONCLUSTERED COLUMNSTORE INDEX X_NCCI ON dbo.Users (Age);

Следующий запрос завершается через 3 мс на моей машине:

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18 OR u.Age IS NULL;

Это будет сложно победить.

Джо Оббиш
источник
7

Хотя у меня нет локальной копии базы данных Stack Overflow, я смог выполнить несколько запросов. Моя мысль заключалась в том, чтобы получить количество пользователей из представления системного каталога (в отличие от непосредственного получения количества строк из базовой таблицы). Затем получите количество строк, которые соответствуют (или, возможно, не соответствуют) критериям Эрика, и выполните простую математику.

Я использовал Stack Exchange Data Explorer (вместе с SET STATISTICS TIME ON;и SET STATISTICS IO ON;) для проверки запросов. Для справки, вот некоторые запросы и статистика CPU / IO:

QUERY 1

--Erik's query From initial question.
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE ISNULL(u.Age, 17) < 18;

Время выполнения SQL Server: время ЦП = 0 мс, прошедшее время = 0 мс. (1 ряд (ы) вернулся)

Таблица «Пользователи». Сканирование 17, логическое чтение 201567, физическое чтение 0, чтение с опережением 2740, логическое чтение LOB 0, физическое чтение LOB 0, чтение с упреждением LOB чтение 0.

Время выполнения SQL Server: время ЦП = 1829 мс, прошедшее время = 296 мс.

QUERY 2

--Erik's "OR" query.
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;

Время выполнения SQL Server: время ЦП = 0 мс, прошедшее время = 0 мс. (1 ряд (ы) вернулся)

Таблица «Пользователи». Сканирование 17, логическое чтение 201567, физическое чтение 0, чтение с опережением 0, логическое чтение 1, физическое чтение 1, чтение с опережением 0.

Время выполнения SQL Server: время ЦП = 2500 мс, прошедшее время = 147 мс.

QUERY 3

--Erik's derived tables/UNION ALL query.
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);

Время выполнения SQL Server: время ЦП = 0 мс, прошедшее время = 0 мс. (1 ряд (ы) вернулся)

Таблица «Пользователи». Число сканирований 34, логическое чтение 403134, физическое чтение 0, чтение с опережением 0, логическое чтение 1, физическое чтение 1, чтение с опережением 0.

Время выполнения SQL Server: время ЦП = 3156 мс, прошедшее время = 215 мс.

1-я попытка

Это было медленнее, чем все запросы Эрика, которые я перечислил здесь ... по крайней мере, с точки зрения прошедшего времени.

SELECT SUM(p.Rows)  -
  (
    SELECT COUNT(*)
    FROM dbo.Users AS u
    WHERE u.Age >= 18
  ) 
FROM sys.objects o
JOIN sys.partitions p
    ON p.object_id = o.object_id
WHERE p.index_id < 2
AND o.name = 'Users'
AND SCHEMA_NAME(o.schema_id) = 'dbo'
GROUP BY o.schema_id, o.name

Время выполнения SQL Server: время ЦП = 0 мс, прошедшее время = 0 мс. (1 ряд (ы) вернулся)

Стол «Рабочий стол». Сканирование счетчик 0, логическое чтение 0, физическое чтение 0, чтение с опережением 0, логическое чтение с бита 0, физическое чтение с бита 0, чтение с опережением чтения 0. Таблица 'sysrowsets'. Сканирование 2, логическое чтение 10, физическое чтение 0, чтение с опережением 0, логическое чтение с бита 0, физическое чтение с бита 0, чтение с опережением чтения 0. Таблица 'sysschobjs'. Сканирование 1, логическое чтение 4, физическое чтение 0, чтение с опережением 0, логическое чтение с бита 0, физическое чтение с бита 0, чтение с опережением чтения 0. Таблица «Пользователи». Сканирование 1, логическое чтение 201567, физическое чтение 0, чтение с опережением 0, логическое чтение 1, физическое чтение 1, чтение с опережением 0.

Время выполнения SQL Server: время ЦП = 593 мс, прошедшее время = 598 мс.

2-я попытка

Здесь я выбрал переменную для хранения общего числа пользователей (вместо подзапроса). Количество сканирований увеличилось с 1 до 17 по сравнению с 1-й попыткой. Логические чтения остались прежними. Однако прошедшее время значительно сократилось.

DECLARE @Total INT;

SELECT @Total = SUM(p.Rows)
FROM sys.objects o
JOIN sys.partitions p
    ON p.object_id = o.object_id
WHERE p.index_id < 2
AND o.name = 'Users'
AND SCHEMA_NAME(o.schema_id) = 'dbo'
GROUP BY o.schema_id, o.name

SELECT @Total - COUNT(*)
FROM dbo.Users AS u
WHERE u.Age >= 18

Время выполнения SQL Server: время ЦП = 0 мс, прошедшее время = 0 мс. Стол «Рабочий стол». Сканирование счетчик 0, логическое чтение 0, физическое чтение 0, чтение с опережением 0, логическое чтение с бита 0, физическое чтение с бита 0, чтение с опережением чтения 0. Таблица 'sysrowsets'. Сканирование 2, логическое чтение 10, физическое чтение 0, чтение с опережением 0, логическое чтение с бита 0, физическое чтение с бита 0, чтение с опережением чтения 0. Таблица 'sysschobjs'. Сканирование 1, логическое чтение 4, физическое чтение 0, чтение с опережением 0, логическое чтение с бита 0, физическое чтение с бита 0, чтение с опережением чтения 0.

Время выполнения SQL Server: время ЦП = 0 мс, прошедшее время = 1 мс. (1 ряд (ы) вернулся)

Таблица «Пользователи». Сканирование 17, логическое чтение 201567, физическое чтение 0, чтение с опережением 0, логическое чтение 1, физическое чтение 1, чтение с опережением 0.

Время выполнения SQL Server: время ЦП = 1471 мс, прошедшее время = 98 мс.

Другие примечания: DBCC TRACEON не разрешен в Stack Exchange Data Explorer, как указано ниже:

Пользователь 'STACKEXCHANGE \ svc_sede' не имеет разрешения на запуск DBCC TRACEON.

Дейв Мейсон
источник
1
Они, вероятно, не имеют те же индексы, что и я, отсюда и различия. И кто знает? Может быть, мой домашний сервер работает на лучшем оборудовании;) Отличный ответ!
Эрик Дарлинг
Вы должны были использовать следующий запрос для вашей первой попытки (будет намного быстрее, поскольку он избавляет от большей части sys.objects-overhead): 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')
Thomas Franz
PS: имейте в виду, что индексы In-Memory (NONCLUSTERED HASH) не имеют индекса id = 0/1, как было бы у общей кучи / кластерного индекса)
Томас Франц
1

Использовать переменные?

declare @int1 int = ( select count(*) from table_1 where bb <= 1 )
declare @int2 int = ( select count(*) from table_1 where bb is null )
select @int1 + @int2;

По комментарию можно пропустить переменные

SELECT (select count(*) from table_1 where bb <= 1) 
     + (select count(*) from table_1 where bb is null);
папараццо
источник
3
Также:SELECT (select count(*) from table_1 where bb <= 1) + (select count(*) from table_1 where bb is null);
ypercubeᵀᴹ
3
Могу попробовать это при проверке процессора и ввода-вывода. Подсказка: это так же, как один из ответов Эрика.
Брент Озар
0

Хорошо используя SET ANSI_NULLS OFF;

SET ANSI_NULLS OFF; 
SET STATISTICS TIME ON;
SET STATISTICS IO ON;

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE age=NULL or age<18

Table 'Users'. Scan count 17, logical reads 201567

 SQL Server Execution Times:
 CPU time = 2344 ms,  elapsed time = 166 ms.

Это что-то, что пришло мне в голову. Просто выполнил это на https://data.stackexchange.com.

Но не так эффективно, как @blitz_erik, хотя

Бижу Хосе
источник
0

Тривиальным решением является подсчет количества (*) - количество (возраст> = 18):

SELECT
    (SELECT COUNT(*) FROM Users) -
    (SELECT COUNT(*) FROM Users WHERE Age >= 18);

Или же:

SELECT COUNT(*)
     - COUNT(CASE WHEN Age >= 18)
FROM Users;

Результаты здесь

Салман А
источник