Как реализовать эффективную пагинацию с курсорами и индексами
Что проверяют
Понимаете ли вы, почему OFFSET замедляется на глубоких страницах, и умеете ли построить keyset-пагинацию так, чтобы её реально обслуживал индекс. Следом обычно просят показать курсор для сортировки по неуникальному полю, например по дате.
Как отвечать
С LIMIT 20 OFFSET 100000 база всё равно читает и выбрасывает сто тысяч строк, а при вставках между запросами записи на границе страниц дублируются или пропадают. Keyset-пагинация продолжает с последнего увиденного ключа:
- уникальный ключ сортировки: если сортируете по
created_at, добавьтеidвторым ключом. Запрос в PostgreSQL:WHERE (created_at, id) < ($1, $2) ORDER BY created_at DESC, id DESC LIMIT $3; - индекс под запрос: составной по
(created_at, id), в том же порядке, что иORDER BY. Тогда в планеIndex Scanбез отдельной сортировки, и глубина страницы на скорость не влияет. Проверяйте черезEXPLAIN ANALYZE; - непрозрачный курсор: клиент получает
next_cursor, например base64 от JSON сcreated_atиid. Его можно подписать HMAC, чтобы клиент не подсовывал произвольные значения; - признак следующей страницы: запрашивайте
limit + 1строку. Лишняя строка говорит, что продолжение есть, без отдельногоCOUNT(*).
На Go это обычный запрос через pgx (часто pgxpool) или database/sql. Обязательные мелочи: defer rows.Close(), проверка rows.Err() после цикла и context с таймаутом. Для сканирования в структуры в pgx удобны pgx.CollectRows и pgx.RowToStructByName.
Назовите и цену: к произвольной странице номер 57 не перейти, только вперёд и назад, а общее количество считается отдельно и дорого, поэтому его часто показывают приблизительно или не показывают вовсе.
Где спотыкаются
Строят курсор только по created_at. Строки с одинаковым временем на границе страницы теряются или повторяются.
Путают курсор в API с серверным курсором базы (DECLARE ... CURSOR). Серверный курсор держит транзакцию и соединение из пула, для HTTP-пагинации он не подходит.
Забывают rows.Err(). Ошибка посреди выборки тогда выглядит как короткая последняя страница, и клиент молча получает неполные данные.