1. 先记住 10 条核心结论
- 不是"数据量小就一定快",2 万行如果执行计划错了,一样能慢到分钟级。
- 多表关联是否快,核心不在 JOIN 语法,而在驱动表和索引是否匹配。
- ORDER BY 想快,最好让排序列来自驱动表,并且能命中索引。
- LIMIT 想真正生效,必须尽量做到"先排序、先取前 N,再回表/再补关联"。
- 多列排序要配复合索引,单列索引通常不够。
- LEFT JOIN 后按右表字段排序,最容易退化成 Using filesort。
- WHERE、JOIN、ORDER BY 三者要一起设计索引,不能各管各的。
- 分页列表和 count(*) 应该分开优化,count 往往比 list 更容易被忽略。
- EXPLAIN 比"我觉得应该走索引"更重要。
- 优化 SQL,本质是在减少:扫描行数、排序行数、回表次数、临时表和 filesort。
2. 多表关联为什么会慢
典型慢点有 6 个:
- 驱动表选错
- 你想按 storage_size 排,却从 dataset 起表,数据库就很可能先扫 dataset,再去关联 storage,最后统一排序。
- 排序发生在 JOIN 之后
- 这时不是排 2 万条,而是排"JOIN 后的结果集",中间还可能有重复放大。
- 索引不匹配排序
- 例如 SQL 是:
但你只有:
这通常不够。
- LEFT JOIN + 按右表字段排序
- 优化器经常保守处理,容易 filesort。
- 一对多 JOIN 导致结果膨胀
- 主表 2 万行不代表结果还是 2 万行。
- 条件、排序、关联列各自有索引,但不是同一个复合索引
- MySQL 很多时候只能高效用一个主索引路径。
3. 先理解执行顺序
一条常见 SQL:
数据库不是"看起来这样写,就一定先按 b.score 排"。
它更可能做的是:
- 扫 a
- 按 a.id = b.a_id 找 b
- 拼出结果集
- 对整个结果排序
- 取前 10
如果 b.score 的索引没法直接参与排序,就会看到:
- Using filesort
- Using temporary
这时候再小的数据量也能慢。
4. 多表 JOIN 的通用优化原则
4.1 先确定"主诉求"
SQL 只有三种主诉求:
- 先筛选主表
- 比如"查某个用户的订单列表"
- 先按关联表排序
- 比如"按存储大小排数据集"
- 先按关联表筛选
- 比如"查某路径下的数据集"
不同诉求,驱动表通常不同。
4.2 谁负责排序,谁尽量做驱动表
比如:
- 按 dataset.create_time 排序:通常 dataset 适合作驱动表
- 按 dataset_storage.storage_size 排序:通常 dataset_storage 适合作驱动表
这是你这次从 1 分钟降到 1 秒的核心原因。
4.3 先取主键,再补字段
最稳的分页套路:
- 在最合适的表上筛选 + 排序 + limit
- 只取 id
- 再回表查详情
- 再补市场、权限、用户信息等
这比"大 SQL 一把梭"稳定得多。
5. 索引设计原则
5.1 复合索引顺序
通用顺序:
- 等值过滤列
- 排序列
- 次排序列 / 关联列
- 需要覆盖的少量字段
例如:
建议索引:
如果 SQL 是:
建议索引:
5.2 单列索引什么时候不够
SQL:
如果只有:
通常不如:
5.3 排序索引要看"完整 ORDER BY"
如果 SQL 是:
只给 storage_size 建索引,不一定能避免排序。
5.4 范围条件会截断索引能力
例如:
这时索引能否同时兼顾过滤和排序,要看列顺序和优化器选择,很多时候会顾此失彼。
6. 多表关联的常见场景与最佳写法
6.1 场景一:主表分页,关联表只展示,不参与排序
适合:
- 列表按主表时间排序
- 关联表只是展示补充字段
推荐:
优点:
- 先把分页做小
- 避免全量 join 再 limit
6.2 场景二:按关联表字段排序
适合:
- 按 storage_size
- 按 file_count
- 按 score
推荐:
优点:
- 排序发生在最应该排序的表上
- 更容易用到排序索引
6.3 场景三:关联表既参与筛选又参与排序
推荐:
关键点:
- 让排序列所在表主导排序
- 过滤条件也尽量下推到子查询
7. LEFT JOIN、INNER JOIN、EXISTS 怎么选
7.1 INNER JOIN
适合:
- 你只要有匹配记录的数据
- 想让关联表参与筛选或排序
- 更利于优化器选择高效计划
7.2 LEFT JOIN
适合:
- 主表必须保留,即使关联表没有记录
- 展示性补字段
不适合:
- 高频排序还按右表列排
- 很容易拖慢
7.3 EXISTS
适合权限判断、是否存在判断:
通常优于:
前提是有索引:
8. 多表分页最常见的 8 个坑
- 先 join 再 limit
- 先排序全量结果再 limit
- count 也 join 一堆无关表
- 一对多 join 后没去重
- 按右表字段排序却从左表起表
- 排序列没有和过滤列组成复合索引
- like '%xxx%' 让索引失效
- 以为"有索引就一定走索引"
9. 如何判断索引有没有真正生效
看 EXPLAIN 重点关注:
- type
- key
- rows
- Extra
重点解释:
- Using filesort
- 说明排序没完全走索引
- Using temporary
- 说明临时表参与了处理
- ALL
- 全表扫
- ref / range / const
- 一般比 ALL 好
- rows
- 预估扫描行数,太大就危险
你最想看到的是:
- 排序子查询的驱动表就是排序列所在表
- key 命中预期复合索引
- Extra 没有明显 Using filesort
10. 典型索引模板
10.1 主表按时间分页
适用:
10.2 按存储大小排序
适用:
10.3 按文件数排序
10.4 按路径长度排序
10.5 权限过滤
10.6 按 owner + 时间分页
11. count() 的优化原则count 常见错误:
如果 count 根本不依赖 storage 或 market,就不要 join。正确思路:1. 能不 join 就不 join 2. 必须 join 一对多时,使用 count(distinct d.id) 3. 权限过滤优先用 exists 4. count 不要排序 5. count 不要 select 无关列---## 12. 排序和分页的高性能模板## 12.1 主表排序模板
select d.*, s.storage_size
from (
select id
from starlake_dataset
where delete_flag = 0
order by create_time desc, id desc
limit 0, 10
) p
inner join starlake_dataset d on d.id = p.id
left join starlake_dataset_storage s on s.dataset_id = d.id and s.delete_flag = 0;
12.2 关联表排序模板
select d.*, p.storage_size
from (
select s.dataset_id, s.storage_size
from starlake_dataset_storage s
where s.delete_flag = 0
order by s.storage_size desc, s.dataset_id desc
limit 0, 10
) p
inner join starlake_dataset d on d.id = p.dataset_id and d.delete_flag = 0;
12.3 带条件的关联表排序模板
select d.*, p.storage_size
from (
select s.dataset_id, s.storage_size
from starlake_dataset_storage s
inner join starlake_dataset d on d.id = s.dataset_id and d.delete_flag = 0
where s.delete_flag = 0
and d.owner = ?
and d.dataset_type = ?
order by s.storage_size desc, s.dataset_id desc
limit ?, ?
) p
inner join starlake_dataset d on d.id = p.dataset_id;