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