当前位置: 首页 > news >正文

Java开发经验——阿里巴巴编码规范实践解析7

摘要

本文主要解析了阿里巴巴 Java 开发中的 SQL 编码规范,涉及 SQL 查询优化、索引建立、字符集选择、分页查询处理、外键与存储过程的使用等多个方面,旨在帮助开发者提高代码质量和数据库操作性能,避免常见错误和性能陷阱。

1. 【强制】业务上具有唯一特性的字段,即使是组合字段, 也必须建成唯一索引。

说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。

  1. 唯一索引的作用:唯一索引能够保证数据库层面数据的唯一性约束,防止重复数据的插入,确保数据一致性和业务正确性。
  2. 为什么不仅靠应用层校验?:应用层校验可能因并发、网络异常、程序bug等原因出现遗漏,导致重复数据写入。如果没有数据库唯一索引,脏数据(重复、冲突)就难以避免。
  3. 性能考虑:虽然唯一索引会稍微增加写入时的开销,但通常这种开销是微乎其微的,远远小于因数据重复引发的业务混乱和数据清理成本。
  4. 组合唯一索引:当唯一约束不是单个字段,而是多个字段的组合时,也必须在数据库层创建组合唯一索引,保证这组字段的联合唯一性。

假设系统中有一张用户表 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_idorder_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 如果没有索引,同样可能导致慢查询。

假设有三个表:ordersusersproducts,进行关联查询:

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_idusers.id 的数据类型完全相同(例如都是 BIGINT UNSIGNED)。
  • orders.product_idproducts.id 的数据类型也完全相同。
  • users.idproducts.id 都是主键,天然有索引。
  • orders.user_idorders.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. 关键点总结

  1. 索引的顺序决定排序能否利用索引:组合索引(如 (a, b, c))是有顺序的,MySQL只能根据索引前缀部分利用有序性。
  2. ORDER BY 的字段必须是索引的连续后缀,且放在索引的最后
    • 比如索引 (a, b, c)WHERE a=? AND b=? ORDER BY c 可以利用索引避免 filesort。
    • 这是因为 abWHERE 过滤且是等值条件,索引顺序完整,c 排序可以直接利用索引。
  1. 范围查询打断索引有序性
    • 如果 WHERE 条件中出现范围查询(如 a > 10),索引在这个字段之后的顺序无法被利用。
    • 例子:WHERE a > 10 ORDER BY b,索引 (a, b) 无法利用索引顺序进行 ORDER BY b,导致 filesort。
  1. 避免 filesort 影响性能:filesort 是MySQL的额外排序操作,会增加磁盘IO和CPU开销。利用好索引顺序可避免。

5.1.2. 举例说明

查询

索引

是否能避免 filesort?

原因

WHERE a = ? AND b = ? ORDER BY c

(a, b, c)

等值条件过滤,索引有序可直接排序

WHERE a > 10 ORDER BY b

(a, b)

范围查询 a > 10

破坏索引有序,无法用索引排序

WHERE a = ? ORDER BY b

(a, b)

a

为等值条件,b

索引顺序可用

WHERE a = ? ORDER BY c

(a, b, c)

b

被跳过,索引顺序断裂

5.1.3. 优化建议

  • 设计索引时,考虑查询中 WHEREORDER 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 的执行方式是:

  1. 扫描前 100020 条记录
  2. 丢弃前 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 idWHERE 条件 中涉及的字段应该建立组合索引。

✅ 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 最好。

说明:

  1. consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。
  2. ref 指的是使用普通的索引(normal index)。
  3. 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. 【参考】创建索引时避免有如下极端误解:

  1. 索引宁滥勿缺。认为一个查询就需要建一个索引。
  2. 吝啬索引的创建。认为索引会消耗空间、严重拖慢记录的更新以及行的新增速度。
  3. 抵制唯一索引。认为唯一索引一律需要在应用层通过“先查后插”方式解决。

12. 【强制】不要使用 count(列名) 或 count(常量) 来替代 count(*),count(*) 是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。

说明:count(*) 会统计值为 NULL 的行,而 count(列名) 不会统计此列为 NULL 值的行。

12.1. ✅ 语义区别:COUNT(*)COUNT(列名) 根本不同

方式

统计内容

是否统计 NULL

COUNT(*)

统计 所有行数(包含 NULL)

✅ 是

COUNT(列名)

统计该列 非 NULL 的行数

❌ 否

COUNT(1)

COUNT(常量)

COUNT(*)

