При постраничной выгрузке изменяемого набора данных записи дублируются или пропускаются. Какой механизм контракта устраняет зависимость от смещения?
Используйте курсорную, или keyset-пагинацию: следующая страница определяется не номером страницы, а позицией последней прочитанной записи в стабильном порядке. Курсор должен однозначно описывать эту позицию, обычно через пару из значения сортировки и уникального идентификатора.
Если требуется согласованное содержимое всей выгрузки, одной курсорной пагинации недостаточно: контракту нужен ещё идентификатор снимка или иная гарантия фиксированного набора данных.
Классическая пагинация через offset и limit проста для небольших почти неизменяемых наборов. Однако при добавлении или удалении записей между запросами смещение начинает указывать уже на другую часть набора.
Курсорный подход появился как способ продолжать чтение относительно уже просмотренной записи, а не относительно её нестабильного номера. Это уменьшает ошибки при изменениях данных и обычно лучше масштабируется на больших наборах.
Предположим, первая страница содержит записи с позициями от 1 до 20. До запроса второй страницы перед первой добавилась новая запись: при использовании смещения 20 часть прежних записей сдвинется, поэтому одна запись может попасть на обе страницы или быть пропущена.
Удаление создаёт обратную проблему: после исчезновения записи смещение перескочит через следующую запись. Даже курсор не решает задачу автоматически, если порядок сортировки нестабилен или сортировочное поле может измениться между запросами.
Контракт должен задавать стабильную полную сортировку. Например, записи можно упорядочить по времени создания, а при совпадении времени — по уникальному идентификатору. Следующая страница выбирается после последней пары значений предыдущей страницы, а не после количества уже пропущенных строк.
Курсор обычно кодирует позицию в этом порядке и возвращается клиенту вместе с результатом. Клиент передаёт его обратно без попытки разобрать или изменить; сервер вправе подписать или зашифровать курсор, чтобы защитить его от подделки и скрыть внутреннюю структуру сортировки.
Курсор должен быть связан с теми же параметрами выборки и сортировки. Если клиент изменил фильтр, порядок, владельца данных или размер страницы, серверу следует отклонить курсор либо начать новую выдачу, иначе позиция может стать бессмысленной.
Для обычной навигации достаточно гарантии, что запись не будет повторно выдана относительно выбранного порядка. Для требования «выгрузить ровно состояние на момент начала» нужен снимок: например, токен версии данных, по которому все страницы читаются из одной логической версии.
Компромисс состоит в том, что курсорная пагинация хуже подходит для перехода сразу на произвольную страницу и усложняет отладку. Снимки дают более сильную согласованность, но требуют хранения версий или удержания снимка и могут увеличить нагрузку на хранилище.
Сервис каталога отдавал выгрузку товаров по offset. Пока менеджер выгружал несколько тысяч записей, параллельный импорт добавлял и удалял товары, из-за чего итоговый файл содержал дубликаты и пропуски.
Рассматривались три варианта. Увеличение размера страницы уменьшало число запросов, но не устраняло смещение; блокировка изменений на время выгрузки была слишком дорогой и ухудшала доступность; переход на курсорную пагинацию устранял зависимость от числа пропущенных записей, но требовал стабильной сортировки.
Выбрали курсор по неизменяемому времени создания и уникальному идентификатору, а для аудиторских выгрузок добавили токен снимка. В результате обычные клиенты перестали получать дубликаты из-за вставок, а регламентные выгрузки получили воспроизводимый набор данных. Новые записи, появившиеся после начала обычной выдачи, не гарантировались к включению — это явно зафиксировали в контракте.
1. Достаточно ли курсора без стабильной сортировки?
Нет. Если сортировка выполняется только по неуникальному или изменяемому полю, несколько записей могут иметь одинаковую позицию, а изменение этого поля переместит запись относительно курсора. Нужен детерминированный порядок с уникальным вторичным ключом, причём желательно, чтобы используемые поля не менялись во время выдачи.
2. Гарантирует ли курсор, что клиент увидит согласованный снимок данных?
Нет. Он предотвращает зависимость от смещения, но новые записи и изменения существующих могут повлиять на последующие страницы. Гарантию единого состояния даёт отдельный механизм снимка или версия чтения; это более сильное требование и более дорогая реализация.
3. Что произойдёт, если клиент повторно использует курсор после изменения фильтра или сортировки?
Курсор может указывать на позицию, которая не имеет смысла в новом запросе. Поэтому сервер должен связывать курсор с параметрами запроса и проверять их при продолжении выдачи; при несовпадении следует вернуть ошибку или потребовать начать чтение заново. Молчаливое принятие такого курсора создаёт трудно обнаружимые пропуски и дубликаты.