Какие сложности при реализации пагинации в API для большого объема данных
Что проверяют
Понимаете ли вы, что OFFSET это не «перейти на страницу», а «прочитать и выбросить». Раз объёмы большие, ждут разговор про глубокие страницы и стабильность выдачи. Следом почти всегда просят вслух построить курсор для сортировки по дате.
Как отвечать
- offset/limit:
LIMIT 20 OFFSET 100000заставляет базу отсортировать и пройти 100020 строк, чтобы вернуть двадцать. Цена растёт линейно от номера страницы, и на глубине запрос упирается в таймаут; - сдвиг данных: между запросами страниц вставили строку, и запись с первой страницы уезжает на вторую. Пользователь видит дубль, а скрипт выгрузки молча теряет элемент;
- курсорная (keyset) пагинация: вместо номера страницы условие
where (created_at, id) < (:last_created_at, :last_id) order by created_at desc, id desc limit 20. Составной индекс по тем же колонкам в том же порядке даёт постоянное время независимо от глубины; - уникальный тайбрейкер: сортировка только по
created_atрвётся на одинаковых значениях, поэтому в ключ всегда добавляетсяid; - непрозрачный курсор: отдавайте base64 от полей ключа, а не сырые значения. Так можно поменять сортировку, не ломая клиентов;
- *`COUNT()
**: полный подсчёт на десятках миллионов строк стоит дороже самой страницы. Отдавайтеhas_next(запросилиlimit + 1и проверили) или приблизительную оценку изpg_class.reltuples`.
Ограничения: потолок limit порядка 100, дефолт 20, ответ 400 на превышение. Для выгрузок отдельная ручка со стримингом, а не пять тысяч страниц по API. Курсор отдавайте явно: заголовок Link с rel="next" в REST либо pageInfo с endCursor и hasNextPage в GraphQL.
Где спотыкаются
Предлагают курсор там, где продукту нужен переход на произвольную страницу. Честный ответ: keyset даёт «дальше» и «назад», номеров страниц он не даёт, и это компромисс, а не недоделка.
Забывают про индекс. Курсорный запрос без составного индекса по колонкам сортировки ровно так же медленный, как и offset.
Не думают про фильтры и права. Курсор, выданный при одном наборе фильтров, при другом бессмыслен, а на закрытых данных курсор без привязки к пользователю это уязвимость.