Java开发经验——阿里巴巴编码规范实践解析7
摘要
本文主要解析了阿里巴巴 Java 开发中的 SQL 编码规范,涉及 SQL 查询优化、索引建立、字符集选择、分页查询处理、外键与存储过程的使用等多个方面,旨在帮助开发者提高代码质量和数据库操作性能,避免常见错误和性能陷阱。
1. 【强制】业务上具有唯一特性的字段,即使是组合字段, 也必须建成唯一索引。
说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。
- 唯一索引的作用:唯一索引能够保证数据库层面数据的唯一性约束,防止重复数据的插入,确保数据一致性和业务正确性。
- 为什么不仅靠应用层校验?:应用层校验可能因并发、网络异常、程序bug等原因出现遗漏,导致重复数据写入。如果没有数据库唯一索引,脏数据(重复、冲突)就难以避免。
- 性能考虑:虽然唯一索引会稍微增加写入时的开销,但通常这种开销是微乎其微的,远远小于因数据重复引发的业务混乱和数据清理成本。
- 组合唯一索引:当唯一约束不是单个字段,而是多个字段的组合时,也必须在数据库层创建组合唯一索引,保证这组字段的联合唯一性。
假设系统中有一张用户表 user
,要求用户的手机号 phone
是唯一的:
CREATE TABLE user (id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,phone VARCHAR(20) NOT NULL,username VARCHAR(50),...UNIQUE KEY uk_phone (phone)
);
即使应用层每次插入数据时都校验手机号是否存在,还是必须在数据库上建立唯一索引 uk_phone
来防止脏数据产生。再举个组合唯一索引的例子:订单系统中,user_id
和 order_no
组合必须唯一:
CREATE TABLE orders (id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,user_id BIGINT UNSIGNED NOT NULL,order_no VARCHAR(50) NOT NULL,amount DECIMAL(10, 2),...UNIQUE KEY uk_user_order (user_id, order_no)
);
这样确保同一个用户的订单号不重复。
总结
- 唯一索引保证数据库层面数据唯一性,是数据质量保障的最后防线。
- 不要只依赖应用层校验,避免因脏数据导致后续业务和数据分析混乱。
- 即使性能开销存在,也远远小于维护脏数据的成本。
2. 【强制】超过三个表禁止 join。 需要 join 的字段,数据类型保持绝对一致; 多表关联查询时, 保证被关联的字段需要有索引。
说明:即使双表 join 也要注意表索引、SQL 性能。
- 限制多表 Join(超过三个表禁止):多表 join 会导致 SQL 查询复杂度显著增加,影响数据库性能和响应时间。超过三张表的 join,尤其是在大数据量环境下,容易导致查询效率低下、锁表、内存消耗高等问题。
- 保持 join 字段数据类型一致:如果 join 字段的数据类型不一致,数据库执行时会进行隐式类型转换,导致索引失效,查询性能严重下降,甚至出错。
- 关联字段必须有索引:索引是数据库快速定位数据的关键。没有索引,join 查询会变成全表扫描,性能极差,特别是数据量大的情况下。
- 即使双表 join 也要关注索引:不是说只有多表 join 才影响性能,双表 join 如果没有索引,同样可能导致慢查询。
假设有三个表:orders
、users
、products
,进行关联查询:
sql复制编辑
SELECT o.id, u.username, p.product_name
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id
WHERE o.status = 1;
要求:
orders.user_id
和users.id
的数据类型完全相同(例如都是BIGINT UNSIGNED
)。orders.product_id
和products.id
的数据类型也完全相同。users.id
、products.id
都是主键,天然有索引。orders.user_id
、orders.product_id
应该建立普通索引(如果业务查询频繁)。
如果要多于三个表关联,比如加上 order_details
,就要考虑是否能拆分查询或优化,否则禁止这么做。
可能的优化建议:
- 如果业务需要超过三个表关联,尽量拆分查询,或者做缓存处理。
- 保证所有 join 字段都建索引。
- 严格检查字段类型,避免隐式转换导致索引失效。
- 使用 SQL 执行计划分析(EXPLAIN)查看 join 是否走索引。
3. 【强制】在 varchar 字段上建立索引时, 必须指定索引长度, 没必要对全字段建立索引, 根据实际文本区分度决定索引长度。
说明: 索引的长度与区分度是一对矛盾体, 一般对字符串类型数据, 长度为 20 的索引, 区分度会高达 90%以上,可以使用 count(distinct left(列名,索引长度)) / count(*) 的区分度来确定。
4. 【强制】页面搜索严禁左模糊或者全模糊, 如果需要请走搜索引擎来解决。
说明:索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
5. 【推荐】如果有 order by 的场景, 请注意利用索引的有序性。order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 filesort 的情况,影响查询性能。
正例:where a = ? and b = ? order by c;索引:a_b_c
反例:索引如果存在范围查询,那么索引有序性无法利用,如:WHERE a > 10 ORDER BY b;索引 a_b 无法排序。
在执行带有 ORDER BY
的查询时,MySQL 会尝试利用索引的有序性来避免额外的排序(filesort),从而提升查询性能。
5.1.1. 关键点总结
- 索引的顺序决定排序能否利用索引:组合索引(如
(a, b, c)
)是有顺序的,MySQL只能根据索引前缀部分利用有序性。 ORDER BY
的字段必须是索引的连续后缀,且放在索引的最后
-
- 比如索引
(a, b, c)
,WHERE a=? AND b=? ORDER BY c
可以利用索引避免 filesort。 - 这是因为
a
和b
被WHERE
过滤且是等值条件,索引顺序完整,c
排序可以直接利用索引。
- 比如索引
- 范围查询打断索引有序性
-
- 如果
WHERE
条件中出现范围查询(如a > 10
),索引在这个字段之后的顺序无法被利用。 - 例子:
WHERE a > 10 ORDER BY b
,索引(a, b)
无法利用索引顺序进行ORDER BY b
,导致 filesort。
- 如果
- 避免 filesort 影响性能:filesort 是MySQL的额外排序操作,会增加磁盘IO和CPU开销。利用好索引顺序可避免。
5.1.2. 举例说明
查询 | 索引 | 是否能避免 filesort? | 原因 |
|
| 是 | 等值条件过滤,索引有序可直接排序 |
|
| 否 | 范围查询 破坏索引有序,无法用索引排序 |
|
| 是 |
为等值条件, 索引顺序可用 |
|
| 否 |
被跳过,索引顺序断裂 |
5.1.3. 优化建议
- 设计索引时,考虑查询中
WHERE
和ORDER BY
的字段顺序,尽量让等值过滤字段排在前面,排序字段紧随其后。 - 避免在索引的前缀字段上使用范围查询,否则后续字段的排序将无法利用索引。
- 结合
EXPLAIN
分析查询计划,确认是否出现了Using filesort
,及时调整索引。
6. 【推荐】利用覆盖索引来进行查询操作, 避免回表。
说明:如果一本书需要知道第 11 章是什么标题,会翻开第 11 章对应的那一页吗?目录浏览一下就好,这个目录就是起到覆盖索引的作用。
正例:能够建立索引的种类分为主键索引、唯一索引、普通索引三种,而覆盖索引只是一种查询的一种效果,用 explain的结果,extra 列会出现:using index。
覆盖索引是指:查询的所有列(SELECT、WHERE、ORDER BY 中涉及的列)都在同一个索引中,MySQL 无需回表即可返回结果。
6.1.1. 回表与覆盖索引的区别
类型 | 行为描述 | 性能影响 |
回表查询 | 先用二级索引定位主键,再去主键索引找完整记录 | 需要更多I/O,性能较低 |
覆盖索引查询 | 所有字段直接在索引中拿到,无需访问主表 | 性能更高,I/O更少 |
6.1.2. 覆盖索引的判断方式
使用 EXPLAIN
查看执行计划时,Extra
列出现:
- ✅
Using index
(使用了覆盖索引)
❌Using where; Using index
(部分使用索引,但仍可能回表)
6.1.3. 举例说明
表结构:
CREATE TABLE user (id INT PRIMARY KEY,name VARCHAR(50),age INT,email VARCHAR(50),INDEX idx_name_age (name, age)
);
覆盖索引查询(不回表):
SELECT name, age FROM user WHERE name = 'Tom';
-- ✅ name 和 age 都在 idx_name_age 中,形成覆盖索引
回表查询(需要访问主表):
SELECT email FROM user WHERE name = 'Tom';
-- ❌ email 不在 idx_name_age 中,需通过主键回表获取 email
6.1.4. 优化建议
- SELECT 尽量只取必要字段,避免使用
SELECT *
,更容易利用覆盖索引。 - 组合索引包含 SELECT 和 WHERE 字段,能最大化覆盖索引的利用。
- 覆盖索引尤其适用于:高频读操作 + 查询字段固定少量 + 读性能要求高 的场景。
7. 【推荐】利用延迟关联或者子查询优化超多分页场景。
说明:MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。
正例:先快速定位需要获取的 id 段,然后再关联:
SELECT t1.* FROM 表 1 as t1 , (select id from 表 1 where 条件 LIMIT 100000 , 20) as t2 where t1.id = t2.id
你提到的这条优化建议是大分页性能优化中非常重要的一种方式,适用于 LIMIT offset, N
中 offset 非常大的情况。下面我将从原理、适用场景、示例 SQL、数据库设计时的思考等方面进行深入讲解,帮助你真正理解并能在系统设计中灵活运用。
7.1. ✅ 核心问题:大 offset 分页效率低
在 MySQL 中,分页语句如:
SELECT * FROM 表 WHERE 条件 LIMIT 100000, 20;
MySQL 的执行方式是:
- 扫描前 100020 条记录。
- 丢弃前 100000 条,仅返回最后的 20 条。
📌 当 offset 非常大(例如上万行),MySQL 会浪费大量资源扫描无用数据,从而导致严重的性能问题。
7.2. ✅ 优化方案:延迟关联 / 子查询方式
思路:先用一个子查询只查 id
,快速定位目标记录,再用主查询根据 id
执行 精确关联查询。
7.2.1. 🔍 示例 SQL(延迟关联方式)
-- 子查询快速定位分页 id
SELECT t1.*
FROM 表 AS t1
JOIN (SELECT idFROM 表WHERE 条件ORDER BY idLIMIT 100000, 20
) AS t2 ON t1.id = t2.id;
📌 优点:
- 子查询只处理 id 字段,数据量少,扫描快。
- 主查询通过 id 精准获取数据,不受 offset 大小影响。
7.3. ✅ 适用场景
- 高频访问的大分页列表(如历史数据、日志、交易明细)。
- 用户下拉加载下一页数据(cursor 模式更佳)。
- 分页数据量巨大(超过 1 万行以上)。
7.4. ✅ 数据库设计时的思考方式
设计数据库和索引时,如果预期存在大量分页跳转需求,可以考虑:
❗避免盲目使用 LIMIT offset, size
- 对于大数据量分页,应使用“基于游标的分页”或“延迟关联”。
✅ 分页基准字段要建索引
- 子查询中
ORDER BY id
、WHERE 条件
中涉及的字段应该建立组合索引。
✅ id 或排序字段设计应具备可预测性(如自增、时间戳)
- 有助于实现“基于最后一条记录”的分页(cursor-based pagination)。
7.5. ✅ 进一步优化方式(基于游标分页)
这是延迟关联的终极形式,适用于用户只“向后翻页”的场景:
-- 使用上一次查询的最大 ID 作为游标
SELECT *
FROM 表
WHERE id > 上一次最大 id
ORDER BY id
LIMIT 20;
👍 优点:
- 不依赖 offset,查询永远是 LIMIT N,性能稳定。
- 前提是 id 单调递增,且分页顺序与 id 保持一致。
7.6. 总结一句话:
大分页千万不能硬跳 offset,延迟关联或游标分页是优化之道。
📌 记住分页优化的三个“不要”:
- 不要在大 offset 上直接分页;
- 不要 SELECT *(避免回表);
- 不要忽略索引对排序字段的支持。
8. 【推荐】SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,如果可以是 const 最好。
说明:
- consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。
- ref 指的是使用普通的索引(normal index)。
- range 对索引进行范围检索。
反例:explain 表的结果,type = index,索引物理文件全扫描,速度非常慢,这个 index 级别比较 range 还低,与全表扫描是小巫见大巫。
9. 【推荐】建组合索引的时候,区分度最高的在最左边。
正例:如果 where a = ? and b = ?,a 列的几乎接近于唯一值,那么只需要单建 idx_a 索引即可。
说明:存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如:where c > ? and d = ? 那么即使c 的区分度更高,也必须把 d 放在索引的最前列,即建立组合索引 idx_d_c。
10. 【推荐】防止因字段类型不同造成的隐式转换,导致索引失效。
11. 【参考】创建索引时避免有如下极端误解:
- 索引宁滥勿缺。认为一个查询就需要建一个索引。
- 吝啬索引的创建。认为索引会消耗空间、严重拖慢记录的更新以及行的新增速度。
- 抵制唯一索引。认为唯一索引一律需要在应用层通过“先查后插”方式解决。
12. 【强制】不要使用 count(列名) 或 count(常量) 来替代 count(*),count(*) 是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。
说明:count(*) 会统计值为 NULL 的行,而 count(列名) 不会统计此列为 NULL 值的行。
12.1. ✅ 语义区别:COUNT(*)
与 COUNT(列名)
根本不同
方式 | 统计内容 | 是否统计 NULL |
| 统计 所有行数(包含 NULL) | ✅ 是 |
| 统计该列 非 NULL 的行数 | ❌ 否 |
或 | 与 一样(MySQL特性)但不标准 | ✅ 是 |
12.2. ✅ SQL92 标准推荐使用 COUNT(*)
它表示“行级别计数”,而非某个列的计数,是最安全、语义最明确、兼容性最好的写法。
12.3. ✅ 示例对比
假设有如下数据表:
CREATE TABLE user (id INT PRIMARY KEY,name VARCHAR(50),email VARCHAR(100)
);INSERT INTO user (id, name, email) VALUES
(1, 'Tom', 'tom@example.com'),
(2, 'Jerry', NULL),
(3, 'Bob', 'bob@example.com');
12.3.1. 使用 COUNT(*)
SELECT COUNT(*) FROM user;
-- 返回结果:3
✅ 所有行都统计,无论 email
是否为 NULL。
12.3.2. 使用 COUNT(email)
sql复制编辑
SELECT COUNT(email) FROM user;
-- 返回结果:2
❌ 只有 email 不为 NULL 的两条数据被统计。
12.3.3. 使用 COUNT(1)
或 COUNT('x')
SELECT COUNT(1) FROM user;
-- 返回结果:3SELECT COUNT('abc') FROM user;
-- 返回结果:3
✅ 和 COUNT(*)
在 MySQL 下等价,但并不是标准写法,可读性差,不推荐。
12.4. ✅ 三、为什么推荐强制使用 COUNT(*)
- ✔ 语义最清晰:表示“统计总行数”,没有歧义。
- ✔ 不会遗漏 NULL 行:在数据质量不一致时,避免错误理解。
- ✔ 最具通用性:SQL92 标准定义,各数据库平台支持度最高。
- ❌
COUNT(列名)
容易被误用,统计结果可能出错(尤其在报表场景)。
12.5. ✅ 数据库设计/开发中的实践建议
场景 | 推荐使用方式 |
统计表的总记录数 |
|
判断某字段是否有非空数据的记录数 |
|
多表关联后用于计数逻辑 |
|
ORM 框架中用 count 查询 | 显式指定 |
总结一句话:COUNT(*)
是最安全的计数方式,应作为默认使用。除非你明确知道自己只统计非 NULL 字段。
13. 【强制】count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col1 , col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0。
14. 【强制】当某一列的值全是 NULL 时,count(col) 的返回结果为 0;但 sum(col) 的返回结果为 NULL,因此使用 sum() 时需注意 NPE 问题。
正例:可以使用如下方式来避免 sum 的 NPE 问题:SELECT IFNULL(SUM(column) , 0) FROM table;
你这条规范非常重要,特别是在财务统计、数据报表等场景中,SUM(NULL) ≠ 0
常常是导致空指针异常(NPE)或业务逻辑错误的隐性原因。
14.1. ✅ COUNT(col)
与 SUM(col)
的行为差异
函数 | NULL 全部出现时的行为 | 是否易出错 | 原因说明 |
| 返回 | ✅ 安全 | 忽略 NULL,返回行数为 0 |
| 返回 | ❌ 危险 | 无法参与求和,返回 NULL(不是 0) |
14.2. ✅ 案例对比
假设表 transaction
:
id | amount
---|--------
1 | NULL
2 | NULL
14.2.1. COUNT(amount)
SELECT COUNT(amount) FROM transaction;
-- 结果:0 ✅
14.2.2. SUM(amount)
SELECT SUM(amount) FROM transaction;
-- 结果:NULL ❌(不是 0)
在 Java、Python、Go、JS 等语言中将 NULL
映射为数值时,往往无法自动转换为 0,会触发异常或逻辑错误。
14.3. ✅ NPE 避坑推荐写法
-- 避免 null 问题,强制设定默认值为 0
SELECT IFNULL(SUM(amount), 0) AS total_amount
FROM transaction;
其他数据库中的写法(等价):
数据库 | 函数 |
MySQL |
|
PostgreSQL |
|
Oracle |
|
SQL Server |
|
14.4. ✅ 为什么这点在系统设计中很重要?
- 业务误差:财务类、积分类、流量类报表中,“空”不能代表 0,会导致账目对不上。
- 程序崩溃:后端取数据库结果为 NULL,没有判断就使用
.intValue()
或加法,容易出现 NPE。 - 前端展示异常:如果传回 NULL,不做处理显示为“空白”,用户体验差。
15. 【强制】使用 ISNULL() 来判断是否为 NULL 值。
说明:NULL 与任何值的直接比较都为 NULL。
- NULL<>NULL 的返回结果是 NULL,而不是 false。
- NULL=NULL 的返回结果是 NULL,而不是 true。
- NULL<>1 的返回结果是 NULL,而不是 true。
反例:在 SQL 语句中,如果在 null 前换行,影响可读性。
select * from table where column1 is null and column3 is not null;而 ISNULL(column) 是一个整体,简洁易懂。从性能数据上分析,ISNULL(column) 执行效率更快一些。
16. PageHelper
和 LIMIT
是 MySQL 分页中常见的两种实现方式?
16.1. 基本定义
名称 | 类型 | 简介 |
| SQL语法 | 原生 MySQL 分页语法,形式为 |
| Java插件(MyBatis) | 第三方分页插件,自动拦截 MyBatis 的查询语句,自动拼接分页逻辑 |
16.2. 使用方式对比
LIMIT
分页示例(SQL层)
SELECT * FROM user ORDER BY id LIMIT 20, 10;
-- 表示从第 21 条开始,取 10 条
通常你需要自己手动计算 offset:
int offset = (pageNum - 1) * pageSize;
16.3. ✅ PageHelper
分页示例(Java层)
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectAll(); // 会自动拼接 LIMIT 分页 SQL
PageHelper 会自动:
- 计算 offset
- 拼接
LIMIT
- 自动执行
SELECT COUNT(*)
获取总条数(可配置)
16.4. ✅ 核心区别
项目 | PageHelper | LIMIT |
作用层级 | Java 代码层(MyBatis 插件) | SQL 层(数据库原生) |
是否自动分页 | ✅ 自动拦截并分页 | ❌ 需要自己写分页 SQL |
是否自动统计总条数 | ✅ 自动执行 count 查询(可关闭) | ❌ 需要手动写 count SQL |
使用成本 | 高:需要引入依赖、使用特定 API | 低:只用 SQL 即可 |
控制灵活性 | 中:依赖框架,粒度有限 | 高:SQL 自定义能力强 |
性能优化空间 | 中:依赖插件逻辑 | 高:可配合子查询、索引优化等 |
16.5. ✅ 选择建议
场景 | 建议方式 |
使用 MyBatis + 快速开发项目 | ✅ 推荐 PageHelper |
数据量大、分页逻辑复杂、需要优化极致性能 | ✅ 推荐手写 LIMIT |
使用其他 ORM(如 JPA、Hibernate) | ❌ PageHelper 不适用 |
与前端对接灵活分页(如游标分页、延迟关联等) | ✅ 手写 LIMIT 更灵活 |
16.6. ✅ 性能提示
LIMIT offset, size
在 offset 很大时会性能下降(PageHelper 也会遇到相同问题)。- PageHelper 的
PageHelper.startPage()
必须在查询语句前调用,否则不会生效。 - PageHelper 可配置是否执行 count 查询(在某些场景可以关闭提高性能)。
PageHelper 是 LIMIT 的封装和增强,用于 Java 层 MyBatis 自动分页,适合快速开发;而 LIMIT 是底层 SQL 原语,适合需要高性能、复杂控制的分页场景。
17. 【强制】代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句。
17.1. 规则含义:为什么 count = 0
要直接返回?
分页查询一般有两步:
- 先查总数:执行
SELECT COUNT(*) FROM ...
- 再查分页数据:执行
SELECT * FROM ... LIMIT offset, size
当第一步 count = 0
,说明根本就没有数据,第二步分页语句毫无意义,却仍会执行:
- 产生不必要的数据库连接与查询压力
- 增加网络 I/O 与序列化成本
- 增加代码复杂性(后续还得处理空结果)
17.2. ✅ 正确做法(Java 示例,以 MyBatis + PageHelper 为例)
int total = userMapper.countByCondition(condition);
if (total == 0) {return PageResult.empty(); // 或 return Collections.emptyList();
}PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectByCondition(condition);
return new PageResult<>(total, users);
17.3. ✅ 推荐封装分页方法:
public <T> PageResult<T> doPageQuery(Supplier<List<T>> dataQuery, Supplier<Integer> countQuery) {int total = countQuery.get();if (total == 0) {return new PageResult<>(0, Collections.emptyList());}List<T> list = dataQuery.get();return new PageResult<>(total, list);
}
17.4. ✅ 示例说明(分页优化对比)
场景 | 非优化做法 | 优化后做法 |
count = 0 | 执行 2 条 SQL,第二条 LIMIT 语句返回空结果 | 只执行 1 条 COUNT 语句 |
count > 0 | 正常执行分页查询 | 正常执行分页查询 |
总结一句话:分页查询必须先查总数,若为 0 则立刻返回,避免无意义的分页 SQL 查询。
18. 【强制】不得使用外键与级联,一切外键概念必须在应用层解决。
说明:(概念解释)学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的
student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。
18.1. ✅ 外键与级联的概念
外键(Foreign Key)
- 表 A 的字段指向表 B 的主键。
- 用于保证数据引用的完整性。
- 可配置 级联更新(ON UPDATE CASCADE) 和 级联删除(ON DELETE CASCADE)。
例子:
-- 成绩表成绩关联学生表 student_id 外键
ALTER TABLE score ADD CONSTRAINT fk_student_id FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE;
18.2. ❌ 为什么在分布式/高并发环境下禁用外键与级联?
问题类别 | 原因 |
✅ 高耦合 | 表之间强关联,修改主表字段会影响多个从表,破坏模块边界。 |
❌ 可用性风险 | 外键错误会导致插入失败,不能插入“暂时孤儿数据”(先插从表再插主表)。 |
❌ 性能问题 | 插入/更新时需实时检查外键约束,降低写入性能。 |
❌ 阻塞与锁冲突 | 级联更新/删除涉及多个表,事务大、锁多,容易引发阻塞与死锁。 |
❌ 不适合分布式 | 分布式中不同表可能分库分表,外键无法跨节点生效。 |
❌ 数据迁移困难 | 有外键约束的数据不方便导入导出、数据同步时容易失败。 |
18.3. ✅ 最佳实践:在应用层维护逻辑外键
把数据库层的“关系约束”转为应用层的“逻辑约束”。
✅ 正例:学生和成绩表设计
-- student 表
CREATE TABLE student (id BIGINT PRIMARY KEY,name VARCHAR(50)
);-- score 表(没有外键约束)
CREATE TABLE score (id BIGINT PRIMARY KEY,student_id BIGINT, -- 虽然逻辑上关联 student.id,但无外键score INT
);
应用层约束方式:
- 插入成绩前,先检查学生是否存在。
- 删除学生时,先显式删除成绩。
- 通过数据库唯一索引 + 应用校验,避免脏数据。
18.4. ✅ 如何在应用层实现级联逻辑
示例:删除学生时删除对应成绩
@Transactional
public void deleteStudent(Long studentId) {
scoreMapper.deleteByStudentId(studentId); // 先删子表
studentMapper.deleteById(studentId); // 再删主表
}
这样你就掌握了级联逻辑的顺序和可控性,可避免数据库层隐性操作带来的性能和一致性风险。
总结:外键和级联 = 数据库强耦合,适合低并发内网系统;应用层维护关系 = 弱耦合+高性能,适合分布式与互联网架构。
19. 【强制】禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。
20. 【强制】数据订正(特别是删除或修改记录操作)时,要先 select,避免出现误删除的情况,确认无误才能执行更新语句。
21. 【强制】对于数据库中表记录的查询和变更,只要涉及多个表,都需要在列名前加表的别名(或表名)进行限定。
说明:对多表进行查询记录、更新记录、删除记录时,如果对操作列没有限定表的别名(或表名),并且操作列在多个表中存在时,就会抛异常。
正例:select t1.name from first
table as t1 , second
table as t2 where t1.id = t2.id;
反例:在某业务中,由于多表关联查询语句没有加表的别名(或表名)的限制,正常运行两年后,最近在某个表中增加一个同名字段,在预发布环境做数据库变更后,线上查询语句出现出 1052 异常:Column 'name' infield list is ambiguous。
22. 【推荐】SQL 语句中表的别名前加 as,并且以 t1、t2、t3、...的顺序依次命名。
说明:
- 别名可以是表的简称,或者是依照表在 SQL 语句中出现的顺序,以 t1、t2、t3 的方式命名。
- 别名前加 as 使别名更容易识别。
正例:
select t1.name from first
table as t1 , second
table as t2 where t1.id = t2.id;
23. 【推荐】in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在1000 个之内。
23.1. 规范原文理解
IN (...)
操作符适用于小数据量的集合匹配,但当集合过大(> 1000 个元素)时,容易导致性能下降、SQL 解析异常甚至执行失败。因此应尽量避免,实在无法避免时必须控制集合数量。
23.2. IN
的风险与问题点
问题类型 | 原因说明 |
❌ SQL 长度超限 | SQL 文本过长会超过数据库语法限制(如 Oracle 限制 1000 个 |
❌ 查询计划复杂 |
|
❌ 命中索引差 | 索引优化器难以对大 |
❌ 安全隐患 | 大量拼接 |
24. 【参考】因国际化需要, 所有的字符存储与表示,均采用 utf8mb4 字符集,字符计数方法需要注意。
说明:
SELECT LENGTH("轻松工作");--返回为 12
SELECT CHARACTER_LENGTH("轻松工作");--返回为 4
表情需要用 utf8mb4 来进行存储,注意它与 utf8 编码的区别。
25. 【参考】TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少,但 TRUNCATE无事务且不触发 trigger,有可能造成事故,故不建议在开发代码中使用此语句。
说明:TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同。
博文参考
《阿里java规范》