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

Як пагінувати запит з GROUP BY чи складними умовами, коли підрахунок загальної кількості дорогий або неправильний?

paginate() має порахувати, скільки рядків поверне запит. Для простого запиту це select count(*) from ..., але зі складними запитами виникають дві проблеми.

1. GROUP BY чи HAVING. count(*) над запитом з групуванням повертає кількість у кожній групі, а не кількість груп. Laravel це враховує: якщо в запиті є groupBy чи having, він обгортає запит у підзапит:

select count(*) as aggregate from (
    select customer_id, sum(total) from orders group by customer_id
) as aggregate_table

Результат правильний, але база виконує весь запит з групуванням лише для підрахунку - на великих даних це повільно.

2. DISTINCT, об'єднання з дублями, UNION - підрахунок або дорогий, або без уважності дає неправильне число.

Варіанти розв'язання:

Не рахувати взагалі - simplePaginate() чи cursorPaginate(). Для більшості стрічок і звітів кнопок «далі/назад» достатньо.

Передати кількість самому - paginate() приймає параметр total:

$total = Cache::remember('reports:customers:count', 300, fn () => DB::table('orders')->distinct()->count('customer_id'));

$customers = DB::table('orders')
    ->select('customer_id', DB::raw('sum(total) as revenue'))
    ->groupBy('customer_id')
    ->orderByDesc('revenue')
    ->paginate(perPage: 50, total: $total);

Кількість груп можна порахувати простішим запитом (count(distinct customer_id)), закешувати чи взяти з денормалізованого лічильника.

Приблизна кількість: для «приблизно 1,2 млн результатів» - оцінка з EXPLAIN чи статистики таблиці (reltuples у PostgreSQL) замість точного count.

Перенести групування в окрему таблицю: для звітів, які відкривають часто, - матеріалізоване подання чи таблиця агрегатів, що оновлюється за розкладом. Тоді пагінація йде по простій таблиці з індексом.

Обмежити глибину: пошукові системи показують лише перші сотні результатів. Обмеження на кількість сторінок прибирає і дорогий підрахунок, і повільні далекі OFFSET.

Пастки з Eloquent:

  • paginate() з select і groupBy вимагає, щоб усі неагреговані колонки були в GROUP BY (PostgreSQL і MySQL з ONLY_FULL_GROUP_BY);
  • withCount і having разом з пагінацією - перевіряйте згенерований SQL через toRawSql();
  • порядок має бути детермінованим (orderBy('revenue')->orderBy('customer_id')), інакше записи з однаковим значенням перескакують між сторінками.

Докладніше в документації: Пагінація запитів

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