📌 今日关键词:SQL 规范、代码审查、反模式、命名规范、SQL Review
大家好,我是 数据库小学妹 👋
之前我们讲过慢查询诊断,把慢SQL改好后能跑通,但在后期维护上却犯了难!
刚转行那会儿,我写的 SQL 长这样:
SELECT a.*, b.name, c.tt
FROM u a, o b, p c
WHERE a.id=b.uid AND b.sid=c.id AND a.st=1
功能没错,跑得也挺快。但同事看了半天,问我三个问题:
a、b、c分别是什么表?tt和st代表什么意思?- 为什么不用 JOIN 而是隐式连接?
我当场石化。其实这也很多新手都会踩的坑,SQL不是能跑起来就行,能跑的SQL和好维护的SQL之间还藏着一个SQL规范。这篇小学妹就把在SQL规范上踩过的坑跟大家分享一下,帮你少走弯路少踩坑!
一、为什么需要规范?
一个场景感受一下
上周你写了一条 SQL,这周产品改需求,你打开代码一看:
SELECT a.x, b.y, a.z FROM t1 a, t2 b WHERE a.id=b.tid AND a.st=1 AND b.tp='A'
你盯着看了 5 分钟,然后默默打开了 git blame——发现是自己写的。
这就是没规范的下场:连自己写的东西都不认识。
规范的 SQL 应该是什么样?
SELECT
orders.order_id,
orders.amount,
customers.customer_name
FROM orders
INNER JOIN customers ON orders.customer_id = customers.id
WHERE orders.status = 'confirmed'
AND customers.type = 'vip';
一眼看过去:操作什么表、关联逻辑、筛选条件——全清楚。
二、命名规范:别让名字成为阅读理解题
2.1 表名
❌ 坏习惯:
-- 缩写到看不懂
SELECT * FROM u; -- user? url? upload?
SELECT * FROM ord; -- orders? ordinary?
SELECT * FROM t1; -- 请问 t1 是什么?
✅ 规范写法:
-- 复数、下划线分隔、见名知义
SELECT * FROM users;
SELECT * FROM orders;
SELECT * FROM order_items;
2.2 字段名
❌ 坏习惯:
-- 单字母、拼音、缩写
SELECT uid, unm, pwd, crt_tm FROM users;
✅ 规范写法:
-- 完整单词、下划线分隔
SELECT user_id, username, password_hash, created_at FROM users;
2.3 别名
❌ 坏习惯:
-- 无意义的单字母
SELECT a.name, b.total FROM users a JOIN orders b ON a.id = b.uid;
✅ 规范写法:
-- 有意义的缩写
SELECT u.name, o.total
FROM users u
JOIN orders o ON u.id = o.user_id;
2.4 统一命名风格
| 类型 | 规范 | 示例 |
|---|---|---|
| 表名 | 复数、小写、下划线 | users、order_items |
| 字段名 | 单数、小写、下划线 | created_at、updated_by |
| 主键 | id 或 表名_id |
id、user_id |
| 外键 | 关联表_关联字段 |
order_id、customer_id |
| 布尔字段 | is_ 前缀 |
is_active、is_deleted |
| 时间字段 | _at 后缀 |
created_at、deleted_at |
| 索引名 | idx_表名_字段名 |
idx_users_phone |
| 唯一索引 | uk_表名_字段名 |
uk_users_email |
三、格式规范:排版好,bug 少
3.1 关键字大写,表名字段名小写
-- ❌ 全部小写,混在一起
select name,email,created_at from users where status=1 order by created_at desc;
-- ✅ 关键字大写,一眼能看到结构
SELECT name, email, created_at
FROM users
WHERE status = 1
ORDER BY created_at DESC;
3.2 每个子句单独一行
-- ❌ 挤在一行
SELECT u.name, o.total, o.created_at FROM users u JOIN orders o ON u.id=o.user_id WHERE o.status='done' AND o.total>100 ORDER BY o.created_at DESC LIMIT 20;
-- ✅ 结构清晰
SELECT u.name, o.total, o.created_at
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 'done'
AND o.total > 100
ORDER BY o.created_at DESC
LIMIT 20;
3.3 逗号放行首
-- ✅ 逗号放前面,方便增删字段
SELECT
user_id
, username
, email
, created_at
, updated_at
FROM users;
这种写法有两个好处:
- 新增字段只加一行,不会因为上个字段忘加逗号报错
- 注释某一行不用处理逗号
3.4 JOIN 必须指定类型,ON 条件紧跟其后
-- ❌ 隐式连接 + WHERE
SELECT u.name, o.total FROM users u, orders o WHERE u.id = o.user_id;
-- ✅ 显式 JOIN,逻辑清晰
SELECT u.name, o.total
FROM users u
INNER JOIN orders o ON u.id = o.user_id;
四、10 大常见反模式:写完就提桶跑路
反模式 1:SELECT *
-- ❌ 坏
SELECT * FROM users WHERE id = 1;
-- ✅ 好
SELECT id, username, email FROM users WHERE id = 1;
为什么不行?
- 表加了字段,你的结果集就变了——代码崩了你都不知道为什么
- 不需要的字段也要传输,浪费 IO 和内存
- 覆盖索引失效(本来查两个字段走索引就够,SELECT * 可能回表)
反模式 2:隐式类型转换
-- ❌ 坏:phone 是 VARCHAR,传入的是数字
SELECT * FROM users WHERE phone = 13800138000;
-- ✅ 好
SELECT * FROM users WHERE phone = '13800138000';
隐式转换会让索引失效。之前监控那篇提到过"Select_scan 增多",很多时候就是这个问题。
反模式 3:COUNT(column) vs COUNT(*)
-- ❌ 误区:以为 COUNT(1) 比 COUNT(*) 快
SELECT COUNT(1) FROM users;
-- ✅ COUNT(*) 和 COUNT(1) 在 MySQL 8.0 性能一样
-- 区别在这:
SELECT COUNT(col) FROM users; -- 不统计 NULL
SELECT COUNT(*) FROM users; -- 统计所有行,包括 NULL
MySQL 8.0 对 COUNT() 做了优化,它会选择短那条二级索引来计数。COUNT(col) 反而要多一个判断是否为 NULL 的步骤。没有特殊需求,直接用 COUNT()。
反模式 4:WHERE 条件中做函数操作
-- ❌ 坏:索引失效!
SELECT * FROM orders WHERE DATE(created_at) = '2026-05-23';
-- ✅ 好:让字段保持裸状态
SELECT * FROM orders
WHERE created_at >= '2026-05-23 00:00:00'
AND created_at < '2026-05-24 00:00:00';
反模式 5:负向查询
-- ❌ 坏:NOT IN、NOT LIKE、!=、<> 通常不走索引
SELECT * FROM users WHERE status != 'deleted';
-- ✅ 好:用 IN 枚举正向条件
SELECT * FROM users WHERE status IN ('active', 'pending', 'frozen');
反模式 6:ORDER BY RAND()
-- ❌ 坏:全表扫描 + 文件排序,数据量大直接跪
SELECT * FROM articles ORDER BY RAND() LIMIT 10;
-- ✅ 好:应用层随机,或取最大 ID 后随机
SELECT MAX(id) INTO @max_id FROM articles;
SELECT * FROM articles WHERE id >= FLOOR(RAND() * @max_id) LIMIT 10;
反模式 7:LIKE '%keyword%' 前缀模糊
-- ❌ 坏:左模糊不走索引
SELECT * FROM articles WHERE title LIKE '%MySQL%';
-- ✅ 好:用全文索引
ALTER TABLE articles ADD FULLTEXT INDEX ft_title (title);
SELECT * FROM articles WHERE MATCH(title) AGAINST('MySQL' IN NATURAL LANGUAGE MODE);
反模式 8:大范围 LIMIT 分页
-- ❌ 坏:OFFSET 1000000 需要扫描前 100 万行
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;
-- ✅ 好:基于上一次查询的最大 ID
SELECT * FROM orders
WHERE id > 1000000
ORDER BY id LIMIT 20;
反模式 9:无脑使用 LEFT JOIN
-- ❌ 坏:不需要外联的场景用了 LEFT JOIN
SELECT u.*, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.status = 'done'; -- 这个 WHERE 直接把 LEFT JOIN 退化成 INNER JOIN
-- ✅ 好:明确逻辑,该用什么用什么
SELECT u.*, o.total
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.status = 'done';
LEFT JOIN 保留左表全部行。但你在 WHERE 里过滤右表字段,等于告诉数据库右表为 NULL 的行我不要——这和 INNER JOIN 逻辑一样,但优化器可能选错执行计划。
反模式 10:字段值与 NULL 的比较
-- ❌ 错:NULL 不能用 =、!= 比较
SELECT * FROM users WHERE deleted_at = NULL; -- 永远返回空
SELECT * FROM users WHERE deleted_at != NULL; -- 也永远返回空
-- ✅ 对
SELECT * FROM users WHERE deleted_at IS NULL;
SELECT * FROM users WHERE deleted_at IS NOT NULL;
NULL 是"不知道",不是"等于空"。NULL = NULL 返回的不是 TRUE,是 UNKNOWN。这是 SQL 新手最容易掉的一个坑。
五、SQL Review 审查清单
![[images/SQL审查四维清单.svg]]
每次提交 SQL 代码前,对着这个清单过一遍:
正确性检查
- [ ] 查询逻辑是否符合需求?
- [ ] JOIN 类型是否正确(INNER/LEFT/RIGHT)?
- [ ] WHERE 条件是否遗漏(特别是软删除标记
deleted_at IS NULL)? - [ ] NULL 值的处理是否正确?
性能检查
- [ ] 有没有 SELECT *?
- [ ] WHERE 条件中的字段有没有索引?
- [ ] 有没有在 WHERE 里对字段做函数操作?
- [ ] 有没有隐式类型转换?
- [ ] JOIN 的关联字段有没有索引?
- [ ] 大表分页有没有用对方式?
- [ ] 有没有不必要的排序(ORDER BY)或分组(GROUP BY)?
规范检查
- [ ] 命名是否清晰(表名、字段名、别名)?
- [ ] 格式是否统一(关键字大写、每个子句换行)?
- [ ] 复杂逻辑有没有注释?
- [ ] 有没有硬编码的魔法数字或日期?
数据安全
- [ ] UPDATE / DELETE 有没有 LIMIT 保护?(防止一波误删)
- [ ] 有没有 WHERE 条件?(防止全表更新)
-- ✅ 安全的写法
UPDATE orders SET status = 'cancelled'
WHERE id = 12345
LIMIT 1; -- 只更新一行,多一行算我输
六、怎么在团队落地?
规范写得再漂亮,落不了地等于白写。分享一下我们团队推规范的方法:
Step 1:先搞一个"最小规范"
别一上来搞 100 条规则,没人看得完。先推 10 条最容易犯的:
- 禁止 SELECT *
- 显式 JOIN,不要隐式连接
- 表名、字段名用下划线命名
- 每个子句单独一行
- 关键字大写
- WHERE 中不对字段做函数操作
- UPDATE/DELETE 必须有 WHERE
- 避免大范围 OFFSET
- NULL 用 IS NULL 判断
- 复杂 SQL 加注释
Step 2:把规范放进 Code Review
提交代码时,Reviewer 对着上面那 10 条看。一开始可能会慢一点,但两周后大家都形成肌肉记忆了。
Step 3:收集"血泪案例"
团队里谁因为规范问题出了事故——记下来。下次有人质疑"为什么这么麻烦",直接把案例甩过去。
我自己的收藏:
同事 A 写
UPDATE orders SET status = 'done'忘了加 WHERE,全表 500 万条订单状态被改。还好有备份,恢复了整整一下午。
Step 4:上工具辅助
手动查总有遗漏,可以用工具兜底:
- SQLFluff:SQL 代码格式化工具,开源免费
- SonarQube:代码质量平台,有 SQL 检测规则
- Inception / Yearning:SQL 审核平台,提交前自动审查
# SQLFluff 使用示例
pip install sqlfluff
sqlfluff lint my_query.sql # 检查规范
sqlfluff fix my_query.sql # 自动格式化
七、今日学习心得
- "能跑"和"能维护"是两码事——三个月后你自己也看不懂自己写的 SQL
- 命名规范是最便宜的注释——名字起好了,注释都可以少写
- 10 大反模式里,SELECT * 和隐式转换是最容易出事的两个
- SQL Review 清单对着查,养成习惯就不会漏
- 规范推落地要小步快跑——先 10 条,再用工具,别一口气搞 100 条
👋 我是 数据库小学妹 一个用设计师思维学数据库的转行人。我们一起,把 SQL 写出工业级质量!💕
本文示例基于 MySQL 8.0 + InnoDB。