порядок пунктов в «СУЩЕСТВУЕТ (…) ИЛИ СУЩЕСТВУЕТ (…)»

11

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

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)
младший
источник

Ответы:

11

Как общее практическое правило, SQL Server будет выполнять части CASEинструкции по порядку, но может изменять порядок ORусловий. Для некоторых запросов вы можете получить стабильно лучшую производительность, изменив порядок WHENвыражений внутри CASEоператора. Иногда вы также можете получить лучшую производительность при изменении порядка условий в ORвыражении, но это не гарантированное поведение.

Наверное, лучше всего пройтись по простому примеру. Я тестирую на SQL Server 2016, поэтому возможно, что вы не получите точно такие же результаты на своем компьютере, но, насколько я знаю, применяются те же принципы. Сначала я положу один миллион целых чисел от 1 до 1000000 в две таблицы: одну с кластеризованным индексом, а другую как кучу:

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL, FLUFF VARCHAR(100));

INSERT INTO dbo.X_HEAP  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE dbo.X_CI (ID INT NOT NULL, FLUFF VARCHAR(100), PRIMARY KEY (ID));

INSERT INTO dbo.X_CI  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

Рассмотрим следующий запрос:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

Мы знаем, что оценка подзапроса по сравнению X_CIбудет намного дешевле, чем подзапрос X_HEAP, особенно, когда нет соответствующей строки. Если подходящей строки нет, тогда нам нужно всего лишь выполнить несколько логических операций чтения таблицы с кластеризованным индексом. Однако нам нужно будет просканировать все строки кучи, чтобы узнать, что соответствующей строки нет. Оптимизатор тоже это знает. Вообще говоря, использование кластерного индекса для поиска одной строки очень дешево по сравнению со сканированием таблицы.

Для данных этого примера я бы написал запрос следующим образом:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
ELSE 0 END;

Это фактически заставляет SQL Server сначала запускать подзапрос к таблице с кластеризованным индексом. Вот результаты из SET STATISTICS IO, TIME ON:

Таблица «X_CI». Количество сканирований 0, логическое чтение 3, физическое чтение 0

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

Если посмотреть на план запроса, если поиск по метке 1 возвращает какие-либо данные, тогда сканирование по метке 2 не требуется и не произойдет:

хороший запрос

Следующий запрос гораздо менее эффективен:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
ELSE 0 END
OPTION (MAXDOP 1);

Глядя на план запроса, мы видим, что сканирование по метке 2 всегда происходит. Если строка найдена, то поиск по метке 1 пропускается. Это не тот порядок, который мы хотели:

плохой план запроса

Результаты производительности подтверждают это:

Таблица «X_HEAP». Сканирование 1, логическое чтение 7247

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

Возвращаясь к исходному запросу, для этого запроса я вижу результаты поиска и сканирования в порядке, который хорош для производительности:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

И в этом запросе они оцениваются в обратном порядке:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
THEN 1 ELSE 0 END;

Однако, в отличие от предыдущей пары запросов, ничто не заставляет оптимизатор запросов SQL Server оценивать один перед другим. Вы не должны полагаться на это поведение для чего-то важного.

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

Приложение:

Естественный последующий вопрос: что вы можете сделать, если хотите, чтобы SQL Server решал, какой запрос дешевле, и выполнял его первым? До сих пор все методы реализованы SQL Server в том порядке, в котором написан запрос, даже если для некоторых из них поведение не гарантировано.

Вот одна опция, которая работает для простых демонстрационных таблиц:

SELECT CASE
  WHEN EXISTS (
    SELECT 1
    FROM (
        SELECT TOP 2 1 t
        FROM 
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_HEAP 
            WHERE ID = 50000 
        ) h
        CROSS JOIN
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_CI
            WHERE ID = 50000
        ) ci
    ) cnt
    HAVING COUNT(*) = 2
)
THEN 1 ELSE 0 END;

Вы можете найти демо db fiddle здесь . Изменение порядка производных таблиц не меняет план запроса. В обоих запросах X_HEAPтаблица не затрагивается. Другими словами, оптимизатор запросов, кажется, сначала выполняет более дешевый запрос. Я не могу рекомендовать использовать что-то подобное в производстве, так что это в основном для любопытства. Там может быть гораздо более простой способ сделать то же самое.

Джо Оббиш
источник
4
Или 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 автоматически переупорядочивал, так что сначала автоматически оценивается дешевый.
Мартин Смит