Включення ORDER BY у запиті, який не повертає жодних рядків, різко впливає на продуктивність


15

З огляду на просте приєднання трьох таблиць, продуктивність запитів різко змінюється, коли ORDER BY включений навіть без повернення рядків. Справжній сценарій проблеми займає 30 секунд, щоб повернути нульові рядки, але миттєвий, коли ЗАМОВЛЕННЯ ВНЕ включено. Чому?

SELECT * 
FROM tinytable t                          /* one narrow row */
JOIN smalltable s on t.id=s.tinyId        /* one narrow row */
JOIN bigtable b on b.smallGuidId=s.GuidId /* a million narrow rows */
WHERE t.foreignId=3                       /* doesn't match */
ORDER BY b.CreatedUtc          /* try with and without this ORDER BY */

Я розумію, що я міг би мати індекс на bigtable.smallGuidId, але, я вважаю, це насправді погіршило б у цьому випадку.

Ось сценарій для створення / заповнення таблиць для тесту. Цікаво, що, мабуть, має значення те, що в маленькому таблицю є поле nvarchar (max). Також, мабуть, має значення те, що я приєднуюся до великого столу з орієнтиром (який, мабуть, змушує використовувати хеш-відповідність).

CREATE TABLE tinytable
  (
     id        INT PRIMARY KEY IDENTITY(1, 1),
     foreignId INT NOT NULL
  )

CREATE TABLE smalltable
  (
     id     INT PRIMARY KEY IDENTITY(1, 1),
     GuidId UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
     tinyId INT NOT NULL,
     Magic  NVARCHAR(max) NOT NULL DEFAULT ''
  )

CREATE TABLE bigtable
  (
     id          INT PRIMARY KEY IDENTITY(1, 1),
     CreatedUtc  DATETIME NOT NULL DEFAULT GETUTCDATE(),
     smallGuidId UNIQUEIDENTIFIER NOT NULL
  )

INSERT tinytable
       (foreignId)
VALUES(7)

INSERT smalltable
       (tinyId)
VALUES(1)

-- make a million rows 
DECLARE @i INT;

SET @i=20;

INSERT bigtable
       (smallGuidId)
SELECT GuidId
FROM   smalltable;

WHILE @i > 0
  BEGIN
      INSERT bigtable
             (smallGuidId)
      SELECT smallGuidId
      FROM   bigtable;

      SET @i=@i - 1;
  END 

Я тестував на SQL 2005, 2008 та 2008R2 з однаковими результатами.

Відповіді:


32

Я погоджуюся з відповіддю Мартіна Сміта, але проблема полягає не просто в статистиці. Статистика стовпця ForeignId (якщо припустити автоматичну статистику) точно показує, що немає рядків для значення 3 (є лише один, зі значенням 7):

DBCC SHOW_STATISTICS (tinytable, foreignId) WITH HISTOGRAM

вихід статистики

SQL Server знає , що все може змінитися , так як статистичні дані були взяті в полон, так що може бути рядок для значення 3 , коли план виконується . Крім того, між складанням плану та виконанням плану може пройти будь-який проміжок часу (зрештою, плани кешуються для повторного використання). Як каже Мартін, SQL Server містить логіку, щоб виявити, коли було внесено достатньо модифікацій для обгрунтування перекомпіляції будь-якого кешованого плану з міркувань оптимальності.

Однак, нічого з цього не має значення. За одним винятковим випадком, оптимізатор ніколи не оцінить кількість рядків, отриманих операцією таблиці, до нуля. Якщо він може статично визначити, що вихід завжди повинен дорівнювати нулю рядків, операція буде зайвою і буде видалена повністю.

Натомість модель оптимізатора оцінює як мінімум один рядок. Використання цього евристичного характеру має тенденцію створювати кращі плани в середньому, ніж це було б у випадку, якщо можлива нижча оцінка. План, який виробляє оцінку нульових рядків на якомусь етапі, був би марним з цього моменту в потоці обробки, оскільки не було б ніякої основи для прийняття рішень на основі витрат (нульові рядки - це нульові рядки незалежно від того, що). Якщо оцінка виявиться помилковою, форма плану над нульовим рядом майже не має шансів бути розумною.

Другий фактор - це ще одне припущення моделювання, яке називається стримуючим припущенням. Це по суті говорить про те, що якщо запит приєднується до діапазону значень з іншим діапазоном значень, це пояснюється тим, що діапазони перекриваються. Ще один спосіб сказати це - сказати, що приєднання задається, оскільки очікується повернення рядків. Без цього міркування витрати, як правило, занижуватимуться, що призводить до поганих планів для широкого кола загальних запитів.

