
本教程详细讲解如何优化 Laravel Eloquent 查询以高效生成基于关联记录计数的排行榜。通过识别并消除冗余的 whereHas 子句,并巧妙利用 withCount 的条件闭包,我们能显著提升查询性能,大幅缩短数据获取时间,从而改善用户体验并降低数据库负载。
在 laravel 应用开发中,eloquent orm 极大地简化了数据库操作。然而,当处理涉及关联模型和复杂聚合查询的场景时,如果不注意查询的编写方式,很容易导致性能瓶颈。一个典型的例子是构建用户排行榜,根据用户发布的图片数量(或其他关联记录数量)进行排名,并按不同时间维度(如本周、上周、总计)展示。本文将深入探讨如何优化这类 eloquent 查询,以显著提升数据获取效率。
原始查询的性能瓶颈分析
考虑以下 Eloquent 查询代码,其目标是获取在当前周、上周以及总计发布图片数量最多的前10名用户:
public function show(){ $currentWeek = User::whereHas('pictures') ->whereHas('pictures', fn ($q) => $q->whereBetween('created_at', [Carbon::now()->startOfWeek(), Carbon::now()->endOfWeek()])) ->withCount(['pictures' => fn ($q) => $q->whereBetween('created_at', [Carbon::now()->startOfWeek(), Carbon::now()->endOfWeek()])]) ->orderBy('pictures_count', 'DESC') ->limit(10) ->get(); $lastWeek = User::whereHas('pictures') ->whereHas('pictures', fn ($q) => $q->whereBetween('created_at', [Carbon::now()->startOfWeek()->subWeek(), Carbon::now()->endOfWeek()->subWeek()])) ->withCount(['pictures' => fn ($q) => $q->whereBetween('created_at', [Carbon::now()->startOfWeek()->subWeek(), Carbon::now()->endOfWeek()->subWeek()])]) ->orderBy('pictures_count', 'DESC') ->limit(10) ->get(); $overall = User::whereHas('pictures') ->whereHas('pictures') // 再次冗余 ->withCount('pictures') ->orderBy('pictures_count', 'DESC') ->limit(10) ->get(); return view('users.leaderboard', [ 'currentWeek' => $currentWeek, 'lastWeek' => $lastWeek, 'overall' => $overall, ]);}
上述代码在实际运行中可能耗时较长(例如1.5秒),主要原因在于其存在以下问题:
冗余的 whereHas 调用: 对于 currentWeek 和 lastWeek 的查询,whereHas(‘pictures’) 后面紧跟着一个带有条件闭包的 whereHas(‘pictures’, fn ($q) => $q->whereBetween(…))。前一个 whereHas 仅仅检查用户是否有任何图片,而后一个 whereHas 则检查用户是否有在特定日期范围内的图片。如果用户有在特定日期范围内的图片,那么他必然有图片。因此,第一个无条件的 whereHas 是多余的。whereHas 与 withCount 的重复逻辑: withCount 方法本身就可以接受一个条件闭包来计算符合条件的关联记录数量。如果某个用户在指定日期范围内没有图片,withCount 会将其 pictures_count 设置为 0。由于最终结果是按 pictures_count 降序排列,计数为 0 的用户自然会被排到末尾。这意味着,whereHas 的作用(过滤掉没有符合条件图片的用户)在大多数情况下可以通过 withCount 的结果和 orderBy 隐式实现,从而避免额外的 EXISTS 子查询。
优化步骤一:消除冗余的 whereHas 子句
首先,针对 currentWeek 和 lastWeek 的查询,我们可以移除重复且无条件的 whereHas(‘pictures’)。
优化前 SQL 示例(针对 currentWeek):
select `users`.*, ( select count(*) from `pictures` where `users`.`id` = `pictures`.`user_id` and `created_at` between ? and ? and `pictures`.`deleted_at` is null) as `pictures_count`from `users`where exists (select * from `pictures` where `users`.`id` = `pictures`.`user_id` and `pictures`.`deleted_at` is null) -- 冗余的 EXISTS and exists (select * from `pictures` where `users`.`id` = `pictures`.`user_id` and `created_at` between ? and ? and `pictures`.`deleted_at` is null) and `users`.`deleted_at` is null order by `pictures_count` desc limit 10
优化后的 Eloquent 代码片段:
$currentWeek = User::whereHas('pictures', fn ($q) => $q->whereBetween('created_at', [Carbon::now()->startOfWeek(), Carbon::now()->endOfWeek()])) ->withCount(['pictures' => fn ($q) => $q->whereBetween('created_at', [Carbon::now()->startOfWeek(), Carbon::now()->endOfWeek()])]) ->orderBy('pictures_count', 'DESC') ->limit(10) ->get();
优化后 SQL 示例:
select `users`.*, ( select count(*) from `pictures` where `users`.`id` = `pictures`.`user_id` and `created_at` between ? and ? and `pictures`.`deleted_at` is null) as `pictures_count`from `users`where exists (select * from `pictures` where `users`.`id` = `pictures`.`user_id` and `created_at` between ? and ? and `pictures`.`deleted_at` is null) -- 移除了冗余的 EXISTS 子句 and `users`.`deleted_at` is null order by `pictures_count` desc limit 10
通过这一步,我们减少了一个不必要的 EXISTS 子查询,使得 SQL 查询语句更为简洁。
优化步骤二:利用 withCount 实现过滤与计数一体化
这是性能优化的关键一步。由于 withCount 已经能够通过闭包对关联记录进行计数,并且会在没有匹配记录时返回 0,那么我们完全可以移除 whereHas。因为排行榜是根据 pictures_count 降序排列的,计数为 0 的用户自然会被排到列表末尾。如果排行榜要求不显示计数为 0 的用户,可以在获取结果后进行过滤。
最终优化后的 Eloquent 代码片段:
$currentWeek = User::withCount(['pictures' => fn ($q) => $q->whereBetween('created_at', [Carbon::now()->startOfWeek(), Carbon::now()->endOfWeek()])]) ->orderBy('pictures_count', 'DESC') ->limit(10) ->get();
最终优化后 SQL 示例:
select `users`.*, ( select count(*) from `pictures` where `users`.`id` = `pictures`.`user_id` and `created_at` between ? and ? and `pictures`.`deleted_at` is null) as `pictures_count`from `users`where `users`.`deleted_at` is null -- 彻底移除了所有的 where exists 子句 order by `pictures_count` desc limit 10
可以看到,SQL 查询中不再包含任何 EXISTS 子句,这极大地简化了数据库的执行计划,从而显著提升查询速度。
重要提示: 这种优化方式可能导致结果集中包含 pictures_count 为 0 的用户(如果他们被 limit 包含在内)。如果您的排行榜不希望显示这些用户,可以在 get() 之后使用 filter() 方法进行过滤:
$currentWeek = User::withCount(['pictures' => fn ($q) => $q->whereBetween('created_at', [Carbon::now()->startOfWeek(), Carbon::now()->endOfWeek()])]) ->orderBy('pictures_count', 'DESC') ->limit(10) ->get() ->filter(fn ($user) => $user->pictures_count > 0); // 过滤掉计数为0的用户
完整的优化方案示例
将上述优化应用到所有三个查询中,并对 Carbon 日期计算进行变量提取,避免重复计算,最终的 show 方法如下:
use CarbonCarbon;use IlluminateSupportFacadesCache; // 如果需要缓存public function show(){ // 提取日期计算,避免重复调用 $startOfCurrentWeek = Carbon::now()->startOfWeek(); $endOfCurrentWeek = Carbon::now()->endOfWeek(); $startOfLastWeek = Carbon::now()->startOfWeek()->subWeek(); $endOfLastWeek = Carbon::now()->endOfWeek()->subWeek(); // 当前周排行榜 $currentWeek = User::withCount(['pictures' => fn ($q) => $q->whereBetween('created_at', [$startOfCurrentWeek, $endOfCurrentWeek])]) ->orderBy('pictures_count', 'DESC') ->limit(10) ->get(); // 上周排行榜 $lastWeek = User::withCount(['pictures' => fn ($q) => $q->whereBetween('created_at', [$startOfLastWeek, $endOfLastWeek])]) ->orderBy('pictures_count', 'DESC') ->limit(10) ->get(); // 总排行榜 (无需日期过滤) $overall = User::withCount('pictures') ->orderBy('pictures_count', 'DESC') ->limit(10) ->get(); return view('users.leaderboard', [ 'currentWeek' => $currentWeek, 'lastWeek' => $lastWeek, 'overall' => $overall, ]);}
重要考量与建议
为了进一步提升性能和应用健壮性,以下几点至关重要:
数据库索引:确保 pictures 表的 user_id 和 created_at 字段上都有合适的索引。这是任何关联查询性能优化的基石。一个复合索引 (user_id, created_at) 通常是理想的选择,它可以加速按用户ID过滤并按创建时间范围查找的操作。
ALTER TABLE pictures ADD INDEX idx_user_id_created_at (user_id, created_at);
缓存策略:对于排行榜这类数据,其内容在短时间内不会频繁变动,但访问量可能非常大。强烈建议使用 Laravel 的缓存系统将查询结果缓存起来。例如,可以缓存几分钟或几小时,以减少对数据库的重复查询压力。
// 示例:缓存 currentWeek 数据,缓存时间为5分钟$currentWeek = Cache::remember('leaderboard_current_week', 60 * 5, function () use ($startOfCurrentWeek, $endOfCurrentWeek) { return User::withCount(['pictures' => fn ($q) => $q->whereBetween('created_at', [$startOfCurrentWeek, $endOfCurrentWeek])]) ->orderBy('pictures_count', 'DESC') ->limit(10) ->get();});
软删除 (Soft Deletes):如果 User 或 Picture 模型使用了软删除(即存在 deleted_at 字段),Eloquent 会自动在查询中加入 where deleted_at is null 条件。这通常是期望的行为,但也要注意其对索引和查询的影响。确保 deleted_at 字段也有索引可以进一步优化带软删除的查询。
总结
通过本文介绍的两个关键优化步骤——消除冗余的 whereHas 子句以及充分利用 withCount 的条件闭包,我们能够显著提升 Laravel Eloquent 关联查询的效率。这种优化策略减少了不必要的数据库子查询,从而大幅缩短了数据获取时间。结合适当的数据库索引和缓存策略,可以确保您的应用程序在处理复杂数据聚合和排行榜功能时,保持高性能和良好的响应速度。
以上就是优化 Laravel Eloquent 查询:高效构建用户排行榜数据的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1319550.html
微信扫一扫
支付宝扫一扫