适用读者:能写
SELECT但不熟悉 DDL/函数/字符集细节的人;要补"命令背后的语义"的人。
前置:S11-B01(一条 SQL 怎么被执行)、B03(查询基础)、S08(MyBatis)。
覆盖旧文:2782–2815(MySQL 教程命令碎片 30 篇)+2792(函数)+2802(基本语法)+2812(运算符)→ 36 篇 / 约 33 万字符。
扩展点:E-S11-019(DDL 五类语句的边界与锁行为)、E-S11-020(函数与运算符的三类陷阱:NULL、隐式转换、时区)、E-S11-021(字符集与排序规则的实际影响)。
下一篇:S11-J07 · 多表查询、子查询与聚合。
1. 一句话结论
旧教程把 SQL 拆成一条条命令来教,于是丢掉了三件事:DDL 的元数据锁、NULL 的三值逻辑、字符集对比较与索引的影响。 这三件事才是生产事故的来源。
出处:
A-2782–A-2815(命令碎片)、A-2792(函数)、A-2802(基本语法)、A-2812(运算符)。
2. DDL 五类语句与锁行为(E-S11-019)
CREATE DATABASE shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; -- 8.0 默认
CREATE TABLE t (id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL) ENGINE=InnoDB;
ALTER TABLE t ADD COLUMN age INT NULL, ALGORITHM=INPLACE, LOCK=NONE; -- 8.0 支持在线加列
DROP TABLE t;
TRUNCATE TABLE t; -- DDL,不可回滚,重置自增
| 语句 | 是否可回滚 | 元数据锁(MDL) | 典型阻塞场景 |
|---|---|---|---|
CREATE |
DDL 隐式提交 | 短暂 | 名字冲突 |
ALTER |
否(8.0 有 INSTANT/INPLACE 分级) | 需 MDL 排他 | 长事务持有 MDL 读锁 → 后续查询全排队 |
DROP / TRUNCATE |
否 | 排他 | 大表 DROP 需清 buffer pool,可能抖动 |
RENAME |
否 | 排他 | — |
判据:线上 DDL 的真正风险不是"改表慢",是"拿不到 MDL 导致全表查询排队"——所以变更前必须查 information_schema.innodb_trx 有无长事务。
-- 变更前检查:有没有长事务会挡住 MDL
SELECT trx_id, trx_started, trx_mysql_thread_id, trx_query
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60;
8.0 的三种 ALTER 算法:
| 算法 | 是否重建表 | 是否允许并发 DML | 例子 |
|---|---|---|---|
INSTANT |
否(只改元数据) | ✅ | 末尾加列、改默认值(8.0.12+) |
INPLACE |
可能(重建索引) | 多数允许 | 加索引、改索引 |
COPY |
是(全表复制) | ❌ | 改列类型、改字符集 |
3. 数据类型与字符集(E-S11-021)
| 类型 | 要点 | 坑 |
|---|---|---|
VARCHAR(n) |
n 是字符数(utf8mb4 下最多 4n 字节) | 索引长度限制:单列索引 ≤ 3072 字节(DYNAMIC 行格式) |
CHAR(n) |
定长,尾部空格被去掉(比较时) | 不要用于变长内容 |
TEXT/BLOB |
行外存储 | 不能有默认值;大字段拖慢扫描 |
DECIMAL(p,s) |
精确小数 | 金额必须用它,不要 FLOAT/DOUBLE |
DATETIME vs TIMESTAMP |
前者不随时区变;后者存 UTC 并按会话时区转换 | TIMESTAMP 上限 2038 |
utf8 vs utf8mb4 |
utf8 是 3 字节(不能存 emoji) |
8.0 默认已是 utf8mb4;老库要迁移 |
字符集影响的不只是存储:
-- 排序规则决定比较与索引可用性
WHERE name = 'Alice' -- 若列与常量排序规则不同 → 可能无法用索引(隐式转换)
ORDER BY name -- utf8mb4_0900_ai_ci 不区分大小写/重音(ci = case-insensitive)
判据:"查询突然不走索引"的一个隐蔽原因是排序规则不匹配(列 utf8mb4_general_ci,常量按 utf8mb4_0900_ai_ci 比较)。
4. 函数与运算符的三类陷阱(E-S11-020)
4.1 NULL 的三值逻辑
SELECT NULL = NULL; -- NULL(不是 true)
SELECT NULL <> NULL; -- NULL
WHERE col <> 'x' -- 不包含 col IS NULL 的行!
WHERE col IS NOT NULL AND col <> 'x' -- ✅ 正确
COUNT(*) vs COUNT(col) -- 后者不统计 NULL
SUM/AVG 忽略 NULL -- AVG 的分母是非 NULL 行数
坑 1:NOT IN (子查询含 NULL) 会返回空集——这是最经典的静默错误。改 NOT EXISTS。
4.2 隐式类型转换
-- phone 是 VARCHAR,索引在 phone 上
WHERE phone = 13800000000 -- ❌ 常量是数字 → 列被转成数字 → 索引失效
WHERE phone = '13800000000' -- ✅
判据:"字符串列传数字"和"数字列传字符串"都会毁掉索引(见 J02 §索引失效)。
4.3 时间函数与时区
NOW() -- 会话时区
UTC_TIMESTAMP()-- UTC
CURDATE() -- 日期部分
DATE_ADD(d, INTERVAL 7 DAY)
TIMESTAMPDIFF(SECOND, a, b)
| 坑 | 说明 |
|---|---|
NOW() 依赖会话 time_zone |
应用与 DB 时区不一致 → 统计口径错 |
WHERE DATE(create_time) = '2026-11-07' |
函数包裹列 → 索引失效,改范围查询 |
TIMESTAMP 存 UTC |
展示层要统一转换,别在 SQL 里混用 |
4.4 常用函数速查(旧教程的函数章节压缩)
| 类别 | 函数 | 备注 |
|---|---|---|
| 字符串 | CONCAT、CONCAT_WS、SUBSTRING、REPLACE、TRIM、LPAD |
CONCAT 遇 NULL 返回 NULL |
| 数值 | ROUND、CEIL/FLOOR、MOD、ABS |
ROUND 是四舍五入(银行家舍入需注意版本) |
| 日期 | DATE_FORMAT、STR_TO_DATE、LAST_DAY、DATEDIFF |
格式化会影响索引 |
| 聚合 | COUNT/SUM/AVG/MIN/MAX、GROUP_CONCAT |
GROUP_CONCAT 默认长度 1024,需调 group_concat_max_len |
| 窗口 | ROW_NUMBER/RANK/DENSE_RANK、LAG/LEAD、SUM() OVER |
8.0 才有;旧文按 5.7 写的示例需改写 |
| JSON | JSON_EXTRACT、->>、JSON_TABLE(8.0) |
函数索引可加速(8.0.13+) |
5. 分组与排序的三个边界
SELECT dept, COUNT(*) c FROM emp GROUP BY dept HAVING c > 5 ORDER BY c DESC LIMIT 10;
| 问题 | 答 |
|---|---|
WHERE 与 HAVING 差别 |
WHERE 在分组前过滤行;HAVING 在分组后过滤组 |
ONLY_FULL_GROUP_BY |
8.0 默认开启:SELECT 的非聚合列必须在 GROUP BY 中 |
ORDER BY 与索引 |
排序列与索引顺序一致才能免排序;ORDER BY RAND() 必排序 + 全表 |
LIMIT 深分页 |
LIMIT 1000000, 20 要扫 100 万行 → 用游标(见 S25-J09 §7.1) |
6. 对照实验:utf8 vs utf8mb4
-- MySQL 8.0.36
CREATE TABLE t1 (s VARCHAR(10)) CHARSET=utf8; -- utf8mb3
INSERT INTO t1 VALUES ('😀'); -- ❌ Incorrect string value
CREATE TABLE t2 (s VARCHAR(10)) CHARSET=utf8mb4;
INSERT INTO t2 VALUES ('😀'); -- ✅
SELECT LENGTH('😀'), CHAR_LENGTH('😀'); -- 4, 1
口径:上例为按文档推断的行为(utf8mb3 无法表示 4 字节字符),本机未跑;如需引用请自测。
7. 反例:六种"能跑但会出事"的写法
| 写法 | 后果 |
|---|---|
WHERE DATE(t) = '2026-11-07' |
索引失效,全表扫 |
WHERE phone = 13800000000 |
隐式转换毁索引 |
NOT IN (SELECT ...) 子查询含 NULL |
返回空集 |
SELECT * 生产查询 |
传输放大 + 覆盖索引失效 |
utf8 存 emoji |
插入报错或截断 |
无长事务检查就 ALTER |
MDL 阻塞全表查询 |
8. 自测
- 三种 ALTER 算法?哪种允许并发 DML?
- 为什么线上 DDL 的主要风险是 MDL?
NOT IN遇 NULL 会怎样?怎么改?- 字符集/排序规则如何影响索引使用?
LIMIT 1000000,20为什么慢?
答案
- INSTANT/INPLACE/COPY;INSTANT 与多数 INPLACE 允许。
- 拿不到 MDL 会让后续查询排队,表现为"整表不可用"。
- 返回空集;改
NOT EXISTS。 - 列与常量排序规则不一致会触发转换,导致无法用索引。
- 需先扫描并丢弃前 100 万行;用游标或二次查询。
9. 下一篇
S11-J07 · 多表查询、子查询与聚合 —— 内/外连接的语义差异、ON 与 WHERE 的位置、子查询的三种形态与性能、GROUP BY 的执行顺序,以及教材章练习题的答案补写。
本篇属于系列《S11 · 数据库与 SQL》。旧文来源:A-2781–2815(命令碎片)、A-2792、A-2802、A-2812;扩展点 E-S11-019/020/021。教材练习原为题干,答案由本系列补写(新增内容)。