Пагінація зі зміщенням (offset) - номер сторінки:
GET /api/posts?page=3&per_page=20
SELECT * FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 40;
Курсорна пагінація - «продовжити після цього запису»:
GET /api/posts?cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wOS0zMCJ9
SELECT * FROM posts WHERE created_at < '2026-09-30 12:00:00' ORDER BY created_at DESC LIMIT 20;
Курсор - закодоване значення ключа сортування останнього елемента попередньої сторінки.
Порівняння:
| Зміщення | Курсор | |
|---|---|---|
| перехід на довільну сторінку | так | ні, лише вперед/назад |
| загальна кількість сторінок | так (потрібен COUNT(*)) |
зазвичай ні |
| швидкість на далеких сторінках | падає: база читає й відкидає OFFSET рядків |
стабільна: пошук за індексом |
| нові записи під час гортання | дублікати й пропуски | коректно |
Проблема зміщення з даними, що змінюються: поки користувач на сторінці 2, з'явилося 5 нових постів. Сторінка 3 тепер починається на 5 записів «раніше» - частину постів він побачить двічі. Курсор прив'язаний до значення, а не до позиції, і такої проблеми не має.
У Laravel:
Post::latest()->paginate(20); // зміщення + COUNT(*), номери сторінок
Post::latest()->simplePaginate(20); // зміщення без COUNT(*), лише «далі/назад»
Post::latest()->cursorPaginate(20); // курсор
cursorPaginate вимагає, щоб сортування було за унікальною комбінацією колонок (зазвичай додають id як останній ключ) і щоб для неї був індекс.
Що обрати:
- адмінки, таблиці з переходом на сторінку N, невеликі обсяги - зміщення;
- стрічки, нескінченний скрол, мобільні застосунки, синхронізація й експорт великих обсягів - курсор.
У відповіді API варто віддавати посилання на наступну сторінку (links.next чи заголовок Link), а не змушувати клієнта збирати URL самостійно - тоді механізм пагінації можна змінити без зміни клієнтів.