Разбиение страницы в SQL - проблема с производительностью

Я пытаюсь использовать разбиение на страницы, и я получил идеальную ссылку в SO

qaru.site/info/21336/...

SELECT  *
FROM    ( SELECT    ROW_NUMBER() OVER ( ORDER BY OrderDate ) AS RowNum, *
          FROM      Orders
          WHERE     OrderDate >= '1980-01-01'
        ) AS RowConstrainedResult
WHERE   RowNum >= 1
    AND RowNum < 20
ORDER BY RowNum

Точный же запрос пытается использовать с дополнительным объединением нескольких таблиц в моем внутреннем запросе.

Получается несколько проблем с производительностью в следующих сценариях

WHERE   RowNum >= 1
    AND RowNum < 20  ==>executes faster approx 2 sec


    WHERE   RowNum >= 1000
    AND RowNum < 1010      ==>  more time  approx 10 sec

    WHERE   RowNum >= 30000
    AND RowNum < 30010    ==> more time approx 17 sec

Каждый раз я выбираю 10 строк, но огромную разницу во времени. Любая идея или предложения?

Я выбрал этот подход, так как динамически связываю столбцы и формирую Query. Есть ли другой лучший способ организовать Pagination Query в SQl Server 2008.

Есть ли способ повысить производительность запроса?

Спасибо

Ответ 1

Я всегда проверяю, сколько данных я получаю в запросе, и пытаюсь устранить ненужные столбцы, а также строки. Ну, это просто очевидные моменты, которые вы, возможно, уже проверяли, но просто хотели указать на то, что вы уже не имеете. В вашем запросе медленная производительность может быть вызвана тем, что вы делаете "Выбрать *". Выбор всех столбцов из таблицы не позволяет прийти с хорошим планом выполнения. Проверьте, нужны ли только выбранные столбцы и убедитесь, что у вас есть правильный индекс покрытия в таблице Заказы.

Поскольку явная функция SKIPP или OFFSET недоступна в версии SQL 2008, нам нужно создать одно, и мы можем создать INNER JOIN. В одном запросе мы сначала сгенерируем идентификатор с OrderDate, и больше ничего не будет в этом запросе. Мы делаем то же самое во втором запросе, но здесь мы также выбираем некоторые другие заинтересованные столбцы из таблицы ORDER или ALL, если вам нужен ВСЕ столбец. Затем мы присоединяем это для запроса результатов с помощью фильтра ID и OrderDate и ADD SKIPP для первого запроса, где набор данных имеет минимальный размер, что требуется. Попробуйте этот код.

    SELECT q2.*
    FROM
    (
        SELECT ROW_NUMBER() OVER ( ORDER BY OrderDate ) AS RowNum, OrderDate
        FROM      Orders
        WHERE     OrderDate >= '1980-01-01'
    )q1
    INNER JOIN 
    (
        SELECT ROW_NUMBER() OVER ( ORDER BY OrderDate ) AS RowNum, *
        FROM      Orders
        WHERE     OrderDate >= '1980-01-01'
    )q2
        ON q1.RowNum=q2.RowNum AND q1.OrderDate=q2.OrderDate AND q1.rownum BETWEEN 30000 AND 30020

Чтобы дать вам оценку, я пробовал это с помощью следующих тестовых данных и независимо от того, в каком окне вы запрашиваете результаты, менее чем за 2 секунд, и обратите внимание, что таблица HEAP (без индекса) Таблица имеет общее количество 2M строк. test select запрашивает 10 строк от 50 000 до 50,010

Внизу "Вставка" заняло около 8 минут.

    IF object_id('TestSelect','u') IS NOT NULL
        DROP TABLE TestSelect
    GO
    CREATE TABLE TestSelect
    (
        OrderDate   DATETIME2(2)
    )
    GO

    DECLARE @i bigint=1, @dt DATETIME2(2)='01/01/1700'
    WHILE @I<=2000000
    BEGIN

        IF @i%15 = 0
            SELECT @DT = DATEADD(DAY,1,@dt)

        INSERT INTO dbo.TestSelect( OrderDate )
        SELECT @dt

        SELECT @[email protected]+1
    END

Выбор окна от 50 000 до 50,010 занял менее 3 секунд.

Выбор последней одной строки от 2,000,000 до 2,000,000 также занял 3 секунды.

    SELECT q2.*
    FROM
    (
        SELECT  ROW_NUMBER() OVER ( ORDER BY OrderDate ) AS RowNum 
                ,OrderDate
        FROM TestSelect
        WHERE OrderDate >= '1700-01-01'
    )q1
    INNER JOIN
    (
        SELECT  ROW_NUMBER() OVER ( ORDER BY OrderDate ) AS RowNum 
                ,*
        FROM TestSelect
        WHERE OrderDate >= '1700-01-01'
    )q2
        ON q1.RowNum=q2.RowNum 
        AND q1.OrderDate=q2.OrderDate 
        AND q1.RowNum BETWEEN 50000 AND 50010

enter image description here

Ответ 2

ROW_NUMBER - это дерьмовый способ сделать разбивку на страницы, поскольку стоимость операции растет.

Вместо этого вы должны использовать предложение double ORDER BY.

Предположим, что вы хотите получать записи с помощью ROW_NUMBER between 1200 and 1210. Вместо использования ROW_NUMBER() OVER (...) и последующего связывания результата в WHERE вам следует:

SELECT TOP(11) *
FROM (
    SELECT TOP(1210) *
    FROM [...]
    ORDER BY something ASC
) subQuery
ORDER BY something DESC.

Обратите внимание, что этот запрос даст результат в обратном порядке. Это не должно, вообще говоря, быть проблемой, так как легко отменить установку в пользовательском интерфейсе, то есть С#, тем более что результирующий набор должен быть относительно небольшим.

