这个情况其实是 MySQL 查询设计和索引使用问题导致的典型性能瓶颈
我们先分析一下你的 SQL、瓶颈原因,再讲优化思路,最后给出几种实用的解决方案。👇
一、SQL 性能瓶颈分析
你的慢 SQL:
SELECT COUNT(*) AS think_count
FROM (
SELECT count(*) AS think_count
FROM `eb_user` `u`
LEFT JOIN `eb_wechat_user` `w` ON `u`.`uid` = `w`.`uid`
GROUP BY `u`.`uid`
) `_group_count_`
问题点:
- GROUP BY + COUNT 嵌套子查询: 内层 GROUP BY 会对 200w 用户执行一次分组,再计算每组数量,生成临时表,代价极高。
- LEFT JOIN: eb_wechat_user 若也有大量记录,JOIN 会放大数据量,尤其当索引不合理时。
- 外层 COUNT(): 实际只需要统计行数,却先让内层生成完整的 200w 分组结果。
- 没有用索引覆盖: 如果 u.uid 或 w.uid 没有合适索引,MySQL 只能全表扫描 + 文件排序。
结果:执行时间 32.7 秒,非常合理(对于200万行 JOIN + GROUP BY)。
二、MySQL 执行逻辑简化图
eb_user (200w) ---LEFT JOIN--> eb_wechat_user (~200w)
| |
| |
+---> 按 uid 分组 -> 临时表 -> 外层 COUNT(*)
每个阶段都在全表扫描,磁盘IO、排序、分组都耗时。
三、优化方向
1. 去掉不必要的子查询
如果只是想统计用户数量,其实根本不需要子查询:
SELECT COUNT(DISTINCT u.uid)
FROM eb_user u
LEFT JOIN eb_wechat_user w ON u.uid = w.uid;
这条语句能直接用索引优化。
2. 确保关键字段有索引
确保下面的索引存在:
ALTER TABLE eb_user ADD INDEX idx_uid(uid);
ALTER TABLE eb_wechat_user ADD INDEX idx_uid(uid);
如果 uid 已经是主键,就不用加。
3. 不用 LEFT JOIN 时改为 INNER JOIN
如果逻辑上允许(比如每个用户在 wechat 表中必定有记录):
SELECT COUNT(DISTINCT u.uid)
FROM eb_user u
JOIN eb_wechat_user w ON u.uid = w.uid;
INNER JOIN 比 LEFT JOIN 快得多(MySQL 优化器可提前剪枝)。
4. 避免实时 COUNT 大表
后台列表一般只需展示分页数据和总数,可以:
- 分页查询 + 预估总数 SELECT * FROM eb_user LIMIT 0, 20; 总数通过缓存或定时任务更新。
- 或使用 EXPLAIN + 查询缓存表 每隔5分钟更新一次 user_count 缓存表,前端不实时查 COUNT。
5. 调整 SQL 写法让优化器可用索引
MySQL 优化器对 COUNT(DISTINCT) + 索引优化很敏感。
-- 优化版 1:直接使用 DISTINCT
SELECT COUNT(DISTINCT u.uid)
FROM eb_user u
LEFT JOIN eb_wechat_user w ON u.uid = w.uid;
-- 优化版 2:只查主表统计
SELECT COUNT(*) FROM eb_user;
如果业务上只统计用户总数,这条最快
6. 分页数据展示时优化 JOIN
后台用户列表只需显示部分字段,可以:
SELECT u.uid, u.nickname, w.openid
FROM eb_user u
LEFT JOIN eb_wechat_user w ON u.uid = w.uid
ORDER BY u.uid DESC
LIMIT 0, 20;
再加上:
ALTER TABLE eb_user ADD INDEX idx_uid_desc(uid DESC);
四、针对 ThinkPHP 场景(你的 SQL 日志看起来是 ThinkPHP 生成的)
如果你用的是 ThinkPHP ORM:
问题代码示例
Db::name('user')
->alias('u')
->leftJoin('wechat_user w', 'u.uid=w.uid')
->count();
ThinkPHP 会生成你贴的那种“二层 COUNT”语句。
解决方法
直接使用:
Db::query("SELECT COUNT(DISTINCT u.uid) as total FROM eb_user u LEFT JOIN eb_wechat_user w ON u.uid=w.uid");
或
Db::name('user')->count(); // 仅统计主表数量
五、如果确实需要统计JOIN分组数量
那就使用 临时表 + 预聚合方案:
CREATE TABLE user_wechat_summary AS
SELECT u.uid, COUNT(w.uid) AS wechat_count
FROM eb_user u
LEFT JOIN eb_wechat_user w ON u.uid = w.uid
GROUP BY u.uid;
然后后台查询直接:
SELECT COUNT(*) FROM user_wechat_summary;
通过定时任务每晚刷新一次这个表,性能可以提升百倍以上。
六、总结优化建议清单
| 问题 | 优化方案 |
|---|---|
| 查询结构嵌套 COUNT | 改为 COUNT(DISTINCT) |
| JOIN 未建索引 | 给 uid 加索引 |
| LEFT JOIN 无必要 | 改成 INNER JOIN |
| COUNT 大表慢 | 改成缓存统计 |
| GROUP BY 全表扫描 | 预聚合中间表 |
| ORM 自动生成的复杂 SQL | 改写为原生 SQL |
针对 200w 用户列表的分页+缓存方案(含 SQL + ThinkPHP 实现示例)

