Eloquent: Оптимизация подсчёта Моделей по Отношениям

Источник: «Eloquent: Count Models by Relations - Two Performance Optimizations»
При подсчёте записей Модели, сгруппированных по типу в отношении, возникает соблазн загрузить в память слишком много запросов к БД или слишком много данных. Есть несколько способов оптимизировать его, давайте рассмотрим пример.

Допустим, у вас есть отношение User -> manyToMany -> Role, и вам нужно подсчитать количество пользователей на роль.

Самый простой (и худший) способ:

use App\Models\Role;
use App\Models\User;

class UserController extends Controller
{
public function index()
{
return [
'administrators' => User::whereHas('roles',
fn($query) => $query->where('id', 1))->count(),
'editors' => User::whereHas('roles',
fn($query) => $query->where('id', 2))->count(),
'viewers' => User::whereHas('roles',
fn($query) => $query->where('id', 3))->count(),
];
}
}

Это вызовет три запроса к базе данных, просматривая один и тот же список пользователей, фильтруя его по разным критериям.

Оптимизация 1. Загрузите все данные и отфильтруйте коллекцию

Если загрузить всех пользователей одним запросом к базе данных, то всякий раз, когда нужно подсчитать количество больше не нужно обращаться к базе данных:

$users = User::with('roles')->get();

return [
'admins' => $users->filter(fn ($user) => $user->roles->contains('id', 1))->count(),
'editors' => $users->filter(fn ($user) => $user->roles->contains('id', 2))->count(),
'viewers' => $users->filter(fn ($user) => $user->roles->contains('id', 3))->count(),
];

Это вызовет один запрос к базе данных вместо трёх.

Но у него есть обратная сторона: вы загружаете всех пользователей в память. Если у вас много пользователей, например 100000+, производительность может быть ещё хуже, даже при меньшем количестве запросов. Поэтому предлагаю использовать этот метод только в случае меньшего объёма данных.

Оптимизация 2. Инверсия того, что вам действительно нужно

Если нужны только count() с отношениями, нужно подсчитать отношения, основная Модель даже не нужна.

Итак, вместо загрузки всех User с отношениями, нужно загрузить Role с количеством User.

$roles = Role::withCount('users')->get()->keyBy('id');

return [
'admins' => $roles[1]->users_count,
'editors' => $roles[2]->users_count,
'viewers' => $roles[3]->users_count,
];

Это вызовет только один запрос к базе данных, не загружая всех пользователей и вернёт только то, что действительно нужно.

Дополнительные материалы

Предыдущая Статья

Как предотвратить XSS атаку. Два уровня защиты

Следующая Статья

Laravel: Как тестировать invokable правила