測試環境
- OS: Linux
- DB: MySQL 5.5.18
- Table: InnoDB 存儲引擎
- 表結構:主鍵
id (INT)、二級索引 group_id (INT)、普通列 user_id (INT)、可空列 col_null (INT NULL)
實測結果與執行計劃分析
| SQL | 使用索引 | 掃描行數 | 耗時 | 結果 |
SELECT COUNT(*) FROM group_message | 二級索引 group_id (最短) | 852,226 | ~0.3s | 852226 |
SELECT COUNT(1) FROM group_message | 二級索引 group_id (最短) | 852,226 | ~0.3s | 852226 |
SELECT COUNT(group_id) FROM group_message | 二級索引 group_id | 852,226 | ~0.3s | 852226 |
SELECT COUNT(user_id) FROM group_message | 組合索引 (含 user_id) | 852,226 | ~0.4s | 852226 |
SELECT COUNT(col_null) FROM group_message | **全表掃描 (聚簇索引)** | 852,226 | ~1.2s | 1 (僅 1 非空) |
核心結論
COUNT(*) = COUNT(1) = COUNT(非空索引列):優化器識別為「統計行數」,自動選擇**最短的二級索引**掃描 (InnoDB 主鍵即數據,掃描主鍵即掃描全行,開銷最大)。性能**完全一致**。
COUNT(可空列) 最慢:因可空列不在索引中 (或索引不包含 NULL),無法走索引掃描,退化為**聚簇索引全表掃描** (讀取完整行數據),I/O 開銷最大。**且不統計 NULL 值**,結果與業務「總行數」不符。
- 主鍵索引最「重」**:InnoDB 聚簇索引葉子節點包含完整行數據,掃描主鍵等同掃描全表。二級索引葉子節點僅含索引列+主鍵,體積小得多。
- 優化器智慧**:
COUNT(user_id) 走組合索引而非全表掃描,證明優化器能識別「統計非空列行數」可利用包含該列的最小索引。
生產選型建議
-- ✅ 統計表總行數:首選 COUNT(*),語義清晰、標準兼容、性能最優
SELECT COUNT(*) FROM `table_name`;
-- ✅ 統計某非空列非空行數:COUNT(col) 走該列索引
SELECT COUNT(`not_null_col`) FROM `table_name`;
-- ❌ 統計可空列非空行數:全表掃描,慎用大表
SELECT COUNT(`nullable_col`) FROM `table_name`;
-- 大表建議:維護計數表 / Redis 計數器 / 觸發器異步更新
避坑指南
- 不要信「COUNT(1) 比 COUNT(*) 快」的傳言:MySQL 內部將
COUNT(*) 優化為「不檢查列值,直接計數」,與 COUNT(1) 完全等價。
- 大表統計行數方案**:
- 信息庫估算:
SELECT TABLE_ROWS FROM information_schema.TABLES WHERE TABLE_SCHEMA='db' AND TABLE_NAME='t'; (誤差大,僅參考)。
- 計數表:單獨表存計數,觸發器/應用層維護,讀取 O(1)。
- Redis 緩存:定時任務
SELECT COUNT(*) 回寫 Redis,業務讀 Redis。
- 近似值:
SHOW TABLE STATUS LIKE 't'; 的 Rows 欄位。
- 事務隔離級別影響**:RR 級別下
COUNT(*) 可能因 MVCC 看到不同快照行數,但通常可接受。