Пагинация
Пагинация — разбиение большого набора данных на страницы фиксированного размера, чтобы отдавать клиенту не весь список сразу, а порциями.
Что такое Пагинация
Пагинация (от лат. pagina — страница) — разбиение большого набора данных на страницы фиксированного размера. Вместо того чтобы вернуть все два миллиона записей одним ответом, сервер отдаёт 20–50 штук и способ запросить следующую порцию.
Причина не в удобстве интерфейса, а в ресурсах. Запрос SELECT * FROM orders без лимита читает всю таблицу, тянет её в память приложения и раздувает latency до секунд. Один такой эндпоинт под нагрузкой кладёт и базу, и сервис.
Простыми словами
Книгу на 900 страниц не выдают читателю одним листом — её листают. Пагинация делает то же самое со списком: клиент просит «20 записей начиная с 41-й», сервер отдаёт ровно их.
Offset-пагинация
Классика — LIMIT и OFFSET:
-- страница 3 при размере страницы 20
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
LIMIT 20 OFFSET 40;
В URL это обычно query-параметры: /api/articles?page=3&per_page=20.
Просто, позволяет прыгнуть на произвольную страницу — и разваливается на глубине. OFFSET 100000 заставляет СУБД прочитать и выбросить сто тысяч строк, прежде чем начать отдавать нужные: время растёт линейно с номером страницы.
Вторая проблема — сдвиг данных. Если между запросом страницы 1 и страницы 2 вставили новую запись, одна строка уедет с первой страницы на вторую и покажется дважды.
Cursor-пагинация (keyset)
Вместо «пропустить N строк» — «дай строки после вот этой»:
SELECT id, title, created_at
FROM articles
WHERE (created_at, id) < ('2026-05-01 12:00:00', 8814)
ORDER BY created_at DESC, id DESC
LIMIT 20;
Курсор — закодированное значение последней отданной строки, обычно base64 от пары «поле сортировки + id». База по индексу сразу прыгает в нужное место, поэтому время выборки не зависит от номера страницы. Дублей при вставках тоже нет: позиция привязана к данным, а не к счётчику.
Сравнение подходов
| Offset/limit | Cursor/keyset | |
|---|---|---|
| Прыжок на страницу N | Да | Нет, только вперёд/назад |
| Скорость на глубине | Падает линейно | Постоянная |
| Дубли при вставках | Есть | Нет |
| Общее число страниц | Легко через COUNT |
Дорого или никак |
| Сложность реализации | Минимальная | Средняя |
Правило: админка со списком на 500 строк — offset. Лента, выгрузка, публичное API с миллионами записей — cursor.
Формат ответа API
{
"data": [ { "id": 8814, "title": "Как мы чинили индексы" } ],
"meta": { "per_page": 20, "has_more": true },
"links": { "next": "/api/articles?after=eyJpZCI6ODgxNH0" }
}
Отдавайте next готовой ссылкой: клиенту не придётся собирать курсор самому, а вы сможете поменять его формат, не ломая клиентов.
Что не забыть
- Жёсткий потолок
per_page.?per_page=1000000— это готовый способ уронить сервис. Ограничьте сотней и обрежьте на сервере, а не в UI. - Индекс под сортировку. Без индекса по полю из
ORDER BYбаза отсортирует таблицу целиком на каждый запрос — пагинация не поможет. - Детерминированный порядок.
ORDER BY created_atпри совпадающих датах даёт нестабильную сортировку и те же дубли. Добавляйтеidвторым ключом. COUNT(*)дорог. На больших таблицах точное число страниц стоит дороже самой выборки. Часто достаточно флагаhas_more.
Связанные термины
- SQL — где живут LIMIT и OFFSET
- Индексы в БД — без них пагинация на глубине не работает
- REST API — основной потребитель пагинации
- URI и query-параметры — как передают page и cursor
- latency — то, что пагинация удерживает в норме
- PostgreSQL — типичная база, где offset упирается в глубину
Готовы запустить GPU-задачу?
Запустить GPU-сервер