Получить доступ

Лента с бесконечной прокруткой использует LIMIT 20 OFFSET N. С ростом N страницы отдаются всё медленнее, а пользователи жалуются на дубли. В чём причина и что делать?

MiddleПроектирование API
Ещё спрашивают так:
  • Почему OFFSET медленный на больших страницах?
  • Что такое keyset-пагинация?
  • Как сделать бесконечную ленту без дублей?
Коротко

OFFSET заставляет базу пройти и отбросить N строк — чем глубже страница, тем медленнее; а вставки между запросами сдвигают окно — отсюда дубли и пропуски. Решение — keyset (курсор): «дай 20 после последнего показанного».

  • WHERE (created_at, id) < (:ts, :id) ORDER BY created_at DESC, id DESC LIMIT 20
  • Стоимость не зависит от глубины: индекс сразу находит позицию
  • Ключ сортировки уникален — id как тайбрейкер

А ещё Цена: нельзя прыгнуть на страницу 57; курсор отдают клиенту непрозрачной строкой

Так подсказку видит пользователь sotto на живом собеседовании — за пару секунд, пока вы думаете вслух.

Подробно

Два независимых дефекта 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 быстрым.
  • Сортировать курсор только по времени без тайбрейкера.

Проверьте себя

Один верный вариант. Подумайте, потом откройте ответ.

Показать варианты и ответ
  • ✓База отбрасывает все пропускаемые строки, а вставки сдвигают выдачу; нужна курсорная (keyset) пагинация по составному ключу
  • ✗Не хватает индекса; после его создания offset начнёт работать за константу
  • ✗Проблема в кэшировании ответов; нужно добавить ETag
  • ✗Нужно увеличить размер страницы, чтобы уменьшить число запросов

Что спросят дальше

  • Почему курсор отдают клиенту непрозрачной строкой?
  • Когда offset всё-таки допустим?

Разбор темы

Связанные вопросы

Закрытая бета

Подсказки на собеседовании — в реальном времени

sotto слышит вопрос интервьюера и показывает короткий ответ и план, как в блоке «Коротко». Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем