Разбиение страницы в 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).
Это лучший способ реализовать пейджинг при ленивой загрузке большего количества данных в веб-приложениях, например.
Обратите внимание, что "метод поиска" также называется подкачкой набора ключей.