加索引是最快见效的手段,但需按WHERE条件、SELECT字段和JOIN字段协同设计单列、复合、覆盖三类索引,并确保统计信息准确以避免驱动表选错或全表扫描。

加索引是最快见效的手段,但只加ON字段还不够,得看WHERE条件和SELECT字段一起设计复合索引。
为什么INNER JOIN在千万级表上会慢成“顺序扫描”
数据库执行INNER JOIN时,若连接字段(如orders.user_id)没索引,优化器只能退化为嵌套循环(Nested Loop),对每行驱动表数据都全表扫描被驱动表——1000万 × 1000万行 = 10¹⁴次比对,根本不是“慢”,是卡死。你看到的EXPLAIN里type: ALL或rows接近表总行数,就是这个信号。
- 本地快、线上慢,99%是因为本地表小,没触发全表扫描;生产环境统计信息过期也会误导优化器选错算法
- 即使加了单列索引,如果查询还带
WHERE status = 'completed'或要SELECT amount, created_at,仍可能回表,I/O翻倍 - MySQL默认用
Nested Loop,PostgreSQL/SQL Server在内存够时倾向Hash Join,但前提是连接字段可哈希——NULL值多、字段太长都会降级
必须建的三类索引:单列、复合、覆盖
别只给user_id建单列索引。按实际查询结构分层建:
-
单列索引:确保两边连接字段都有,例如
CREATE INDEX idx_orders_user_id ON orders(user_id)和CREATE INDEX idx_users_id ON users(id)——这是底线,缺一不可 -
复合索引:把
WHERE过滤条件塞进索引最左前缀,比如查询带WHERE orders.status = 'completed',就建CREATE INDEX idx_orders_user_status ON orders(user_id, status) -
覆盖索引:把
SELECT里用到的字段也包含进去,避免回表,例如CREATE INDEX idx_orders_cover ON orders(user_id, status) INCLUDE (amount, created_at)(PostgreSQL语法)或 MySQL 用CREATE INDEX idx_orders_cover ON orders(user_id, status, amount, created_at)
验证是否生效:跑EXPLAIN ANALYZE,看到type: ref或eq_ref、key显示你建的索引名、Extra含Using index才算到位。
驱动表选错会让索引白建
优化器通常能自动选小表当驱动表,但前提是统计信息准确。千万级表如果长期没ANALYZE TABLE(MySQL)或VACUUM ANALYZE(PostgreSQL),优化器会误判行数,强行用大表驱动小表——索引再好,外层循环1000万次,性能照样崩。
- 手动干预方式(MySQL):
SELECT /*+ STRAIGHT_JOIN */ u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id WHERE o.status = 'completed',强制users为驱动表 - 更稳妥的做法是先过滤再关联:把大表的
WHERE条件下推到子查询,例如JOIN (SELECT user_id, amount FROM orders WHERE status = 'completed') o ON u.id = o.user_id,让参与JOIN的orders行数从千万级压到几万 - 注意:如果
users表本身也很大(比如用户ID范围稀疏),且orders里user_id选择性高(重复少),那orders反而更适合当驱动表——关键看rows估算值,不是看表名
真正卡住性能的往往不是JOIN语法本身,而是索引没覆盖WHERE+SELECT+JOIN三者组合,或者统计信息陈旧导致优化器“瞎指挥”。上线前跑一遍EXPLAIN ANALYZE,盯着rows和Extra字段,比猜逻辑靠谱得多。


















