Чому існують відмінності в плані виконання між OFFSET… FETCH та старою схемою ROW_NUMBER?


15

Нова OFFSET ... FETCHмодель, представлена ​​разом із SQL Server 2012, пропонує просте та швидше підключення сторінки. Чому взагалі є якісь відмінності, враховуючи, що дві форми є семантично однаковими і дуже поширеними?

Можна припустити, що оптимізатор розпізнає обидва і оптимізує їх (тривіально) в повній мірі.

Ось дуже простий випадок, коли OFFSET ... FETCHвідповідно до оцінки вартості ~ 2 рази швидше.

SELECT * INTO #objects FROM sys.objects

SELECT *
FROM (
    SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
    FROM #objects
) x
WHERE r >= 30 AND r < (30 + 10)
    ORDER BY object_id

SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY

offset-fetch.png

Можна змінити цей тестовий випадок, створивши CI object_idабо додавши фільтри, але неможливо видалити всі відмінності в плані. OFFSET ... FETCHзавжди швидше, тому що робить менше роботи під час виконання.


Не дуже впевнений, тому ставлю це як коментар, але, мабуть, його тому, що ви маєте той самий порядок за умовою нумерації рядків та набору кінцевих результатів. Оскільки у другій умові оптимізатор це знає, не потрібно повторно сортувати результати. Однак у першому випадку потрібно переконатися, що результати від зовнішнього вибору сортуються, а також нумерація рядків у внутрішньому результаті. Створення належного індексу на #objects має вирішити проблему
Акаш

Відповіді:


13

Приклади у запитанні не зовсім дають однакові результати (у OFFSETприкладі є помилка по одному). Оновлені форми нижче виправляють цю проблему, видаляють зайвий сорт для цього ROW_NUMBERвипадку та використовують змінні, щоб зробити рішення загальнішим:

DECLARE 
    @PageSize bigint = 10,
    @PageNumber integer = 3;

WITH Numbered AS
(
    SELECT TOP ((@PageNumber + 1) * @PageSize) 
        o.*,
        rn = ROW_NUMBER() OVER (
            ORDER BY o.[object_id])
    FROM #objects AS o
    ORDER BY 
        o.[object_id]
)
SELECT
    x.name,
    x.[object_id],
    x.principal_id,
    x.[schema_id],
    x.parent_object_id,
    x.[type],
    x.type_desc,
    x.create_date,
    x.modify_date,
    x.is_ms_shipped,
    x.is_published,
    x.is_schema_published
FROM Numbered AS x
WHERE
    x.rn >= @PageNumber * @PageSize
    AND x.rn < ((@PageNumber + 1) * @PageSize)
ORDER BY
    x.[object_id];

SELECT
    o.name,
    o.[object_id],
    o.principal_id,
    o.[schema_id],
    o.parent_object_id,
    o.[type],
    o.type_desc,
    o.create_date,
    o.modify_date,
    o.is_ms_shipped,
    o.is_published,
    o.is_schema_published
FROM #objects AS o
ORDER BY 
    o.[object_id]
    OFFSET @PageNumber * @PageSize - 1 ROWS 
    FETCH NEXT @PageSize ROWS ONLY;

ROW_NUMBERПлан має орієнтовну вартість 0.0197935 :

План рядків

OFFSETПлан має орієнтовну вартість 0.0196955 :

План зміщення

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

Якщо використовуються постійні літеральні значення (наприклад, OFFSET 30в оригінальному прикладі), оптимізатор може використовувати сортування TopN замість повного сортування, а потім Top. Коли рядки, необхідні для сортування TopN, є постійним буквальним значенням і <= 100 (сума OFFSETта FETCH), механізм виконання може використовувати інший алгоритм сортування, який може виконувати швидше, ніж узагальнений сортування TopN. Усі три випадки мають загальну характеристику продуктивності.

