Кейс: пагинация 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 — что спрашивают на самом деле
В этом посте были ссылки, но мы их удалили по правилам Сетки