student:10,000 行,主鍵 student_idstudent_to_class:20,000 行,每生 2 門課,外鍵 student_id 無索引SELECT a.student_id, student_name, COUNT(*) FROM student a LEFT JOIN student_to_class b ON a.student_id = b.student_id GROUP BY a.student_id;
EXPLAIN 结果: +----+-------------+-------+------+---------------+------+---------+------+-------+----------------------------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+------+-------+----------------------------------------------+ | 1 | SIMPLE | a | ALL | NULL | NULL | NULL | NULL | 10649 | Using temporary; Using filesort | | 1 | SIMPLE | b | ALL | NULL | NULL | NULL | NULL | 20287 | Using where; Using join buffer (Block Nested)| +----+-------------+-------+------+---------------+------+---------+------+-------+----------------------------------------------+
現象: 兩表均 type=ALL 全表掃描,驅動表 1 萬行 × 被驅動表 2 萬行 = 2 億次比較,耗時 75.467 秒。
ALTER TABLE student_to_class ADD INDEX idx_student_id (student_id);
EXPLAIN 结果: +----+-------------+-------+------+---------------+-----------------+---------+------+------+----------------------------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+-----------------+---------+------+------+----------------------------------------------+ | 1 | SIMPLE | a | ALL | NULL | NULL | NULL | NULL | 10649| Using temporary; Using filesort | | 1 | SIMPLE | b | ref | idx_student_id| idx_student_id | 5 | func | 1 | Using where; Using index | +----+-------------+-------+------+---------------+-----------------+---------+------+------+----------------------------------------------+
關鍵變化:
type: ALL → refkey: idx_student_id 生效rows: 20287 → 1(每次驅動僅掃 1 行)Using index 覆蓋索引,無需回表| 指標 | 優化前 | 優化後 | 提升 |
|---|---|---|---|
| 執行時間 | 75.467 s | 0.077 s | 980 倍 |
| 被驅動表掃描行數 | 20,287 | 1 (per loop) | 20,000 倍 |
| 連接算法 | Block Nested Loop | Index Nested Loop Join | 質變 |