本文目录导读:

- 核心:分析并优化慢查询(最重要的第一步)
- 索引优化(见效最快)
- SQL语句本身优化(减少数据量)
- PHP代码与数据库交互优化
- 架构和缓存层(更上层的优化)
- 终极手段:分库分表与读写分离
- 一个优化流程案例
在PHP项目中优化SQL查询速度,通常需要从SQL语句本身、数据库结构、PHP代码与数据库交互方式以及服务器/缓存四个层面入手。
以下是针对PHP项目的系统性优化方案,按优先级排列:
核心:分析并优化慢查询(最重要的第一步)
不要凭感觉优化,要先找到真正慢的SQL。
-
开启慢查询日志:
- 在MySQL配置文件中设置:
slow_query_log = 1 slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 2 # 超过2秒的查询
- 或使用即时命令(需重启后失效):
SET GLOBAL slow_query_log = 'ON';
- 在MySQL配置文件中设置:
-
使用
EXPLAIN分析SQL:- 在SQL前加上
EXPLAIN,查看执行计划。 - 重点关注:
type(性能从好到差:const>eq_ref>ref>range>index>ALL,ALL是全表扫描,必须避免)、rows(扫描的行数)、Extra(出现Using filesort或Using temporary说明需要优化)。 - 例子:
EXPLAIN SELECT * FROM users WHERE email = 'test@test.com';
- 在SQL前加上
索引优化(见效最快)
大多数慢查询的根源是缺少合适的索引或索引未被使用。
-
为高频查询字段加索引:
WHERE、JOIN、ORDER BY、GROUP BY中出现的字段,优先考虑建立索引。- 联合索引(复合索引): 按查询条件顺序建立,遵循最左前缀原则,经常
WHERE status = 1 AND created_at > '2023-01-01',可以建立INDEX(status, created_at)。 - 覆盖索引: 让索引包含查询所需的所有列(
SELECT中的列都在索引里),避免“回表”查询磁盘数据,这是极致的性能提升。
-
避免让索引失效的典型写法:
- 不要在索引列上使用函数:
WHERE DATE(created_at) = '2023-01-01'→ 改为WHERE created_at >= '2023-01-01' AND created_at < '2023-01-02'。 - 隐式类型转换:
WHERE user_id = '123'(如果user_id是整型)→ 改为WHERE user_id = 123。 LIKE以通配符开头:LIKE '%keyword'无法使用索引,考虑使用全文索引(FULLTEXT)或搜索引擎(Elasticsearch)。OR连接条件:OR两边的字段没有分别建索引,会导致全表扫描,可改为UNION ALL。
- 不要在索引列上使用函数:
SQL语句本身优化(减少数据量)
-
只查询需要的字段:
- *不要写 `SELECT
**,只取SELECT id, name, email` 这样需要的列,减少IO和内存占用。
- *不要写 `SELECT
-
限制结果集:
- 分页查询必须带
LIMIT,并且对于大偏移量(LIMIT 100000, 20),要优化为游标分页或子查询延迟关联。-- 低效 SELECT * FROM articles ORDER BY id LIMIT 100000, 20; -- 高效(利用覆盖索引) SELECT * FROM articles WHERE id > 100000 LIMIT 20;
- 分页查询必须带
-
优化
JOIN和子查询:- 小表驱动大表:
JOIN时,让数据量小的表作为驱动表(MySQL优化器通常会自动选择,但可以强制指定STRAIGHT_JOIN)。 - 尽量使用
JOIN代替子查询(MySQL 8.0之前),现代MySQL已经优化了,但JOIN通常仍更可控。 GROUP BY+ORDER BY:确保使用了索引,避免Using filesort和Using temporary。
- 小表驱动大表:
PHP代码与数据库交互优化
PHP本身不是瓶颈,但代码写法会影响数据库连接和查询次数。
-
使用数据库连接池(长连接):
- 避免每次请求都创建/销毁连接,使用
mysqli的p:host持久连接,或使用Swoole/Workerman的连接池(生产环境强烈建议)。
- 避免每次请求都创建/销毁连接,使用
-
减少查询次数(N+1问题):
- 在循环中逐条查询(N+1)是性能杀手。
- 错误例子:
foreach ($users as $user) { $posts = $db->query("SELECT * FROM posts WHERE user_id = {$user['id']}"); } - 正确做法: 先用
WHERE user_id IN (...)批量查询,再在PHP中组装数据。
-
批量操作:
- 插入或更新多条数据时,使用一条SQL语句包含多条记录,而不是循环执行。
-- 改为一条语句 INSERT INTO table (col1, col2) VALUES (1, 'a'), (2, 'b'), (3, 'c');
- 插入或更新多条数据时,使用一条SQL语句包含多条记录,而不是循环执行。
-
使用预处理语句(Prepared Statements):
不仅是防SQL注入,还能让MySQL解析一次SQL,多次执行,使用PDO或mysqli的prepare/execute。
-
优化ORM:
- 如果使用Laravel Eloquent、ThinkPHP等ORM,务必开启懒加载并在需要时使用预加载(Eager Loading),避免N+1。
- Laravel例子:
User::with('posts')->get()会只发2条SQL,而User::all()再循环$user->posts会发N+1条。
- Laravel例子:
- 如果使用Laravel Eloquent、ThinkPHP等ORM,务必开启懒加载并在需要时使用预加载(Eager Loading),避免N+1。
架构和缓存层(更上层的优化)
如果SQL已经优化到极致,考虑用缓存扛住高并发。
-
查询结果缓存:
- Redis/Memcached:将高频查询、耗时查询的结果存入缓存(如热门文章列表、用户信息),PHP读取缓存比查询MySQL快几个数量级。
- MySQL Query Cache(MySQL 8.0已废弃):不推荐,容易失效。
-
MySQL内部配置调优:
innodb_buffer_pool_size:设置为服务器物理内存的70%-80%,这是InnoDB最重要的配置,决定了数据和索引能在内存中缓存多少。query_cache_size:8.0之前可以适当开启,但建议为0(让操作系统缓存文件)。tmp_table_size/max_heap_table_size:增大内存临时表大小,避免排序/分组时使用磁盘临时表。
终极手段:分库分表与读写分离
当单表数据量达到千万/亿级别,上述方法失效。
- 读写分离: 主库写,从库读(PHP连接多个数据库实例)。
- 分表: 垂直分表(将大字段分离出去)、水平分表(按ID取模分表)。
- 分库: 按业务(用户、订单)分独立数据库。
一个优化流程案例
假设你的PHP项目中有一个文章列表页很慢。
- 发现问题: 打开慢查询日志,找到
SELECT * FROM articles ORDER BY created_at DESC LIMIT 100000, 20; - 分析: 使用
EXPLAIN,发现type: ALL(全表扫描),rows: 500000。 - 优化SQL: 改为
SELECT * FROM articles WHERE id > 100000 ORDER BY id LIMIT 20;(假设ID连续)或使用子查询延迟关联。 - 加索引: 给
created_at和id加索引(如果上面改了,用覆盖索引)。 - PHP代码: 检查是否在循环中查附件表,改为IN查询+预加载。
- 加缓存: 列表页结果用Redis缓存1分钟,减轻数据库压力。
- 结果: 加载时间从5秒降到20毫秒。
记住一条铁律: 先定位慢查询,针对性加索引,最后才考虑改代码或加缓存。不要盲目优化。