Выбор между пагинацией со смещением (offset) и курсором (cursor) определяет стабильность работы списка при изменении данных. Смещение ломается при добавлении или удалении записей, так как позиция следующего элемента сдвигается. Курсорная пагинация использует уникальные ключи для навигации, что позволяет корректно отображать список даже при динамических изменениях базы данных между запросами.

Выбор стратегии пагинации: смещение (offset) против курсора

Стратегия выбора зависит от частоты изменений данных и требований к консистентности. Смещение работает по принципу «просто посчитай N строк после предыдущих». Это эффективно для статических отчетов, где данные не меняются в момент чтения.

Курсорная пагинация (keyset pagination) использует уникальный идентификатор последнего просмотренного элемента (например, ID или составной ключ). При запросе система возвращает следующий элемент с ID строго больше текущего. Это гарантирует, что пользователь всегда видит актуальную позицию в списке, независимо от того, были ли добавлены новые записи.

Основное различие: смещение ищет по позиции, курсор — по значению ключа. В изменяемом списке позиция нестабильна, а значение ключа уникально.

Когда смещение становится критическим фактором производительности

Смещение (OFFSET) начинает показывать признаки неэффективности при больших значениях OFFSET и частых изменениях данных. При запросе с LIMIT 10 и OFFSET 10000 база данных должна отсортировать все записи, пропустить первые 10000 и вернуть следующие 10. Это требует полного сканирования индекса или таблицы.

Проблема усугубляется при добавлении новых записей. Если между двумя запросами пользователя добавилась новая запись в начало списка, смещение вернет не те элементы, которые ожидал пользователь. Он увидит «дрейф» контента: данные, которые были на 10001-й позиции, теперь могут оказаться на 10002-й.

Для статических списков это допустимо, но для динамических (чат, лента новостей, комментарии) смещение создает ложное ощущение задержки и нестабильности интерфейса.

Как курсорная пагинация решает проблему изменения данных между запросами

Курсорная пагинация работает на уникальных ключах. При переходе на следующую страницу клиент получает ID последнего просмотренного элемента (например, user_id=500). Следующий запрос ищет элементы с user_id > 500.

Если в системе добавили нового пользователя с ID 501 после того, как пользователь посмотрел запись 500, курсорная пагинация корректно покажет его на следующей странице. Смещение же вернет бы старые данные, так как оно ориентируется на количество строк, а не на их уникальность.

Это решает проблему консистентности: пользователь всегда видит последовательность событий или записей в том порядке, в котором они произошли, без «прыжков» по контенту.

Алгоритм выбора метода под тип ваших операций чтения и записи

Используйте смещение (OFFSET), если: список статичен, данные обновляются редко (например, раз в сутки), или требуется строгое соответствие позиции данным на момент формирования отчета.

Используйте курсор (KEYSET), если: список обновляется в реальном времени (чат, уведомления, лента), важна мгновенная реакция на новые записи, или объем данных превышает 10–20 тысяч элементов.

Гибридный подход: для начальных страниц (OFFSET < 100) можно использовать смещение для простоты реализации. Для глубоких страниц (OFFSET > 100) переключаться на курсорную пагинацию, чтобы избежать падения производительности базы данных.

Технические требования к реализации стабильного курсора

Курсорная пагинация требует наличия уникального индекса по полю, используемому для сортировки (например, created_at или id). Без этого база данных не сможет эффективно отфильтровать следующий элемент.

Необходимо гарантировать моноотонность ключей. Если используется временная метка (timestamp), она должна быть уникальной до миллисекунды. В противном случае при одновременной записи двух элементов с одинаковым временем курсор может «застрять» или пропустить запись.

Рекомендуется использовать составные ключи, если одного поля недостаточно для уникальности. Например, комбинация user_id и message_id.

Ошибки при переходе от смещения к курсору

Ошибка «пустой страницы»: при использовании курсора пользователь может перейти на страницу, где нет данных, если все элементы с ключом больше последнего уже просмотрены. Это нормально для конечного списка, но требует обработки в интерфейсе.

Игнорирование моноотонности: использование поля created_at без учета микросекунд или часовых поясов приводит к пропуску записей при высокой нагрузке на запись.

Отсутствие индекса: попытка использовать курсор без соответствующего индекса приведет к полному сканированию таблицы, что убьет производительность.

Критерии оценки готовности инфраструктуры для курсорной пагинации

Проверьте наличие уникального индекса по полю сортировки. Это обязательное условие для эффективного выполнения запросов с ключами.

Оцените нагрузку на запись. Если записи создаются быстрее, чем они могут быть обработаны курсором, потребуется оптимизация логики или использование дополнительных индексов.

Убедитесь, что приложение корректно обрабатывает ситуации, когда список заканчивается. Это частая ошибка при миграции с OFFSET на KEYSET.

Вопросы и ответы

Можно ли использовать курсорную пагинацию для статических отчетов?

Да, но это избыточно. Для статических данных смещение проще в реализации и не требует уникальных индексов по времени создания.

Что делать, если поле сортировки не является уникальным?

Нужно добавить составной ключ или использовать комбинацию поля сортировки и случайного числа (random salt) для обеспечения уникальности курсора.

По теме