Кейс: пагинация OFFSET 1 000 000 положила API

Листинг товаров отдавал страницы через LIMIT 20 OFFSET n. На первых страницах всё летало - продакшен работал месяцами без единой жалобы, пока бот не начал перебирать каталог с конца.

Postgres не умеет «перепрыгнуть» к нужной строке - для OFFSET 1000000 движок должен прочитать и отбросить миллион строк перед тем, как отдать нужные двадцать. Каждый следующий запрос стоил дороже предыдущего, а индекс на сортировку не спасал: он ускоряет ORDER BY, а не пропуск строк.

Починили keyset-пагинацией: вместо OFFSET передают последний увиденный id или created_at из предыдущей страницы, и WHERE id > :lastId ORDER BY id LIMIT 20 читает ровно двадцать строк независимо от того, какая это страница по счёту.

SELECT * FROM products WHERE id > :lastId ORDER BY id LIMIT 20;

Что спросят следом: как отдавать «страница 500 из 1000» пользователю, если keyset не даёт перескочить на произвольную страницу? Обычно отвечают, что для UI с номерами страниц это осознанный компромисс - либо приблизительный подсчёт total через отдельный дешёвый запрос, либо интерфейс без номеров страниц, только «далее».

Финал: OFFSET дешёвый в коде и дорогой в проде - его стоимость растёт линейно с номером страницы, а не с размером страницы.

Тренажёр: 600 вопросов, мок с таймером, план повторов

senior·base — что спрашивают на самом деле


В этом посте были ссылки, но мы их удалили по правилам Сетки