У меня есть класс запросов, которые проверяют наличие одной из двух вещей. Это имеет форму
SELECT CASE
WHEN EXISTS (SELECT 1 FROM ...)
OR EXISTS (SELECT 1 FROM ...)
THEN 1 ELSE 0 END;
Фактический оператор генерируется в C и выполняется как специальный запрос по соединению ODBC.
Недавно стало известно, что второй SELECT, вероятно, будет быстрее, чем первый SELECT, в большинстве случаев и что переключение порядка двух предложений EXISTS вызвало резкое ускорение по крайней мере в одном тестовом случае, который мы только что создали.
Очевидная вещь, которую нужно сделать, это просто пойти дальше и переключить два предложения, но я хотел посмотреть, не захочет ли кто-нибудь, более знакомый с SQL Server, взвесить это. Такое ощущение, что я полагаюсь на совпадение и «детали реализации».
(Похоже, что если бы SQL Server был умнее, он бы выполнял оба предложения EXISTS параллельно и позволял одному из них завершить первое замыкание другого.)
Есть ли лучший способ заставить SQL Server последовательно улучшать время выполнения такого запроса?
Обновить
Спасибо за ваше время и интерес к моему вопросу. Я не ожидал вопросов о реальных планах запросов, но я готов поделиться ими.
Это для программного компонента, который поддерживает SQL Server 2008R2 и выше. Форма данных может сильно отличаться в зависимости от конфигурации и использования. Мой коллега подумал о том, чтобы внести это изменение в запрос, потому что (в примере) dbf_1162761$z$rv$1257927703
таблица всегда будет иметь большее или равное количество строк в ней, чем dbf_1162761$z$dd$1257927703
таблица - иногда значительно больше (порядки величины).
Вот оскорбительный случай, который я упомянул. Первый запрос медленный и занимает около 20 секунд. Второй запрос завершается в одно мгновение.
Для чего бы то ни было, бит «OPTIMIZE FOR UNKNOWN» также был добавлен недавно, потому что сниффинг параметров сбивал некоторые случаи.
Исходный запрос:
SELECT CASE
WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)
Оригинальный план:
|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
|--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
|--Constant Scan
|--Concatenation
|--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
| |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
| |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
|--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
|--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]), WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
|--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]), WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)
Фиксированный запрос:
SELECT CASE
WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)
Фиксированный план:
|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
|--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
|--Constant Scan
|--Concatenation
|--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
| |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]), WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
| |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]), WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)
|--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
|--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
|--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
источник
Ответы:
Как общее практическое правило, SQL Server будет выполнять части
CASE
инструкции по порядку, но может изменять порядокOR
условий. Для некоторых запросов вы можете получить стабильно лучшую производительность, изменив порядокWHEN
выражений внутриCASE
оператора. Иногда вы также можете получить лучшую производительность при изменении порядка условий вOR
выражении, но это не гарантированное поведение.Наверное, лучше всего пройтись по простому примеру. Я тестирую на SQL Server 2016, поэтому возможно, что вы не получите точно такие же результаты на своем компьютере, но, насколько я знаю, применяются те же принципы. Сначала я положу один миллион целых чисел от 1 до 1000000 в две таблицы: одну с кластеризованным индексом, а другую как кучу:
Рассмотрим следующий запрос:
Мы знаем, что оценка подзапроса по сравнению
X_CI
будет намного дешевле, чем подзапросX_HEAP
, особенно, когда нет соответствующей строки. Если подходящей строки нет, тогда нам нужно всего лишь выполнить несколько логических операций чтения таблицы с кластеризованным индексом. Однако нам нужно будет просканировать все строки кучи, чтобы узнать, что соответствующей строки нет. Оптимизатор тоже это знает. Вообще говоря, использование кластерного индекса для поиска одной строки очень дешево по сравнению со сканированием таблицы.Для данных этого примера я бы написал запрос следующим образом:
Это фактически заставляет SQL Server сначала запускать подзапрос к таблице с кластеризованным индексом. Вот результаты из
SET STATISTICS IO, TIME ON
:Если посмотреть на план запроса, если поиск по метке 1 возвращает какие-либо данные, тогда сканирование по метке 2 не требуется и не произойдет:
Следующий запрос гораздо менее эффективен:
Глядя на план запроса, мы видим, что сканирование по метке 2 всегда происходит. Если строка найдена, то поиск по метке 1 пропускается. Это не тот порядок, который мы хотели:
Результаты производительности подтверждают это:
Возвращаясь к исходному запросу, для этого запроса я вижу результаты поиска и сканирования в порядке, который хорош для производительности:
И в этом запросе они оцениваются в обратном порядке:
Однако, в отличие от предыдущей пары запросов, ничто не заставляет оптимизатор запросов SQL Server оценивать один перед другим. Вы не должны полагаться на это поведение для чего-то важного.
В заключение, если вам нужно, чтобы один подзапрос оценивался раньше другого, используйте
CASE
оператор или другой метод для принудительного упорядочения. В противном случае не стесняйтесь заказывать подзапросы в нужномOR
вам состоянии, но знайте, что нет гарантии, что оптимизатор выполнит их в указанном порядке.Приложение:
Естественный последующий вопрос: что вы можете сделать, если хотите, чтобы SQL Server решал, какой запрос дешевле, и выполнял его первым? До сих пор все методы реализованы SQL Server в том порядке, в котором написан запрос, даже если для некоторых из них поведение не гарантировано.
Вот одна опция, которая работает для простых демонстрационных таблиц:
Вы можете найти демо db fiddle здесь . Изменение порядка производных таблиц не меняет план запроса. В обоих запросах
X_HEAP
таблица не затрагивается. Другими словами, оптимизатор запросов, кажется, сначала выполняет более дешевый запрос. Я не могу рекомендовать использовать что-то подобное в производстве, так что это в основном для любопытства. Там может быть гораздо более простой способ сделать то же самое.источник
CASE WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000 UNION ALL SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 ELSE 0 END
может быть альтернативой, хотя это по-прежнему зависит от того, как вручную решить, какой запрос быстрее, и поставить его первым. Я не уверен, есть ли способ выразить это так, чтобы SQL Server автоматически переупорядочивал, так что сначала автоматически оценивается дешевый.