适用读者:能写出结果但不确定"为什么这么写"的人。
前置:S11-B02。
实测环境:MySQL 8.0.46(s11lab,20,000 行,2026-11-05)。
覆盖旧文:2598(296) 1734(168) 1733(93) 1218(85) 1221(70) 1225(66) 1228(54) 1226(42) 1224(28) 1222(52) 1736(149) 844(70) 2594(479) 1227(79) 1223(79) = 1,810 点。
覆盖扩展:E-S11-004(书写顺序 vs 执行顺序)。
上一篇:S11-B02 · 建表:数据类型、字符集与范式
下一篇:S11-B04 · 增删改与表管理。
1. 一句话结论
SQL 的书写顺序是 SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT,但执行顺序是 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT。
这条顺序解释了三件事:
| 现象 | 原因 |
|---|---|
WHERE 里不能直接用 SELECT 的别名 |
WHERE 早于 SELECT |
HAVING 可以用聚合结果,WHERE 不行 |
GROUP BY 早于 HAVING |
ORDER BY 可以用别名 |
ORDER BY 晚于 SELECT |
2. 五种 LIKE 与通配符
| 写法 | 含义 |
|---|---|
like 'abc%' |
前缀匹配(可走索引) |
like '%abc' |
后缀匹配(不可走索引范围) |
like '%abc%' |
包含(索引失效,J02) |
like 'a_c' |
_ 匹配单字符 |
连续 % |
%% 与 % 等价(MySQL 会规整) |
实测(J02 会展开):name like 'u1%' → type=range;name like '%u1%' → type=index(全索引扫描)——都不是"完全不用索引",但代价差很多。
3. 聚合与分组
select city, count(*) cnt, avg(age) from t_user group by city having cnt > 500 order by cnt desc limit 5;
| 子句 | 作用 | 注意 |
|---|---|---|
GROUP BY |
分组 | 非聚合列必须出现在 group by 中(ONLY_FULL_GROUP_BY) |
HAVING |
分组后过滤 | 能用 WHERE 就别用 HAVING(后者无法用索引) |
count(*) / count(1) / count(列) |
计数 | count(列) 不计 NULL(A-844) |
判据:"这个条件能提前到分组前吗?" 能 → WHERE(更快)。
4. JOIN 与子查询
| 类型 | 说明 |
|---|---|
INNER JOIN |
只保留匹配 |
LEFT JOIN |
左表全保留 |
子查询(IN/EXISTS) |
语义清晰,优化器可能改写成 JOIN |
| 派生表 | from (select ...) t |
实践判据:
需要"过滤后关联" → 子查询/CTE(可读性好)
需要"关联后过滤" → JOIN + WHERE
数据量大 → 看 EXPLAIN 再决定(优化器常会重写)
5. 分页:LIMIT 与深翻
select id,name from t_user order by id limit 10 offset 10; -- 正常
select id,name from t_user order by id limit 10 offset 19990; -- 深翻
实测(20,000 行):
offset 10 → 29 ms
offset 19990 → 54 ms
| 观察 | 说明 |
|---|---|
| 深翻确实更慢 | 但要扫过前 19,990 行 |
| 差距不是"数量级" | 2 万行规模下只是约 1.9× |
| 数据量上去后会放大 | 百万行时 offset 越大越糟 |
判据:"用户会翻到很深的页吗?" 不会 → limit offset 够用;会 → 游标分页(S08-J01)。
6. UNION 与 UNION ALL
| 写法 | 去重 | 性能 |
|---|---|---|
UNION |
✅(隐含排序) | 慢 |
UNION ALL |
❌ | 快 |
判据:"两个结果集可能有重复吗?" 不可能 → 用 UNION ALL(省一次去重)。
7. 五个误区
| 误区 | 真相 |
|---|---|
"WHERE 能用 SELECT 别名" |
不能(执行顺序) |
"HAVING 和 WHERE 一样" |
HAVING 在分组后,无法用索引 |
"count(列) 等于 count(*)" |
count(列) 不计 NULL |
"UNION 总是更好" |
有去重开销,能确定不重复就用 UNION ALL |
| "深翻只是慢一点" | 数据量大时是线性恶化 |
8. 自检
- 写出执行顺序(七个阶段)。
- 为什么
WHERE不能用SELECT别名? - 三种
LIKE写法各自能否用索引? count(列)与count(*)的差别?UNION与UNION ALL怎么选?
9. 下一篇
S11-B04 · 增删改与表管理
查询讲完,下一篇讲写入侧:truncate/delete/drop 的三层差别,以及 ALTER 的代价。
本篇属于系列《S11 · 数据库与 SQL》。旧文来源:A-2598/1734/1733/1218/1221/1225/1228/1226/1224/1222/1736/844/2594/1227/1223;扩展点 E-S11-004。深翻计时为 MySQL 8.0.46 实测(2 万行)。