порядок пунктів у розділі "ІСНУЄТЬСЯ (...) АБО Є" (...) "


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 NEKNOWN", оскільки нюхання параметрів переносило певні випадки.

Оригінальний запит:

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-скрипки тут . Зміна порядку отриманих таблиць не змінює план запитів. В обох запитах 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 автоматично переупорядкував, тому дешевий автоматично оцінюється спочатку.
Мартін Сміт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.