Пагинация через
LIMIT / OFFSET есть почти везде — и работает отлично ровно до тех пор, пока юзер не улистал далекоПервая страница летает, а где-нибудь на 500-й всё еле ползёт
Хотя отдаёшь ты те же 20 строк
Как сделать, чтобы скорость не зависела от номера страницы
🐌 Что не так с OFFSET:
SELECT * FROM posts
ORDER BY created_at DESC
LIMIT 20 OFFSET 100000;
Ты думаешь «дай мне 20 штук со сдвигом»
А база понимает это буквально: она проходит первые 100 000 строк, отсчитывает их одну за другой, выбрасывает — и только потом отдаёт следующие 20
То есть чем дальше страница, тем больше строк она перелопачивает впустую
На первой странице OFFSET 0, летает
На тысячной — прогоняет сотню тысяч строк ради двадцати
🚀 Keyset-пагинация
Идея простая: не считать сдвиг, а запоминать, на чём остановились, и просить «дай мне то, что идёт после вот этого»
SELECT * FROM posts
WHERE created_at < :last_seen_created_at
ORDER BY created_at DESC
LIMIT 20;
Здесь база не отсчитывает ничего — она по индексу сразу прыгает в нужное место и берёт двадцать штук
Скорость одинаковая что на первой странице, что на миллионной
Ты просто передаёшь с каждой страницей «якорь» последней строки (обычно id или дату), а на следующий запрос отдаёшь его обратно
⚖️ В чём подвох Keyset не панацея, у него есть ограничение: нельзя прыгнуть сразу на «страницу 500»
Ты можешь идти только вперёд и назад, от текущего места
Для нумерации страниц 1-2-3...500 это не годится
Но честно — а где тебе реально нужны номера страниц?
В бесконечной ленте, подгрузке по скроллу, выгрузке данных пачками, API с курсором — везде листают последовательно, и keyset там идеален
А классический OFFSET оставь для админок и мест, где страниц пара десятков и на скорость плевать
🎭 Бонусная беда OFFSET — он ещё и врёт
Про скорость понятно, но есть второй, менее очевидный косяк
Пока юзер листает, в таблицу сыплются новые записи
Добавили пару строк в начало (а сортировка-то по свежести) — и весь список поехал на две позиции вниз
В итоге на второй странице юзер снова видит записи, которые уже пролистал на первой
Классика лент, где при скролле «мелькают одинаковые посты»
Keyset этим не болеет: якорь привязан к конкретной строке, а не к порядковому номеру, поэтому новые вставки список не сдвигают
И маленький нюанс по самому keyset: сортируй по чему-то уникальному
Если у двух строк одинаковая дата, а якорь только по дате — на границе страниц можно потерять или задублировать запись
С уникальным ключом такого не будет
💡 Про счётчик «страница 3 из 500»
Частый вопрос: а как же показать общее число страниц, если мы листаем по Keyset?
На больших таблицах точный total ты и с OFFSET безболезненно не получишь,
COUNT(*) по миллионам строк сам по себе тормозитПоэтому в больших лентах его обычно и не показывают: либо примерное число («около 10k+»), либо просто кнопка «ещё»
Если точный счётчик реально нужен — это отдельная история с кэшированием, а не то, ради чего стоит держать медленный OFFSET
А вы как реализуете большие списки? 🤔
#database #postgres #backend #performance #sql #dev