15 个 Eloquent 高级技巧,瞬间提升你的 Laravel 应用性能

AI摘要
本文分享15个优化Laravel Eloquent性能的高级技巧,适用于大数据量场景。核心建议包括:用withCount替代关联计数、游标分页避免偏移性能损耗、分块预加载控制内存、JSON聚合减少查询次数、软删除使用部分索引、lazyById流式处理大数据。这些方法通过优化SQL生成和数据库操作,将查询性能从秒级提升至毫秒级,显著改善高并发系统表现。

15 个 Eloquent 高级技巧,瞬间提升你的 Laravel 应用性能

Eloquent 的优雅语法很容易让人忽略性能问题,特别是当数据表增长到千万级别时。我在调优处理上亿记录的高并发系统过程中,总结了 15 个实战技巧——远不止基础的预加载——能把慢查询优化到毫秒级。

“真正掌握 Eloquent 不是靠魔法——而是理解它生成的 SQL。”

用 Raw Count 代替关联关系的完整加载

为什么重要:加载整个关联只为了计个数,内存和性能都会受影响。

// ❌ N+1 查询 + 模型实例化
$posts = Post::all();
foreach ($posts as $post) {
    echo $post->comments->count();
}

// ✅ 使用 withCount
$posts = Post::select('id','title')
    ->withCount('comments')
    ->get();
foreach ($posts as $post) {
    echo $post->comments_count;
}

性能对比:10k 条文章从 500 ms 降到 8 ms

深度分页用 Cursor Pagination

为什么重要:OFFSET 分页的偏移量越大,性能越差。

// ❌ OFFSET 100k
$users = User::paginate(25);

// ✅ 使用 cursorPaginate
$users = User::where('created_at','>','2023-01-01')
    ->orderBy('id')
    ->cursorPaginate(25);

生成的 SQL

-- 下一页
SELECT * FROM users WHERE id > ? ORDER BY id LIMIT 25;

优势:任何深度的分页都是恒定时间。

大量关联数据用分块预加载

为什么重要:一条 IN() 语句里塞 5 万个 ID,很容易超时。

Post::chunkById(200, fn($posts) =>
    $posts->load(['comments' => fn($q) =>
        $q->select('id','post_id','content')
          ->latest()
          ->limit(10)
    ])
);

要点:分批加载关联数据,控制每批大小,内存占用更少。

JSON 聚合处理嵌套数据

为什么重要:一条查询替代多个 JOIN 和循环。

$users = User::selectRaw("users.*, JSON_AGG(
    JSON_BUILD_OBJECT(
        'address', addresses.street,
        'orders', orders.total
    )
) AS user_data")
->leftJoin('addresses','users.id','addresses.user_id')
->leftJoin('orders','users.id','orders.user_id')
->groupBy('users.id')
->get()
->map(fn($u) => array_merge(
    $u->toArray(),
    json_decode($u->user_data, true)
));

结果:彻底消除嵌套关联的 N+1 问题。

软删除用部分索引

为什么重要WHERE deleted_at IS NULL 需要专门的索引。

Schema::table('users', fn(Blueprint $t) =>
    $t->index(['deleted_at'], 'active_idx')
      ->where('deleted_at','IS',null)
);

// 查询会使用部分索引
User::whereNull('deleted_at')->get();

性能对比:20 万行数据从 1.2 s 降到 14 ms。

lazyById() 实现真正的流式处理

为什么重要:chunk() 用的是 OFFSET,大表上性能会越来越差。

User::where('last_login','<', now()->subYear())
    ->lazyById(1000)
    ->each->delete();

优势:在百万级以上的表上快 10 倍。

基于表达式的排序

为什么重要:按子表数据排序不用 JOIN,避免繁重的连接操作。

Post::orderByDesc(
    Comment::select('created_at')
        ->whereColumn('post_id','posts.id')
        ->latest()->limit(1)
)->get();

⏱ 瞬间获得”最近活跃”列表,无需 JOIN。

条件关联加载

为什么重要:一次性过滤父级和子级数据。

// 在模型中
public function activeSubscriptions() {
    return $this->subscriptions()
        ->where('expires_at','>', now())
        ->where('status','active');
}

