在上篇《筑基篇》中,我们带着大家从零搭建了MySQL环境,吃透了核心数据类型、企业级建表规范,熟练掌握了单表增删改查与基础权限管理。相信很多同学学完之后,都会遇到这样的瓶颈:
- 单表查询写得溜,但真实业务里用户、订单、商品、分类拆分在不同表中,一到关联查询就无从下手,JOIN写出来要么结果不对,要么重复数据满天飞?
- 为了实现复杂业务,写了一堆嵌套子查询,SQL又臭又长,不仅可读性极差,跑起来还巨慢,完全不知道怎么优化?
- 面试被问事务的ACID、隔离级别,只会背概念,真到线上出现并发数据不一致的问题,完全不知道怎么排查和解决?
- 听说过窗口函数、CTE这些MySQL8.0的高级特性,但不知道怎么用,也不知道能解决什么业务痛点?
别慌,这不是你学的不好,而是你还没打通MySQL进阶的核心关卡。本篇作为系列的第二篇------进阶篇,我们将完全承接上篇的基础,从业务开发最常用的多表关联查询入手,一步步带你吃透MySQL进阶核心特性,搞懂事务的底层实现原理,让你能独立写出复杂业务场景下的高质量SQL,同时搞定面试中100%会问到的进阶核心考点。
系列整体回顾与中篇规划
系列整体进度回顾
- 上篇(筑基篇):MySQL核心认知、环境全场景搭建、核心数据模型、基础SQL语法(DDL/DML/DQL基础/DCL)、单表查询全解
- 中篇(进阶篇,本篇):多表关联查询全解、子查询与高级查询特性、视图/存储过程/触发器、事务核心原理与ACID、InnoDB引擎核心架构
- 下篇(精通篇):索引底层原理与设计规范、锁机制、MVCC多版本并发控制、执行计划解析、SQL性能优化、集群架构、备份与恢复
- 附加篇(八股文全集):从基础到高阶全覆盖的MySQL面试题,附标准答案与答题思路,适配校招、社招全场景
本篇核心学习目标
学完本篇,你将彻底突破SQL入门瓶颈,达到企业级开发的MySQL进阶水平:
- 完全吃透多表关联查询的所有用法,能独立写出业务中99%场景的关联SQL,避开JOIN的所有高频坑点
- 熟练使用子查询、CTE、窗口函数等高级特性,用简洁优雅的SQL实现复杂业务逻辑
- 搞懂视图、存储过程、触发器的用法与适用场景,明确生产环境的使用红线,避免踩坑
- 彻底吃透事务的ACID特性、隔离级别与底层实现,能解决并发场景下的数据一致性问题
- 深入理解InnoDB存储引擎的核心架构,搞懂redo log、undo log、binlog的核心区别与作用,搞定面试高频考点
第一章 核心进阶:多表关联查询全解,搞定复杂业务查询
业务开发中,我们绝不会把所有数据都存在一张表里------根据数据库三大范式,我们会把不同业务维度的数据拆分到不同的表中,避免数据冗余。比如用户数据存在用户表、订单数据存在订单表、商品数据存在商品表,想要查询"某个用户的订单对应的商品信息",就必须用到多表关联查询。
1.1 前置准备:统一业务示例表(与上篇无缝衔接)
为了保证学习的连贯性,我们基于上篇创建的sys_user系统用户表,扩展出业务中最常用的4张核心表,后续所有示例都将基于这几张表展开,零基础也能无缝跟上。
sql
-- ===================== 1. 商品分类表 =====================
CREATE TABLE IF NOT EXISTS sys_category (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '分类主键ID',
category_name VARCHAR(50) NOT NULL COMMENT '分类名称',
parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父分类ID,0为一级分类',
sort INT NOT NULL DEFAULT 0 COMMENT '排序号',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
KEY idx_parent_id (parent_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品分类表';
-- ===================== 2. 商品表 =====================
CREATE TABLE IF NOT EXISTS sys_goods (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '商品主键ID',
goods_name VARCHAR(100) NOT NULL COMMENT '商品名称',
category_id BIGINT NOT NULL COMMENT '所属分类ID',
price DECIMAL(10,2) NOT NULL COMMENT '商品价格',
stock INT NOT NULL DEFAULT 0 COMMENT '库存数量',
description TEXT COMMENT '商品描述',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-下架,1-上架',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
KEY idx_category_id (category_id),
KEY idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
-- ===================== 3. 订单表 =====================
CREATE TABLE IF NOT EXISTS sys_order (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单主键ID',
order_no VARCHAR(32) NOT NULL COMMENT '订单编号,唯一',
user_id BIGINT NOT NULL COMMENT '下单用户ID,关联sys_user表id',
total_amount DECIMAL(12,2) NOT NULL COMMENT '订单总金额',
pay_amount DECIMAL(12,2) NOT NULL COMMENT '实付金额',
order_status TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态:0-待付款,1-待发货,2-待收货,3-已完成,4-已取消',
pay_time DATETIME COMMENT '付款时间',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_order_no (order_no),
KEY idx_user_id (user_id),
KEY idx_order_status (order_status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
-- ===================== 4. 订单详情表 =====================
CREATE TABLE IF NOT EXISTS sys_order_item (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单详情主键ID',
order_id BIGINT NOT NULL COMMENT '订单ID,关联sys_order表id',
goods_id BIGINT NOT NULL COMMENT '商品ID,关联sys_goods表id',
goods_name VARCHAR(100) NOT NULL COMMENT '商品名称快照',
price DECIMAL(10,2) NOT NULL COMMENT '商品单价快照',
num INT NOT NULL DEFAULT 1 COMMENT '购买数量',
total_price DECIMAL(10,2) NOT NULL COMMENT '商品小计金额',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
KEY idx_order_id (order_id),
KEY idx_goods_id (goods_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单详情表';
-- 插入测试数据,方便后续示例演示
-- 分类数据
INSERT INTO sys_category (category_name, parent_id, sort) VALUES
('数码产品', 0, 1), ('家用电器', 0, 2), ('手机', 1, 1), ('笔记本电脑', 1, 2), ('冰箱', 2, 1), ('洗衣机', 2, 2);
-- 商品数据
INSERT INTO sys_goods (goods_name, category_id, price, stock) VALUES
('iPhone 15 Pro', 3, 7999.00, 100), ('华为Mate 60 Pro', 3, 6999.00, 200),
('联想拯救者Y9000P', 4, 8999.00, 50), ('惠普暗影精灵10', 4, 7999.00, 80),
('海尔双开门冰箱', 5, 3999.00, 30), ('小天鹅滚筒洗衣机', 6, 2999.00, 40);
-- 订单数据(关联上篇的sys_user表,确保user_id存在)
INSERT INTO sys_order (order_no, user_id, total_amount, pay_amount, order_status, pay_time) VALUES
('ORD20240501001', 1, 7999.00, 7999.00, 3, '2024-05-01 10:00:00'),
('ORD20240502001', 1, 6999.00, 6999.00, 3, '2024-05-02 14:00:00'),
('ORD20240503001', 2, 8999.00, 8999.00, 2, '2024-05-03 09:00:00'),
('ORD20240504001', 3, 3999.00, 3999.00, 4, NULL),
('ORD20240505001', 4, 2999.00, 2999.00, 3, '2024-05-05 16:00:00');
-- 订单详情数据
INSERT INTO sys_order_item (order_id, goods_id, goods_name, price, num, total_price) VALUES
(1, 1, 'iPhone 15 Pro', 7999.00, 1, 7999.00),
(2, 2, '华为Mate 60 Pro', 6999.00, 1, 6999.00),
(3, 3, '联想拯救者Y9000P', 8999.00, 1, 8999.00),
(4, 5, '海尔双开门冰箱', 3999.00, 1, 3999.00),
(5, 6, '小天鹅滚筒洗衣机', 2999.00, 1, 2999.00);
1.2 关联查询的核心基础:笛卡尔积
所有关联查询的底层都是笛卡尔积,90%的新手写JOIN出错,都是因为没搞懂笛卡尔积的原理。
笛卡尔积指的是:两个表关联时,左表的每一行都会和右表的每一行进行匹配,最终生成的结果行数 = 左表行数 × 右表行数。
举个例子:左表有5行数据,右表有6行数据,不加任何关联条件的话,最终会生成30行数据,这就是完整的笛卡尔积。而我们业务中使用的关联查询,本质就是在笛卡尔积的基础上,通过关联条件过滤出符合业务逻辑的数据。
重要提醒:业务中绝对禁止出现不加关联条件的关联查询,一旦表数据量较大,笛卡尔积会直接打满数据库CPU,导致线上故障。
1.3 MySQL JOIN 分类与全场景用法
MySQL中关联查询的核心关键字是JOIN,主要分为6大类,我们按照业务使用频率从高到低,逐个讲透用法、适用场景、示例与避坑点。
1.3.1 INNER JOIN(内连接)
核心定义 :只返回两个表中满足关联条件的行,也就是两个表的交集部分,是业务中最常用的JOIN类型。
语法格式:
sql
SELECT 字段列表
FROM 左表
INNER JOIN 右表
ON 左表.关联字段 = 右表.关联字段
[WHERE 过滤条件];
INNER关键字可以省略,直接写JOIN,效果完全一致ON后面跟的是关联条件,用来指定两个表的关联规则- 可以通过
WHERE对关联后的结果进行二次过滤
业务示例:查询有下单记录的用户信息,以及对应的订单编号、订单金额
sql
SELECT
u.id AS user_id,
u.username,
u.phone,
o.order_no,
o.total_amount,
o.order_status
FROM sys_user u
INNER JOIN sys_order o
ON u.id = o.user_id
WHERE o.order_status != 4; -- 过滤掉已取消的订单
结果说明 :只会返回sys_user和sys_order中user_id匹配,且订单未取消的数据,没有下单记录的用户、没有对应用户的订单,都不会出现在结果中。
1.3.2 LEFT JOIN(左外连接)
核心定义 :以左表为基准 ,返回左表的所有行,右表中满足关联条件的行会匹配显示,不满足的行,右表字段全部填充NULL。
这是业务中第二常用的JOIN类型,核心解决"需要保留左表全部数据,同时匹配右表关联数据"的场景,比如"查询所有用户,以及他们的订单记录,哪怕用户没有下单也要显示"。
语法格式:
sql
SELECT 字段列表
FROM 左表
LEFT JOIN 右表
ON 左表.关联字段 = 右表.关联字段
[WHERE 过滤条件];
业务示例1:查询所有用户,以及他们的订单记录,没有下单的用户也要显示
sql
SELECT
u.id AS user_id,
u.username,
u.phone,
o.order_no,
o.total_amount,
o.order_status
FROM sys_user u
LEFT JOIN sys_order o
ON u.id = o.user_id;
结果说明 :sys_user表中的所有用户都会显示,有订单的用户会匹配对应的订单数据,没有订单的用户,订单相关的字段全部为NULL。
高频面试考点+新手致命坑点:LEFT JOIN 中 ON 和 WHERE 的区别
90%的新手写LEFT JOIN都会踩这个坑:把右表的过滤条件写在WHERE里,导致LEFT JOIN失效,变成了INNER JOIN。
我们用两个示例对比,一眼看懂核心区别:
sql
-- 示例1:右表过滤条件写在ON中(正确写法,保留左表全部数据)
SELECT
u.id AS user_id,
u.username,
o.order_no,
o.order_status
FROM sys_user u
LEFT JOIN sys_order o
ON u.id = o.user_id
AND o.order_status = 3; -- 已完成的订单,过滤条件写在ON中
结果说明:所有用户都会显示,只有已完成的订单会匹配,没有已完成订单的用户,订单字段为NULL,完全符合LEFT JOIN的预期。
sql
-- 示例2:右表过滤条件写在WHERE中(错误写法,LEFT JOIN失效)
SELECT
u.id AS user_id,
u.username,
o.order_no,
o.order_status
FROM sys_user u
LEFT JOIN sys_order o
ON u.id = o.user_id
WHERE o.order_status = 3; -- 过滤条件写在WHERE中
结果说明 :只会返回有已完成订单的用户,没有订单的用户会被WHERE过滤掉,因为NULL = 3的结果是NULL,不会被WHERE条件匹配,最终效果和INNER JOIN完全一致。
核心原理总结:
ON是关联匹配条件:在生成关联结果时执行,不满足ON条件的右表数据,会填充NULL,不会过滤左表的行WHERE是结果过滤条件:在关联结果生成后执行,会过滤掉所有不满足条件的行,包括左表的行
1.3.3 RIGHT JOIN(右外连接)
核心定义 :以右表为基准 ,返回右表的所有行,左表中满足关联条件的行会匹配显示,不满足的行,左表字段全部填充NULL。
本质上和LEFT JOIN是完全对称的,只需要把两个表的位置互换,RIGHT JOIN就可以完全用LEFT JOIN实现,因此业务中极少使用RIGHT JOIN,推荐统一使用LEFT JOIN,提升SQL可读性。
语法格式与示例:
sql
-- RIGHT JOIN 写法
SELECT
u.id AS user_id,
u.username,
o.order_no,
o.total_amount
FROM sys_user u
RIGHT JOIN sys_order o
ON u.id = o.user_id;
-- 等价的LEFT JOIN 写法(推荐)
SELECT
u.id AS user_id,
u.username,
o.order_no,
o.total_amount
FROM sys_order o
LEFT JOIN sys_user u
ON o.user_id = u.id;
1.3.4 FULL JOIN(全外连接)
核心定义:返回左表和右表的所有行,无论是否满足关联条件,不匹配的字段填充NULL,也就是两个表的并集。
重要提醒 :MySQL不直接支持 FULL JOIN语法,我们可以通过LEFT JOIN + UNION + RIGHT JOIN的方式实现全外连接。
实现示例:查询所有用户和所有订单,无论是否匹配
sql
-- 全外连接实现
SELECT u.id AS user_id, u.username, o.order_no, o.total_amount
FROM sys_user u
LEFT JOIN sys_order o ON u.id = o.user_id
UNION
SELECT u.id AS user_id, u.username, o.order_no, o.total_amount
FROM sys_user u
RIGHT JOIN sys_order o ON u.id = o.user_id;
1.3.5 CROSS JOIN(交叉连接)
核心定义:生成两个表的完整笛卡尔积,没有关联条件,业务中极少使用,仅适用于生成测试数据、字典表全匹配等极少数场景。
语法格式:
sql
SELECT 字段列表
FROM 左表
CROSS JOIN 右表;
1.3.6 多表关联查询(业务高频场景)
真实业务中,我们经常需要关联3张及以上的表,比如"查询订单对应的用户信息、商品信息、分类信息",只需要在JOIN后面继续追加JOIN语句即可,核心是保证关联条件的正确性。
业务示例:查询所有已完成的订单,包含下单用户信息、购买的商品信息、商品分类信息
sql
SELECT
o.order_no,
o.total_amount,
o.pay_time,
u.username,
u.phone,
g.goods_name,
g.price,
oi.num,
c.category_name
FROM sys_order o
-- 关联用户表
LEFT JOIN sys_user u ON o.user_id = u.id
-- 关联订单详情表
LEFT JOIN sys_order_item oi ON o.id = oi.order_id
-- 关联商品表
LEFT JOIN sys_goods g ON oi.goods_id = g.id
-- 关联分类表
LEFT JOIN sys_category c ON g.category_id = c.id
WHERE o.order_status = 3; -- 只查询已完成的订单
1.4 关联查询企业级最佳实践与避坑指南
- 关联字段必须加索引:JOIN的关联字段必须建立索引,否则数据量一大,会触发全表扫描,性能极差,这是最基础的优化原则
- 关联字段类型必须完全一致:比如左表关联字段是BIGINT,右表必须也是BIGINT,类型不一致会导致索引失效,哪怕是INT和BIGINT也不行
- 优先使用INNER JOIN和LEFT JOIN:禁止使用RIGHT JOIN,避免SQL可读性差,逻辑混乱
- 严格区分ON和WHERE的过滤条件:LEFT JOIN中,左表过滤条件写在WHERE,右表过滤条件写在ON,避免JOIN失效
- 禁止不加关联条件的JOIN:避免生成笛卡尔积,导致数据库性能雪崩
- 控制关联表的数量:业务中尽量不要关联超过7张表,表越多,MySQL查询优化器的选择成本越高,越容易生成错误的执行计划,性能越差
- 小表驱动大表:INNER JOIN中,MySQL会自动选择小表驱动大表;LEFT JOIN中,左表尽量选择小表,减少循环匹配的次数,提升性能(下篇精通篇会深入讲解底层原理)
第二章 进阶查询特性:子查询、CTE与窗口函数
搞定了多表关联,我们再来学习MySQL的进阶查询特性,这些特性能帮你用简洁优雅的SQL,实现之前需要嵌套多层、甚至代码才能实现的复杂业务逻辑,同时也是面试的高频考点。
2.1 子查询
子查询指的是嵌套在另一个SQL语句中的查询语句,也叫内查询,外层的SQL叫主查询。子查询的结果会作为主查询的条件、数据源,用来实现复杂的过滤和统计。
根据子查询返回的结果类型,我们可以分为4大类,逐个讲透用法与适用场景。
2.1.1 标量子查询
核心定义:返回**单个值(一行一列)**的子查询,是最简单的子查询类型,通常用在WHERE条件的比较运算符(=、!=、>、<等)中。
业务示例:查询和用户"zhangsan"年龄相同的所有用户
sql
SELECT id, username, age, phone
FROM sys_user
WHERE age = (
SELECT age FROM sys_user WHERE username = 'zhangsan'
);
2.1.2 列子查询
核心定义 :返回一列多行的子查询,通常用在WHERE条件的IN、NOT IN、ANY、ALL运算符中。
业务示例1:查询有下单记录的用户信息(IN子查询)
sql
SELECT id, username, phone
FROM sys_user
WHERE id IN (
SELECT DISTINCT user_id FROM sys_order WHERE order_status != 4
);
业务示例2:查询比所有手机类商品价格都高的商品(ALL子查询)
sql
SELECT goods_name, price, category_id
FROM sys_goods
WHERE price > ALL (
SELECT price FROM sys_goods WHERE category_id = 3
);
高频避坑点 :NOT IN子查询中,如果子查询的结果包含NULL,主查询会返回空结果,因为NOT IN (1,2,NULL)等价于!=1 AND !=2 AND !=NULL,而!=NULL永远为false,导致整个条件为false。
2.1.3 行子查询
核心定义 :返回一行多列的子查询,通常用在WHERE条件的=、IN等运算符中,需要主查询的字段列表和子查询的字段列表数量、类型完全一致。
业务示例:查询和用户"zhangsan"年龄、性别都相同的用户
sql
SELECT id, username, age, gender
FROM sys_user
WHERE (age, gender) = (
SELECT age, gender FROM sys_user WHERE username = 'zhangsan'
);
2.1.4 表子查询
核心定义 :返回多行多列的子查询,通常用在FROM子句中,作为临时表使用,也叫派生表。
业务示例:统计每个用户的订单总金额,然后查询总金额超过5000的用户
sql
SELECT user_id, username, total_order_amount
FROM (
-- 子查询作为临时表,统计每个用户的订单总金额
SELECT
u.id AS user_id,
u.username,
SUM(o.total_amount) AS total_order_amount
FROM sys_user u
LEFT JOIN sys_order o ON u.id = o.user_id
GROUP BY u.id, u.username
) AS user_order_stat
WHERE total_order_amount > 5000;
2.1.5 子查询避坑指南
- 避免使用相关子查询:相关子查询指的是子查询依赖主查询的字段,会导致主查询每一行都执行一次子查询,数据量大的时候性能极差,优先用JOIN替代
- 避免多层嵌套子查询:嵌套超过3层的子查询,可读性极差,维护成本高,优先用CTE替代
- IN子查询优先用EXISTS替代:当子查询的结果集很大时,
EXISTS比IN性能更好,因为EXISTS只要找到匹配的行就会停止遍历,而IN会遍历整个结果集
2.2 CTE公用表表达式(MySQL8.0+ 核心特性)
CTE(公用表表达式)是MySQL8.0推出的核心特性,用来定义一个临时的结果集,在单个SQL语句中可以多次引用,完美解决了子查询嵌套多层、可读性差的问题。
CTE分为普通CTE 和递归CTE两大类,我们逐个讲解。
2.2.1 普通CTE
语法格式:
sql
WITH cte名称 AS (
子查询语句
)
SELECT * FROM cte名称;
- 可以在一个WITH子句中定义多个CTE,用逗号分隔
- 定义后的CTE可以在主查询中多次引用,就像临时表一样
业务示例:用CTE实现上面的用户订单统计需求,代码可读性大幅提升
sql
-- 定义CTE,统计每个用户的订单总金额
WITH user_order_stat AS (
SELECT
u.id AS user_id,
u.username,
SUM(o.total_amount) AS total_order_amount
FROM sys_user u
LEFT JOIN sys_order o ON u.id = o.user_id
GROUP BY u.id, u.username
)
-- 主查询引用CTE,过滤数据
SELECT user_id, username, total_order_amount
FROM user_order_stat
WHERE total_order_amount > 5000;
多CTE示例:定义多个CTE,实现复杂的多步统计
sql
WITH
-- 第一个CTE:统计每个分类的商品数量
category_goods_count AS (
SELECT category_id, COUNT(1) AS goods_count
FROM sys_goods
GROUP BY category_id
),
-- 第二个CTE:统计每个分类的订单总销量
category_sale_stat AS (
SELECT
g.category_id,
SUM(oi.num) AS total_sale_num,
SUM(oi.total_price) AS total_sale_amount
FROM sys_order_item oi
LEFT JOIN sys_goods g ON oi.goods_id = g.id
GROUP BY g.category_id
)
-- 主查询关联两个CTE,获取分类的完整统计数据
SELECT
c.id,
c.category_name,
cgc.goods_count,
css.total_sale_num,
css.total_sale_amount
FROM sys_category c
LEFT JOIN category_goods_count cgc ON c.id = cgc.category_id
LEFT JOIN category_sale_stat css ON c.id = css.category_id;
2.2.2 递归CTE
递归CTE是CTE的高级用法,核心用来处理树形结构、层级结构的数据,比如商品分类、部门组织架构、地区树形结构等,这是之前用MySQL很难实现的功能。
语法格式:
sql
WITH RECURSIVE cte名称 AS (
-- 锚点成员:递归的起始点,也就是顶层数据
SELECT 字段列表 FROM 表名 WHERE 父级条件 = 初始值
UNION ALL
-- 递归成员:引用CTE自身,实现递归查询
SELECT 字段列表 FROM 表名
INNER JOIN cte名称 ON 表名.父级字段 = cte名称.主键字段
)
SELECT * FROM cte名称;
业务示例:递归查询商品分类的完整树形结构,从一级分类到子分类
sql
WITH RECURSIVE category_tree AS (
-- 锚点:查询一级分类(parent_id=0)
SELECT
id,
category_name,
parent_id,
1 AS level, -- 分类层级,一级分类为1
CAST(category_name AS CHAR(200)) AS full_path -- 分类完整路径
FROM sys_category
WHERE parent_id = 0
UNION ALL
-- 递归:查询子分类,关联上一级的结果
SELECT
c.id,
c.category_name,
c.parent_id,
ct.level + 1 AS level,
CONCAT(ct.full_path, ' > ', c.category_name) AS full_path
FROM sys_category c
INNER JOIN category_tree ct ON c.parent_id = ct.id
)
-- 查询完整的树形结构
SELECT id, category_name, parent_id, level, full_path
FROM category_tree
ORDER BY level, id;
执行结果说明:会返回所有分类,标注每个分类的层级,以及从一级分类到当前分类的完整路径,完美实现树形结构的查询。
2.3 窗口函数(MySQL8.0+ 核心特性,面试高频考点)
窗口函数是MySQL8.0推出的杀手级特性,彻底解决了之前SQL很难实现的分组TOP N、排名、累计求和、同比环比等复杂业务需求,不需要再写嵌套子查询,性能也大幅提升。
窗口函数的核心特点:不会改变原表的行数,不会进行GROUP BY合并,而是在每行数据上生成计算结果,同时可以实现分组内的计算,这是和聚合函数最大的区别。
2.3.1 窗口函数基础语法
sql
窗口函数名(参数) OVER (
[PARTITION BY 分组字段] -- 可选,按指定字段分组,分组内独立计算
[ORDER BY 排序字段 排序规则] -- 可选,分组内按指定规则排序
[ROWS/RANGE 窗口范围] -- 可选,指定计算的窗口范围
)
OVER():窗口函数的标志,里面的内容用来定义计算的窗口范围PARTITION BY:类似GROUP BY的分组,分组后窗口函数在每个分组内独立计算ORDER BY:指定分组内的排序规则,决定窗口函数的计算顺序- 窗口函数可以用在SELECT、ORDER BY子句中,不能用在WHERE、GROUP BY子句中
2.3.2 常用窗口函数分类与示例
窗口函数主要分为3大类:排名函数、聚合窗口函数、取值函数,我们逐个讲透业务中最常用的函数。
第一类:排名函数(面试必问,业务高频)
排名函数是最常用的窗口函数,核心用来实现排名需求,比如"按订单金额排名""每个分类下的商品按价格排名"。
常用的排名函数有3个,核心区别如下:
| 函数名 | 核心特点 | 适用场景 |
|---|---|---|
| ROW_NUMBER() | 连续排名,即使值相同,排名也不会重复,比如1、2、3、4 | 分页、去重、需要唯一序号的场景 |
| RANK() | 跳跃排名,值相同排名相同,后续排名跳跃,比如1、1、3、4 | 比赛排名、分数排名,同分同名次 |
| DENSE_RANK() | 连续排名,值相同排名相同,后续排名连续,比如1、1、2、3 | 需要连续排名,同分同名次的场景 |
业务示例:对所有商品按价格从高到低排名,对比3个排名函数的区别
sql
SELECT
goods_name,
category_id,
price,
ROW_NUMBER() OVER (ORDER BY price DESC) AS row_num_rank,
RANK() OVER (ORDER BY price DESC) AS rank_num,
DENSE_RANK() OVER (ORDER BY price DESC) AS dense_rank_num
FROM sys_goods;
分组排名示例:每个商品分类下,按价格从高到低排名
sql
SELECT
c.category_name,
g.goods_name,
g.price,
ROW_NUMBER() OVER (PARTITION BY g.category_id ORDER BY g.price DESC) AS rank_in_category
FROM sys_goods g
LEFT JOIN sys_category c ON g.category_id = c.id;
经典业务场景:分组TOP N查询
需求:查询每个商品分类下,价格最高的2个商品,这是业务开发和面试中100%会遇到的场景,用窗口函数可以轻松实现。
sql
WITH goods_rank AS (
SELECT
c.category_name,
g.goods_name,
g.price,
-- 按分类分组,价格降序排名
ROW_NUMBER() OVER (PARTITION BY g.category_id ORDER BY g.price DESC) AS rank_num
FROM sys_goods g
LEFT JOIN sys_category c ON g.category_id = c.id
)
-- 取每个分类排名前2的商品
SELECT category_name, goods_name, price, rank_num
FROM goods_rank
WHERE rank_num <= 2;
第二类:聚合窗口函数
聚合窗口函数就是我们常用的SUM、AVG、COUNT、MAX、MIN,配合OVER()子句,实现分组内的累计求和、移动平均、整体聚合值对比等需求。
业务示例1:累计求和,统计每个用户的订单累计付款金额
sql
SELECT
u.username,
o.order_no,
o.pay_time,
o.pay_amount,
-- 按用户分组,按付款时间排序,累计求和
SUM(o.pay_amount) OVER (PARTITION BY o.user_id ORDER BY o.pay_time ASC) AS cumulative_amount
FROM sys_order o
LEFT JOIN sys_user u ON o.user_id = u.id
WHERE o.order_status = 3;
业务示例2:查询每个商品的价格,以及所属分类的平均价格,对比差值
sql
SELECT
c.category_name,
g.goods_name,
g.price,
-- 按分类分组,计算分类内的平均价格
AVG(g.price) OVER (PARTITION BY g.category_id) AS category_avg_price,
-- 计算商品价格和分类平均价格的差值
g.price - AVG(g.price) OVER (PARTITION BY g.category_id) AS price_diff
FROM sys_goods g
LEFT JOIN sys_category c ON g.category_id = c.id;
第三类:取值函数
取值函数核心用来获取当前行的前后行数据,或者分组内的首尾数据,常用的有LAG()和LEAD(),完美解决同比环比、连续登录天数等需求。
| 函数名 | 核心作用 |
|---|---|
| LAG(字段, 偏移量, 默认值) | 获取当前行前面第N行的指定字段值,偏移量默认1,超出范围返回默认值 |
| LEAD(字段, 偏移量, 默认值) | 获取当前行后面第N行的指定字段值,偏移量默认1,超出范围返回默认值 |
业务示例:查询每个用户的上一笔订单金额和下一笔订单金额
sql
SELECT
u.username,
o.order_no,
o.pay_time,
o.pay_amount,
-- 获取上一笔订单的金额
LAG(o.pay_amount, 1, 0) OVER (PARTITION BY o.user_id ORDER BY o.pay_time ASC) AS prev_order_amount,
-- 获取下一笔订单的金额
LEAD(o.pay_amount, 1, 0) OVER (PARTITION BY o.user_id ORDER BY o.pay_time ASC) AS next_order_amount
FROM sys_order o
LEFT JOIN sys_user u ON o.user_id = u.id
WHERE o.order_status = 3;
第三章 MySQL常用数据库对象:视图、存储过程、函数与触发器
这一章我们讲解MySQL中常用的数据库对象,这些对象可以帮我们封装复杂SQL、实现业务逻辑复用,同时也要明确生产环境的使用红线,避免踩坑。
3.1 视图(View)
视图是一张虚拟的表,它的内容由SELECT查询语句定义,本身不存储数据,数据还是存在底层的基础表中。当我们查询视图时,MySQL会执行视图定义的SELECT语句,返回对应的结果。
3.1.1 视图的核心作用
- 简化复杂SQL:把复杂的多表关联、统计查询封装成视图,业务代码只需要简单查询视图即可,不需要重复写复杂SQL
- 权限控制:给用户开放视图的查询权限,而不是开放基础表的权限,隐藏敏感字段和数据,提升数据安全性
- 数据隔离:视图可以只返回业务需要的字段和数据,屏蔽底层表结构的变化,只要视图的字段不变,基础表结构变更不会影响业务代码
3.1.2 视图的基础语法
sql
-- ===================== 1. 创建视图 =====================
CREATE OR REPLACE VIEW 视图名称
AS
SELECT 查询语句;
-- 示例:创建用户订单统计视图,封装复杂的关联统计SQL
CREATE OR REPLACE VIEW v_user_order_stat
AS
SELECT
u.id AS user_id,
u.username,
u.phone,
COUNT(o.id) AS order_count,
SUM(o.total_amount) AS total_order_amount,
SUM(CASE WHEN o.order_status = 3 THEN o.pay_amount ELSE 0 END) AS total_pay_amount
FROM sys_user u
LEFT JOIN sys_order o ON u.id = o.user_id
GROUP BY u.id, u.username, u.phone;
-- ===================== 2. 查询视图 =====================
-- 和查询普通表完全一致,非常简单
SELECT * FROM v_user_order_stat WHERE total_pay_amount > 5000;
-- ===================== 3. 查看视图定义 =====================
SHOW CREATE VIEW v_user_order_stat;
-- ===================== 4. 修改视图 =====================
-- 和创建语法一致,用CREATE OR REPLACE VIEW即可
-- 也可以用ALTER VIEW语法
ALTER VIEW v_user_order_stat
AS
SELECT 查询语句;
-- ===================== 5. 删除视图 =====================
DROP VIEW IF EXISTS v_user_order_stat;
3.1.3 视图的使用限制与最佳实践
- 视图主要用于查询场景,尽量不要通过视图进行INSERT、UPDATE、DELETE操作,尤其是包含聚合、分组、多表关联的视图,无法更新
- 视图中不要嵌套视图,也就是视图的定义中不要引用其他视图,会导致性能急剧下降,排查问题困难
- 视图不会提升查询性能,它只是封装了SQL语句,底层还是执行定义的SELECT语句,不要指望用视图优化性能
- 生产环境中,视图非常适合用于报表统计、数据查询场景,简化业务代码
3.2 存储过程与自定义函数
存储过程和自定义函数都是一组预先编译好的SQL语句的集合,封装在数据库中,可以重复调用,实现业务逻辑的复用。
3.2.1 存储过程与自定义函数的核心区别
| 特性 | 存储过程 | 自定义函数 |
|---|---|---|
| 返回值 | 可以有0个、多个返回值,通过OUT参数输出 | 必须有且只有一个返回值 |
| 调用方式 | 用CALL关键字调用 | 可以在SELECT语句中直接调用,和内置函数用法一致 |
| 适用场景 | 复杂的业务逻辑、批量数据处理、事务处理 | 数据计算、格式转换,用于SELECT语句中 |
3.2.2 存储过程的基础语法与示例
sql
-- ===================== 1. 创建存储过程 =====================
-- 语法格式
DELIMITER // -- 临时修改语句结束符为//,避免存储过程内的;导致提前结束
CREATE PROCEDURE 存储过程名称(
IN 入参名称 数据类型, -- IN 入参,调用时传入
OUT 出参名称 数据类型, -- OUT 出参,调用后返回结果
INOUT 入出参名称 数据类型 -- INOUT 既是入参也是出参
)
BEGIN
-- 存储过程体,SQL语句、逻辑判断、循环等
END //
DELIMITER ; -- 恢复语句结束符为;
-- 业务示例:创建存储过程,根据用户ID查询用户的订单统计信息
DELIMITER //
CREATE PROCEDURE sp_get_user_order_stat(
IN in_user_id BIGINT, -- 入参:用户ID
OUT out_order_count INT, -- 出参:订单总数
OUT out_total_pay_amount DECIMAL(12,2) -- 出参:累计付款金额
)
BEGIN
-- 统计数据,赋值给出参
SELECT
COUNT(id),
SUM(pay_amount)
INTO out_order_count, out_total_pay_amount
FROM sys_order
WHERE user_id = in_user_id AND order_status = 3;
END //
DELIMITER ;
-- ===================== 2. 调用存储过程 =====================
-- 定义变量接收出参
SET @order_count = 0;
SET @total_pay_amount = 0;
-- 调用存储过程
CALL sp_get_user_order_stat(1, @order_count, @total_pay_amount);
-- 查询返回结果
SELECT @order_count, @total_pay_amount;
-- ===================== 3. 查看存储过程定义 =====================
SHOW CREATE PROCEDURE sp_get_user_order_stat;
-- ===================== 4. 删除存储过程 =====================
DROP PROCEDURE IF EXISTS sp_get_user_order_stat;
3.2.3 自定义函数的基础语法与示例
sql
-- ===================== 1. 创建自定义函数 =====================
DELIMITER //
CREATE FUNCTION 函数名称(参数名称 数据类型)
RETURNS 返回值数据类型
DETERMINISTIC -- 确定性函数,相同的入参返回相同的结果
BEGIN
-- 函数体,必须有RETURN语句返回结果
RETURN 返回值;
END //
DELIMITER ;
-- 示例:创建函数,根据出生日期计算年龄
DELIMITER //
CREATE FUNCTION func_calculate_age(birthday DATE)
RETURNS TINYINT
DETERMINISTIC
BEGIN
RETURN TIMESTAMPDIFF(YEAR, birthday, NOW());
END //
DELIMITER ;
-- ===================== 2. 调用函数 =====================
-- 和内置函数用法完全一致,直接在SELECT中调用
SELECT
username,
birthday,
func_calculate_age(birthday) AS age
FROM sys_user;
-- ===================== 3. 查看函数定义 =====================
SHOW CREATE FUNCTION func_calculate_age;
-- ===================== 4. 删除函数 =====================
DROP FUNCTION IF EXISTS func_calculate_age;
3.2.4 生产环境使用红线(重中之重)
互联网企业的生产环境中,强烈不推荐使用存储过程和自定义函数,核心原因如下:
- 维护成本极高:存储过程的代码没有版本管理,调试困难,出问题很难排查,远不如代码里的业务逻辑好维护
- 耦合度极高:把业务逻辑写在存储过程里,会导致业务和数据库强耦合,后续业务迭代、分库分表、迁移都非常困难
- 性能瓶颈:数据库的核心能力是存储数据,不是处理业务逻辑,存储过程会占用大量数据库CPU资源,很容易导致数据库性能瓶颈,而数据库是最难扩展的
- 无法分布式扩展:微服务、分布式架构下,存储过程无法跨库、跨服务调用,完全不适用
- 安全风险:存储过程的权限很难控制,容易出现SQL注入、越权操作等安全问题
唯一适用场景:极少数的纯数据库批量数据处理场景,比如数据迁移、离线统计,且必须在DBA的管控下使用,业务代码绝对禁止调用存储过程。
3.3 触发器(Trigger)
触发器是一种特殊的存储过程,它会在指定的表发生INSERT、UPDATE、DELETE操作时,自动触发执行,不需要手动调用。
3.3.1 触发器的基础语法
sql
-- 语法格式
DELIMITER //
CREATE TRIGGER 触发器名称
触发时机 触发事件 ON 表名
FOR EACH ROW -- 行级触发器,每一行数据变化都会触发
BEGIN
-- 触发器执行的逻辑
END //
DELIMITER ;
- 触发时机 :
BEFORE(操作执行前触发)、AFTER(操作执行后触发) - 触发事件 :
INSERT、UPDATE、DELETE NEW关键字:代表插入/更新后的新数据行OLD关键字:代表更新/删除前的旧数据行
3.3.2 触发器示例
需求:订单付款后,自动更新用户的累计消费金额,我们给sys_user表加一个total_consume字段,然后创建触发器实现。
sql
-- 给用户表添加累计消费字段
ALTER TABLE sys_user ADD COLUMN total_consume DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计消费金额' AFTER phone;
-- 创建触发器:订单状态更新为已完成后,自动累加用户的累计消费金额
DELIMITER //
CREATE TRIGGER tri_after_order_complete
AFTER UPDATE ON sys_order
FOR EACH ROW
BEGIN
-- 当订单状态从非3更新为3(已完成)时,触发累加
IF OLD.order_status != 3 AND NEW.order_status = 3 THEN
UPDATE sys_user
SET total_consume = total_consume + NEW.pay_amount
WHERE id = NEW.user_id;
END IF;
END //
DELIMITER ;
3.3.3 生产环境使用红线
互联网企业的生产环境中,绝对禁止使用触发器,核心原因如下:
- 隐性业务逻辑,排查问题极难:触发器的执行是隐性的,业务代码里看不到,出了问题开发人员根本想不到是触发器导致的,排查成本极高
- 死锁风险极高:触发器的执行会在同一个事务中,很容易导致锁等待、死锁,尤其是高并发场景下
- 性能问题:触发器会增加数据库的IO压力,比如插入一条数据,触发器又去更新其他表,会导致额外的数据库操作,高并发下性能雪崩
- 数据一致性风险:触发器的执行失败会导致主操作回滚,而且分布式场景下,触发器无法跨库执行,会导致数据不一致
- 无法分库分表:分库分表场景下,触发器完全无法使用,后续架构迁移成本极高
正确做法:所有的业务逻辑都应该放在业务代码中处理,而不是交给数据库的触发器,保证业务逻辑的可观测性和可维护性。
第四章 事务核心原理:ACID、隔离级别与并发一致性
事务是MySQL的核心特性,也是面试100%会问到的内容,更是保证业务数据一致性的核心。比如用户下单,需要扣减库存、生成订单、扣减余额,这几个操作必须要么全部成功,要么全部失败,否则就会出现资金损失、库存错乱的问题,这就需要事务来保证。
4.1 事务的核心定义
事务是一组原子性的SQL操作集合,这组SQL操作是一个不可分割的整体,要么全部执行成功,要么全部执行失败回滚,不会出现部分成功部分失败的情况。
注意:只有InnoDB存储引擎支持事务,MyISAM不支持事务,这也是InnoDB成为默认引擎的核心原因之一。
4.2 事务的ACID四大特性(面试必问,必须吃透)
ACID是事务的四大核心特性,是事务保证数据一致性的基础,我们不仅要背下概念,更要搞懂每个特性的底层实现原理。
| 特性 | 全称 | 核心定义 | 底层实现 |
|---|---|---|---|
| 原子性 | Atomicity | 事务是一个原子操作,不可分割,所有操作要么全部提交成功,要么全部失败回滚,不会出现中间状态 | undo log(回滚日志),记录数据的反向操作,事务回滚时执行undo log,把数据恢复到事务开始前的状态 |
| 一致性 | Consistency | 事务执行前后,数据的完整性约束、业务规则不会被破坏。比如转账前后,两个账户的总金额不变;扣减库存不能出现负数 | 一致性是事务的最终目的,原子性、隔离性、持久性都是为了保证一致性,同时需要业务代码保证业务规则的正确性 |
| 隔离性 | Isolation | 多个事务并发执行时,互相之间是隔离的,不会互相干扰,避免并发执行导致的数据不一致问题 | 锁机制 + MVCC(多版本并发控制),下篇精通篇会深入讲解底层原理 |
| 持久性 | Durability | 事务一旦提交成功,对数据的修改就会永久生效,即使数据库宕机、重启,数据也不会丢失 | redo log(重做日志),MySQL采用WAL预写日志机制,修改数据前先写redo log,宕机后可以通过redo log恢复数据 |
核心逻辑关系:ACID不是四个孤立的特性,原子性是基础,隔离性是手段,持久性是保障,一致性是最终目的。
4.3 事务并发执行带来的三大问题
当多个事务同时操作同一批数据时,如果没有隔离性,就会出现脏读、不可重复读、幻读三大问题,我们逐个讲透定义与示例。
4.3.1 脏读
核心定义 :一个事务读到了另一个事务未提交的数据。因为未提交的事务随时可能回滚,所以读到的数据是无效的"脏数据",会导致业务逻辑错误。
示例场景:
- 事务A:给用户zhangsan的账户余额加100元,还未提交事务
- 事务B:读取zhangsan的账户余额,读到了事务A未提交的100元,基于这个数据做了业务处理
- 事务A:发生异常,回滚了事务,加的100元撤销了
- 结果:事务B读到的余额是无效的脏数据,导致业务出错
4.3.2 不可重复读
核心定义:一个事务内,多次读取同一批数据,两次读取的结果不一样。因为在两次读取之间,另一个事务修改并提交了这批数据,导致同一个事务内的重复读取结果不一致。
示例场景:
- 事务A:第一次读取zhangsan的账户余额是1000元,事务还未结束
- 事务B:修改了zhangsan的账户余额为500元,并提交了事务
- 事务A:第二次读取zhangsan的账户余额,变成了500元,和第一次读取的结果不一致
- 结果:同一个事务内,两次读取的结果不一样,无法实现可重复读
4.3.3 幻读
核心定义:一个事务内,两次用相同的条件查询数据,第二次查询多了/少了一些行,就像出现了"幻觉"一样。因为在两次查询之间,另一个事务插入/删除了符合查询条件的数据,并提交了事务。
示例场景:
- 事务A:查询余额大于1000元的用户,第一次查询到了5条数据,事务还未结束
- 事务B:插入了一个余额2000元的用户,并提交了事务
- 事务A:第二次用相同的条件查询,查到了6条数据,比第一次多了一条
- 结果:同一个事务内,相同的查询条件,两次查询的行数不一样,出现了幻读
不可重复读和幻读的核心区别:
- 不可重复读:针对的是数据的修改,同一行数据的内容变了
- 幻读:针对的是数据的新增/删除,查询结果的行数变了
4.4 SQL标准的四大隔离级别
为了解决事务并发带来的问题,SQL标准定义了四大事务隔离级别,每个级别解决的问题不同,性能也不同,隔离级别越高,性能越差。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 核心说明 |
|---|---|---|---|---|
| 读未提交(READ UNCOMMITTED) | 可能 | 可能 | 可能 | 最低的隔离级别,事务中的修改即使未提交,对其他事务也是可见的,基本不会使用 |
| 读已提交(READ COMMITTED,RC) | 不可能 | 可能 | 可能 | 解决了脏读问题,一个事务只能看到其他事务已经提交的修改,Oracle、SQL Server的默认隔离级别 |
| 可重复读(REPEATABLE READ,RR) | 不可能 | 不可能 | 可能 | 解决了脏读、不可重复读问题,MySQL InnoDB的默认隔离级别,InnoDB通过MVCC+间隙锁解决了大部分幻读场景 |
| 串行化(SERIALIZABLE) | 不可能 | 不可能 | 不可能 | 最高的隔离级别,所有事务串行执行,完全避免了并发问题,但是性能极差,基本不会使用 |
面试高频考点 :MySQL InnoDB的默认隔离级别是可重复读(RR),和标准SQL的RR级别不同,InnoDB通过Next-Key Lock(临键锁,间隙锁+记录锁) 解决了幻读问题,在RR级别下就可以达到SQL标准串行化的隔离效果,同时保证了不错的性能。
4.5 事务的基础语法与实操
4.5.1 事务的基础语法
sql
-- 1. 查看当前的事务隔离级别
SELECT @@transaction_isolation;
-- 2. 修改事务隔离级别(会话级别,当前会话生效)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 全局级别,所有新会话生效
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 3. 开启事务,两种写法等价
START TRANSACTION;
-- 或者
BEGIN;
-- 4. 提交事务,事务提交后,所有修改永久生效
COMMIT;
-- 5. 回滚事务,事务执行出错时,回滚所有修改
ROLLBACK;
-- 6. 事务保存点,实现部分回滚
SAVEPOINT 保存点名称; -- 创建保存点
ROLLBACK TO SAVEPOINT 保存点名称; -- 回滚到指定保存点
RELEASE SAVEPOINT 保存点名称; -- 删除保存点
4.5.2 事务实操示例
我们用经典的转账场景,演示事务的完整流程:
sql
-- 给用户表添加余额字段
ALTER TABLE sys_user ADD COLUMN balance DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '账户余额' AFTER total_consume;
-- 初始化用户余额
UPDATE sys_user SET balance = 1000.00 WHERE id = 1;
UPDATE sys_user SET balance = 1000.00 WHERE id = 2;
-- 转账场景:用户1给用户2转账500元
-- 开启事务
START TRANSACTION;
-- 1. 扣减用户1的余额
UPDATE sys_user SET balance = balance - 500.00 WHERE id = 1;
-- 2. 增加用户2的余额
UPDATE sys_user SET balance = balance + 500.00 WHERE id = 2;
-- 检查执行结果,确认无误后提交事务
-- SELECT * FROM sys_user WHERE id IN (1,2);
-- 提交事务
COMMIT;
-- 如果执行出错,执行回滚
-- ROLLBACK;
4.5.3 自动提交机制
MySQL默认开启自动提交(autocommit) 模式,也就是说,如果你没有显式开启事务,每条SQL语句都会被当成一个独立的事务,执行完成后自动提交。
sql
-- 查看自动提交状态,1为开启,0为关闭
SELECT @@autocommit;
-- 关闭自动提交,关闭后,所有SQL都需要手动COMMIT才会生效
SET autocommit = 0;
-- 开启自动提交(默认)
SET autocommit = 1;
4.6 生产环境事务最佳实践与避坑指南
- 事务粒度尽可能小:避免长事务,事务内的操作要尽可能少,执行时间尽可能短。长事务会导致锁长时间不释放、undo log膨胀、MVCC视图过期,是生产环境最常见的性能杀手
- 禁止在事务中执行DDL语句:DDL语句会触发隐式提交,导致事务提前提交,无法回滚
- 禁止在事务中进行外部调用、等待用户输入:外部调用的超时、用户输入的延迟,会导致事务长时间不提交,生成长事务
- 合理设置隔离级别:绝大多数业务场景,使用MySQL默认的RR级别即可;如果是读多写少、对一致性要求不高的场景,可以用RC级别,性能更好
- 避免循环内提交事务:批量操作时,尽量批量提交,而不是循环里每次都提交事务,减少事务提交的IO开销,提升性能
- 不要在事务中执行SELECT * 等大查询:大查询会导致事务执行时间变长,生成长事务
第五章 InnoDB存储引擎核心架构
MySQL的核心是存储引擎,而InnoDB是MySQL默认的存储引擎,也是企业开发中唯一会使用的存储引擎。想要真正吃透MySQL,就必须搞懂InnoDB的核心架构,这也是面试的高频核心考点。
InnoDB的整体架构分为两大核心部分:内存结构 和磁盘结构,我们逐个拆解核心组件的作用与原理。
5.1 InnoDB内存结构
内存结构是InnoDB的核心,用来缓存磁盘上的数据、索引,减少磁盘IO,提升查询性能,主要分为四大核心组件。
5.1.1 Buffer Pool(缓冲池)
Buffer Pool是InnoDB内存中最大的一块区域,默认占物理内存的50%-70%,核心作用是缓存磁盘上的数据页和索引页。
MySQL读写数据时,不会直接操作磁盘,而是先把数据所在的页加载到Buffer Pool中,后续的读写都操作内存中的缓冲页,后续通过后台线程异步刷新到磁盘中,极大减少了磁盘IO,提升了性能。
Buffer Pool采用LRU(最近最少使用) 算法来管理缓冲页,当Buffer Pool满了之后,会淘汰最近最少使用的缓冲页,加载新的数据页。
5.1.2 Change Buffer(写缓冲)
Change Buffer是Buffer Pool的一部分,核心作用是优化非唯一二级索引的写入操作。
当我们更新二级索引的数据时,如果对应的索引页不在Buffer Pool中,InnoDB不会直接加载磁盘上的索引页(避免随机磁盘IO),而是先把更新操作记录在Change Buffer中,后续当索引页被加载到Buffer Pool时,再把Change Buffer中的操作合并到索引页中,大幅减少了随机磁盘IO,提升了写入性能。
5.1.3 Log Buffer(日志缓冲区)
Log Buffer是用来缓存redo log的内存区域,核心作用是减少redo log的磁盘IO。
我们修改数据时,生成的redo log会先写入Log Buffer,然后根据配置的刷盘策略,定时刷新到磁盘的redo log文件中,避免每次修改都刷盘,提升了写入性能。
5.1.4 自适应哈希索引(AHI)
自适应哈希索引是InnoDB自动创建的,用来优化热点数据的查询性能。InnoDB会监控Buffer Pool中索引页的查询,如果发现某个索引被频繁查询,会自动为这个索引创建哈希索引,把O(logn)的B+树查询优化为O(1)的哈希查询,大幅提升热点数据的查询性能,整个过程完全自动,不需要人工干预。
5.2 InnoDB磁盘结构
InnoDB的磁盘结构主要分为表空间、重做日志、回滚日志三大核心部分,用来持久化存储数据、索引、日志,保证数据的持久性和原子性。
5.2.1 表空间
表空间是InnoDB数据存储的顶层结构,所有的数据、索引都存在表空间中,主要分为以下几类:
- 系统表空间 :对应的文件是
ibdata1,用来存储InnoDB的元数据、数据字典、undo log(MySQL5.7之前)、change buffer的持久化数据,一个MySQL实例只有一个系统表空间。 - 独立表空间 :MySQL8.0默认开启,每个表都会创建一个单独的
.ibd文件,表的数据、索引都存在这个文件中,优点是管理方便,删除表时会直接释放磁盘空间,而系统表空间不会。 - undo表空间:专门用来存储undo log,MySQL8.0默认独立出来,保证事务的原子性和MVCC的实现。
- 临时表空间:用来存储临时表的数据,比如排序、分组操作生成的临时表,MySQL重启后会自动清空。
5.2.2 redo log(重做日志)
redo log是InnoDB引擎层的物理日志,对应的磁盘文件是ib_logfile0和ib_logfile1,核心作用是保证事务的持久性,实现崩溃恢复。
MySQL采用WAL(预写日志) 机制:修改数据时,不是直接把修改刷新到磁盘的数据页,而是先写redo log,记录数据页的修改,然后再异步把Buffer Pool中的脏页刷新到磁盘。这样即使数据库宕机,重启后可以通过redo log恢复未刷新到磁盘的数据,保证事务提交后的数据不会丢失。
redo log是循环写的,文件大小固定,写满后会从头开始循环写,只要对应的修改已经刷新到磁盘,对应的redo log就可以被覆盖。
5.2.3 undo log(回滚日志)
undo log是InnoDB引擎层的逻辑日志,核心作用有两个:
- 保证事务的原子性:事务回滚时,执行undo log中记录的反向操作,把数据恢复到事务开始前的状态
- 实现MVCC多版本并发控制:事务隔离级别中的可重复读,就是通过undo log中的数据快照实现的,下篇精通篇会深入讲解
5.3 三大日志核心区别(面试必问)
很多人会搞混redo log、undo log、binlog的区别,这里用一张表彻底讲透:
| 特性 | redo log | undo log | binlog |
|---|---|---|---|
| 所属层级 | InnoDB引擎层 | InnoDB引擎层 | MySQL服务层,所有存储引擎都可用 |
| 日志类型 | 物理日志,记录数据页的修改 | 逻辑日志,记录数据的反向操作 | 逻辑日志,记录SQL语句的原始逻辑 |
| 核心作用 | 保证持久性,崩溃恢复 | 保证原子性,实现MVCC | 主从复制、数据备份恢复 |
| 写入方式 | 循环写,文件大小固定 | 追加写,事务提交后不会立即删除,会被purge线程清理 | 追加写,写满一个文件后生成新的文件,不会覆盖 |
| 生命周期 | 数据库重启后,完成崩溃恢复就可以覆盖 | 事务提交后,对应的undo log会被标记为可回收 | 永久保存,除非手动删除 |
5.4 两阶段提交(面试必问)
当我们提交一个事务时,redo log和binlog都需要写入,为了保证两个日志的一致性,MySQL采用了两阶段提交机制:
- 准备阶段(prepare):把redo log写入磁盘,标记为prepare状态
- 提交阶段(commit):把binlog写入磁盘,然后把redo log标记为commit状态,事务提交完成
核心作用:保证redo log和binlog的一致性,避免出现一个日志有记录,另一个没有的情况。如果数据库在两阶段之间宕机,重启后会检查redo log的状态:如果是prepare状态,且binlog已经写入,就提交事务;如果binlog没有写入,就回滚事务,保证数据的一致性。
第六章 中篇最佳实践与避坑指南
6.1 多表关联查询最佳实践
- 关联字段必须建立索引,且类型完全一致,避免索引失效和全表扫描
- 优先使用INNER JOIN和LEFT JOIN,禁止使用RIGHT JOIN,统一SQL风格,提升可读性
- 严格区分LEFT JOIN中ON和WHERE的过滤条件,避免JOIN失效
- 控制关联表的数量,尽量不超过7张,避免查询优化器生成错误的执行计划
- 小表驱动大表,减少循环匹配的次数,提升查询性能
6.2 高级查询特性最佳实践
- 优先使用CTE替代多层嵌套子查询,提升SQL的可读性和可维护性
- 分组TOP N、排名、累计求和等场景,优先使用窗口函数,避免低效的子查询
- 避免使用相关子查询,优先用JOIN替代,防止性能问题
- 递归CTE适合处理树形结构数据,避免代码中多次循环查询数据库
6.3 数据库对象使用红线
- 视图仅用于查询场景,简化复杂SQL,禁止通过视图进行增删改操作,禁止嵌套视图
- 生产环境业务代码禁止调用存储过程和自定义函数,业务逻辑放在代码中处理
- 生产环境绝对禁止使用触发器,避免隐性业务逻辑、死锁和性能问题
- 所有数据库对象必须添加注释,明确用途,方便后续维护
6.4 事务使用最佳实践
- 严格控制事务粒度,避免长事务,事务内的操作要尽可能少,执行时间尽可能短
- 禁止在事务中执行DDL语句、外部调用、大查询操作,避免生成长事务
- 合理设置隔离级别,绝大多数场景使用默认的RR级别即可
- 批量操作尽量批量提交,避免循环内频繁提交事务
- 业务代码必须处理事务异常,保证出错时能正确回滚,避免数据不一致
6.5 InnoDB引擎使用最佳实践
- 所有业务表必须使用InnoDB存储引擎,禁止使用MyISAM等其他引擎
- 必须为每个表设置主键,优先使用BIGINT自增主键
- 合理设置Buffer Pool的大小,默认设置为物理内存的50%-70%
- 开启独立表空间,方便表的管理和磁盘空间回收
- 合理设置redo log的大小,避免频繁的日志切换,影响写入性能
本篇总结与下篇预告
本篇总结
学完本篇,你已经彻底突破了MySQL入门瓶颈,达到了企业级开发的进阶水平:
- 完全吃透了多表关联查询的所有用法,能独立写出业务中99%场景的关联SQL,避开了JOIN的所有高频坑点
- 熟练使用子查询、CTE、窗口函数等高级特性,能用简洁优雅的SQL实现复杂业务逻辑
- 搞懂了视图、存储过程、触发器的用法与适用场景,明确了生产环境的使用红线,不会踩坑
- 彻底吃透了事务的ACID特性、隔离级别与底层实现,能解决并发场景下的数据一致性问题
- 深入理解了InnoDB存储引擎的核心架构,搞懂了redo log、undo log、binlog的核心区别,搞定了面试高频考点
下篇预告
《零基础从入门到精通MySQL(下篇):精通篇------吃透索引底层、锁机制与性能优化,成为MySQL实战高手》
在下篇中,我们将进入MySQL的精通阶段,深入讲解核心高阶内容:
- 索引的底层数据结构、B+树原理、聚簇索引与二级索引、联合索引的最左匹配原则
- 索引设计规范与失效场景,教你写出能命中索引的高质量SQL
- InnoDB锁机制:行锁、表锁、间隙锁、临键锁,搞懂并发场景下的锁冲突问题
- MVCC多版本并发控制的底层原理,彻底搞懂事务隔离级别的实现
- 执行计划EXPLAIN全解析,教你看懂SQL的执行计划,定位性能问题
- SQL性能优化全攻略,从表结构设计、索引设计、SQL编写全维度优化
- MySQL高可用集群架构、主从复制、备份与恢复的企业级方案
互动环节
如果你在学习过程中遇到任何问题,比如JOIN结果不对、事务隔离级别实操不明白、窗口函数不会用,都可以在评论区留言,我会一一回复解答。
如果本篇内容对你有帮助,欢迎点赞、收藏、转发,关注我,后续的精通篇、八股文附加篇会第一时间更新,带你一步步吃透MySQL,从入门到精通!