API
Pagination: миграция offset-ленты на keyset
Прочитать, что offset умирает на глубине, не то же самое, что увидеть, как страница 10000 уходит в таймаут и как пользователь получает один и тот же пост дважды. Соберите сломанную версию, воспроизведите оба отказа с доказательствами, затем мигрируйте на keyset cursor API и докажите каждое исправление измерениями.
Превратите ментальную модель юнита в воспроизводимый инженерный цикл: засейте большую таблицу, продемонстрируйте стоимость offset на глубине и дрейф при записи, спроектируйте корректный keyset cursor (уникальный ключ, совпадающий индекс, opaque-кодирование, n+1 hasNextPage) и проверьте плоскую задержку и нулевой дрейф при конкурентных записях.
Возьмите list-эндпоинт на большой таблице, продемонстрируйте, что offset pagination и медленна на глубине, и некорректна при конкурентных записях, затем мигрируйте на keyset cursor API, остающийся плоским и стабильным — доказывая каждое утверждение измерениями до/после, а не словами.
- Таблица до/после: задержка страниц 1, 1k, 10k для offset против keyset плюс 'actual rows', прочитанные на каждой глубине — измеренные под EXPLAIN ANALYZE, а не оценённые.
- Задержка keyset плоская на всех глубинах (в пределах нормального разброса), а использование составного индекса подтверждено (Index Scan, без лишней сортировки) в плане.
- Тест конкурентности показывает задокументированный дубликат/пропуск под offset и ноль дубликатов или пропусков под keyset при той же нагрузке вставок/удалений.
- Короткое описание: какой уникальный ключ сортировки выбран и почему, как кодируется/декодируется opaque cursor, как выводится hasNextPage и что заменило точный подсчёт и почему.
- Добавьте двунаправленную пагинацию (before/after, first/last) по-Relay: переверните оператор сравнения и направление ORDER BY, затем разверните возвращаемый срез, чтобы клиент видел стабильный порядок. Проверьте, что движение назад возвращает ровно страницы вперёд в обратном порядке.
- Добавьте offset-поверхность рядом с keyset для ограниченной админской таблицы, которой действительно нужен 'переход на страницу N', и задокументируйте, почему две стратегии сосуществуют для тех же данных.
- Добавьте защиту от подмены cursor: подписывайте или валидируйте opaque cursor, чтобы рукотворный cursor не уходил в дорогие диапазоны seek, и покажите, что некорректный cursor отвергается чисто, а не сканирует.
- Повторите эксперимент на не-временной сортировке (например, ORDER BY score DESC, id DESC, где score сильно совпадают) и покажите, что tiebreaker предотвращает дубликаты на стыке, которые дал бы только score.
Это миграция, которую вы будете выполнять на реальных list-эндпоинтах: сначала докажите отказы offset — стоимость скана глубокой страницы из плана и дрейф дубликат/пропуск при конкурентных записях — затем замените позицию на значение. Уникальный индексированный кортеж (created_at, id), opaque cursor, n+1 зонд hasNextPage и убранный или оценённый подсчёт превращают ленту, умирающую на глубине, в ленту, остающуюся плоской и корректной. Сделав это раз на засеянной таблице, вы переводите продакшен-версию в мышечную память — и понимаете, когда offset всё ещё верный инструмент.