// 控制器中
$users = User::withWhereHas('activeSubscriptions')->get();

神奇之处:一条查询同时过滤父级和子级数据。

Update From Select

为什么重要:跨表原子更新,单条查询搞定。

DB::table('users')
    ->join('teams','teams.owner_id','=','users.id')
    ->where('teams.status','premium')
    ->update(['users.plan'=>'premium']);

告别“查出来-循环-再更新”的笨办法。

物化视图处理重度聚合

为什么重要:对上千万行数据实时计算 SUM/AVG,基本会超时。

CREATE MATERIALIZED VIEW user_stats AS
SELECT user_id, SUM(amount) AS ltv, COUNT(*) AS orders
FROM orders GROUP BY user_id;

REFRESH MATERIALIZED VIEW user_stats;
$stats = DB::table('user_stats')->where('user_id',$id)->first();

性能对比:LTV 查询从 2 s 降到 0.2 ms。

多列过滤用复合索引

为什么重要:正确的顺序避免全表扫描。

Schema::table('users', fn(Blueprint $t) =>
    $t->index(['state','city'])
);

注意:只有在单独查询 state 或同时查询 state 和 city 时才有效。

pluck/toBase 实现选择性实例化

为什么重要:只要 ID 却实例化整个模型,内存白白浪费。

$ids = User::active()->toBase()->pluck('id');

内存对比:每 1k 条记录从 1.5 MB 降到 50 KB。

事务中的行级锁

为什么重要:高并发下防止竞态条件。

DB::transaction(fn() =>
    tap(User::where('id',$id)->lockForUpdate()->first(), fn($u) =>
        $u->decrement('stock')
    )
);

关键:秒杀场景下保证库存更新不会乱。

计算属性用表达式列

为什么重要:把重复计算交给数据库处理。

User::selectRaw("*, (
    SELECT COUNT(*) FROM orders WHERE user_id=users.id
) AS order_count")->get();

替代方案:通过视图物化(见技巧 #10)。

地理空间索引和查询

为什么重要:在数十万个点中快速搜索”附近”。

Schema::table('places', fn(Blueprint $t) =>
    $t->point('loc')->spatialIndex()
);

Place::selectDistance('loc',DB::raw($point))
    ->whereDistance('loc',DB::raw($point),'<',10000)
    ->get();

性能对比:50 万个位置从 1.5 s 降到 8 ms。

📊 大规模性能对比

技术 1 万条 100 万条 1000 万条
常规分页 5 ms 120 ms 1.2 s
Cursor 分页 5 ms 8 ms 10 ms
预加载 80 ms 800 ms 超时
分块预加载 85 ms 150 ms 300 ms
软删除扫描 20 ms 1.5 s 15 s
部分索引 1 ms 2 ms 3 ms

原文链接 15 个 Eloquent 高级技巧,瞬间提升你的 Laravel 应用性能

题外话:最近花了很久的时间零零散散的将 Laravel Livewire4 的文档翻译成了中文,如果对 Laravel Livewire 感兴趣,可以查看文档 Laravel Livewire4 中文文档

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 2周前 自动加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 13

没活你就咬个打火机吧

2周前 评论
JaguarJack (楼主) 2周前
Asuna (作者) 2周前
JaguarJack (楼主) 2周前

:+1: 赠人玫瑰,手留余香!

2周前 评论
porygonCN

最大的提升就是根据情况少用 model 在查询多条数据时 这玩意时间损耗极大 查询只要20毫秒 加载为model对象要300毫秒的那种

2周前 评论
JaguarJack (楼主) 2周前

现在少有的技术文章分享,感谢楼主!

2周前 评论

file

请教下兄弟这个是啥子意思

2周前 评论
JaguarJack (楼主) 2周前
lucifergit

游标分页 只能在上一页下一页可以使用。一般后台分页组件一般都可以直接跳转页码的。那游标分页就无法实现了。

2周前 评论
JaguarJack (楼主) 2周前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
开发 @ 家里蹲开发公司
文章
154
粉丝
86
喜欢
499
收藏
338
排名:18
访问:29.4 万
私信
所有博文
社区赞助商