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 中文文档

相关推荐
golang学习记2 小时前
用 Go + Redis + HTMX 手撸一个超快 URL 短链接服务 🚀
后端
codervibe2 小时前
Spring Boot 热启动配置实战:从手动重启到秒级反馈
spring boot·后端
skyeeeeee2 小时前
kubeadm安装k8s集群
后端·kubernetes
michaelzhouh3 小时前
php项目ueditor上传pdf文件,防止XSS攻击
pdf·php·xss·ueditor
chxii3 小时前
Spring Boot 响应给客户端的常见返回类型
java·spring boot·后端
韩立学长3 小时前
【开题答辩实录分享】以《植物爱好者交流平台的设计与实现》为例进行答辩实录分享
spring boot·后端·mysql
Wzx1980123 小时前
go基础语法练习
开发语言·后端·golang
sp423 小时前
漫谈 Java 轻量级的模板技术:从字符串替换到复杂模板
java·后端
2301_795167203 小时前
玩转Rust高级应用. ToOwned trait 提供的是一种更“泛化”的Clone 的功能,Clone一般是从&T类型变量创造一个新的T类型变量
开发语言·后端·rust