➡️
Keyset-пагинация (seek-метод)
Если при OFFSET мы говорим БД:
"пропусти первые 70 000 записей и дай следующие 500",
то при Keyset говорим:
"дай следующие 500 после той записи, на которой мы закончили прошлый раз".
То есть выборка опирается на уникальное поле последнего элемент предыдущей страницы.
Упрощенный пример:
SELECT ...
FROM payments
WHERE id > 70000
ORDER BY id
LIMIT 500;
При таком запросе время обработки не увеличивается пропорционально номеру запрашиваемой страницы, потому что базе не нужно проходить записи предыдущих страниц, она сразу начинает выборку с указанной строки.
Для этого на ключевом поле (в упрощенном примере - это id) должен быть индекс. В примере предполагается, что id упорядочены.
Часто используется сортировка по дате создания и id:
SELECT ...
FROM payments
WHERE (created_at, id) < ('2026-09-08 12:30:00', 70000)
ORDER BY created_at DESC, id DESC
LIMIT 500;
Здесь для эффективной работы нужен составной индекс по
(created_at, id).
Также keyset решает проблему с дублирующимися записями в следующем ответе, которая возникает в случае вставки новых записей между запросами.
💭
Что в случае keyset пагинации передает фронтДля простого случая с сортировкой только по id:
GET /payments?limit=500&lastId=1000
Упрощенный ответ бэка:
{
"items": [...],
"lastId": 1500 //если идем по возрастанию
}K
eyset-пагинация — это способ выбрать следующую страницу.
Cursor-based пагинация — это подход к реализации API, в котором клиент получает некий курсор и возвращает его серверу.
Бэк берет нужные поля, упаковывает их в ДТО, кодирует его (например Base64URL, а при необходимости подписывает или шифрует) и отдает:
{
"items": [...],
"cursor": "eyJpZCI6ODQyMX0"
}Фронт в следующий запрос прикладывает полученный курсор:
GET /payments?limit=500&cursor=eyJpZCI6ODQyMX0
Бэк его раскодирует и действует согласно своей логике.
Это удобно тем, что бэк может поменять поля в ДТО по своему усмотрению, и это не потребует никаких доработок от фронта, ему как приходила строка, так и приходит. Правда, может понадобиться поддержать обратную совместимость со старым курсором.
С помощью keyset-пагинации нельзя реализовать переход на конкретную страницу, но зато удобно делать бесконечный скролл 📱
Плюсы:➕ Стабильная производительность даже при миллионах строк при подходящем индексе. Нет замедления по мере продвижения по списку.
➕ Нет проблем со сдвигами, если данные добавляются
➕ Удобно для бесконечного скролла
В случае OFFSET пагинации на UI часто показывают общее количество страниц. Для получения totalElements обычно выполняют дополнительный SELECT COUNT(*).
Если мы реализовали keyset-пагинацию и хотим в ответе бэка сообщить, есть ли еще данные или это конец списка, то это можно реализовать без дополнительных запросов в БД.
Просто делаем исходный запрос с LIMIT на 1 больше:
... LIMIT 501;
Если вернулась 501 строка, то отвечаем:
{
"items": [...], // тут запрошенные 500
"hasNext": true // есть еще записи, запрашивайте
}
Если вернулось меньше 501 строки, то отвечаем:
{
"items": [...], // тут запрошенные 500
"hasNext": false // больше записей нет
}
Но у keyset-пагинации есть и минусы.При произвольных сортировках преимущество keyset может сильно уменьшиться. Keyset опирается на индекс, а мы не можем создавать индексы на все комбинации полей.
Поэтому для keyset API часто заранее ограничивают допустимые варианты сортировки и создают индексы под несколько востребованных сценариев. Да и NULL в полях сортировки усложняет keyset.
Есть библиотеки, которые поддерживают keyset-пагинацию для разных языков.
Можно написать свою обертку или конкретную функцию для таких случаев.
Подытожим минусы:➖ сложнее реализация
➖ нельзя сделать переход на конкретную страницу
➖ для эффективной работы ограничивают варианты сортировки
Когда использовать Keyset-пагинацию:✅ на UI бесконечный скролл
✅ список часто пополняется новыми записями
✅ для API c большими коллекциями, где клиент последовательно выгружает данные и ему не нужен переход сразу на страницу 456