Щодо того, чому оптимізатор не автоматично трансформує ROW_NUMBERсинтаксичний зразок у використанні OFFSET, є низка причин:

  1. Практично неможливо написати перетворення, яке б відповідало всім існуючим напрямкам
  2. Якщо деякі запити підкачки автоматично трансформуються, а інші не можуть бентежити
  3. OFFSETПлан не гарантовано буде краще в усіх випадках

Один із прикладів третьої точки вище - там, де набір підказок досить широкий. Це може бути набагато ефективніше шукати потрібні ключі, використовуючи некластеризований індекс і вручну шукати кластерний індекс в порівнянні зі скануванням індексу з OFFSETабо ROW_NUMBER. Є додаткові питання, які слід врахувати, чи потрібно додатку підкачки знати, скільки всього рядків чи сторінок. Існує ще одна хороша дискусія відносних переваг «ключ шукати» і «зміщення» методи тут .

В цілому, напевно, краще, щоб люди приймали усвідомлене рішення змінити свої пошукові запити на користування OFFSET, якщо це доречно, після ретельного тестування.


1
Тож причиною того, що трансформація не робиться у звичайних випадках, ймовірно, було надто важко знайти прийнятну компромісну техніку. Ви навели вагомі причини, чому це могло бути так; Треба сказати, що це хороша відповідь. Багато думок і нових думок. Я залишу питання відкритим на трохи, а потім виберу найкращу відповідь.
usr

5

З невеликим підбором вашого запиту я отримую рівну оцінку витрат (50/50) та рівну статистику IO:

; WITH cte AS
(
    SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
    FROM #objects
)
SELECT *
FROM cte
WHERE r >= 30 AND r < 40
ORDER BY r

SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY

Це дозволяє уникнути додаткового сортування, яке з’являється у вашій версії шляхом сортування rзамість object_id.


Дякую за це розуміння. Тепер, коли я замислююся над цим, я побачив, що оптимізатор раніше не розуміє відсортовану природу виходу ROW_NUMBER. Він вважає набір не упорядкованим object_id. Або принаймні не відсортовано як за r, так і за object_id.
usr

2
@usr ПОРЯДОК, який використовує ROW_NUMBER (), визначає, як він присвоює числа. Це нічого не обіцяє на замовлення виходу - це окремо. Так буває, що це часто збігається, але це не гарантується.
Аарон Бертран

@AaronBertrand Я розумію, що ROW_NUMBER не замовляє вихід. Але якщо ROW_NUMBER замовлений ті ж стовпчики , як вихід, то той же порядок буде гарантований, вірно? Тож оптимізатор запитів міг би скористатися цим фактом. Тож дві операції сортування завжди непотрібні в цьому запиті.
usr

1
@usr Ви потрапили у звичайний випадок використання, який оптимізатор не враховує, але це не єдиний випадок використання. Розглянемо випадки, коли замовлення всередині ROW_NUMBER () - це стовпець та щось інше. Або коли зовнішній порядок виконує вторинне сортування в іншому стовпці. Або коли ви хочете замовити низхідний. Або зовсім іншим. Мені подобається впорядкування за виразом rзамість базового стовпця, хоча б тому, що воно відповідає тому, що я б робив у невкладеному запиті та впорядкуванні за виразом - я б використовував псевдонім, призначений виразу, замість повторення виразу.
Аарон Бертран

4
@usr І, на думку Павла, трапляються випадки, коли ви можете знайти прогалини у функціональності в оптимізаторі. Якщо вони не будуть виправлені, і ви знаєте кращий спосіб написати запит, скористайтеся кращим способом. Пацієнт: "Докторе, боляче, коли я роблю х". Лікар: "Не робіть х". :-)
Аарон Бертран

-3

Вони змінили оптимізатор запитів, щоб додати цю функцію. Значить, вони реалізували механізми спеціально для підтримки команди offset ... fetch. Іншими словами, для верхнього запиту SQL Server повинен зробити набагато більше роботи. Таким чином, різниця в планах запитів.

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