Почему существуют различия в плане выполнения между 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

Смещение-fetch.png

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


Не очень уверенно, поэтому выкладывайте это как комментарий, но я думаю, это потому, что у вас одинаковый порядок по порядку для нумерации строк и окончательного набора результатов. Поскольку во 2-м условии оптимизатор это знает, ему не нужно снова сортировать результаты. В первом случае, однако, необходимо убедиться, что результаты внешнего выбора отсортированы, а также нумерация строк во внутреннем результате. Создание правильного индекса на #objects должно решить проблему
Akash

Ответы:


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) и равную статистику ввода-вывода:

; 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 ORDER BY, который использует 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.