LikeShop 单商户标准版(PHP)慢查询排查与数据库索引优化实战
一、引言:PHP 电商系统的性能瓶颈,往往先从数据库开始
基于 ThinkPHP + MySQL 架构的商城系统,在业务初期通常运行平稳。但随着商品、订单、会员、运营数据不断增长,商品列表、订单查询、后台统计、关联加载等场景会逐渐成为性能瓶颈。
很多团队在遇到页面慢、接口超时、数据库压力升高时,第一反应是升级服务器配置。但实际上,PHP 电商系统的多数性能问题,并不是硬件不足导致的,而是由慢查询、索引缺失、索引失效、SQL 写法不规范、关联查询过多等问题叠加造成的。
本文结合 LikeShop 单商户标准版(PHP)的常见业务场景,从慢查询发现、执行计划分析、索引设计、SQL 改写、代码优化、效果验证六个环节,整理一套可直接落地的数据库性能优化方法论。
二、慢查询排查的正确顺序:先定位,再优化
优化数据库不能从“加索引”开始,而要先明确:哪些 SQL 慢、为什么慢、影响哪些业务。LikeShop PHP 环境下,建议按以下顺序排查。
2.1 第一步:通过 ThinkPHP SQL 日志定位慢 SQL
ThinkPHP 框架在开启数据库调试后,可以记录每条 SQL 的执行时间、参数和调用位置,是开发阶段最直接的排查入口。
开启数据库调试
// database.php
return [
'debug' => true,
'sql_explain' => true,
];
开启后,框架会将 SQL 执行日志写入运行时目录:
runtime/log/
排查时重点关注以下几类 SQL:
- 执行时间超过 100ms 的 SQL;
- 商品列表查询;
- 订单列表查询;
- 后台统计查询;
- 带
LIKE、GROUP BY、ORDER BY、JOIN的复杂查询。
2.2 第二步:通过 Trace 分析单页面 SQL 耗时
对于页面级性能问题,可以使用 ThinkPHP Trace 工具查看当前页面执行的所有 SQL。
Trace 工具可以直观展示:
- 当前页面执行了多少条 SQL;
- 每条 SQL 的具体耗时;
- 是否存在重复查询;
- 是否存在明显最慢的 SQL。
如果页面中某条 SQL 耗时占比超过 50%,说明该 SQL 是主要性能瓶颈,应优先优化。
2.3 第三步:开启 MySQL 慢查询日志
生产环境不建议长期开启框架调试,但可以开启 MySQL 慢查询日志,持续捕获超时 SQL。
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = ON
参数说明:
| 参数 | 说明 |
|---|---|
slow_query_log |
开启慢查询日志 |
long_query_time |
超过该秒数的 SQL 被记录 |
log_queries_not_using_indexes |
记录未使用索引的 SQL |
慢查询日志可以帮助发现:
- 高频慢 SQL;
- 低频但执行极慢的 SQL;
- 未走索引的查询;
- 统计报表类复杂查询。
2.4 第四步:用 EXPLAIN 判断索引是否真正生效
找到慢 SQL 后,必须使用 EXPLAIN 分析执行计划。这是判断索引是否生效的标准方法。
EXPLAIN SELECT * FROM ls_goods
WHERE category_id = 12
AND status = 1
AND price BETWEEN 10 AND 100
ORDER BY create_time DESC
LIMIT 0, 20;
重点关注以下字段:
| 字段 | 说明 | 优化判断 |
|---|---|---|
type |
MySQL 访问数据的方式 | 出现 ALL 通常表示全表扫描,需要优化 |
key |
实际命中的索引 | 为 NULL 表示未命中索引 |
rows |
预估扫描行数 | 数值越大,通常性能越差 |
Extra |
额外执行信息 | 出现 Using filesort、Using temporary 通常需要优化 |
三、LikeShop 单商户版常见慢查询场景与优化案例
3.1 场景一:商品列表多条件筛选
商品列表是商城访问量最高的场景之一。如果查询条件包含分类、状态、价格、排序等多个维度,而索引设计不合理,很容易出现全表扫描。
问题 SQL
SELECT * FROM ls_goods
WHERE category_id = 12
AND status = 1
AND price BETWEEN 10 AND 100
ORDER BY create_time DESC
LIMIT 0, 20;
问题分析
如果只有单列索引,MySQL 可能只能利用部分条件,其他条件需要在回表后过滤。当商品数据量较大时,性能会明显下降。
优化方案:建立联合索引
ALTER TABLE ls_goods
ADD INDEX idx_status_category_price_create_time (
status,
category_id,
price,
create_time
);
优化思路
联合索引设计遵循以下原则:
- 等值查询字段放在前面;
- 范围查询字段放在后面;
- 排序字段尽量放在索引末尾;
- 优先过滤高基数字段,减少扫描范围。
优化效果
| 对比项 | 优化前 | 优化后 |
|---|---|---|
type |
可能为 ALL |
提升为 range |
key |
可能未命中有效索引 | 命中联合索引 |
rows |
扫描大量行 | 扫描行数明显下降 |
Extra |
可能出现 Using filesort |
排序开销降低 |
3.2 场景二:用户中心订单查询
用户中心订单查询通常按用户维度查询,并叠加订单状态、时间排序等条件。
问题 SQL
SELECT * FROM ls_order
WHERE user_id = 1001
AND order_status = 2
ORDER BY create_time DESC
LIMIT 0, 10;
问题分析
如果只给 user_id 建立单列索引,MySQL 虽然可以快速定位用户订单,但仍然需要在回表后过滤状态并排序。数据量大时,可能出现 Using filesort。
优化方案:建立用户、状态、时间联合索引
ALTER TABLE ls_order
ADD INDEX idx_user_status_create_time (
user_id,
order_status,
create_time
);
优化思路
user_id是等值条件,放在最前面;order_status是等值条件,继续缩小范围;create_time用于排序,利用索引有序性减少文件排序。
3.3 场景三:商品名称模糊搜索
商品搜索是商城高频功能,但 LIKE '%关键词%' 是典型的慢查询来源。
问题 SQL
SELECT * FROM ls_goods
WHERE name LIKE '%手机%'
AND status = 1;
问题分析
前导通配符会导致 B+Tree 索引失效,MySQL 只能通过全表扫描完成匹配。商品数量越大,性能越差。
优化方案
根据业务量级选择不同方案:
方案一:改为前缀匹配
如果业务允许,可改为:
SELECT * FROM ls_goods
WHERE name LIKE '手机%'
AND status = 1;
这种方式可以命中普通索引,但适用场景有限。
方案二:建立全文索引
ALTER TABLE ls_goods
ADD FULLTEXT INDEX ft_goods_name (name);
查询方式改为:
SELECT * FROM ls_goods
WHERE MATCH(name) AGAINST('手机' IN BOOLEAN MODE)
AND status = 1;
方案三:引入搜索引擎
对于商品数量较大、搜索需求复杂的场景,建议引入 ElasticSearch 等搜索引擎,承担商品检索任务。
3.4 场景四:统计报表聚合查询
后台统计类查询经常包含 COUNT、SUM、GROUP BY、DATE() 等操作。如果直接在大表上实时统计,性能通常较差。
问题 SQL
SELECT DATE(create_time) AS day, COUNT(*)
FROM ls_order
WHERE create_time BETWEEN '2026-01-01' AND '2026-01-31'
GROUP BY day;
优化思路
- 时间字段必须建立索引;
- 尽量避免对索引列使用函数;
- 大跨度统计不要实时查明细表;
- 通过定时任务预计算统计结果。
优化方向
SELECT create_time AS day, COUNT(*)
FROM ls_order
WHERE create_time >= '2026-01-01'
AND create_time < '2026-02-01'
GROUP BY DATE(create_time);
更推荐的方式是:
- 按天预计算订单统计;
- 将结果写入统计表;
- 后台页面直接查询统计表。
3.5 场景五:关联查询过多
ThinkPHP 的关联查询虽然方便,但如果不加以控制,容易导致 SQL 复杂度上升、回表字段过多、N+1 查询等问题。
常见问题
- 商品列表同时关联多张表;
- 使用
SELECT *返回大量字段; - 在循环中逐条查询关联数据;
- 关联条件未有效使用索引。
优化方案
1. 只查询必要字段
$list = GoodsModel::where('status', 1)
->field('id, name, image, price, sales')
->select();
2. 使用预加载,避免循环查询
$orderList = OrderModel::with('orderGoods')
->where($where)
->select();
3. 列表页减少关联层级
列表页只获取展示所需字段,详情页再加载完整关联信息。
四、索引设计的核心原则
4.1 等值优先,范围在后
联合索引中,等值查询字段应尽量放在前面,范围查询字段放在后面。
ALTER TABLE ls_goods
ADD INDEX idx_status_category_price (status, category_id, price);
原因是:范围条件后面的列,无法继续有效利用索引。
4.2 遵循最左前缀原则
联合索引 (a, b, c) 可以支持以下查询:
WHERE a = ?WHERE a = ? AND b = ?WHERE a = ? AND b = ? AND c = ?
但不一定支持:
WHERE b = ?WHERE b = ? AND c = ?
因此,索引顺序必须围绕实际高频查询设计。
4.3 排序字段尽量纳入索引
如果查询中有固定排序字段,应尽量将排序字段放入联合索引末尾,减少 Using filesort。
ALTER TABLE ls_order
ADD INDEX idx_user_status_create_time (user_id, order_status, create_time);
4.4 索引不是越多越好
索引虽然能提升读性能,但会带来以下代价:
- 占用磁盘空间;
- 降低写入性能;
- 增加优化器选择成本;
- 增加维护复杂度。
因此,索引应围绕高频核心查询建立,而不是“哪里慢就加哪里”。
五、常见索引失效原因与改写方法
5.1 索引列使用函数
-- 索引失效
WHERE DATE(create_time) = '2026-01-01'
-- 推荐改写
WHERE create_time >= '2026-01-01'
AND create_time < '2026-01-02'
5.2 隐式类型转换
字符串字段使用数字查询,或字符集、排序规则不一致,都可能导致索引失效。
例如:
-- 可能导致索引失效
WHERE mobile = 13800000000
如果 mobile 是字符串类型,应使用字符串查询:
WHERE mobile = '13800000000'
5.3 前导通配符
-- 索引失效
WHERE name LIKE '%手机%'
解决方式:
- 能前缀匹配就前缀匹配;
- 不适合前缀匹配则使用全文索引;
- 量级较大时引入搜索引擎。
5.4 OR 条件未全部命中索引
如果 OR 连接的条件中,某一列没有索引,优化器可能放弃索引,选择全表扫描。
建议尽量将可索引条件组织到同一索引中,或拆分为更明确的查询。
六、代码层优化:从源头减少慢查询
6.1 禁止 SELECT *
列表查询只取页面所需字段,减少回表数据量和网络传输开销。
// 不推荐
$list = GoodsModel::where('status', 1)->select();
// 推荐
$list = GoodsModel::where('status', 1)
->field('id, name, image, price')
->select();
6.2 避免 N+1 查询
循环中查询数据库是典型性能问题。
// 不推荐
foreach ($orderList as $order) {
$order['goods'] = OrderGoodsModel::where('order_id', $order['id'])->select();
}
// 推荐
$orderList = OrderModel::with('orderGoods')
->where($where)
->select();
6.3 分页查询优化
深分页性能较差:
LIMIT 10000, 20
如果业务允许,可改为游标分页:
SELECT * FROM ls_goods
WHERE id > last_id
AND status = 1
ORDER BY id ASC
LIMIT 20;
6.4 高频查询使用缓存
商城中很多数据并不是每次都必须查库,例如:
- 首页轮播;
- 分类树;
- 热销商品;
- 基础配置;
- 活动状态。
对于这类数据,可以使用 Redis 缓存,降低数据库压力。
七、优化效果验证与持续治理
7.1 优化前后对比
每条 SQL 优化后,应从以下维度验证:
| 验证项 | 说明 |
|---|---|
EXPLAIN |
是否命中索引,type 是否提升,rows 是否下降 |
| SQL 执行时间 | 耗时是否明显降低 |
| 接口响应时间 | 页面或接口整体耗时是否改善 |
| 数据库负载 | CPU、慢查询数量、连接数是否下降 |
7.2 建立慢查询巡检机制
数据库优化不是一次性工作,需要持续治理。建议建立以下机制:
- 每周巡检慢查询日志;
- 对高频慢 SQL 建立优化记录;
- 定期检查冗余索引和无效索引;
- 大表加索引必须评估影响范围;
- 核心接口建立性能告警。
八、LikeShop 单商户版优化建议总结
针对 LikeShop 单商户标准版(PHP),建议按以下优先级优化:
- 先通过日志和
EXPLAIN找到核心慢 SQL; - 优先优化商品列表、订单列表等高频接口;
- 围绕高频查询建立联合索引;
- 对搜索、统计等复杂场景做专项优化;
- 规范代码写法,避免
SELECT *、N+1 查询和深分页; - 高频数据引入缓存,减少数据库压力;
- 建立慢查询巡检机制,持续治理。
九、结语
LikeShop 单商户 PHP 版的数据库性能优化,不是简单地“加几个索引”,而是一个从问题发现、SQL 分析、索引设计、代码优化到持续监控的完整过程。
对于多数 PHP 电商系统来说,只要商品、订单、会员数据持续增长,慢查询就一定会逐渐暴露出来。越早建立慢查询排查机制,越容易控制优化成本;等到数据库压力已经影响交易时,再临时优化,风险和成本都会明显增加。
数据库是电商系统的底座。底座稳定,上层业务才能稳定迭代。