SQL 规范
TIP
规定 SQL 编写规范,适用于 MySQL 8.4 数据库,包含动态 SQL(MyBatis SQL Provider)编写规范。
设计原则
| 原则 | 说明 |
|---|---|
| 软删除 | 所有查询必须包含 del_flag = 0 条件 |
| 防注入 | 动态参数使用 #{} 占位符,禁止字符串拼接 |
| 索引友好 | 避免在索引列上使用函数或运算 |
| 精简查询 | 只查询需要的字段,禁止 SELECT * |
| 批量操作 | 大数据量操作分批执行,避免长事务 |
通用规范
大小写
- SQL 关键字大写:
SELECT、FROM、WHERE、AND、OR - 表名、字段名小写:
sys_user、user_name - 别名使用驼峰:
user_name userName
sql
-- ✅
SELECT u.id, u.account, u.user_name userName
FROM sys_user u
WHERE u.del_flag = 0 AND u.status = 0;
-- ❌
select u.id,u.account,u.user_name username
from sys_user u
where u.del_flag=0 and u.status=0;缩进与换行
- 每个子句(SELECT、FROM、WHERE)独占一行
- 字段列表每行一个字段
- 条件较多时每个 AND/OR 换行
sql
-- ✅
SELECT
u.id,
u.account,
u.user_name userName,
u.mobile,
u.email
FROM
sys_user u
LEFT JOIN sys_dept d ON d.id = u.dept_id
WHERE
u.del_flag = 0
AND u.status = 0
AND u.dept_id = 1001
ORDER BY
u.create_time DESC;查询规范
禁止 SELECT *
sql
-- ❌ 查询所有字段
SELECT * FROM sys_user WHERE del_flag = 0;
-- ✅ 只查询需要的字段
SELECT id, account, user_name, mobile FROM sys_user WHERE del_flag = 0;必须包含软删除条件
sql
-- ❌ 缺少 del_flag 条件
SELECT id, account FROM sys_user WHERE status = 0;
-- ✅ 包含 del_flag 条件
SELECT id, account FROM sys_user WHERE del_flag = 0 AND status = 0;JOIN 规范
- 明确指定 JOIN 类型(INNER JOIN / LEFT JOIN)
- 每个 JOIN 必须有 ON 条件
- 多表 JOIN 时使用表别名
sql
-- ✅
SELECT
u.id,
u.account,
u.user_name userName,
d.dname deptName
FROM
sys_user u
LEFT JOIN sys_dept d ON d.id = u.dept_id AND d.del_flag = 0
WHERE
u.del_flag = 0;
-- ❌ 缺少 ON 条件
SELECT u.id, d.dname
FROM sys_user u, sys_dept d
WHERE u.dept_id = d.id;分页查询
sql
-- ✅ 使用 MyBatis-Plus Page 分页
@SelectProvider(SysUserSqlProvider.class)
IPage<Map<String, Object>> findUsers(Page<Map<String, Object>> page, String conditions);
-- ✅ SQL 中不需要 LIMIT,由 MyBatis-Plus 自动拼接
public static String findUsers(Page<Map<String, Object>> page, String conditions) {
StringBuffer sql = new StringBuffer();
sql.append("SELECT u.id, u.account FROM sys_user u ");
sql.append("WHERE ").append(conditions);
sql.append(" AND u.del_flag = 0 ");
sql.append("ORDER BY u.create_time DESC");
return sql.toString();
}IN 条件规范
sql
-- ✅ 使用集合参数
SELECT id, account FROM sys_user WHERE id IN (1, 2, 3, 4, 5);
-- ❌ 禁止超长 IN 列表(超过 100 个应分批)
SELECT id, account FROM sys_user WHERE id IN (/* 1000 个 ID */);写入规范
INSERT
sql
-- ✅ 明确指定字段
INSERT INTO sys_user (account, user_name, password, salt, create_by, create_time)
VALUES ('admin', '管理员', 'xxx', 'xxx', 'system', NOW());
-- ❌ 不指定字段
INSERT INTO sys_user VALUES (null, 'admin', ...);UPDATE
sql
-- ✅ 带乐观锁更新
UPDATE sys_user
SET user_name = '新名称',
last_update_by = 'admin',
last_update_time = NOW(),
version = version + 1
WHERE id = 1001
AND version = 5
AND del_flag = 0;
-- ❌ 无条件更新(全表更新风险)
UPDATE sys_user SET status = 1;DELETE(软删除)
sql
-- ✅ 软删除(更新 del_flag)
UPDATE sys_user
SET del_flag = 1,
last_update_by = 'admin',
last_update_time = NOW()
WHERE id = 1001 AND del_flag = 0;
-- ❌ 物理删除(禁止)
DELETE FROM sys_user WHERE id = 1001;动态 SQL Provider 规范
Provider 类结构
java
@Slf4j
public class SysUserSqlProvider implements ProviderMethodResolver {
/**
* 查询用户列表
*/
public static String findUsers(Page<Map<String, Object>> page, String conditions) {
StringBuffer sql = new StringBuffer();
sql.append(" SELECT ");
sql.append(" u.id, ");
sql.append(" u.account, ");
sql.append(" u.user_name userName, ");
sql.append(" FROM ");
sql.append(" sys_user u ");
sql.append(" LEFT JOIN sys_dept d ON d.id = u.dept_id ");
sql.append(" WHERE ");
sql.append(conditions);
sql.append(" AND u.del_flag = 0 ");
sql.append(" ORDER BY u.create_time DESC ");
return sql.toString();
}
}动态条件拼接
java
-- ✅ 使用 StringBuffer 拼接动态条件
public String findUserByCondition(Map<String, Object> param) {
StringBuffer sql = new StringBuffer();
sql.append("SELECT id, account, user_name FROM sys_user ");
sql.append("WHERE del_flag = 0 ");
if (param.get("account") != null) {
sql.append(" AND account = '").append(param.get("account")).append("'");
}
if (param.get("deptId") != null) {
sql.append(" AND dept_id = ").append(param.get("deptId"));
}
return sql.toString();
}防注入
java
-- ❌ 直接拼接用户输入(SQL 注入风险)
String sql = "SELECT * FROM sys_user WHERE account = '" + userInput + "'";
-- ✅ 使用参数化或白名单校验
String sql = "SELECT * FROM sys_user WHERE account = '" + escapeInput(userInput) + "'";
-- ✅ 最佳:使用 MyBatis-Plus 的 QueryWrapper
QueryWrapper<SysUserPO> wrapper = new QueryWrapper<>();
wrapper.eq("account", userInput); // 自动参数化索引使用规范
避免索引失效
sql
-- ❌ 在索引列上使用函数
SELECT * FROM sys_user WHERE DATE(create_time) = '2024-01-01';
-- ✅ 改为范围查询
SELECT * FROM sys_user WHERE create_time >= '2024-01-01 00:00:00'
AND create_time < '2024-01-02 00:00:00';
-- ❌ 隐式类型转换
SELECT * FROM sys_user WHERE mobile = 1*********0; -- mobile 是 VARCHAR
-- ✅ 类型匹配
SELECT * FROM sys_user WHERE mobile = '1*********0';
-- ❌ 使用 OR 导致全表扫描
SELECT * FROM sys_user WHERE account = 'admin' OR user_name = '管理员';
-- ✅ 改为 UNION
SELECT * FROM sys_user WHERE account = 'admin'
UNION
SELECT * FROM sys_user WHERE user_name = '管理员';LIKE 查询
sql
-- ❌ 前置通配符无法使用索引
SELECT * FROM sys_user WHERE user_name LIKE '%admin%';
-- ✅ 后置通配符可以使用索引
SELECT * FROM sys_user WHERE user_name LIKE 'admin%';事务规范
短事务原则
- 事务中禁止包含网络调用(Feign、HTTP)、文件操作
- 事务执行时间不超过 3 秒
- 批量操作分批提交
java
-- ❌ 事务中包含远程调用
@Transactional
public void saveUser(SysUserPO user) {
sysUserMapper.insert(user);
redisUserFeignService.updateUser(user); // 网络调用在事务中
}
-- ✅ 事务外调用远程服务
public void saveUser(SysUserPO user) {
saveUserInDB(user); // 事务方法
redisUserFeignService.updateUser(user); // 事务外调用
}
@Transactional(rollbackFor = Exception.class)
public void saveUserInDB(SysUserPO user) {
sysUserMapper.insert(user);
}批量操作
java
-- ✅ 分批插入(每批 100 条)
int batchSize = 100;
for (int i = 0; i < list.size(); i += batchSize) {
List<SysUserPO> batch = list.subList(i, Math.min(i + batchSize, list.size()));
sysUserMapper.batchInsert(batch);
}反例与正例
全表扫描
sql
-- ❌ 缺少 del_flag 条件,可能扫描已删除数据
SELECT id, account FROM sys_user WHERE status = 0;
-- ✅ 包含软删除过滤
SELECT id, account FROM sys_user WHERE del_flag = 0 AND status = 0;子查询性能
sql
-- ❌ 相关子查询(每行执行一次子查询)
SELECT u.*,
(SELECT d.dname FROM sys_dept d WHERE d.id = u.dept_id) deptName
FROM sys_user u
WHERE u.del_flag = 0;
-- ✅ 改为 JOIN
SELECT u.*, d.dname deptName
FROM sys_user u
LEFT JOIN sys_dept d ON d.id = u.dept_id AND d.del_flag = 0
WHERE u.del_flag = 0;分页深度翻页
sql
-- ❌ 深度翻页性能差
SELECT id, account FROM sys_user WHERE del_flag = 0 LIMIT 1000000, 20;
-- ✅ 使用游标分页(记录上一页最大 ID)
SELECT id, account FROM sys_user WHERE del_flag = 0 AND id > 1000000 ORDER BY id LIMIT 20;SQL Checklist
提交 SQL 或 Provider 代码前自查:
- 查询包含
del_flag = 0条件 - 禁止
SELECT *,只查询需要的字段 - 字段别名使用驼峰命名
- JOIN 有明确的 ON 条件
- 动态参数防注入处理
- 避免在索引列上使用函数
- 批量操作分批执行
- 事务中无远程调用
- 深度翻页使用游标分页