Лента с бесконечной прокруткой использует LIMIT 20 OFFSET N. С ростом N страницы отдаются всё медленнее, а пользователи жалуются на дубли. В чём причина и что делать?
- Почему OFFSET медленный на больших страницах?
- Что такое keyset-пагинация?
- Как сделать бесконечную ленту без дублей?
OFFSET заставляет базу пройти и отбросить N строк — чем глубже страница, тем медленнее; а вставки между запросами сдвигают окно — отсюда дубли и пропуски. Решение — keyset (курсор): «дай 20 после последнего показанного».
WHERE (created_at, id) < (:ts, :id) ORDER BY created_at DESC, id DESC LIMIT 20- Стоимость не зависит от глубины: индекс сразу находит позицию
- Ключ сортировки уникален — id как тайбрейкер
А ещё Цена: нельзя прыгнуть на страницу 57; курсор отдают клиенту непрозрачной строкой
Подробно
Два независимых дефекта OFFSET:
- производительность —
OFFSET 100000 LIMIT 20читает 100 020 строк по индексу и выбрасывает 100 000. Индекс не спасает: строки всё равно нужно пройти. Глубокие страницы (и боты, которые до них доходят) нагружают базу; - корректность — пока пользователь листает, в начало ленты добавляются записи. Окно сдвигается: следующая страница начинается с элементов, которые уже были показаны (дубли), а при удалениях — элементы пропускаются.
Keyset-пагинация запоминает позицию последнего показанного элемента и просит следующие после неё. С составным индексом (created_at, id) база находит позицию за O(log n) и читает ровно 20 строк — на любой глубине. Вставки в начало не влияют: «после элемента X» не зависит от того, сколько элементов появилось перед ним.
Детали:
- ключ сортировки должен быть уникальным — иначе при одинаковом времени элементы теряются или дублируются; добавляют
id; - сравнение кортежей
(a, b) < (x, y)поддерживается в PostgreSQL и использует составной индекс; - курсор кодируют в непрозрачную строку (base64 от значений), чтобы клиент не зависел от формата и его можно было менять;
- направление «назад» — обратный порядок и разворот результата.
Когда OFFSET допустим: маленькие таблицы, админки с переходом на номер страницы, где точность не важна, или при ограничении глубины.
Что хотят услышать
- Обе причины: линейная стоимость и сдвиг окна при вставках.
- Keyset-пагинация с запросом и составным индексом.
- Уникальный тайбрейкер и непрозрачный курсор.
- Ограничение: нет перехода на произвольную страницу.
Частые ошибки
- Думать, что индекс по created_at делает OFFSET быстрым.
- Сортировать курсор только по времени без тайбрейкера.
Проверьте себя
Один верный вариант. Подумайте, потом откройте ответ.
Показать варианты и ответ
- ✓OFFSET отбрасывает пропущенные строки, вставки сдвигают выдачу; нужна keyset-пагинация
- ✗Проблема в кэшировании ответов; нужно добавить ETag
- ✗Не хватает индекса по сортировке; после его создания OFFSET будет работать за константное время
- ✗Нужно увеличить размер страницы, чтобы уменьшить число запросов
Что спросят дальше
- Почему курсор отдают клиенту непрозрачной строкой?
- Когда offset всё-таки допустим?
Разбор темы
- Проектирование API: REST, gRPC и события
Выбор стиля взаимодействия, семантика и идемпотентность HTTP, формат ошибок, курсорная пагинация против offset, версионирование и обратная совместимость, контрактное тестирование.
Связанные вопросы
Разбор собеседования с sotto
После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.