這個情況其實是 MySQL 查詢設計和索引使用問題導致的典型性能瓶頸
我們先分析一下你的 SQL、瓶頸原因,再講優(yōu)化思路,最后給出幾種實用的解決方案。??
一、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 用戶執(zhí)行一次分組,再計算每組數(shù)量,生成臨時表,代價極高。
- LEFT JOIN: eb_wechat_user 若也有大量記錄,JOIN 會放大數(shù)據(jù)量,尤其當索引不合理時。
- 外層 COUNT(): 實際只需要統(tǒng)計行數(shù),卻先讓內層生成完整的 200w 分組結果。
- 沒有用索引覆蓋: 如果 u.uid 或 w.uid 沒有合適索引,MySQL 只能全表掃描 + 文件排序。
結果:執(zhí)行時間 32.7 秒,非常合理(對于200萬行 JOIN + GROUP BY)。
二、MySQL 執(zhí)行邏輯簡化圖
eb_user (200w) ---LEFT JOIN--> eb_wechat_user (~200w)
| |
| |
+---> 按 uid 分組 -> 臨時表 -> 外層 COUNT(*)
每個階段都在全表掃描,磁盤IO、排序、分組都耗時。
三、優(yōu)化方向
1. 去掉不必要的子查詢
如果只是想統(tǒng)計用戶數(shù)量,其實根本不需要子查詢:
SELECT COUNT(DISTINCT u.uid)
FROM eb_user u
LEFT JOIN eb_wechat_user w ON u.uid = w.uid;
這條語句能直接用索引優(yōu)化。
2. 確保關鍵字段有索引
確保下面的索引存在:
ALTER TABLE eb_user ADD INDEX idx_uid(uid);
ALTER TABLE eb_wechat_user ADD INDEX idx_uid(uid);
如果 uid 已經(jīng)是主鍵,就不用加。
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 優(yōu)化器可提前剪枝)。
4. 避免實時 COUNT 大表
后臺列表一般只需展示分頁數(shù)據(jù)和總數(shù),可以:
- 分頁查詢 + 預估總數(shù) SELECT * FROM eb_user LIMIT 0, 20; 總數(shù)通過緩存或定時任務更新。
- 或使用 EXPLAIN + 查詢緩存表 每隔5分鐘更新一次 user_count 緩存表,前端不實時查 COUNT。
5. 調整 SQL 寫法讓優(yōu)化器可用索引
MySQL 優(yōu)化器對 COUNT(DISTINCT) + 索引優(yōu)化很敏感。
-- 優(yōu)化版 1:直接使用 DISTINCT
SELECT COUNT(DISTINCT u.uid)
FROM eb_user u
LEFT JOIN eb_wechat_user w ON u.uid = w.uid;
-- 優(yōu)化版 2:只查主表統(tǒng)計
SELECT COUNT(*) FROM eb_user;
如果業(yè)務上只統(tǒng)計用戶總數(shù),這條最快
6. 分頁數(shù)據(jù)展示時優(yōu)化 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(); // 僅統(tǒng)計主表數(shù)量
五、如果確實需要統(tǒng)計JOIN分組數(shù)量
那就使用 臨時表 + 預聚合方案:
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;
通過定時任務每晚刷新一次這個表,性能可以提升百倍以上。
六、總結優(yōu)化建議清單
| 問題 | 優(yōu)化方案 |
|---|---|
| 查詢結構嵌套 COUNT | 改為 COUNT(DISTINCT) |
| JOIN 未建索引 | 給 uid 加索引 |
| LEFT JOIN 無必要 | 改成 INNER JOIN |
| COUNT 大表慢 | 改成緩存統(tǒng)計 |
| GROUP BY 全表掃描 | 預聚合中間表 |
| ORM 自動生成的復雜 SQL | 改寫為原生 SQL |
針對 200w 用戶列表的分頁+緩存方案(含 SQL + ThinkPHP 實現(xiàn)示例)

