Практическая ситуация: записи добавляются во время постраничной выдачи. Как спроектировать переход между страницами, чтобы элемент не пропускался?
Используйте курсорную пагинацию на основе стабильного порядка, а не смещения по номеру страницы. Курсор должен содержать значения последнего элемента предыдущей страницы, а сортировка — опираться на неизменяемое поле и уникальный дополнительный ключ.
Например, записи можно упорядочить по времени создания и идентификатору. Если требуется полностью неизменный результат во всех запросах, одного курсора недостаточно — нужен согласованный снимок данных или явно зафиксированная версия набора.
Смещение, или offset-пагинация, удобно для простых списков: клиент передаёт номер страницы и размер страницы. Однако при добавлении или удалении записей между запросами границы страниц сдвигаются.
Из-за этого один элемент может попасть на две страницы или быть пропущен. Кроме того, получение больших смещений часто требует обработать множество предшествующих строк, что ухудшает производительность.
Курсорная пагинация появилась как способ продолжать выборку относительно конкретной позиции в упорядоченном наборе, а не относительно его текущего размера.
Пусть клиент получил первую страницу списка, а затем между запросами добавилась новая запись, расположенная в начале списка. При использовании смещения следующая страница будет вычислена уже по изменённому набору, поэтому часть ранее не показанных записей может сместиться за границу выдачи.
Неверное решение приводит к неполному экспорту, повторной обработке объектов или расхождениям между интерфейсом и сервером. Риск особенно важен для синхронизаций, массового импорта и фоновых обработчиков, где пропуск элемента может долго оставаться незамеченным.
Сервер выбирает устойчивый порядок, например по возрастанию времени создания и идентификатора. Идентификатор нужен как детерминирующий дополнительный ключ, потому что время создания может совпадать у нескольких записей.
В ответе сервер возвращает элементы и непрозрачный следующий курсор. Курсор кодирует позицию последнего элемента, но клиент не должен зависеть от его внутреннего формата. Следующий запрос означает: вернуть записи после этой позиции в том же порядке.
Ключевые условия:
Если новые записи добавляются после уже пройденной позиции, они не попадут в текущий обход. Это обычно желательное поведение: клиент получает последовательный проход по исходной области данных. Записи, удалённые после выдачи, могут исчезнуть из дальнейших результатов, поэтому курсор сам по себе не гарантирует снимок на время всей операции.
Для строгой воспроизводимости нужен снимок данных: сервер фиксирует версию или границу набора и использует её во всех запросах. Такой подход повышает согласованность, но усложняет хранение снимков, увеличивает нагрузку или требует поддержки версий чтения на уровне хранилища.
Смещение проще реализовать и оно позволяет перейти сразу к условной странице, но хуже подходит для изменяющихся больших наборов. Курсор устойчивее и эффективнее для последовательного обхода, однако усложняет отладку, произвольную навигацию и поддержку закладок на конкретную страницу.
Сервис выгружает несколько миллионов заказов для сверки с внешней системой. Список сортируется по времени создания, а новые заказы постоянно появляются.
Рассматривались три варианта. Offset-пагинация была простой, но при добавлении заказов могла пропускать записи. Курсор только по времени создания был эффективнее, но заказы с одинаковым временем могли обрабатываться нестабильно. Полный снимок всей таблицы обеспечивал бы воспроизводимость, однако требовал бы значительных ресурсов и усложнял эксплуатацию.
Выбрали курсор по паре время создания плюс уникальный идентификатор и зафиксировали верхнюю границу времени начала выгрузки. Записи, созданные после этой границы, переходили в следующую выгрузку. Это обеспечило последовательный обход без пропусков в пределах операции, сохранило умеренную нагрузку и не потребовало длительного снимка всей таблицы.
Почему одного времени создания недостаточно для курсора?
Несколько записей могут иметь одинаковое значение времени из-за точности часов, особенностей хранилища или пакетной записи. Если продолжение выборки сравнивает только время, часть записей с тем же значением может быть пропущена или повторно включена.
Поэтому нужен уникальный дополнительный ключ. Пара полей должна задавать полный порядок, например время создания и идентификатор; сравнение следующей страницы выполняется лексикографически относительно всей пары.
Гарантирует ли курсор отсутствие дублей при изменении сортируемого поля?
Нет. Если запись уже выдана, а затем её поле сортировки изменилось, она может переместиться относительно сохранённой позиции и появиться повторно либо быть пропущенной. Курсорная схема предполагает, что поля порядка стабильны на протяжении обхода.
Практические варианты — сортировать по неизменяемым полям, запретить изменение таких полей после создания или выполнять чтение в рамках снимка. Дополнительно потребитель может делать обработку идемпотентной, но это снижает последствия дублей, а не устраняет причину.
Когда курсорная пагинация не решает задачу полной выгрузки?
Она задаёт устойчивое продолжение относительно позиции, но не обязательно фиксирует состав данных. Удалённая запись может исчезнуть, а изменённая — переместиться; значит, результат не всегда будет снимком состояния на момент первого запроса.
Если требуется точная репликация или аудит, нужно зафиксировать границу или версию данных и применять её во всех страницах. Если хранилище не поддерживает подходящий снимок, можно сохранить в начале операции список идентификаторов, но это увеличит объём хранения и стоимость подготовки. Выбор зависит от требований к полноте, длительности операции и допустимой нагрузке.