Короткий ответ
Offset-пагинация (LIMIT/OFFSET) проста, но даёт сдвиги и дубликаты/пропуски при параллельных вставках/удалениях и деградирует по производительности на больших смещениях; cursor-based пагинация (по последнему увиденному значению ключа) устойчива к изменениям данных между запросами страниц и быстрее на больших датасетах, но не позволяет прыгать на произвольную страницу по номеру.
Как это работает подробнее
offset-пагинация просит БД пропустить N строк и вернуть следующие M — при больших N база всё равно сканирует/сортирует пропущенные строки, что линейно растёт по стоимости. Cursor-пагинация использует значение (обычно ID или комбинацию полей сортировки) последней полученной записи как отправную точку для следующего запроса (WHERE id > :last_id ORDER BY id LIMIT :limit), что позволяет БД использовать индекс для быстрого поиска стартовой точки без сканирования пропущенных строк.
Пример кода
-- Offset-пагинация: стоимость растёт с ростом OFFSET
SELECT id, name FROM events ORDER BY created_at DESC LIMIT 20 OFFSET 100000;
-- Cursor-пагинация: использует индекс по (created_at, id) напрямую
SELECT id, name, created_at FROM events
WHERE (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC
LIMIT 20;Что использовать на практике
- Рекомендация. если пользователю нужно показать номера страниц и дать переход "на страницу 5" — offset удобнее и привычнее для UI, несмотря на риск нестабильности при параллельных изменениях. Для бесконечной прокрутки, API с большими объёмами данных или высокой частотой изменений (лента событий, список логов) cursor-based предпочтительнее. Выбор зависит от объёма данных, частоты изменений и требований UX — универсального правильного ответа нет.
Подводные камни
- Риски. использовать offset-пагинацию для API с высокочастотными вставками и удивляться дубликатам/пропускам записей между страницами у клиента; реализовывать cursor как просто "предыдущий offset в base64", что не решает проблему нестабильности, а лишь маскирует её; не добавлять уникальный tie-breaker (например, ID) к курсору при сортировке по неуникальному полю (дата), что может приводить к зацикливанию или пропуску записей с одинаковым значением сортировки.