


Для последовательной загрузки записей в PostgreSQL можно использовать курсор из значений последней строки: например, пары created_at и id. Следующая страница выбирается относительно этой пары, а не через пропуск первых N строк с OFFSET. Порядок должен быть однозначным.
На Mentoring.digital разберём учебную ленту заказов. Это пример организации выборки, а не обещание одинакового ускорения на любой базе.
Пусть таблица orders содержит уникальный id, created_at типа timestamptz и customer_id. Поля сортировки не равны NULL. Для нашего контракта created_at после создания заказа не меняется. Показываем заказы одного клиента от новых к старым.
SELECT id, created_at
FROM orders
WHERE customer_id = $1
ORDER BY created_at DESC, id DESC
LIMIT 20;
Одного created_at недостаточно: несколько строк могут иметь одинаковое время. Уникальный id разрешает такие совпадения. Без второго ключа граница страницы будет неоднозначной.
SELECT id, created_at
FROM orders
WHERE customer_id = $1
AND (created_at, id) < ($2::timestamptz, $3::bigint)
ORDER BY created_at DESC, id DESC
LIMIT 20;
Параметры $2 и $3 берутся из последней строки предыдущей страницы. Сравнение пары идёт сначала по дате, затем по id при равенстве дат. Для нисходящего порядка требуется знак «меньше». Оба запроса должны использовать одинаковые фильтры и сортировку.
Передавайте timestamp без потери точности. Если API обрежет микросекунды до миллисекунд, курсор может перестать точно представлять границу. Сами значения подставляйте параметрами драйвера, не склейкой SQL-строк.
Для такой формы запроса кандидат на проверку:
CREATE INDEX orders_customer_cursor_idx
ON orders (customer_id, created_at DESC, id DESC);
Это команда для учебной базы или отдельно согласованной миграции. Наличие индекса не гарантирует его выбор планировщиком. Сравните планы и время на репрезентативных данных в тестовой среде; не называйте подход быстрее только потому, что запрос стал короче.
Курсорная схема удобна для «Показать ещё». Произвольный переход сразу к странице 700 она сама по себе не обеспечивает: для него нужна граница или другая модель навигации.
Новая запись в начале ленты не сдвигает уже переданную границу. Но удаление или изменение сортируемых полей всё равно влияет на дальнейшую выдачу. Отдельные запросы не образуют автоматически один согласованный снимок базы.
Если требуется выгрузка состояния на конкретный момент, это отдельное требование к снимку и транзакциям. Курсор также не заменяет проверку доступа: customer_id и разрешения должны проверяться на сервере при каждом запросе.
Потренировать запросы и индексы можно на курсах SQL. План выборки и требования к целостности выдачи помогут разобрать наставники по SQL.