Programming

Пагинация

Пагинация — разбиение большого набора данных на страницы фиксированного размера, чтобы отдавать клиенту не весь список сразу, а порциями.

Что такое Пагинация

Пагинация (от лат. 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-сервер