Последний, как правило, намного быстрее. Обратите внимание, что последнее решение будет значительно улучшено с помощью CLUSTERING (CREATE CLUSTERED INDEX ...) в столбце, который вы используете для сортировки запроса.

Надеюсь, что это поможет.

Ответ 3

Несмотря на то, что вы всегда выбираете одинаковое количество строк, производительность ухудшается, когда вы хотите выбрать строки в конце окна данных. Чтобы получить первые 10 строк, двигатель извлекает всего 10 строк; для получения следующих 10 он должен получить 20, отбросить первые 10 и вернуть 10. Чтобы получить 30000 - 30010, он должен прочитать все 30010, пропустить первые 30k и вернуться 10.

Некоторые трюки для повышения производительности (не полный список, полное OLAP полностью пропущено). Вы упомянули о присоединении; если это возможное соединение не внутри внутреннего запроса, а результат его. Вы также можете попытаться добавить некоторую логику в ORDER BY OrderDate - ASC или DESC в зависимости от того, какое bucket вы извлекаете. Скажем, если вы хотите захватить "последний" 10, ORDER BY ... DESC будет работать намного быстрее. Игла, чтобы сказать, это должен быть индекс orderDate.

Ответ 4

declare @pageOffset int
declare @pageSize int
-- set variables at some point

declare @startRow int
set @startRow = @pageOffset * @pageSize

declare @endRow int
set @endRow + @pageSize - 1

SELECT 
    o.*
FROM
(
    SELECT 
        ROW_NUMBER() OVER ( ORDER BY OrderDate ) AS RowNum
        , OrderId
    FROM      
        Orders
    WHERE     
        OrderDate >= '1980-01-01'
) q1
INNER JOIN Orders o
    on q1.OrderId = o.OrderId
where
    q1.RowNum between @startRow and @endRow
order by
    o.OrderDate

Ответ 5

@peru, если есть лучший способ и построить объяснение, предоставленное @a1ex07, попробуйте следующее -

Если таблица имеет уникальный идентификатор, такой как числовой (идентификатор заказа) или (дата заказа, индекс заказа), по которому может выполняться операция сравнения (больше или меньше), используйте это как смещение вместо номера строки.

Например, если в заказах таблицы есть "order_id" в качестве первичного ключа, то -
Чтобы получить первые десять результатов -
1.

select RowNum, order_id from   
( select 
ROW_NUMBER() OVER ( ORDER BY OrderDate ) AS RowNum, 
o.order_id 
from orders o where  o.order_id > 0 ;  
) 
tmp_qry where RowNum between 1 and 10 order by RowNum; // first 10 

Предполагая, что последний возвращаемый идентификатор заказа равен 17, тогда

Чтобы выбрать следующие 10,
2.

select RowNum, order_id from   
( select 
ROW_NUMBER() OVER ( ORDER BY OrderDate ) AS RowNum, 
o.order_id 
from orders o where  o.order_id > 17 ;  
) 
tmp_qry where RowNum between 1 and 10 order by RowNum; // next 10 

Обратите внимание, что значения row-num не изменены. Сравнивается его значение идентификатора заказа.

Если такой ключ отсутствует, рассмотрите возможность добавления одного!

Ответ 6

Основной недостаток вашего запроса состоит в том, что он сортирует всю таблицу и вычисляет Row_Number для каждого запроса. Вы можете облегчить жизнь SQL Server, используя меньшее количество столбцов на этапе сортировки (например, как было предложено Anup Shah). Однако вы все равно можете читать, сортировать и вычислять номера строк для каждого запроса.

Альтернативой вычислениям на лету являются значения, которые были вычислены раньше.

В зависимости от волатильности вашего набора данных и количества столбцов для сортировки и фильтрации вы можете рассмотреть:

  • Добавьте столбец столбца (или 2-3 столбца) и включите его в качестве первых столбцов в кластерном индексе или создайте некластеризованный индекс inde).

  • Создавайте представления для наиболее часто встречающихся комбинаций, а затем индексируйте их. Он называется индексированными (материализованными) представлениями.

Это позволит читать рябину, а производительность почти не будет зависеть от объема. Хотя сохранение этой воли, но меньше, чем сортировка всей таблицы для каждого запроса.

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

Ответ 7

Невероятно, ни один другой ответ не упомянул самый быстрый способ сделать подкачку во всех версиях SQL Server, особенно в отношении вопроса OP, где смещения могут быть ужасно медленными для больших номеров страниц, как сравнивается здесь.

Существует совершенно другой, гораздо более быстрый способ выполнения подкачки в SQL. Это часто называют "методом поиска", как описано в этот пост в блоге здесь.

SELECT TOP 10 *
FROM Orders
WHERE OrderDate >= '1980-01-01'
AND ((OrderDate > @previousOrderDate)
  OR (OrderDate = @previousOrderDate AND OrderId > @previousOrderId))
ORDER BY OrderDate ASC, OrderId ASC

Значения @previousOrderDate и @previousOrderId являются соответствующими значениями последней записи с предыдущей страницы. Это позволяет вам получать "следующую" страницу. Если направление ORDER BY DESC, просто используйте <.

С помощью вышеуказанного метода вы не можете сразу перейти на страницу 4, не предварительно извлек предыдущие 40 записей. Но часто вы не хотите так далеко прыгать. Вместо этого вы получаете гораздо более быстрый запрос, который мог бы получать данные в постоянное время, в зависимости от вашей индексации. Кроме того, ваши страницы остаются "стабильными", независимо от того, изменяются ли базовые данные (например, на странице 1, когда вы находитесь на странице 4).

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

Обратите внимание, что "метод поиска" также называется подкачкой набора ключей.