开发者经常问我同一个问题:“我的 API 很慢,该从哪里开始优化?”习惯性的反应通常是升级服务器或翻倍内存。这既费钱,又很难解决根本问题。在大多数 Laravel 应用中,瓶颈往往存在于数据库层。Eloquent 优雅的语法很容易让人忘记,每一次 Eloquent 调用最终都会转化为 SQL,而 SQL 正是痛点开始的地方。

在动服务器配置之前,请有条理地检查你的查询。

从正确的诊断开始

不要在黑暗中进行优化。随机重写查询只是在瞎猜,而瞎猜会浪费大量时间。

你需要找到那些消耗总时间最多的语句。在真实负载下观察你的应用。Laravel Telescope 可以让你清晰地看到请求期间执行的每一个查询及其耗时。Laravel Debugbar 则可以在本地开发时在浏览器中展示这些查询,以便你立即发现异常。当你需要在生产环境中捕获问题时,请启用 MySQL 慢查询日志(Slow Query Log)。它会记录超过你定义阈值的语句,这对于发现那些在小数据集上无法显现的“惊喜”非常理想。如果你运行的是更大规模的任务,应用性能监控(APM)工具可以将缓慢的 HTTP 端点与特定的数据库调用关联起来。

在审查数据时,请关注两点:绝对执行时间和调用频率。一个耗时 40 毫秒的查询听起来无伤大雅,直到你发现它每分钟运行两千次。一个每小时运行一次、耗时三秒的报表,可能还不如一个在每个页面都会运行的、耗时半秒的查询重要。先解决高影响的问题。

停止索取所有字段

SELECT * 很方便,但也非常昂贵。当你编写 Model::all() 或在不指定列名的情况下获取结果集时,MySQL 会为每个匹配的行搬运所有字段。这包括大型文本字段、JSON 字段以及表中的任何其他内容。结果集变大,内存占用攀升,序列化响应所花费的时间也会增加。

请明确指定字段。如果你的控制器只需要 idnameemail 字段,请准确请求这些字段:

User::select('id', 'name', 'email')->get();

在查询构造器(Query Builder)中,同样的原则也适用。更小的负载在网络传输中更快,且在应用服务器上消耗更少的 RAM。这是成本最低的优化手段之一,但由于 Laravel 将 SELECT * 作为默认行为,人们很容易忽略它。

让 EXPLAIN 指引你的改进

在没有运行 EXPLAIN 之前,永远不要重构慢查询。在 MySQL 中,EXPLAIN 关键字可以显示查询执行计划。它会揭示优化器打算如何查找你的数据。

请密切关注 type 列。如果你看到 ALL,说明 MySQL 正在进行全表扫描。这意味着它正在读取每一行以满足你的 WHERE 子句。查看 key 列,看看优化器是否使用了索引。然后检查 Extra 列。如果你发现 Using temporaryUsing filesort,说明 MySQL 正在构建中间表或在内存中进行排序,因为你当前的结构无法优雅地满足查询。

在你的 MySQL 客户端中运行 EXPLAIN,或者使用能为你格式化输出结果的工具。一旦看到了执行计划,你就知道问题是缺少索引、错误的连接(Join),还是引擎无法优化的谓词。此时,猜测变得不再必要。

有目的地建立索引

索引是加速查询最强大的工具,但只有当它们与你的查询方式匹配时才有效。如果没有正确的索引,MySQL 会逐行扫描。在只有一千行数据的表上进行开发时,这可能感觉良好,但在生产环境中面对一千万行数据的表时,可能会彻底崩溃。

首先,针对经常出现在 WHERE 子句中的字段建立单列索引。如果你经常根据 status 进行过滤,请为 status 添加索引。

当查询同时对多个列进行过滤时,请使用复合索引(Composite Indexes)。索引内部的列顺序非常重要,因为 MySQL 从左到右读取复合索引。这被称为“最左前缀原则”(leftmost prefix rule)。如果你的查询通过 user_id 进行搜索,然后按 created_at 排序,那么在 (user_id, created_at) 上建立复合索引会有显著帮助。如果反转顺序,优化器可能根本不会使用该索引来进行过滤。

不要为每一列都建立索引。每个索引都会增加插入、更新和删除的开销,因为 MySQL 必须维护其结构。请根据你在测量过程中发现的模式,有目的地添加索引。

避免在列上使用函数

这个错误会悄无声息地使索引失效。当你在 WHERE 子句中对列使用函数时,MySQL 无法