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

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

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

"真正掌握 Eloquent 不是靠魔法------而是理解它生成的 SQL。"

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

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

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

php 复制代码
// ❌ 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 分页的偏移量越大,性能越差。

php 复制代码
// ❌ OFFSET 100k
$users = User::paginate(25);

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

生成的 SQL

sql 复制代码
-- 下一页
SELECT * FROM users WHERE id > ? ORDER BY id LIMIT 25;

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

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

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

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

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

JSON 聚合处理嵌套数据

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

php 复制代码
$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 需要专门的索引。

php 复制代码
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,大表上性能会越来越差。

php 复制代码
User::where('last_login','<', now()->subYear())
    ->lazyById(1000)
    ->each->delete();

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

基于表达式的排序

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

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

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

条件关联加载

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

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

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

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

Update From Select

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

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

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

物化视图处理重度聚合

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

sql 复制代码
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;
php 复制代码
$stats = DB::table('user_stats')->where('user_id',$id)->first();

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

多列过滤用复合索引

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

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

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

pluck/toBase 实现选择性实例化

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

php 复制代码
$ids = User::active()->toBase()->pluck('id');

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

事务中的行级锁

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

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

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

计算属性用表达式列

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

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

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

地理空间索引和查询

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

php 复制代码
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

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

相关推荐
kfaino31 分钟前
码农的AI翻身(六)你好,我叫 Parameter
后端·aigc
掘金者阿豪34 分钟前
把业务数据变成共享仪表盘:Metabase可视化与远程访问实践
前端·后端
猪猪拆迁队2 小时前
虚拟工厂仿真引擎的架构设计:让一条产线可编程、可观测、可干预
后端·ai编程
字节跳动数据库2 小时前
文章分享——相似函数处理方法
人工智能·后端·程序员
云技纵横2 小时前
@Transactional 失效的 7 种场景:第 5 种最难排查
后端
用户6757049885022 小时前
你知道 Go 结构体和结构体指针调用的区别吗?一文带你彻底搞懂!
后端·go
程序员cxuan3 小时前
读懂 Claude Code 架构分析系列,第一篇,开始!
人工智能·后端·架构
用户6757049885023 小时前
面试官问“装饰器模式”,这样回答薪资多要 3000!
后端
tntxia3 小时前
Geo Scene域名修改引起的一些问题
后端
用户298698530143 小时前
Java 实现 Word 文档加密与权限解除
java·后端