主题
MySQL 指南
适用范围
- 读者画像:零 SQL 基础的 Java 后端开发者,想系统掌握 MySQL——从"怎么用对 SQL"到"类型怎么选、索引为什么快"。
- 假定知识:无需 SQL 经验;了解 Java 与关系型数据库的基本概念("库、表、行")即可。
- 本篇视角:两段式——第 1 章讲"SQL 怎么用对"(语句本身),第 2、3 章讲"引擎原理"(类型选型、索引为什么快)。
- 版本口径:MySQL 8.x + InnoDB。5.7 关键差异用
> **MySQL 5.7 差异**callout 就地标注。- 示例锚点:第 1~3 章全程基于自创的
orders+users两张表(1:N),完整 DDL 已前置到「§1.1」,建议先去独立 MySQL 实例建表再结合本文阅读。本仓库sql/init.sql是跨文档共享的 MyBatis 实操脚本,本指南不逐行解读它。- 非目标:不展开事务/锁/MVCC(见 spring-transaction-guide);不写视图、存储过程、触发器、窗口函数等进阶 SQL 特性。
目录
1. SQL 快速上手
SQL 分三类语句:DDL(定义库表结构)、DML(增删改数据)、DQL(查询数据)。本章按"建表 → 填数 → 查数 → 多表查"的顺序,用 orders + users 两张表把基础语句过一遍。第 2、3 章的全部示例沿用本章的表与数据。
1.1 DDL:建库与建表
DDL(Data Definition Language,数据定义语言)——定义数据"存在哪、长什么样":建库、建表、改表结构。
先建库并选中它:
sql
-- 建库(库名自定),显式声明字符集(为什么用 utf8mb4 见 §2.4)
CREATE DATABASE `demo` DEFAULT CHARACTER SET utf8mb4;
USE `demo`;再建本指南全程使用的两张自创表。第一张 orders(订单表):
sql
CREATE TABLE `orders` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_no` VARCHAR(32) NOT NULL COMMENT '订单号(业务唯一)',
`user_id` BIGINT NOT NULL COMMENT '下单用户ID',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0待付1已付2发货3完成',
`amount` DECIMAL(10,2) NOT NULL COMMENT '订单金额',
`remark` VARCHAR(200) DEFAULT NULL COMMENT '备注(可空)',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_status_time` (`user_id`, `status`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';第二张 users(用户表,§1.4 的多表查询要用):
sql
CREATE TABLE `users` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`name` VARCHAR(50) NOT NULL COMMENT '用户名',
`mobile` CHAR(11) DEFAULT NULL COMMENT '手机号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';建表语句速览:每一列 = 列名 + 类型 + 约束/默认值 + 注释。COMMENT 是注释;ENGINE=InnoDB 指定存储引擎;CHARSET=utf8mb4 指定字符集。每列的类型怎么选是第 2 章的内容;NOT NULL/PRIMARY KEY 等约束是 §2.5 的内容。
建表语句里给索引起名用了 uk_/idx_ 前缀——这是业界通行的索引命名约定:
| 前缀 | 索引类型 | 示例 / 说明 |
|---|---|---|
uk_ | 唯一键(Unique Key) | uk_order_no;报错 Duplicate entry ... for key 'uk_order_no' 里出现的就是它 |
idx_ | 普通索引(inDex) | idx_user_status_time,惯例是 idx_ + 按顺序拼列名 |
给已有表加/删索引:
sql
CREATE INDEX idx_status ON orders(status); -- 加单列索引
DROP INDEX idx_status ON orders; -- 删索引改表结构用 ALTER TABLE(加列、改类型),删表用 DROP TABLE——本指南用不到,知道存在这两个语句即可。
1.2 DML:增删改
DML(Data Manipulation Language,数据操纵语言)——对已有表增删改数据。第 2、3 章的示例都基于下面的教学数据,先插入:
sql
-- users:三个用户(id 自增,省略不写)
INSERT INTO `users` (`name`, `mobile`) VALUES
('张三', '13800000001'),
('李四', '13800000002'),
('王五', NULL);
-- orders:三笔订单(user_id 对应 users.id,即 1、2、3;id 自增,省略不写)
INSERT INTO `orders` (`order_no`, `user_id`, `status`, `amount`, `create_time`) VALUES
('A1002', 1, 1, 89.00, '2026-01-02 10:00:00'),
('A1003', 2, 0, 199.00, '2026-01-03 11:00:00'),
('A1004', 1, 2, 55.00, '2026-01-04 12:00:00');语法要点:INSERT INTO 表 (列1, 列2, ...) VALUES (值1, 值2, ...);一次插多行就用逗号分隔多组 VALUES。id 是 AUTO_INCREMENT 自增列,不需要手动赋值——在列清单里省略 id,InnoDB 每次自动分配下一个值(规则见 §2.5)。首次插入后 users、orders 的 id 都是 1、2、3;重复执行会继续往后编号(自增计数器不会因 DELETE 回收旧编号,TRUNCATE TABLE 才会重置,见 §2.5)。
UPDATE 与 DELETE:
sql
-- UPDATE:只把订单 A1002 这一行的状态改成 2
UPDATE orders SET status = 2 WHERE order_no = 'A1002';
-- DELETE:删除指定行(按业务唯一键定位,不依赖自增 id)
DELETE FROM orders WHERE order_no = 'A1004';⚠ 危险警告:
UPDATE/DELETE不带WHERE会命中全表——UPDATE orders SET status = 1会把所有订单状态改掉,DELETE FROM orders会清空整张表。生产环境先SELECT同条件确认命中行数,重要操作放事务里执行(事务见 spring-transaction-guide)。
1.3 单表查询
查询(DQL,Data Query Language)是日常写得最多的语句。骨架:
sql
SELECT 要哪些列
FROM 哪张表
WHERE 哪些行满足条件
ORDER BY 按什么排序
LIMIT 取多少条;① WHERE 条件族——用条件筛行:
| 写法 | 含义 | 示例 |
|---|---|---|
= | 等于 | WHERE status = 1 |
> >= < <= != | 比较 | WHERE amount > 100 |
BETWEEN a AND b | 闭区间 | WHERE amount BETWEEN 50 AND 200 |
IN (...) | 枚举匹配 | WHERE user_id IN (1, 2) |
LIKE 'xx%' | 模糊匹配 | WHERE order_no LIKE 'A1%' |
IS NULL / IS NOT NULL | 判空 | WHERE remark IS NULL |
AND / OR | 组合条件 | WHERE user_id = 1 AND status >= 1 |
② 排序与分页:
sql
-- 按金额从大到小排序,取前 2 条
SELECT order_no, amount FROM orders ORDER BY amount DESC LIMIT 0 2;ORDER BY 列 ASC|DESC(默认升序);LIMIT 偏移, 数量 从 0+1 条数据开始,取出2条数据 (大数据量翻页的性能坑见第 4 章"深分页")。
③ 聚合与分组:
sql
-- 整体统计:共几单、总金额、平均金额
SELECT COUNT(*), SUM(amount), AVG(amount) FROM orders;
-- 按用户分组:查询至少有 2 笔订单的用户
SELECT user_id, COUNT(*) AS order_cnt, SUM(amount) AS total
FROM orders
GROUP BY user_id
HAVING order_cnt >= 2;要点:WHERE 在分组前过滤行,HAVING 在分组后过滤组;聚合函数(COUNT/SUM/AVG/MAX/MIN)配合 GROUP BY 才有分组意义;AS 给结果列起别名。
④ 去重:SELECT DISTINCT user_id FROM orders; 返回出现过的 user_id,重复的只留一个。
1.4 多表查询与子查询
问题:orders 表只存了 user_id,想查出"每笔订单的用户姓名"怎么办?把姓名冗余存在每行订单里会带来不一致(用户改名要改所有订单)。正确做法是用户拆到 users 表、orders 用 user_id 引用 users.id——这就是"关系型数据库"里"关系"的由来。注意两张表的 id 各自独立自增,恰好都从 1 开始,别把 orders.id 和 users.id 混为一谈。
text
users 表 orders 表
┌────┬──────┐ ┌────┬─────────┐
│ id │ name │ │ id │ user_id │
├────┼──────┤ ├────┼─────────┤
│ 1 │ 张三 │◄───────│ 1 │ 1 │
│ 2 │ 李四 │◄───────│ 2 │ 2 │
│ 3 │ 王五 │ │ 3 │ 1 │
└────┴──────┘ └────┴─────────┘JOIN 按"保留哪边的行"分三种:
sql
-- INNER JOIN:只返回两边都匹配的行
SELECT o.order_no, o.amount, u.name
FROM orders o
INNER JOIN users u ON o.user_id = u.id;
-- LEFT JOIN:users 是左表、orders 是右表
-- 左表全保留,右表对不上的补 NULL(王五没订单,仍出现且 order_no 为 NULL)
SELECT u.id, u.name, o.order_no
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;RIGHT JOIN 与 LEFT JOIN 对称(右表全保留)——把两张表位置对调就等价,日常统一用 LEFT JOIN。o/u 是表别名,用来区分两张表的同名列。
子查询——查询里套查询:
sql
-- IN + 子查询:查"有过订单"的用户姓名
SELECT name FROM users WHERE id IN (SELECT DISTINCT user_id FROM orders);
-- 标量子查询(结果只有一行一列):查金额大于平均值的订单
SELECT order_no, amount FROM orders
WHERE amount > (SELECT AVG(amount) FROM orders);收口:JOIN 查出的结果在 Java 侧如何映射成"对象里嵌套对象"(Order 里带 User),属于 ORM 框架的职责,本指南不再展开;第 3 章索引的全部示例回归单表
orders。
2. 数据类型与字符集
建表时每一个列类型、每一个 CHARSET 声明,都会影响后续的存储、索引和查询。这一章讲"怎么选、为什么",最后用约束把建表语句收口。
2.1 数值类型
| 类型 | 字节 | 有符号范围 | 典型用途 |
|---|---|---|---|
TINYINT | 1 | -128 ~ 127 | 状态位、布尔(0/1)、枚举值 |
INT | 4 | 约 -21 亿 ~ 21 亿 | 一般主键、外键、计数 |
BIGINT | 8 | 约 -922 京 ~ 922 京 | 大表主键、雪花 ID、金额分、时间戳毫秒 |
DECIMAL(M,D) | 变长 | 精确 | 金额、利率——绝不用 FLOAT/DOUBLE |
选型要点:
- 主键优先
BIGINT:INT上限 21 亿看着够,但自增主键一旦跳号(自增只增不减,空洞不会回填)或业务暴涨就会触顶,改类型代价极大。 - 金额必须
DECIMAL:FLOAT/DOUBLE是二进制浮点,0.1 + 0.2 != 0.3,做钱会算错。DECIMAL(10,2)表示最多 10 位、小数 2 位,精确存储。 UNSIGNED慎用:无符号把范围翻倍,但两个UNSIGNED相减出现负数会溢出成巨大正数,反而埋雷。
2.2 字符串类型
| 类型 | 适用场景 |
|---|---|
CHAR(n) | 长度固定且常参与比较:手机号、身份证、MD5、国家码 |
VARCHAR(n) | 需 WHERE/索引的文本:姓名、标题、地址、状态 |
TEXT | 不检索的中等文本:备注、说明、JSON 片段 |
选型要点:
VARCHAR(50)的50是字符数不是字节数——utf8mb4下一个汉字/emoji 占 1 个字符(但 3~4 字节),能存 50 个汉字。CHARvsVARCHAR:定长内容用CHAR(省去记录长度的 1~2 字节,且无碎片),如手机号CHAR(11)、身份证CHAR(18);长度不定又要检索的才用VARCHAR,如orders.order_no的VARCHAR(32)。- 别滥用大
VARCHAR:VARCHAR(255)不是免费的,InnoDB 行溢出、内存临时表都按声明长度估算,能小则小。
2.3 时间类型
| 类型 | 字节 | 范围 | 时区行为 |
|---|---|---|---|
DATETIME | 5(8.0) | 1000-01-01 00:00:00 ~ 9999-12-31 23:59:59 | 不带时区,存什么读什么 |
TIMESTAMP | 4 | 1970-01-01 00:00:01 UTC ~ 2038-01-19 UTC | 带时区,按会话时区转换 |
DATE | 3 | 1000-01-01 ~ 9999-12-31 | 仅日期,无时分秒 |
选型要点:
TIMESTAMP有 2038 问题:底层是 32 位 Unix 秒,2038-01-19 后溢出。长期数据用DATETIME。- 跨时区场景:
TIMESTAMP存 UTC、读时按会话时区转换,适合"同一时刻全球统一";DATETIME是"墙上时钟",存进去什么就是什么,适合生日、营业日期这类与时区无关的值。
2.4 字符集与排序规则
字符集(charset) 决定字符怎么编码成字节,排序规则(collation) 决定字符怎么比较和排序。
text
utf8mb3(旧名 utf8):每字符最多 3 字节 —— 存不了 emoji、部分生僻字 ❌
utf8mb4 :每字符最多 4 字节 —— 完整 Unicode,能存 emoji ✅踩坑现场:MySQL 里的
utf8是历史遗留的错误命名,它其实是utf8mb3,最多 3 字节。用户昵称带个 emoji 🙂 就会插入失败或乱码。结论:一律用utf8mb4,建表显式写DEFAULT CHARSET=utf8mb4。
collation 一句话:utf8mb4_general_ci(旧默认,快但不完全准)、utf8mb4_unicode_ci(更准)、utf8mb4_0900_ai_ci(8.0 新默认,基于 Unicode 9.0,ai=不区分重音、ci=不区分大小写)。日常跟随默认即可,除非要做区分大小写的唯一约束。
MySQL 5.7 差异:
utf8mb4的默认排序规则 5.7 是utf8mb4_general_ci,8.0 改为utf8mb4_0900_ai_ci。跨版本迁移时若不显式声明 collation,唯一索引/排序行为可能有细微差异。
2.5 约束
类型定了"这一列存什么",建表语句里还要回答一件事:这一列允许存什么——这就是约束(constraint),数据库在 INSERT/UPDATE 时强制执行的校验规则,不依赖应用代码自觉。三条最常用的:
NOT NULL:必填。列默认可空(不赋值落库即 NULL),所以要显式声明来收紧。orders除备注remark外全部NOT NULL。UNIQUE:值不许重复,一表可建多个;但多个 NULL 互不冲突——业务上必填的唯一键要NOT NULL UNIQUE一起写,光靠 UNIQUE 拦不住空值。orders.order_no就是NOT NULL+UNIQUE。PRIMARY KEY:每行数据的身份证,一表只能有一个;= UNIQUE + NOT NULL。用自增id、不用业务列(业务会变,主键不能变)。
3. 索引
这是全篇最重要的一章,也是基础期必修:不懂索引,写出来的查询在小数据量下全绿,数据一涨就把数据库拖垮——"接口功能正确,但上线后数据库被拖垮"。
四个核心概念层层递进:聚簇索引(数据怎么存)→ 覆盖索引(怎么少读)→ 联合索引 + 最左前缀(怎么组合),最后用"索引失效"收口。
3.0 EXPLAIN:验证走没走索引
建表、加索引的语句在 §1.1 已备齐。现在缺最后一件工具:怎么知道一条查询走没走索引——在查询前加 EXPLAIN:
sql
EXPLAIN SELECT * FROM orders WHERE order_no = 'A1003';输出里看四个关键列:
| 列 | 含义 |
|---|---|
type | 访问方式好坏:const/eq_ref(最优)→ ref(走二级索引)→ range → index → ALL(全表扫描,最差) |
key | 实际使用的索引名,NULL = 没走索引 |
rows | 预估要扫描的行数 |
Extra | 附加信息:Using index(覆盖索引)、Using filesort(额外排序)等 |
§3.3、§3.4 的"命中 vs 失效"对照都用它。
教学示意:本指南所有
EXPLAIN输出均为教学示意,实际输出因数据量/统计信息而异。
3.1 聚簇索引与二级索引:回表
InnoDB 的索引本质是 B+Tree 排序结构——树怎么长不展开,本章关心的是:叶子节点存什么、查询怎么用它。按"叶子存什么"分两种:
text
聚簇索引(主键索引) 二级索引(普通索引/唯一索引)
───────────────────── ─────────────────────
叶子存【整行数据】 叶子存【索引列的值 + 主键值】
主键即表,表和主键索引是一体的 除了主键,其他索引都是二级索引
一张表只有一个聚簇索引 一张表可以有多个二级索引定义有点抽象,看具体数据。orders 表已存入三行(教学示意,略去 remark):
id | order_no | user_id | status | amount | create_time |
|---|---|---|---|---|---|
| 1 | A1002 | 1 | 1 | 89.00 | 2026-01-02 10:00:00 |
| 2 | A1003 | 2 | 0 | 199.00 | 2026-01-03 11:00:00 |
| 3 | A1004 | 1 | 2 | 55.00 | 2026-01-04 12:00:00 |
同一份数据,三棵树里存的内容不一样:
text
① 聚簇索引(按主键 id 组织)——叶子存整行:
key=1 → (1, A1002, 1, 1, 89.00, 2026-01-02 10:00:00)
key=2 → (2, A1003, 2, 0, 199.00, 2026-01-03 11:00:00)
key=3 → (3, A1004, 1, 2, 55.00, 2026-01-04 12:00:00)
② uk_order_no(唯一索引,按 order_no 组织)——叶子存「order_no + 主键」:
A1002 → id=1
A1003 → id=2
A1004 → id=3
③ idx_user_status_time(联合索引,先按 user_id、同值再按 status、最后按 create_time 排序)
——叶子存「三列的值 + 主键」:
(1, 1, 2026-01-02 10:00:00) → id=1
(1, 2, 2026-01-04 12:00:00) → id=3 ← user_id 同为 1,按 status 排
(2, 0, 2026-01-03 11:00:00) → id=2 ← user_id=2 整体靠后三者的差异就在"叶子存什么、按什么排序":
| 索引 | 叶子内容 | 排序依据 | 查整行要回表吗 |
|---|---|---|---|
| 聚簇索引(主键) | 整行所有列 | id | 不用,叶子就是整行 |
uk_order_no | order_no + id | order_no | 要:拿 id 再查 |
idx_user_status_time | user_id + status + create_time + id | 三列依次比较(最左优先) | 要:同上 |
关键推论:用二级索引查询,往往要"回表"——先在二级索引树里找到主键值,再拿主键值去聚簇索引树里查整行数据,两趟树查找:
sql
SELECT * FROM orders WHERE order_no = 'A1003';text
第一趟:走 uk_order_no 树 A1003 → 拿到主键 id=2
↓(回表)
第二趟:走聚簇索引(主键)树 id=2 → 取出整行 (2, A1003, 2, 0, 199.00, ...)走联合索引同理——WHERE user_id = 1 只用到 idx_user_status_time 的最左列,同样命中:
sql
SELECT * FROM orders WHERE user_id = 1;text
第一趟:走 idx_user_status_time 树(只用最左列) user_id=1 → 命中 id=1、id=3
↓(回表两次)
第二趟:走聚簇索引(主键)树 id=1、id=3 → 各取出整行EXPLAIN 里这类查询 type=ref(走了索引,但不是最优)。回表是额外开销——能不能不回表?能,见 §3.2。
3.2 覆盖索引:不回表的查询
覆盖索引(covering index):如果查询要的所有列,二级索引的叶子里都已经有了,就不用回表——一趟树查找搞定。
sql
-- orders 的 idx_user_status_time 叶子里有 (user_id, status, create_time, id)
-- 只查这几列,索引已覆盖,无需回聚簇索引
SELECT id, user_id FROM orders WHERE user_id = 1; -- ✅ 覆盖索引,不回表
SELECT * FROM orders WHERE user_id = 1; -- ❌ 要 amount/remark,必须回表实战价值:SELECT * 是覆盖索引的天敌——它要所有列,必然回表。只查需要的列,既省网络传输,又可能命中覆盖索引。这也是"禁止 SELECT *"这条团队规范背后的性能原因之一。
3.3 联合索引与最左前缀
联合索引(复合索引):一个索引包含多列,如 orders 的 idx_user_status_time (user_id, status, create_time)。它的排序规则是先按第一列排,第一列相同再按第二列排,依次类推——像字典先按首字母、再按第二个字母排序。
text
联合索引 (user_id, status, create_time) 在 B+Tree 里的排列:
(1,0,'…') (1,1,'…') (1,2,'…') │ (2,1,'…') (2,3,'…') │ (3,0,'…')
└──── user_id=1,内部按 status、再按 create_time 有序 ────┘最左前缀原则:查询条件必须从最左列开始、连续使用,索引才生效。对 idx_user_status_time (user_id, status, create_time):
text
✓ WHERE user_id=1 → 命中(用到 user_id)
✓ WHERE user_id=1 AND status=2 → 命中(用到 user_id、status)
✓ WHERE user_id=1 AND status=2 AND create_time>'…' → 命中(三列全用到)
✓ WHERE user_id=1 AND create_time>'…' → 部分命中(只到 user_id;跳过 status,create_time 用不上)
✗ WHERE status=2 → 失效!没有最左的 user_id
✗ WHERE status=2 AND create_time>'…' → 失效!同样缺 user_id范围截断:最左列之后一旦出现范围条件(>/</BETWEEN),它后面的列就用不上了:
text
✗ WHERE user_id=1 AND status>1 AND create_time>'…'
→ user_id 等值命中、status 取范围后,create_time 无法继续利用所以建联合索引时,区分度高的等值列放最左、范围列放最后——本表把 create_time(常被用来查区间)放在三列末尾,正是这个原因。
3.4 索引失效常见写法
即使建了索引,某些写法会让 MySQL 放弃它、退回全表扫描(type=ALL)。三类高频陷阱:
① 对索引列做函数/运算:
sql
-- ❌ 失效:对 create_time 套函数,破坏 B+Tree 的有序性
SELECT * FROM orders WHERE YEAR(create_time) = 2026;
-- ✅ 改写:让索引列"裸奔",把运算移到常量侧
SELECT * FROM orders WHERE create_time >= '2026-01-01'
AND create_time < '2027-01-01';② 隐式类型转换:
sql
-- order_no 是 VARCHAR,传数字会触发隐式转换 → 相当于对列套函数 → 失效
SELECT * FROM orders WHERE order_no = 123456; -- ❌ 数字
SELECT * FROM orders WHERE order_no = '123456'; -- ✅ 字符串,类型一致③ 前导模糊 %:
sql
SELECT * FROM orders WHERE order_no LIKE '%56'; -- ❌ 前导 %,B+Tree 无法定位起点,全表扫描
SELECT * FROM orders WHERE order_no LIKE '12%'; -- ✅ 后缀 %,能用到 uk_order_no(前缀确定)一句话心法:让索引列独立、原样、类型匹配地出现在比较符左侧,别对它做任何加工——加工了就等于把 B+Tree 的有序性破坏了。
4. 进阶预告
以下主题本章仅预告,不展开——它们是"出事/进阶时才需要"的运维与调优知识:
| 主题 | 为什么留到进阶 | 何时需要 / 入口 |
|---|---|---|
| 事务与隔离级别(MVCC/锁) | 引擎并发机制,Spring 侧有完整指南 | 见 spring-transaction-guide.md |
| 锁细节(行锁/间隙锁/死锁/next-key lock) | 抽象自并发事故,无场景硬学易忘 | 遇到锁等待、死锁日志时 |
| 连接池调优(HikariCP 容量/超时/泄漏) | 需要真实压测数据支撑 | 连接不够用、连接泄漏时 |
深分页(LIMIT 100万, 20 为什么慢) | 数据量到百万级才暴露 | 列表页翻到很后面变慢时 |
慢查询定位(慢查询日志、EXPLAIN 深入) | 属于排障技能 | 线上出现慢接口时 |
本章仅预告:事务、锁、连接池、深分页、慢查询的完整讲解不在本指南。本指南覆盖的是"基础期必修"——SQL 用法(第 1 章)、类型与约束(第 2 章)、索引(第 3 章)。
References
- MySQL 8.0 Reference Manual — SQL Statements(DDL/DML/DQL 语句全集)
- MySQL 8.0 Reference Manual — Data Types(类型范围与存储)
- MySQL 8.0 Reference Manual — JOIN Syntax(JOIN 语法与图示)
- MySQL 8.0 Reference Manual — Secondary Indexes and Clustered Indexes(聚簇/二级索引、回表)
- MySQL 8.0 Reference Manual — EXPLAIN Output Format(
type/key/rows/Extra各列含义) - MySQL 8.0 Reference Manual — Character Sets(
utf8mb4与 collation) - 本仓库相关文档:spring-transaction-guide.md(事务与隔离级别)