优化 Laravel Eloquent 查询:高效构建用户排行榜数据

霞舞
发布: 2025-07-29 15:02:39
原创
184人浏览过

优化 Laravel Eloquent 查询:高效构建用户排行榜数据

本教程详细讲解如何优化 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秒),主要原因在于其存在以下问题:

  1. 冗余的 whereHas 调用: 对于 currentWeek 和 lastWeek 的查询,whereHas('pictures') 后面紧跟着一个带有条件闭包的 whereHas('pictures', fn ($q) => $q->whereBetween(...))。前一个 whereHas 仅仅检查用户是否有任何图片,而后一个 whereHas 则检查用户是否有在特定日期范围内的图片。如果用户有在特定日期范围内的图片,那么他必然有图片。因此,第一个无条件的 whereHas 是多余的。
  2. 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 的用户,可以在获取结果后进行过滤。

乐尚商城系统
乐尚商城系统

乐尚商城系统是一项基于PHP+MYSQL为核心开发的一套免费 + 开源专业商城系统。软件具执行效率高、模板自由切换、后台管理功能方便等诸多优秀特点。 本软件是基于Web应用的B/S架构的商城网站建设解决方案的建站系统。它可以让用户高效、快速、低成本的构建个性化、专业化、强大功能的团购网站。从技术层面来看,本程序采用目前软件开发IT业界较为流行的PHP和MYSQL数据库开发技术,基于面向对象的编程

乐尚商城系统 684
查看详情 乐尚商城系统

最终优化后的 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 Carbon\Carbon;
use Illuminate\Support\Facades\Cache; // 如果需要缓存

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,
    ]);
}
登录后复制

重要考量与建议

为了进一步提升性能和应用健壮性,以下几点至关重要:

  1. 数据库索引: 确保 pictures 表的 user_id 和 created_at 字段上都有合适的索引。这是任何关联查询性能优化的基石。一个复合索引 (user_id, created_at) 通常是理想的选择,它可以加速按用户ID过滤并按创建时间范围查找的操作。

    ALTER TABLE pictures ADD INDEX idx_user_id_created_at (user_id, created_at);
    登录后复制
  2. 缓存策略: 对于排行榜这类数据,其内容在短时间内不会频繁变动,但访问量可能非常大。强烈建议使用 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();
    });
    登录后复制
  3. 软删除 (Soft Deletes): 如果 User 或 Picture 模型使用了软删除(即存在 deleted_at 字段),Eloquent 会自动在查询中加入 where deleted_at is null 条件。这通常是期望的行为,但也要注意其对索引和查询的影响。确保 deleted_at 字段也有索引可以进一步优化带软删除的查询。

总结

通过本文介绍的两个关键优化步骤——消除冗余的 whereHas 子句以及充分利用 withCount 的条件闭包,我们能够显著提升 Laravel Eloquent 关联查询的效率。这种优化策略减少了不必要的数据库子查询,从而大幅缩短了数据获取时间。结合适当的数据库索引和缓存策略,可以确保您的应用程序在处理复杂数据聚合和排行榜功能时,保持高性能和良好的响应速度。

以上就是优化 Laravel Eloquent 查询:高效构建用户排行榜数据的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号