Laravel дає три способи гортати вибірку, і вони по-різному поводяться на великих таблицях.
paginate() - класична пагінація з номерами сторінок:
$posts = Post::latest()->paginate(15);
Виконує два запити: сам вибір і count(*) для загальної кількості сторінок.
simplePaginate() - те саме без підрахунку загальної кількості, лише «далі/назад». Прибирає дорогий count(*) на великій таблиці.
cursorPaginate() - гортання за значенням ключа, без offset:
$posts = Post::latest()->cursorPaginate(15);
Чому offset повільний. limit 15 offset 100000 не перестрибує рядки - база читає й відкидає сто тисяч, перш ніж віддати п'ятнадцять. Що глибша сторінка, то повільніше; на останніх сторінках великого списку це секунди.
Курсорна пагінація натомість запитує «наступні 15 після цього значення»:
where (published_at, id) < (?, ?) order by published_at desc, id desc limit 15
Час не залежить від глибини - за умови, що є індекс на колонках сортування.
Друга перевага - стабільність. З offset новий запис, доданий між переглядами сторінок, зсуває всю вибірку, і один запис читач побачить двічі, а інший пропустить. Курсор до цього стійкий.
Ціна курсора: немає номерів сторінок і переходу «на сторінку 7» - лише вперед і назад. Сортування має бути за унікальною чи доповненою id комбінацією, інакше рядки з однаковим значенням загубляться.
Що обирати: адмінка з номерами сторінок - paginate(); довга стрічка чи API - cursorPaginate(); проміжний варіант, де потрібні лише «далі/назад» - simplePaginate().