PHP SaaS系统运行几年以后,经常会出现一种很典型的问题:原来几十毫秒完成的SQL查询,现在突然变成几百毫秒,甚至几秒。很多开发者第一反应是“加一个索引”,但SQL变慢并不一定是索引消失了。数据量增长、执行计划变化、统计信息过期、锁等待、磁盘I/O、返回数据量增加,甚至PHP应用本身重复执行SQL,都可能成为原因。
因此,排查SQL性能最重要的原则不是立即修改数据库,而是先回答一个问题:到底是什么发生了变化。
第一步应该保存当前SQL、参数、执行时间以及执行计划,然后与过去正常运行时进行比较。MySQL的EXPLAIN可以查看优化器选择的执行计划,而MySQL 8.4还支持EXPLAIN ANALYZE,可以实际执行查询并提供各个迭代器的执行时间、实际返回行数和循环次数。这样可以判断优化器估计的数据量与真实数据量之间是否出现明显偏差。
例如,一个SaaS订单查询原来只扫描几十行,现在EXPLAIN ANALYZE发现某个JOIN实际扫描了数百万行,那么真正的问题可能是索引、统计信息或者查询条件,而不是PHP代码本身。
接下来检查索引,但不要简单地把“有没有索引”作为判断标准。真正需要分析的是索引是否匹配查询模式。
例如查询经常是:
SELECT id, status, created_at
FROM orders
WHERE tenant_id = ?
AND status = ?
ORDER BY created_at DESC
LIMIT 50;
那么索引设计就应该围绕实际的过滤条件和排序方式考虑,而不是机械规定“tenant_id必须第一”或者“时间字段必须第一”。如果另一类查询主要按照时间范围扫描所有租户,那么索引顺序又可能完全不同。
JOIN同样如此。需要确认JOIN两侧参与连接的字段是否有合适索引,并观察实际执行计划中的访问方式。不能因为看到JOIN就认为一定需要改成子查询,也不能认为所有子查询都应该改写成JOIN。MySQL优化器本身会对很多查询进行转换,最终还是应该通过执行计划和实际测试判断。
如果查询运行一段时间以后突然变慢,还应该考虑统计信息。数据分布发生变化以后,优化器对基数的估计可能不再准确。MySQL官方也建议在怀疑索引选择异常时检查并更新表统计信息,例如通过ANALYZE TABLE更新相关统计数据。
第二个经常被忽视的问题是数据量本身。
一条SQL在100万行的表上运行正常,并不意味着它在5000万行甚至数亿行的数据上仍然会保持相同速度。尤其是SaaS系统,一个租户可能只有几千条数据,但整个表已经积累了数千万甚至数亿条记录。
这时候应该区分“全表数据量”和“单次查询需要处理的数据量”。如果每次查询都能够通过tenant_id、状态、时间范围等条件快速缩小扫描范围,那么大表本身未必意味着查询一定慢。如果查询需要扫描大量历史数据,再好的PHP代码也无法消除数据库需要处理的数据量。
分页也是SaaS系统中非常常见的性能陷阱。LIMIT 50 OFFSET 500000意味着数据库可能需要先找到并跳过大量记录,然后才能返回最后50行。对于大型数据集,可以根据实际排序规则考虑keyset pagination,例如利用created_at和主键组成稳定的分页条件。
第三个问题是锁和事务。
有些SQL看起来只执行100毫秒,但用户仍然感觉系统卡顿,因为真正等待的时间发生在锁上。特别是在订单、库存、余额、任务队列等高并发业务中,应该区分SQL执行时间和锁等待时间。
SELECT ... FOR UPDATE并不是应该一律避免的技术。它在需要锁定即将修改的数据时非常有用。真正需要排查的是事务范围是否过大、事务持续时间是否过长、多个业务流程是否以不同顺序获取锁,以及是否存在死锁和锁等待。
例如PHP代码可能先查询订单,再调用外部API,最后才提交数据库事务。如果外部API响应需要两秒,那么数据库锁可能也被保持两秒。此时即使SQL本身只执行几十毫秒,整个系统仍然会出现明显的并发等待。
因此,排查数据库慢查询时应该同时观察事务持续时间、锁等待和死锁,而不是只看SQL执行时间。
第四个问题是PHP应用本身。
PHP SaaS中很常见的一类问题是N+1查询。例如页面需要显示100条订单,代码先查询100条订单,然后循环执行100次用户查询、100次商品查询,最终一个页面可能产生数百甚至上千次SQL。
这时候给每一条SQL分别增加索引,并不能解决根本问题。应该首先确认一个HTTP请求到底执行了多少条SQL,每条SQL分别花费多少时间,以及是否存在重复查询。
PHP的PDO和mysqli都支持持久连接,但持久连接不等同于一个可以随意配置大小的传统应用连接池。PHP的连接复用还受到PHP进程、Web服务器部署方式以及连接参数的影响。PDO官方文档也特别提醒,持久连接可能保留事务、锁和临时表等连接状态,因此使用时必须做好连接状态管理。
所以不要简单把max_connections当成“连接池配置”。max_connections首先是MySQL服务器允许的最大并发连接数。如果把它盲目调得非常高,反而可能因为内存和CPU竞争让数据库整体变慢。
第五个问题是返回的数据。
很多慢查询并不是WHERE条件没有索引,而是一次返回了大量无用数据。例如后台列表只需要十几个字段,却使用SELECT *读取一个包含TEXT、JSON甚至BLOB字段的大表。
对于SaaS列表页面,应该尽量只读取页面真正需要的字段。大字段如果只是详情页面才需要,可以在用户打开详情时再查询。
这并不意味着所有TEXT、BLOB都应该搬到另一张表。是否拆分,应该根据访问频率、数据大小、I/O压力和业务模型判断。真正需要解决的是查询是否每次都把大量不必要的数据从磁盘读取并传输到PHP。
第六个问题是数据库服务器资源。
如果EXPLAIN显示执行计划没有明显问题,但查询仍然越来越慢,就需要观察CPU、内存、Buffer Pool、磁盘I/O、临时表以及锁等待。
例如CPU长期接近满载,可能意味着查询计算、排序、聚合或者并发过高;磁盘I/O持续很高,则可能是大量数据扫描、Buffer Pool不足或者存储性能达到瓶颈。
如果数据库等待磁盘,而PHP应用又有大量并发请求,那么单纯增加PHP服务器数量可能适得其反,因为更多PHP进程只会同时向数据库发送更多查询。
第七个问题是慢查询日志和Performance Schema。
慢查询日志适合寻找真正运行缓慢或者频繁出现的SQL,而Performance Schema则能够从数据库服务器内部观察语句执行、等待、I/O和锁等运行事件。MySQL官方明确指出,Performance Schema用于运行时性能监控,而Information Schema主要用于元数据检查,两者用途并不相同。sys schema则提供了基于Performance Schema等数据的更方便查询方式。
因此,“分析information_schema”不应该作为数据库性能诊断的主要表述。更准确的组合应该是慢查询日志、Performance Schema、sys schema、EXPLAIN/EXPLAIN ANALYZE以及操作系统和云平台监控。
Percona Toolkit等工具也可以作为辅助工具,但工具本身不是解决方案。先找到慢在哪里,再决定使用什么工具。
第八个问题是缓存。
如果某个查询被大量重复执行,而且数据变化频率很低,那么Redis等缓存层可能有效。例如SaaS系统中的配置、权限、字典数据和部分热点统计信息,都可能适合缓存。
但缓存不是SQL优化的替代品。如果一个查询本身有严重的全表扫描问题,直接加缓存可能只是把问题暂时隐藏起来。缓存还会引入失效、一致性、热Key和内存管理等新的问题。
第九个问题才是架构调整。
当单个数据库实例经过SQL优化、索引优化、事务优化和硬件调整以后仍然无法满足需求,才应该考虑读写分离、专用数据库、数据归档、汇总表或者分片。
分区表也不能简单理解成“大表解决方案”。MySQL分区适合特定的数据访问和维护模式,例如按照时间组织的数据,并能够在合适的查询条件下利用partition pruning。它并不会自动让所有查询变快。
同样,MySQL没有传统意义上的自动物化视图机制。如果报表查询需要反复扫描数亿条交易记录,更现实的办法可能是建立汇总表,并通过定时任务、增量计算或数据管道维护这些统计数据。
对于历史数据,则应该首先考虑业务上是否真的需要在线查询全部历史记录。把几年前的数据全部留在交易主表里,然后每个列表查询都从头扫描,往往比单纯增加服务器配置更加值得解决。
对于SaaS系统,还必须把租户隔离放在性能问题之前。
正常的租户业务查询通常应该明确限定租户范围,例如:
SELECT ...
FROM orders
WHERE tenant_id = ?
AND ...
但不能把“所有SQL必须包含tenant_id”理解成绝对规则,因为平台管理员、跨租户报表、后台运维任务等场景可能本身就是跨租户查询。真正重要的是访问边界必须明确,并且性能设计要与授权模型一致。
同时,大租户和小租户的数据量可能完全不同。一个超级租户可能拥有整个系统大部分数据,因此同一条SQL在普通租户和超级租户上的执行时间可能完全不同。这种数据倾斜在SaaS系统中非常常见,也应该纳入性能测试。
因此,一个越来越慢的SQL,比较合理的排查顺序是:先确认实际变慢了多少,再比较执行计划;然后检查实际扫描行数、索引和统计信息;接着排查锁等待、事务、PHP重复查询和返回数据量;随后观察CPU、内存、Buffer Pool和磁盘I/O;最后才考虑缓存、读写分离、汇总表、归档或者分片。
最重要的是建立性能基线。每次数据库版本、表结构、索引、SQL、PHP代码或者数据规模发生重大变化以后,都应该重新观察P95/P99延迟、执行次数、扫描行数和资源消耗。MySQL官方也强调,性能优化必须结合实际工作负载进行测试,因为某项优化在一个环境中有效,并不意味着在另一个环境中同样有效。
对于PHP SaaS开发者来说,真正成熟的SQL优化并不是“发现慢了就加索引”,而是建立一条完整的诊断链:PHP请求到底执行了什么SQL,SQL实际扫描了多少数据,优化器为什么选择这个执行计划,数据库究竟在计算、等待锁还是等待磁盘,最终再判断应该修改SQL、索引、事务、代码还是整个架构。只有把这条链跑通,数据库性能问题才会从“感觉越来越慢”变成可以测量、可以定位、也可以验证的工程问题。
一个 SQL 查询越来越慢,PHP SaaS 开发者应该从哪些地方开始排查
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP