概述
- 数据库(
Database)- 数据的容器,一个
MySQL服务可以有多个数据库
- 数据的容器,一个
- 表(
Table)- 数据库中存储数据的基本单位,由行(记录)和列(字段)组成
- 字段(
Column)- 数据类型的约束,比如
INT、VARCHAR、DATE等
- 数据类型的约束,比如
数据类型
整数类
TINYINT,INT,BIGINT。INT最常用,BIGINT用于存ID或大数字
字符串类
CHAR(n)定长,VARCHAR(n)变长- 短且固定的用
CHAR(如性别),长度不定的用VARCHAR(如用户名)
- 短且固定的用
TEXT用于存大段文字
时间类
DATE(日期)、DATETIME(日期+时间)、TIMESTAMP(时间戳,会自动转时区)
浮点类
FLOAT,DOUBLE有精度问题,涉及金额建议用DECIMAL(m,d)
数据库和表的管理
常规操作
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
-- 创建数据库 CREATE DATABASE mydb DEFAULT CHARSET utf8mb4; -- 使用数据库 USE mydb; -- 创建表 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, email VARCHAR(100) UNIQUE, age INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 查看表结构 DESC users; -- 修改表:加字段 ALTER TABLE users ADD COLUMN phone VARCHAR(20); -- 修改表:改字段类型 ALTER TABLE users MODIFY COLUMN name VARCHAR(100); -- 删表 / 删库(慎用) DROP TABLE users; DROP DATABASE mydb; |
关键约束
PRIMARY KEY— 主键,唯一且非空,一般配合AUTO_INCREMENTNOT NULL— 不允许为空UNIQUE— 唯一,不能重复DEFAULT— 默认值FOREIGN KEY— 外键,关联另一张表- 实际开发中很多团队选择在应用层维护,不在数据库层加外键
增
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
-- 插入单条 INSERT INTO users (name, email, age) VALUES ('张三', 'zhangsan@qq.com', 25); -- 插入多条(批量插入性能远优于逐条插入) INSERT INTO users (name, email, age) VALUES ('李四', 'lisi@qq.com', 30), ('王五', 'wangwu@qq.com', 28); -- 插入时忽略冲突(主键或唯一索引冲突时跳过) INSERT IGNORE INTO users (name, email, age) VALUES ('张三', 'zhangsan@qq.com', 25); -- 插入或更新(存在则更新,不存在则插入) INSERT INTO users (name, email, age) VALUES ('张三', 'zhangsan@qq.com', 26) ON DUPLICATE KEY UPDATE age = VALUES(age); |
删
- 注意
- 写
DELETE之前,先把DELETE换成SELECT跑一遍,确认要删的数据是对的,再改回DELETE执行
- 写
|
1 2 3 4 5 6 7 8 |
-- 按条件删除 DELETE FROM users WHERE id = 3; -- 删除全部数据(逐行删,可回滚,慢) DELETE FROM users; -- 清空表(直接截断,不可回滚,快,自增ID会重置) TRUNCATE TABLE users; |
改
- 注意
UPDATE一定要带WHERE条件。不带WHERE会更新整张表,这是生产环境的经典事故
|
1 2 3 4 5 6 7 8 |
-- 基本更新 UPDATE users SET age = 26 WHERE name = '张三'; -- 同时更新多个字段 UPDATE users SET age = 26, email = 'new@qq.com' WHERE id = 1; -- 基于计算更新 UPDATE users SET age = age + 1; -- 所有人年龄加1 |
查
- 基础查询
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
-- 查所有 SELECT * FROM users; -- 查指定字段(推荐,避免 SELECT *) SELECT name, age FROM users; -- 条件查询 SELECT * FROM users WHERE age > 25; SELECT * FROM users WHERE name = '张三' AND age > 20; SELECT * FROM users WHERE age IN (25, 28, 30); SELECT * FROM users WHERE name LIKE '张%'; -- 以"张"开头 SELECT * FROM users WHERE email IS NULL; -- 判断空值用 IS NULL,不能用 = NULL -- 去重 SELECT DISTINCT age FROM users; -- 别名 SELECT name AS 姓名, age AS 年龄 FROM users; |
- 排序与分页
|
1 2 3 4 5 6 |
-- 排序(ASC 升序,DESC 降序) SELECT * FROM users ORDER BY age DESC; -- 分页(跳过前10条,取10条) SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 10; -- 简写:LIMIT 10, 10(第一个是偏移量,第二个是数量) |
- 聚合函数
|
1 2 3 4 5 |
SELECT COUNT(*) FROM users; -- 总数 SELECT MAX(age) FROM users; -- 最大值 SELECT MIN(age) FROM users; -- 最小值 SELECT AVG(age) FROM users; -- 平均值 SELECT SUM(age) FROM users; -- 求和 |
- 分组
|
1 2 3 4 5 |
-- 按年龄分组,统计每个年龄有多少人 SELECT age, COUNT(*) AS cnt FROM users GROUP BY age; -- 分组后筛选(WHERE 在分组前过滤,HAVING 在分组后过滤) SELECT age, COUNT(*) AS cnt FROM users GROUP BY age HAVING cnt > 2; |
- 多级联查
JOIN
|
1 2 3 4 5 6 7 8 9 10 11 |
-- 内连接:只返回两表都能匹配上的数据 SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id; -- 左连接:返回左表全部 + 右表匹配的(没匹配到的补 NULL) SELECT u.name, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id; -- 右连接:与左连接相反,实际中左连接更常用 |
- 子查询
|
1 2 3 4 5 |
-- 查询下过单的用户 SELECT * FROM users WHERE id IN (SELECT user_id FROM orders); -- 查询年龄大于平均年龄的用户 SELECT * FROM users WHERE age > (SELECT AVG(age) FROM users); |
SQL 执行顺序
- 写
SQL的顺序和数据库实际执行的顺序不一样,理解这个很重要- 这就是为什么
WHERE里不能用SELECT中定义的别名,但ORDER BY可以
- 这就是为什么
|
1 |
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT |
实用小技巧
- 如果字段为 NULL,给一个默认值:
|
1 |
`SELECT IFNULL(phone, '未填写') FROM users;` |
- 范围查询
|
1 |
WHERE age BETWEEN 20 AND 30 |
- 拼接字符串
|
1 |
SELECT CONCAT(name, '-', age) FROM users; |
- 当前时间
|
1 |
INSERT INTO users (name, created_at) VALUES ('test', NOW()); |
MySQL 整体架构
概述
MySQL采用的是分层架构,从上到下大致分三层- 连接层
- 服务层
- 存储引擎层
连接层
- 概述
- 客户端通过
TCP连接到MySQL服务端,连接层负责认证(用户名、密码、权限校验)和连接管理 - 每个连接会分配一个线程来处理请求
- 频繁创建销毁连接开销大,所以生产中一般会用连接池(如
HikariCP、Druid)来复用连接
- 客户端通过
服务层
MySQL的"大脑",与存储引擎无关的逻辑都在这一层,包含几个关键组件:- 解析器(
Parser):- 将
SQL文本做词法分析和语法分析,生成一棵解析树(Parse Tree) - 如果语法错误,就在这一步报错
- 将
- 优化器(
Optimizer):- 拿到解析树后,优化器决定"怎么执行最快",比如选择用哪个索引、
JOIN的顺序、是否用临时表等 - 同一条 SQL 可能有多种执行方案,优化器基于成本模型(
Cost-Based Optimization)选出代价最小的那个
- 拿到解析树后,优化器决定"怎么执行最快",比如选择用哪个索引、
- 执行器(
Executor):- 按照优化器给出的执行计划,调用存储引擎的接口逐步执行,拿到数据后返回给客户端
- 执行前会再做一次权限校验
存储引擎层
MySQL的一大特色是存储引擎可插拔- 不同的表可以用不同的存储引擎
InnoDB(默认,最重要):- 支持事务、行级锁、外键、
MVCC、崩溃恢复
- 支持事务、行级锁、外键、
MyISAM:- 不支持事务和行锁,但查询快、全文索引支持好,老系统中常见
Memory:- 数据存在内存中,重启丢失,适合临时表
Archive:- 只支持插入和查询,适合归档日志类数据
完整流程
|
1 |
客户端 → 连接层(认证) → 解析器(语法检查) → 优化器(选执行计划) → 执行器 → 存储引擎(读写数据) → 返回结果 |
InnoDB 存储引擎原理
概述
InnoDB是MySQL最核心的存储引擎
数据结构
InnoDB的数据是以页(Page)为基本单位存储在磁盘上的,每页默认大小16KB- 层级
- 一张表的数据存在一个
.ibd文件中(独立表空间模式下) - 读写数据时,
MySQL不是一行一行读磁盘的,而是一次读一整页到内存中,这是因为磁盘IO的最小单位是页
- 一张表的数据存在一个
|
1 |
表空间(Tablespace)→ 段(Segment)→ 区(Extent,连续64个页,1MB)→ 页(Page,16KB)→ 行(Row) |
索引结构:B+ 树
InnoDB的索引用的是B+树,这是理解MySQL底层最关键的数据结构- 为什么是
B+树而不是其他结构- 二叉树 / 红黑树:层数太高,每一层就是一次磁盘
IO,数据量大时性能差 B树:每个节点都存数据,导致每个节点能存的key数量少,树更高B+树的优势:非叶子节点只存key不存数据,所以一个节点可以存更多key,树更矮更胖,IO次数少
叶子节点之间用双向链表串联,范围查询(BETWEEN、ORDER BY)非常高效
- 二叉树 / 红黑树:层数太高,每一层就是一次磁盘
- 实际数据
- 一棵
3层的B+树大约可以存 两千万行数据 - 也就是说,查两千万行的表,最多只需要
3次磁盘IO
- 一棵
两种索引
- 聚簇索引
Clustered Index- 就是主键索引
InnoDB中表数据本身就是按主键组织的B+树,叶子节点存的是完整的行数据- 所以
InnoDB的表本质上就是一棵B+树
- 二级索引
Secondary Index- 非主键索引
- 叶子节点存的不是行数据,而是主键值
- 通过二级索引查到主键后,还要回到聚簇索引去取完整数据,这个过程叫做回表
Buffer Pool(缓冲池)
- 磁盘
IO是数据库最大的性能瓶颈,所以InnoDB在内存中维护了一个Buffer Pool - 工作原理
- 读数据时,先看
Buffer Pool里有没有对应的页,有就直接从内存读(快几个数量级)
没有就从磁盘读进来放到Buffer Pool中 - 写数据时也是先写
Buffer Pool中的页(变成"脏页"),然后由后台线程异步刷到磁盘
- 读数据时,先看
Buffer Pool用的是改良的LRU(Least Recently Used)算法来管理页的淘汰- 为什么要"改良"?
- 因为如果执行一个全表扫描,会一次性把大量冷数据加载进来,把真正的热数据挤出去
- 所以
InnoDB把LRU链表分成了年轻代和老年代两段
新读入的页先放老年代
只有在老年代待够一定时间后再次被访问才会晋升到年轻代
- 生产环境中
innodb_buffer_pool_size一般设为物理内存的60%~80%
日志系统
-
InnoDB的可靠性和性能都靠日志系统支撑 -
redo log(重做日志)—— 崩溃恢复的保障- 问题:
数据修改先写在Buffer Pool内存中,如果这时候MySQL崩溃了,内存中的脏页还没刷到磁盘,数据不就丢了? - 解决:
每次修改数据时,先把"做了什么修改"写到redo log中(顺序写磁盘,极快),再修改Buffer Pool
这样即使崩溃,重启后也能从redo log中恢复
这就是WAL(Write-Ahead Logging,先写日志再写数据)机制 redo log是InnoDB引擎层的日志,大小固定,循环写入(写满了就把对应的脏页刷盘腾出空间)
- 问题:
-
undo log(回滚日志)—— 事务回滚 +MVCC- 每次修改数据前,先把修改前的旧值记到 undo log 中
- 作用有两个:
事务回滚时,用undo log把数据恢复原样
MVCC中用来构造数据的历史版本,让不同事务能读到不同时刻的快照
-
binlog(归档日志)—— 主从复制 + 数据恢复binlog是Server层(不是InnoDB层)的日志,记录所有对数据有修改的SQL- 它的作用是主从复制(从库通过
binlog重放主库的操作)和数据恢复(配合备份 +binlog可以恢复到任意时间点)
-
redo log和binlog的两阶段提交:- 一次事务提交的流程:
- 为什么要两阶段?
为了保证redo log和binlog的一致性
如果在第2步崩溃,重启后发现redo log是prepare但binlog没写完,就回滚
如果binlog写完了但第3步没执行,就提交。这保证了主从数据一致
|
1 2 3 |
1. 写 redo log(prepare 状态) 2. 写 binlog 3. 将 redo log 改为 commit 状态 |
事务与 MVCC
- 事务的四大特性(
ACID):- 原子性(
Atomicity):靠undo log实现,要么全做要么全不做 - 一致性(
Consistency):是最终目标,靠其他三个特性保证 - 隔离性(
Isolation):靠锁和MVCC实现 - 持久性(
Durability):靠redo log实现
- 原子性(
MVCC(多版本并发控制)的核心原理:- 每一行数据有两个隐藏字段:
trx_id(最后修改该行的事务ID)和roll_pointer(指向undo log中旧版本的指针)
多次修改会形成一个版本链 - 当一个事务执行
SELECT时,InnoDB会生成一个Read View(读视图),里面记录了当前活跃的事务ID列表
通过对比trx_id和Read View,决定该行的哪个版本对当前事务"可见" - 这样读操作不用加锁,读写互不阻塞,大大提升了并发性能
- 每一行数据有两个隐藏字段:
锁机制
- 行锁:
InnoDB默认的锁粒度,只锁住涉及的行
- 间隙锁(
Gap Lock):- 锁住索引记录之间的"间隙",防止其他事务在间隙中插入新行,用于解决幻读
- 临键锁(
Next-Key Lock):- 行锁 + 间隙锁的组合,是
InnoDB在可重复读隔离级别下的默认加锁方式
- 行锁 + 间隙锁的组合,是
- 表锁:
- 粒度大,
MyISAM用的就是表锁
- 粒度大,
其他
一条 UPDATE 语句的完整执行流程
- 在
InnoDB中的执行过程:- 连接层:验证权限
- 解析器:解析
SQL,生成解析树 - 优化器:选择用主键索引查找
- 执行器:调用
InnoDB接口 InnoDB:在Buffer Pool中查找id=1的页,没有则从磁盘读入- 将修改前的旧值写入
undo log(用于回滚和MVCC) - 在
Buffer Pool中修改数据(此时数据页变为脏页) - 将修改操作写入
redo log(prepare状态) - 将修改操作写入
binlog - 将
redo log标记为commit - 返回"更新成功"
- 后台线程会在合适时机将脏页刷到磁盘
|
1 |
UPDATE users SET age = 26 WHERE id = 1; |
学习
全连接和交叉连接(笛卡尔积)的区别
- 条件
- 全连接有ON条件
- 交叉连接没有条件
- 结果行数
- 全连接:小于等于
A+B - 交叉连接:
AxB
- 全连接:小于等于
NULL处理- 全连接:无匹配补
NULL - 交叉连接:不涉及补
NULL
- 全连接:无匹配补
- 用途
- 全连接:保留两表所有记录
- 交叉连接:生成所有组合
主键
- 举例
- 去办事,工作人员要找你的档案。叫"张三"的人太多了,但你的身份证号全国唯一,一找一个准
- 特点
- 唯一:不能有两行的主键值相同
- 非空:不能为
NULL - 一张表只能有一个主键(但主键可以由多列组合)
|
1 2 3 4 5 |
CREATE TABLE 学生 ( 学号 INT PRIMARY KEY, -- 主键 姓名 VARCHAR(20), 年龄 INT ); |
- 作用
- 唯一标识每一行,防止重复
- 自动建索引,加速查询
主键背后MySQL自动建了一棵B+树索引,所以按主键查数据非常快
外键
- 举例
- 学校有"学生表"和"班级表"。学生表里存了一个"班级
ID",这个ID指向班级表的主键 - 这个"班级
ID"就是外键
- 学校有"学生表"和"班级表"。学生表里存了一个"班级
- 外键
- 就是在
A表里,存了一个指向B表主键的字段,用来建立两张表之间的关联
- 就是在
|
1 2 3 4 5 6 7 8 9 10 11 |
CREATE TABLE 班级 ( 班级ID INT PRIMARY KEY, 班级名 VARCHAR(20) ); CREATE TABLE 学生 ( 学号 INT PRIMARY KEY, 姓名 VARCHAR(20), 班级ID INT, FOREIGN KEY (班级ID) REFERENCES 班级(班级ID) -- 外键 ); |
- 作用
- 外键的核心作用是数据完整性约束——防止出现"孤儿数据"
- 插入时校验:你引用的数据必须存在
- 删除时校验:被引用的数据不能随便删
- 主外键关联使用
|
1 2 3 4 5 |
班级表 学生表 课程表 选课表 -------- -------- -------- -------- 班级ID(PK) 学号(PK) 课程ID(PK) 选课ID(PK) 班级名 姓名 课程名 学号(FK→学生) 班级ID(FK→班级) 课程ID(FK→课程) |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
-- 1. 先建"被引用"的表(父表) CREATE TABLE 班级 ( 班级ID INT PRIMARY KEY, 班级名 VARCHAR(20) ); -- 2. 再建"引用别人"的表(子表) CREATE TABLE 学生 ( 学号 INT PRIMARY KEY, 姓名 VARCHAR(20), 班级ID INT, FOREIGN KEY (班级ID) REFERENCES 班级(班级ID) ); -- 3. 多对多关系用中间表 CREATE TABLE 选课 ( 选课ID INT PRIMARY KEY, 学号 INT, 课程ID INT, FOREIGN KEY (学号) REFERENCES 学生(学号), FOREIGN KEY (课程ID) REFERENCES 课程(课程ID) ); |
|
1 2 3 4 5 |
-- 查询每个学生的姓名和所在班级名 SELECT 学生.姓名, 班级.班级名 FROM 学生 JOIN 班级 ON 学生.班级ID = 班级.班级ID; -- ↑ 子表外键 ↑ 父表主键 |
主键的底层原理
B+树索引MySQL(InnoDB)的表数据本身就是按主键排好序、存在一棵B+树里的(叫做"聚簇索引")- 查
学号 = 1001:沿树走几步就找到,O(log n)效率 - 插入数据:按主键值插入树的对应位置,自动保持有序
|
1 2 3 |
[1001, 1050] / | \ [1001..1020] [...] [1040..1050] |
- 主键为什么要求唯一且非空
- 因为树的节点靠这个值来定位,重复或空值会让树无法正确工作
外键的原理
- 外键本质上是
MySQL在增删改时自动执行的约束检查
|
1 2 3 4 5 6 7 8 9 10 11 |
插入子表一行 ↓ MySQL 自动去父表查:这个外键值存在吗? ├── 存在 → 允许插入 ✅ └── 不存在 → 拒绝插入 ❌ 删除父表一行 ↓ MySQL 自动查:有子表引用这行吗? ├── 没有 → 允许删除 ✅ └── 有 → 取决于你设置的策略 |
- 外键还支持级联策略,控制父表被删/改时子表怎么处理:
|
1 2 3 4 |
FOREIGN KEY (班级ID) REFERENCES 班级(班级ID) ON DELETE CASCADE -- 父表删了,子表跟着删 ON DELETE SET NULL -- 父表删了,子表该字段变NULL ON DELETE RESTRICT -- 父表有子表引用,禁止删(默认) |
索引
- 没有索引时会发生什么
MySQL的做法:从第一行翻到最后一行,逐行比对- 这叫全表扫描
- 举例
- 新华字典。想找"张"字,你不会从第
1页翻到最后,而是先查目录(拼音索引),直接翻到对应页 - 目录就是索引
- 新华字典。想找"张"字,你不会从第
- 索引的底层结构:
B+树InnoDB引擎的索引,底层是一棵B+树- 假设有
8个数,普通查找最多比8次。但如果组织成二叉搜索树,只需3次。数据量越大,优势越明显。
|
1 2 3 4 5 |
4 / \ 2 6 / \ / \ 1 3 5 7 |
B+树长什么样?- 只有叶子节点存真实数据,上面的节点只存"路标"
- 所有叶子节点用链表连接,方便范围查询
- 树很矮(通常只有
3-4层),查任何数据最多走3-4步
|
1 2 3 4 5 6 7 |
[30, 60] ← 根节点(只存键值,不存数据) / | \ [10,20] [40,50] [70,80] ← 中间节点(只存键值) / | \ [1-9] [10-19] [20-29] ... ← 叶子节点(存真实数据行) ↕ ↕ ↕ (叶子节点之间用链表串联)→→→→→→→→ |
- 为什么是
B+树而不是其他结构?
| 结构 | 问题 |
| 数组 | 插入/删除要移动大量数据,慢 |
| 普通二叉树 | 极端情况退化成链表,变成O(n) |
| 哈希表 | 只能等值查询,不支持范围查询(>、<、BETWEEN) |
B 树 |
每个节点都存数据,导致树更高,范围查询要回溯 |
B+ 树 |
矮胖、叶子链表支持范围查询、非常适合磁盘IO |
- 索引的常见类型
- 复合索引的"最左前缀"原则
复合索引(姓名, 年龄)的存储是先按姓名排序,姓名相同再按年龄排序
- 复合索引的"最左前缀"原则
|
1 2 3 4 5 6 7 8 9 10 11 |
-- 普通索引 CREATE INDEX idx_name ON 学生(姓名); -- 唯一索引(值不能重复) CREATE UNIQUE INDEX idx_id_card ON 学生(身份证号); -- 复合索引(多列组合) CREATE INDEX idx_name_age ON 学生(姓名, 年龄); -- 主键索引(建表时自动创建) PRIMARY KEY (学号) |
- 索引什么时候会失效?
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
-- ❌ 对索引列做函数运算 WHERE YEAR(create_time) = 2024 -- 应改为范围查询 -- ❌ 隐式类型转换(字段是varchar,传了int) WHERE 手机号 = 13800138000 -- 应该加引号 -- ❌ LIKE 左模糊 WHERE 姓名 LIKE '%张' -- 左边不确定,无法走树 -- ✅ LIKE 右模糊可以走索引 WHERE 姓名 LIKE '张%' -- ❌ OR 连接了没有索引的列 WHERE 姓名 = '张三' OR 地址 = '北京' -- 地址没索引,整体失效 |
两种核心索引
- 聚簇索引(主键索引)
- 数据本身就按主键顺序存在
B+树的叶子节点里
索引即数据,数据即索引 - 每张表只有一棵聚簇索引(因为数据只能按一种顺序存)
- 按主键查询,一次
B+树查找直接拿到完整数据行
- 数据本身就按主键顺序存在
- 非聚簇索引(二级索引)
- 给非主键字段建的索引
- 叶子节点存的不是完整数据,而是该字段值 + 对应的主键值
|
1 2 3 4 5 6 7 |
比如给"姓名"建索引: 叶子节点存:姓名 + 主键 [姓名=李四 | 学号=1002] [姓名=王五 | 学号=1003] [姓名=张三 | 学号=1001] ← 按姓名排序 |
|
1 2 3 4 5 6 |
SELECT * FROM 学生 WHERE 姓名 = '张三'; -- ①走姓名索引的 B+ 树 → 找到"张三"对应主键 = 1001 -- ②拿着 1001 再去主键索引的 B+ 树查一次 → 拿到完整数据行 这个第②步叫做"回表" |
- 回表 vs 覆盖索引
- 覆盖索引是一种重要的性能优化手段,能减少一次
B+树查找
- 覆盖索引是一种重要的性能优化手段,能减少一次
|
1 2 |
-- 需要回表(SELECT * 要取所有字段) SELECT * FROM 学生 WHERE 姓名 = '张三'; |
|
1 2 3 4 |
-- 不需要回表(查询的字段索引里都有) SELECT 学号, 姓名 FROM 学生 WHERE 姓名 = '张三'; -- 姓名索引叶子节点里已经有"学号+姓名",直接返回,无需回表 -- 这就叫"覆盖索引" |
视图
- 为什么需要视图?
- 如果业务里有一个超高频查询——"查每个学生的姓名、班级名、所有课程成绩"
- 这段
SQL又长又复杂,每次用都要写一遍,而且每个同事写的可能还不一样
|
1 2 3 4 5 6 7 |
SELECT s.姓名, c.班级名, co.课程名, sc.成绩 FROM 学生 s JOIN 班级 c ON s.班级ID = c.班级ID JOIN 选课 sc ON s.学号 = sc.学号 JOIN 课程 co ON sc.课程ID = co.课程ID WHERE s.年龄 > 18; |
- 视图就是为了解决这个问题
- 类比:经常要去某个地方,每次都要输完整地址导航太麻烦。于是把这个地址保存成"收藏",下次直接点收藏就行
- 视图就是
SQL查询的"收藏夹"
- 什么是视图?
- 视图是一张虚拟表
- 它本身不存数据,只是把一段
SELECT语句存起来起了个名字,用的时候像查真实表一样去查它
|
1 2 3 4 5 6 7 8 9 |
-- 把复杂查询存成视图 CREATE VIEW 学生成绩单 AS SELECT s.姓名, c.班级名, co.课程名, sc.成绩 FROM 学生 s JOIN 班级 c ON s.班级ID = c.班级ID JOIN 选课 sc ON s.学号 = sc.学号 JOIN 课程 co ON sc.课程ID = co.课程ID WHERE s.年龄 > 18; |
|
1 2 3 4 |
-- 当成普通表查就行 SELECT * FROM 学生成绩单; SELECT * FROM 学生成绩单 WHERE 姓名 = '张三'; SELECT * FROM 学生成绩单 WHERE 成绩 > 90; |
- 视图是怎么起作用的?
- 很多人以为视图是把查询结果"复制"了一份存起来,其实不是
- 具体看下面视图工作流程
- 视图的工作流程:
- 你查询视图
MySQL把视图的定义(那段SELECT)取出来- 和你外层的查询合并,展开成一个完整
SQL - 去真实的表里执行
- 把结果返回给你
|
1 2 3 4 5 6 7 8 9 10 |
-- 你写的 SELECT * FROM 学生成绩单 WHERE 成绩 > 90; -- MySQL 实际执行的(视图被展开) SELECT s.姓名, c.班级名, co.课程名, sc.成绩 FROM 学生 s JOIN 班级 c ON s.班级ID = c.班级ID JOIN 选课 sc ON s.学号 = sc.学号 JOIN 课程 co ON sc.课程ID = co.课程ID WHERE s.年龄 > 18 AND sc.成绩 > 90; ← 条件被合并进去了 |
- 视图能做什么?
- 简化复杂查询
- 数据权限控制
- 数据格式加工
- 兼容旧接口
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
-- 员工表有:姓名、工资、身份证号、部门 CREATE TABLE 员工 ( 员工ID INT PRIMARY KEY, 姓名 VARCHAR(20), 工资 DECIMAL(10,2), -- 敏感! 身份证 VARCHAR(18), -- 敏感! 部门 VARCHAR(20) ); -- 创建一个"脱敏视图",只暴露安全字段 CREATE VIEW 员工公开信息 AS SELECT 员工ID, 姓名, 部门 FROM 员工; -- 普通员工只给他查视图的权限,看不到工资和身份证 GRANT SELECT ON 员工公开信息 TO '普通用户'; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
-- 把原始数据加工成更友好的格式 CREATE VIEW 订单概览 AS SELECT 订单ID, 用户名, FORMAT(金额, 2) AS 金额, CASE 状态 WHEN 1 THEN '待付款' WHEN 2 THEN '已付款' WHEN 3 THEN '已发货' END AS 订单状态, DATE_FORMAT(创建时间, '%Y年%m月%d日') AS 下单日期 FROM 订单 JOIN 用户 ON 订单.用户ID = 用户.用户ID; |
|
1 2 3 4 5 6 7 8 9 10 11 |
-- 旧表叫 user,被拆分成了 user_base 和 user_detail -- 但老代码还在 SELECT * FROM user -- 用视图模拟出一张"旧表" CREATE VIEW user AS SELECT b.id, b.name, b.email, d.phone, d.address FROM user_base b JOIN user_detail d ON b.id = d.user_id; -- 老代码完全不用改,照样能跑 SELECT * FROM user WHERE id = 1; |
- 视图能不能增删改?
- 视图虽然是虚拟表,某些情况下可以对它进行写操作,
MySQL会把操作转到真实表上 - 实际开发中,视图主要用来查,写操作直接操作原始表更清晰
- 视图虽然是虚拟表,某些情况下可以对它进行写操作,
|
1 2 3 |
-- 简单视图(来自单张表,没有聚合)可以UPDATE UPDATE 员工公开信息 SET 姓名 = '李四' WHERE 员工ID = 1; -- MySQL 实际去改的是员工表里的数据 ✅ |
|
1 2 3 4 |
-- ❌ 包含 JOIN(涉及多表,MySQL不知道改哪张) -- ❌ 包含聚合函数 SUM/COUNT/AVG 等 -- ❌ 包含 DISTINCT、GROUP BY、HAVING -- ❌ 包含子查询 |
- 视图的管理
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
-- 创建视图 CREATE VIEW 视图名 AS SELECT ...; -- 修改视图 CREATE OR REPLACE VIEW 视图名 AS SELECT ...; -- 不存在就创建,存在就替换 -- 查看视图定义 SHOW CREATE VIEW 视图名; -- 删除视图 DROP VIEW 视图名; -- 查看当前数据库所有视图 SHOW FULL TABLES WHERE Table_type = 'VIEW'; |
事务
- 为什么需要事务?
- 银行转账,张三给李四转
1000元,需要两步: - 如果第①步执行完,服务器突然宕机,第②步没执行:
张三:少了1000块
李四:没收到钱
1000块人间蒸发 - 事务就是为了解决这个问题
- 银行转账,张三给李四转
|
1 2 |
UPDATE 账户 SET 余额 = 余额 - 1000 WHERE 姓名 = '张三'; -- 第①步 UPDATE 账户 SET 余额 = 余额 + 1000 WHERE 姓名 = '李四'; -- 第②步 |
- 类比:
- 网购下单流程——扣库存、扣余额、生成订单,这三步要么全成功,要么全不做
- 不能扣了钱却没生成订单
- 事务就是给这一组操作套上一个"要么全做,要么全不做"的保险
- 什么是事务?
- 事务是一组
SQL操作的逻辑单元 - 这组操作要么全部成功,要么全部回滚,不存在中间状态
- 事务是一组
|
1 2 3 4 5 6 7 8 9 10 11 |
-- 开启事务 START TRANSACTION; UPDATE 账户 SET 余额 = 余额 - 1000 WHERE 姓名 = '张三'; UPDATE 账户 SET 余额 = 余额 + 1000 WHERE 姓名 = '李四'; -- 全部成功,提交 COMMIT; -- 如果中途出错,回滚(撤销所有操作) ROLLBACK; |
- 加了事务之后:
- 情况一:两步都成功 →
COMMIT→ 数据永久保存 ✅ - 情况二:第①步后宕机 → 重启后自动
ROLLBACK→ 两步都没发生 ✅ - 永远不会出现"只做了一半"的状态
- 情况一:两步都成功 →
- 事务的四大特性(
ACID)- 原子性(
Atomicity)
事务里的操作,要么全做,要么全不做。不可分割,像原子一样 - 一致性(
Consistency)
事务前后,数据必须符合业务规则,不能出现"不合理"的状态 - 隔离性(
Isolation)
多个事务同时执行时,互相不干扰,每个事务感觉自己是"独占"数据库的 - 持久性(
Durability)
事务一旦提交(COMMIT),数据就永久保存了。就算立刻断电,重启后数据还在
- 原子性(
- 事务是怎么起作用的?
- 回滚靠的是:
Undo Log(撤销日志)
每次修改数据前,MySQL先把原来的值记录到Undo Log: - 持久性靠的是:
Redo Log(重做日志)
COMMIT时,MySQL不是直接把数据写磁盘(太慢),而是先写Redo Log
把"做了什么操作"写入Redo Log(速度极快,顺序写)
返回"提交成功"给用户
之后再找机会把数据刷入真正的数据文件
- 回滚靠的是:
|
1 2 3 4 5 6 7 8 9 10 |
执行:UPDATE 账户 SET 余额 = 0 WHERE 姓名 = '张三' MySQL先记日志: Undo Log → "张三的余额原来是1000" 再改数据: 张三余额 → 0 如果需要ROLLBACK: 读取Undo Log → 把张三余额改回1000 ✅ |
事务隔离
- 概述
- 多个事务并发执行时,隔离得"越严",越安全,但性能越差。
MySQL提供四个级别:
- 先认识三种并发问题:
- 脏读:读到了另一个事务还没提交的数据
- 不可重复读:同一事务里,两次读同一行,结果不一样
- 幻读:同一事务里,两次查询,第二次多出(或少了)几行
|
1 2 3 4 |
事务A:把张三余额改成0(还没提交) 事务B:读到张三余额=0 ← 脏数据! 事务A:ROLLBACK,张三余额恢复1000 事务B:拿着错误的0去做后续操作 💥 |
|
1 2 3 |
事务B:第一次读张三余额=1000 事务A:把张三余额改成0并提交 事务B:第二次读张三余额=0 ← 同一事务里读到不同值! |
|
1 2 3 |
事务B:查询余额>500的账户,得到3条 事务A:新插入一个余额=800的账户并提交 事务B:再查余额>500的账户,得到4条 ← 多出来一行! |
- 四个隔离级别
MySQL默认是REPEATABLE READ,通过MVCC机制基本解决了幻读问题,是性能和安全的最佳平衡点
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
READ UNCOMMITTED(读未提交) |
❌有 | ❌有 | ❌有 | 最快 |
READ COMMITTED(读已提交) |
✅无 | ❌有 | ❌有 | 较快 |
REPEATABLE READ(可重复读) |
✅无 | ✅无 | ⚠️基本无 | 适中 |
SERIALIZABLE(串行化) |
✅无 | ✅无 | ✅无 | 最慢 |
|
1 2 3 4 5 |
-- 查看当前隔离级别 SELECT @@transaction_isolation; -- 修改隔离级别 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; |
隔离性的底层原理:MVCC
- 概述
MVCC(多版本并发控制)是MySQL实现隔离性的核心机制,让读写操作互不阻塞
- 核心思想:
- 不同时刻的读,看到的是数据的不同"历史快照"
- 每行数据背后,
MySQL隐藏了两个字段trx_id:最后修改这行的事务IDroll_pointer:指向Undo Log里的历史版本
- 比如张三的账户数据,历史版本链:
- 事务开始时,生成一个快照(
ReadView),记录"此刻哪些事务还没提交" - 读数据时,沿着版本链找到对自己可见的那个版本
- 事务
B(ID=150)开启快照:我能看到ID≤150的已提交数据 - 事务A(
ID=200)把余额改成0,但还没提交 - 事务
B读张三余额:
→ 看到余额=0,是事务200改的,200>150,对我不可见
→ 往历史版本找
→ 看到余额=1000,是事务100改的,100<150且已提交,可见!
→ 返回1000✅ - 事务
B读到的是干净的历史快照,完全不受事务A影响 - 类比:
git的提交历史。你checkout到某个历史commit,看到的是那个时间点的代码,别人在main分支上改什么你都看不到
- 事务开始时,生成一个快照(
|
1 2 3 4 5 |
当前版本:余额=0 (事务ID=200修改的) ↓ 历史版本:余额=1000 (事务ID=100修改的) ↓ 更早版本:余额=500 (事务ID=50修改的) |
事务的实际使用
- 手动事务
|
1 2 3 4 5 6 7 8 9 10 11 12 |
START TRANSACTION; UPDATE 账户 SET 余额 = 余额 - 1000 WHERE 姓名 = '张三'; -- 检查张三余额是否足够 -- 假设用程序判断,余额不够就回滚 IF 余额 < 0 THEN ROLLBACK; -- 撤销 ELSE UPDATE 账户 SET 余额 = 余额 + 1000 WHERE 姓名 = '李四'; COMMIT; -- 提交 END IF; |
- 保存点(部分回滚)
|
1 2 3 4 5 6 7 8 9 10 |
START TRANSACTION; UPDATE 账户 SET 余额 = 余额 - 1000 WHERE 姓名 = '张三'; SAVEPOINT step1; -- 设置保存点 UPDATE 账户 SET 余额 = 余额 + 1000 WHERE 姓名 = '李四'; -- 假设这步出错了 ROLLBACK TO step1; -- 只回滚到保存点,第①步保留 -- 可以重试第②步,不用从头来 |
- 注意:
MySQL默认自动提交
|
1 2 3 4 5 6 7 |
-- MySQL 默认每条SQL都是一个独立事务,自动提交 UPDATE 账户 SET 余额 = 0 WHERE 姓名 = '张三'; -- 执行完立刻提交,无法回滚! -- 关闭自动提交 SET autocommit = 0; -- 此后需要手动 COMMIT 或 ROLLBACK |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ 51CTO:Linux C++网络编程五08/20
- ♥ 树总结相关07/26
- ♥ Windows机制:物理内存、虚拟内存03/28
- ♥ 表_唯一约束、非空约束10/26
- ♥ Linux 高性能服务器编程:I/O复用二12/12
- ♥ 表_约束-主键10/26
热评文章
- 数据处理函数:单行处理函数 0
- 表_约束-主键 0
- 表操作_数据排序 0
- 表_唯一约束、非空约束 0
- 表操作_条件查询-运算符 0
- 表_修改表结构 0