Перейти к содержанию
Бэкенд и системы

Cursor и offset: пагинация со стабильным порядком

Пагинация задаёт границу: какие строки идут после этой страницы? Offset отвечает позицией, keyset-курсор — значением в упорядоченной последовательности. Оба подхода требуют явного порядка. Ни один сам по себе не замораживает меняющийся набор данных между отдельными HTTP-запросами.

Автор: Опубликовано

4 мин чтенияРедакционный разборОбновлено
  • Пагинация
  • SQL
  • Проектирование API
  • PostgreSQL
Главная мысль

Используйте offset для удобного перехода по номерам, если цена запроса приемлема. Составной keyset-курсор подходит для последовательного обхода; определите порядок, область токена и ограничения согласованности.

Позиция offset может сдвинуться

LIMIT 20 OFFSET 40 пропускает сорок строк и возвращает до двадцати. PostgreSQL обрабатывает пропущенные строки, поэтому глубокое смещение может стоить дорого. Всегда задавайте однозначный порядок.

В нашем примере лента содержит идентификаторы 4, 3, 2, 1 по убыванию. Первая страница возвращает 4 и 3. Если до следующего запроса появилась строка 5, OFFSET 2 вернёт 3 и 2: позиция сместилась и возник дубль. Удаление строки перед границей может, наоборот, привести к пропуску. Одинаковый размер страницы этого не исправляет.

Времени нужен дополнительный ключ

Упорядочим статьи по created_at по убыванию, затем по уникальному id по убыванию. PostgreSQL использует следующие выражения сортировки для разрешения равенства предыдущих. В нашем примере оба поля обязательны и не меняются после публикации. Курсор только со временем потеряет позицию внутри группы, опубликованной в один момент.

Для статей 106, 105 и 104 с одинаковым временем страница, заканчивающаяся на 105, должна продолжиться со 104. Поэтому курсор хранит точное время и id 105; при передаче нельзя терять точность времени.

Продолжайте после последней возвращённой строки

Выполните этот самостоятельный пример в одной сессии PostgreSQL 17. Он создаёт сорок пять статей; у 104, 105 и 106 совпадает время — полдень UTC. Запрос сравнивает поля слева направо. Обе сортировки идут по убыванию, поэтому продолжение использует знак меньше. Разные направления требуют другого предиката. Показанная граница 105 возвращает следующей статью 104.

Для страницы из двадцати строк получите двадцать одну, чтобы обнаружить продолжение. Верните двадцать и создайте курсор по двадцатой возвращённой строке, а не дополнительной. Первая страница использует тот же порядок без условия курсора. Дополнительная строка нужна только для признака следующей страницы.

CREATE TEMP TABLE articles (
  id bigint PRIMARY KEY,
  created_at timestamptz NOT NULL,
  title text NOT NULL
);

INSERT INTO articles
SELECT 100 + g,
       TIMESTAMPTZ '2026-10-01 12:00:00+00'
         + (((g - 1) / 3) - 1) * INTERVAL '1 second',
       'Article ' || g
FROM generate_series(1, 45) AS g;

CREATE INDEX articles_order_idx
ON articles (created_at DESC, id DESC);

PREPARE next_articles(timestamptz, bigint) AS
SELECT id, created_at, title
FROM articles
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 21;

EXECUTE next_articles('2026-10-01 12:00:00+00', 105);

Выбирайте под нужную навигацию

Наш пример курсора использует keyset-пагинацию. Непрозрачный токен может скрывать и обычный offset, поэтому слово cursor не доказывает конкретную стратегию базы. Исходите из навигации и нагрузки, затем измеряйте фактический план запроса. Для небольшого списка простое смещение часто удобнее дополнительного протокола курсоров.

Практический выбор пагинации
ПотребностьИсходный подход
Переход по номеру страницыOffset
Обход большой упорядоченной лентыKeyset-курсор
Экспорт фиксированного набораОтдельный механизм снимка

Курсор не заменяет снимок и разрешение

Новая статья перед границей keyset не сдвигает значение начала следующей страницы. Но изменение ключей сортировки или фильтров может переместить строки через границу. При Read Committed отдельные запросы видят вновь зафиксированные данные. Для неизменного экспорта нужен специально спроектированный снимок или сохранённый результат; устойчивого порядка недостаточно.

Свяжите токен с определением сортировки и фильтрами, задайте версию и проверяйте типы. Сделайте представление непрозрачным и защитите целостность, если клиент не должен менять содержимое. В каждом запросе проверяйте доступ клиента к данным. Base64 не обеспечивает ни авторизацию, ни защиту от подмены. Декодированный идентификатор не должен расширять область доступа.

Проверьте совпадения и изменения данных

Создайте сорок пять статей с повторяющимся временем. Обойдите их страницами по двадцать и сравните объединённые идентификаторы с одним полностью упорядоченным запросом. В нашей изолированной проверке на PostgreSQL 17.9 страницы содержали 20, 20 и 5 разных строк без пропущенных исходных идентификаторов. Это проверяет набор упражнения, а не любой сценарий конкурентного редактирования.

  • Добавьте новую статью между запросами и сравните offset с keyset.
  • Удалите более раннюю строку; отдельно измените ключ сортировки.
  • Отклоняйте другие фильтры, некорректные токены и запрещённые области доступа.
  • Сравните планы глубоких страниц с подходящим составным индексом.

Коротко

Частые вопросы

Keyset позволяет сразу открыть страницу 100?

Не по произвольному номеру. Нужны граничный ключ, сохранённая точка обхода или отдельный способ поиска позиции. Offset может быть удобнее для прямого перехода по номерам.

Одного уникального id достаточно для устойчивой пагинации?

Только если именно id определяет нужный порядок. При основной сортировке по времени храните в границе и время, и id, чтобы правильно обработать совпадения.

В полях курсора допустимы null?

Да, но порядок null и продолжение нужно проектировать явно. Приведённое сравнение предполагает обязательные поля; SQL-сравнения с null нельзя считать обычными сравнениями значений.

Источники

Источники и редакционная политика

Сведения проверены 4 октября 2026 г. Ссылки рядом с разделами указывают источники фактов и технических объяснений. Выводы, учебные сценарии и рекомендации по подготовке — редакционная работа RecallDeck.

От чтения к воспроизведению

Отрепетируйте полный цикл интервью.

RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.

Начать подготовку

Продолжить подготовку