一样(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. ✅ 数据库设计/开发中的实践建议

场景

推荐使用方式

统计表的总记录数

COUNT(*)

判断某字段是否有非空数据的记录数

COUNT(列名)

多表关联后用于计数逻辑

COUNT(*)

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 全部出现时的行为

是否易出错

原因说明

COUNT(col)

返回 0

安全

忽略 NULL,返回行数为 0

SUM(col)

返回 NULL

危险

无法参与求和,返回 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

IFNULL(SUM(col), 0)

PostgreSQL

COALESCE(SUM(col), 0)

Oracle

NVL(SUM(col), 0)

SQL Server

ISNULL(SUM(col), 0)

14.4. ✅ 为什么这点在系统设计中很重要?

  • 业务误差:财务类、积分类、流量类报表中,“空”不能代表 0,会导致账目对不上。
  • 程序崩溃:后端取数据库结果为 NULL,没有判断就使用 .intValue() 或加法,容易出现 NPE。
  • 前端展示异常:如果传回 NULL,不做处理显示为“空白”,用户体验差。

15. 【强制】使用 ISNULL() 来判断是否为 NULL 值。

说明:NULL 与任何值的直接比较都为 NULL。

  1. NULL<>NULL 的返回结果是 NULL,而不是 false。
  2. NULL=NULL 的返回结果是 NULL,而不是 true。
  3. NULL<>1 的返回结果是 NULL,而不是 true。

反例:在 SQL 语句中,如果在 null 前换行,影响可读性。

select * from table where column1 is null and column3 is not null;而 ISNULL(column) 是一个整体,简洁易懂。从性能数据上分析,ISNULL(column) 执行效率更快一些。

16. PageHelperLIMIT 是 MySQL 分页中常见的两种实现方式?

16.1. 基本定义

名称

类型

简介

LIMIT

SQL语法

原生 MySQL 分页语法,形式为 LIMIT offset, size

PageHelper

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 要直接返回?

分页查询一般有两步:

  1. 先查总数:执行 SELECT COUNT(*) FROM ...
  2. 再查分页数据:执行 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、...的顺序依次命名。

说明:

  1. 别名可以是表的简称,或者是依照表在 SQL 语句中出现的顺序,以 t1、t2、t3 的方式命名。
  2. 别名前加 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 个 in 参数)

❌ 查询计划复杂

IN 集合过大,数据库生成执行计划开销大,执行效率低

❌ 命中索引差

索引优化器难以对大 IN 集合选择最佳执行路径,导致无法高效命中索引

❌ 安全隐患

大量拼接 IN (...) 参数易引发 SQL 注入和执行失败

24. 【参考】因国际化需要, 所有的字符存储与表示,均采用 utf8mb4 字符集,字符计数方法需要注意。

说明:

SELECT LENGTH("轻松工作");--返回为 12
SELECT CHARACTER_LENGTH("轻松工作");--返回为 4
表情需要用 utf8mb4 来进行存储,注意它与 utf8 编码的区别。

25. 【参考】TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少,但 TRUNCATE无事务且不触发 trigger,有可能造成事故,故不建议在开发代码中使用此语句。

说明:TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同。

博文参考

《阿里java规范》

相关文章:

  • 【stm32开发板】原理图设计(电源部分)附:设计PCB流程
  • sql查询中in不生效的问题
  • 【SQL Server Management Studio 连接时遇到的一个错误】
  • 额度年审领域知识讲解
  • 中间表/中转表笔记
  • ⚽【足球数据全维度解析】从基础统计到高阶分析,数据如何重塑现代足球?
  • 用 Python 模拟下雨效果
  • [智能算法]蚁群算法原理与TSP问题示例
  • 软考-系统架构设计师-第二章 嵌入式基础知识
  • 计算机总线技术深度解析:从系统架构到前沿演进
  • 软考-系统架构设计师-第七章 软件工程基础知识
  • MySQL 8.0中的mysql.ibd文件
  • 6个月Python学习计划 Day 8 - Python 函数基础
  • PCIe走线注意事项
  • [250529] CrateDB 5.10.7 发布:一系列重要修复与升级注意事项
  • 红 黑 树
  • 在windows环境下安装Nmap并使用
  • MySQL 数据库调优指南:提升性能的全面策略
  • Android Studio 解决报错 not support JCEF 记录
  • 面向低端设备的移动网页调试策略:WebDebugX 在性能瓶颈分析中的应用
  • 加强网站内容建设的意见/百度一下你就知道原版
  • seo优化心得/网站怎么seo关键词排名优化推广
  • wordpress 360 google/宁波seo营销平台
  • 河北省建设厅注册中心网站/商品标题seo是什么意思
  • 网站建设所需人力/百度服务
  • wordpress新用户站点/推广专员