Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Як реалізувати keyset-пагінацію для великих таблиць і чому загальна кількість записів дорога?

Keyset-пагінація (курсорна, «seek method») продовжує вибірку з місця, де закінчилась попередня сторінка, умовою за ключем сортування, а не пропуском N рядків:

-- перша сторінка
SELECT id, title, published_at FROM posts
ORDER BY published_at DESC, id DESC
LIMIT 20;

-- наступна: після останнього елемента (published_at = '2026-09-30 10:00', id = 1057)
SELECT id, title, published_at FROM posts
WHERE (published_at, id) < ('2026-09-30 10:00', 1057)
ORDER BY published_at DESC, id DESC
LIMIT 20;

Ключові деталі:

  • унікальність порядку: сортування лише за published_at неоднозначне - у кількох постів однаковий час, і на межі сторінок записи загубляться чи задвояться. До ключа додають унікальну колонку (id) як «розв'язувач»;
  • індекс під сортування: (published_at DESC, id DESC) - тоді кожна сторінка - короткий пошук в індексі незалежно від глибини;
  • порівняння кортежів (a, b) < (x, y) підтримують PostgreSQL і MySQL, але оптимізатор MySQL не завжди використовує для нього індекс - інколи надійніше розгорнута умова a < x OR (a = x AND b < y);
  • NULL у ключі сортування ламає порівняння - такі колонки або виключають, або обробляють окремо;
  • курсор непрозорий для клієнта: значення кодують (base64 JSON) і, бажано, підписують, щоб клієнт не підставляв довільні значення. Laravel cursorPaginate() робить це сам.

Чому total дорогий. COUNT(*) з тими самими фільтрами - окремий запит, що проходить усі відповідні рядки. На таблиці з мільйонами записів і складними фільтрами він може коштувати більше, ніж сама сторінка, - і виконується на кожну сторінку. У PostgreSQL через MVCC навіть COUNT(*) без умов читає всю таблицю чи індекс.

Що робити замість точного total:

  • не показувати - лише «далі» (has_more), як у стрічках і більшості великих API;
  • рахувати до межі: SELECT COUNT(*) FROM (SELECT 1 ... LIMIT 10001) і показувати «10 000+»;
  • приблизна кількість зі статистики бази (reltuples у PostgreSQL, EXPLAIN) - для оцінок «близько 2,3 млн»;
  • кешувати підрахунок на хвилини, якщо точність до запису не потрібна.

Обмеження keyset: немає переходу на довільну сторінку і зручного «сторінка 5 з 120». Для адмінок це інколи прийнятний компроміс з simplePaginate, для API з великими обсягами - стандарт.

Докладніше в документації: No Offset: пагінація без зміщення

Перевір себе

20 випадкових питань за спробу, після завершення - розбір кожної помилки

Схожі питання