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 id
  FROM 表
  WHERE 条件
  ORDER BY id
  LIMIT 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', '[email protected]'),
(2, 'Jerry', NULL),
(3, 'Bob', '[email protected]');

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;
-- 返回结果:3

SELECT 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规范》

相关推荐
编程、小哥哥13 天前
Java大厂面试:从Web框架到微服务技术的场景化提问与解析
java·spring boot·微服务·面试·技术栈·数据库设计·分布式系统
聪明的墨菲特i1 个月前
SQL进阶知识:七、数据库设计
数据库·sql·mysql·oracle·db2·数据库设计·范式
Amd7944 个月前
性能优化与调优:全面解析数据库索引
sql·数据库管理·性能调优·索引·数据库优化·数据库设计·查询性能
Amd7944 个月前
彻底理解数据库设计原则:生命周期、约束与反范式的应用
数据建模·数据库优化·数据库设计·反范式·数据库规则·数据库约束·设计生命周期
Amd7944 个月前
深入剖析实体-关系模型(ER 图):理论与实践全解析
数据建模·数据库设计·数据抽象·关系模型·uml 图·er 图·实体-关系模型
Amd7944 个月前
数据库范式详解:从第一范式到第五范式
数据库·数据库设计·第二范式·第一范式·第三范式·范式·bcnf
码农丁丁4 个月前
为什么数据库不应该使用外键
数据库·mysql·oracle·数据库设计·外键
Amd7944 个月前
深入理解检查约束:确保数据质量的重要工具
数据建模·数据验证·数据一致性·数据库设计·数据完整性·数据约束·检查约束
Amd7944 个月前
深入理解唯一约束:确保数据完整性的关键因素
数据建模·关系型数据库·数据一致性·数据库设计·数据完整性·数据约束·唯一约束