По суті, у вас є запит, який не відповідає моделі оптимізатора. Ми нічого не можемо зробити, щоб «покращити» оцінки за допомогою стовпців або відфільтрованих індексів; тут немає можливості отримати оцінку нижче 1 ряду. У реальній базі даних можуть бути зовнішні ключі, щоб переконатися, що така ситуація не може виникнути, але якщо припустити, що тут не застосовується, ми залишаємося з підказками, щоб виправити стан поза моделлю. З цим запитом буде працювати будь-яка кількість різних підходів. OPTION (FORCE ORDER)це те, що добре працює з запитом, як написано.


21

Основна проблема тут - одна зі статистичних даних.

Для обох запитів підрахунок підрахунку рядків показує, що він вважає, що остаточний SELECTповерне 1,048,580 рядків (стільки ж рядків, за оцінками, існують у bigtable), а не 0, який насправді випливає.

Обидві ваші JOINумови відповідають, і зберегли б усі рядки. Вони в кінцевому підсумку виключаються, оскільки один рядок у tinytableне відповідає t.foreignId=3предикату.

Якщо ти біжиш

SELECT * 
FROM tinytable t  
WHERE t.foreignId=3  AND id=1 

і подивіться на передбачувану кількість рядків 1, ніж 0це, і ця помилка поширюється по всьому плану. tinytableВ даний час містить 1 ряд. Статистика не буде перекомпільована для цієї таблиці, поки не відбудуться 500 модифікацій рядків , щоб відповідна рядок могла бути додана і вона не спровокує перекомпіляцію.

Причина, по якій Порядок приєднання змінюється, коли ви додаєте ORDER BYпункт і є varchar(max)стовпець у smalltableтому, що він оцінює, що varchar(max)стовпці збільшать розмір рядка в середньому на 4000 байт. Помножте це на 1048580 рядків, і це означає, що операція сортування потребує приблизно 4 Гб, тому вона розумно вирішить зробити SORTоперацію до початку JOIN.

Ви можете змусити ORDER BYзапит прийняти ORDER BYстратегію неприєднання, використовуючи підказки, як показано нижче.

SELECT *
FROM   tinytable t /* one narrow row */
       INNER MERGE JOIN smalltable s /* one narrow row */
                        INNER LOOP JOIN bigtable b
                          ON b.smallGuidId = s.GuidId /* a million narrow rows */
         ON t.id = s.tinyId
WHERE  t.foreignId = 3 /* doesn't match */
ORDER  BY b.CreatedUtc
OPTION (MAXDOP 1) 

План показує оператор сортування з розрахунковою вартістю суб-дерева, що становить майже 12,000та помилково оцінені підрахунки рядків та приблизний розмір даних.

План

До UNIQUEIDENTIFIERречі, в моєму тесті я не знайшов заміни стовпців цілими зміненими речами.


2

Увімкніть кнопку "План виконання програми", і ви зможете побачити, що відбувається. Ось план "повільного" запиту: введіть тут опис зображення

І ось "швидкий" запит: введіть тут опис зображення

Подивіться на це - біжіть разом, перший запит на 33 рази дорожчий "(співвідношення 97: 3)". SQL оптимізує перший запит для замовлення BigTable за датою часу, потім запустить невеликий цикл "шукати" над SmallTable & TinyTable, виконуючи їх по 1 мільйона разів кожен (ви можете навести курсор на піктограму "Пошук кластера", щоб отримати більше статистики). Отже, сортування (27%) та 2 х 1 мільйон «шукає» на невеликих столах (23% та 46%) - це основна маса дорогого запиту. Для порівняння, ORDER BYзапит виконує загальну суму 3 сканування.

В основному, ви знайшли дірку в логіці оптимізатора SQL для вашого конкретного сценарію. Але, як стверджує TysHTTP, якщо ви додасте індекс (який уповільнює вставку / оновлення деяких), сканування швидко стає шаленим.


2

Що відбувається, SQL вирішує запустити замовлення до обмеження.

Спробуйте це:

SELECT *
(
SELECT * 
FROM tinytable t
    INNER JOIN smalltable s on t.id=s.tinyId
    INNER JOIN bigtable b on b.smallGuidId=s.GuidId
WHERE t.foreignId=3
) X
ORDER BY b.CreatedUtc

Це дає вам покращену ефективність (у цьому випадку, коли кількість повернутих результатів дуже мала), не маючи фактичного враження від додавання іншого індексу. Хоча це дивно, коли оптимізатор SQL вирішить виконати замовлення до приєднання, це, ймовірно, тому що якщо ви насправді мали повернути дані, то сортування їх після об'єднання зайняло б більше часу, ніж сортування без.

Нарешті, спробуйте запустити наступний скрипт, а потім перевірте, чи оновлені статистичні дані та індекси вирішують проблему, яка виникає:

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

EXEC [sp_MSforeachtable] @command1="RAISERROR('DBCC DBREINDEX(''?'') ...',10,1) WITH NOWAIT DBCC DBREINDEX('?')"

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.