MySQL
1. SQL 是什么?
SQL(Structured Query Language) 是一门操作关系型数据库的标准编程语言,用来:
- 定义数据库中的各种对象(数据库、表、字段、索引等)
- 对表中的数据进行增删改查
- 控制数据库用户和权限
一句话:只要是关系型数据库(MySQL、PostgreSQL、Oracle、SQL Server 等),都要讲 SQL,SQL 是它们的统一“交流语言标准”。
2. SQL 的四大分类
常见会把 SQL 按照“主要用途”拆成四大类,便于记忆:
| 分类 | 全称 | 说明 |
|---|---|---|
| DDL | Data Definition Language | 数据定义语言,用来定义数据库对象(库、表、字段) |
| DML | Data Manipulation Language | 数据操作语言,对表中的数据进行增删改 |
| DQL | Data Query Language | 数据查询语言,从表中查询记录 |
| DCL | Data Control Language | 数据控制语言,创建数据库用户、控制访问权限 |
快速联想:
- DDL:结构相关(创建/修改/删除“库、表、字段”)
- DML:数据相关(对记录做
INSERT / UPDATE / DELETE) - DQL:查询相关(
SELECT为主) - DCL:权限相关(
GRANT / REVOKE / CREATE USER等)
下面先从最基础、最常用的 DDL - 数据库级操作 开始。
3. DDL - 数据库级操作
这些语句主要是针对“数据库本身”进行操作,比如查看有哪些库、创建/删除库等。
3.1 查看数据库
-- 查询所有数据库
SHOW DATABASES;
-- 查询当前正在使用的数据库
SELECT DATABASE();3.2 切换数据库
-- 使用 / 切换到指定数据库
USE 数据库名;小技巧:在命令行或客户端工具中,执行
USE xxx;之后,再执行的所有 SQL 默认都作用在这个数据库上。
3.3 创建数据库
-- 创建数据库,如果已存在则不报错
CREATE DATABASE [IF NOT EXISTS] 数据库名
[DEFAULT CHARSET utf8mb4];说明:
IF NOT EXISTS:防止重复创建时报错DEFAULT CHARSET:指定默认字符集(推荐utf8mb4,支持 emoji)
3.4 删除数据库
-- 删除数据库,如果不存在则不报错
DROP DATABASE [IF EXISTS] 数据库名;一定要谨慎执行
DROP DATABASE,生产环境删除库属于高危操作,一般需要额外确认或通过运维流程处理。
4. DDL - 表操作(查询、修改、删除)
对表进行查看、增删改字段、改表名、删表等,都用下面这些语句(需先 USE 数据库名; 进入对应库)。
4.1 查询表
-- 查询当前数据库的所有表
SHOW TABLES;
-- 查询表结构(字段名、类型、约束等)
DESC 表名;
-- 查询建表语句(完整 DDL)
SHOW CREATE TABLE 表名;4.2 修改表(ALTER TABLE)
-- 添加字段
ALTER TABLE 表名 ADD 字段名 类型(长度) [COMMENT 注释] [约束];
-- 修改字段类型(不改字段名)
ALTER TABLE 表名 MODIFY 字段名 新数据类型(长度);
-- 修改字段名与字段类型
ALTER TABLE 表名 CHANGE 旧字段名 新字段名 类型(长度) [COMMENT 注释] [约束];
-- 删除字段
ALTER TABLE 表名 DROP COLUMN 字段名;
-- 修改表名
ALTER TABLE 表名 RENAME TO 新表名;4.3 删除表
-- 删除表,如果不存在则不报错
DROP TABLE [IF EXISTS] 表名;生产环境慎用
DROP TABLE,删表前务必确认库、表名无误,必要时先备份。
5. 约束(Constraints)
在建表或改表时,通过约束可以限制字段的取值,保证数据的正确性和一致性。常用约束如下:
| 约束 | 描述 | 关键字 |
|---|---|---|
| 非空约束 | 限制该字段值不能为 null | NOT NULL |
| 唯一约束 | 保证字段的所有数据都是唯一、不重复的 | UNIQUE |
| 主键约束 | 主键是一行数据的唯一标识,要求非空且唯一 | PRIMARY KEY |
| 默认约束 | 保存数据时,如果未指定该字段值,则采用默认值 | DEFAULT |
| 外键约束 | 让两张表的数据建立连接,保证数据的一致性和完整性 | FOREIGN KEY |
5.1 在建表中使用约束
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名,非空且唯一',
gender CHAR(1) DEFAULT '男' COMMENT '性别,默认男',
dept_id INT COMMENT '部门 id',
CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES dept(id) -- 外键:关联 dept 表
);- 主键:
id作为一行数据的唯一标识,一般配合AUTO_INCREMENT自增。 - 非空 + 唯一:
username不能为 null,且不能重复。 - 默认值:插入时未指定
gender则使用'男'。 - 外键:
dept_id必须在dept(id)中存在,保证“用户所属部门”一定存在。
5.2 约束小结
- 非空 / 唯一 / 默认:只影响本表当前字段。
- 主键:一张表通常只有一个主键(或联合主键),用于唯一标识一行。
- 外键:在应用层(如 Java 代码)做校验也很常见,是否在数据库建外键可根据项目规范选择。
5.3 约束相关常见问题
1. MySQL 数据库中分为哪几类约束,对应的关键字是什么?
常见分为五类,对应关系见上文「5. 约束」中的表格,速记如下:
| 约束类型 | 关键字 |
|---|---|
| 非空约束 | NOT NULL |
| 唯一约束 | UNIQUE |
| 主键约束 | PRIMARY KEY |
| 默认约束 | DEFAULT |
| 外键约束 | FOREIGN KEY |
2. 如何实现主键自增的效果?
在定义主键字段时加上 AUTO_INCREMENT,插入时可以不写该字段,由数据库自动按递增顺序分配值:
id INT PRIMARY KEY AUTO_INCREMENT- 一般只用于整数类型的主键(如
INT、BIGINT)。 - 每张表通常只有一个
AUTO_INCREMENT列。 - 插入时若指定了该列的值,则使用指定值;未指定或写
NULL时才会自增。
3. 一个字段上是否可以添加多个约束?
可以。 一个字段可以同时加多种约束,只要语义不冲突即可。例如:
- 非空 + 唯一:
username VARCHAR(50) NOT NULL UNIQUE(不能为空,且不能重复) - 主键 + 自增:
id INT PRIMARY KEY AUTO_INCREMENT - 非空 + 默认值:
gender CHAR(1) NOT NULL DEFAULT '男'
书写时多个约束用空格隔开即可,顺序一般不影响效果。
6. 数据类型
MySQL 中的数据类型有很多,主要分为三类:数值类型、字符串类型、日期时间类型。建表时根据业务选对类型,既能保证数据正确,又能节省存储空间。
6.1 数值类型
| 类型 | 大小(byte) | 有符号(SIGNED)范围 | 无符号(UNSIGNED)范围 | 描述 |
|---|---|---|---|---|
tinyint | 1 | -128 ~ 127 | 0 ~ 255 | 小整数值 |
smallint | 2 | -32768 ~ 32767 | 0 ~ 65535 | 大整数值 |
mediumint | 3 | -8388608 ~ 8388607 | 0 ~ 16777215 | 大整数值 |
int | 4 | -2147483648 ~ 2147483647 | 0 ~ 4294967295 | 大整数值 |
bigint | 8 | -2^63 ~ 2^63-1 | 0 ~ 2^64-1 | 极大整数值 |
float | 4 | 单精度浮点范围 | 0 及正浮点范围 | 单精度浮点数值 |
double | 8 | 双精度浮点范围 | 0 及正浮点范围 | 双精度浮点数值 |
decimal | 可变 | 取决于精度与小数位 | 同上 | 小数值(精度更高) |
精度写法说明(浮点与小数):
float(5,2)/double(5,2)/decimal(5,2):5 表示整个数字长度(含小数位),2 表示小数位个数。例如可存123.45。- 金额、数量等需要精确计算时,推荐用
decimal,避免浮点误差。
选型原则:在满足业务的前提下,尽量选择占用空间小的类型。
| 场景示例 | 推荐类型 |
|---|---|
| 年龄 | tinyint unsigned |
| 主键 id | int unsigned 或 bigint unsigned |
6.2 字符串类型
| 类型 | 大小(字节) | 描述 |
|---|---|---|
char | 0-255 | 定长字符串 |
varchar | 0-65535 | 变长字符串 |
tinyblob | 0-255 | 不超过 255 的二进制数据 |
tinytext | 0-255 | 短文本字符串 |
blob | 0-65535 | 二进制形式的长文本 |
text | 0-65535 | 长文本数据 |
mediumblob/mediumtext | 0-约 16M | 中等长度二进制/文本 |
longblob/longtext | 0-约 4G | 极大二进制/文本数据 |
char 与 varchar 对比:
| 类型 | 空间占用 | 优势 | 劣势 |
|---|---|---|---|
char(n) | 固定占 n 个字符;存 "A" 也占 n 个 | 性能略高 | 浪费磁盘空间 |
varchar(n) | 最多 n 个字符;存 "A" 占 1 个,存 "ABC" 占 3 个 | 节约磁盘空间 | 性能略低 |
常见用法示例:
- 长度固定的字段(如身份证、手机号):用
char,例如idcard char(18)、phone char(11)。 - 长度不固定的字段(如用户名、简介):用
varchar,例如username varchar(50)。
6.3 日期时间类型
| 类型 | 大小(byte) | 范围 | 格式 | 描述 |
|---|---|---|---|---|
date | 3 | 1000-01-01 ~ 9999-12-31 | YYYY-MM-DD | 日期值 |
time | 3 | -838:59:59 ~ 838:59:59 | HH:MM:SS | 时间值或持续时间 |
year | 1 | 1901 ~ 2155 | YYYY | 年份值 |
datetime | 8 | 1000-01-01 00:00:00 ~ 9999-12-31 23:59:59 | YYYY-MM-DD HH:MM:SS | 日期+时间 |
timestamp | 4 | 1970-01-01 00:00:01 ~ 2038-01-19 03:14:07 | YYYY-MM-DD HH:MM:SS | 日期+时间(时间戳) |
常见用法示例:
- 只存日期:
birthday date - 需要日期+时间(创建/更新时间等):
operate_time datetime或created_at timestamp timestamp受范围限制(约到 2038 年),超出范围或需更大区间时用datetime
6.4 数据类型常见问题
1. 数值类型在定义时,后面加了 unsigned 关键字是什么意思?
unsigned表示无符号类型,该字段只能取 0 及正数(不能存负数)。- 不加 时默认为
signed(有符号),可以取负数。 - 例如:年龄、主键 id 等不会为负的字段,常用
tinyint unsigned、int unsigned等,既省空间又语义清晰。
2. char 与 varchar 的区别是什么?什么时候用 char,什么时候用 varchar?
- 区别:
char是定长字符串,varchar是变长字符串(详见上文 6.2 对比表)。 - 长度固定的字段,建议用
char,例如:身份证号char(18)、手机号char(11)。 - 长度不固定的字段,建议用
varchar,例如:用户名varchar(50)、姓名varchar(20)。
7. DML - 数据操作语言
DML 英文全称是 Data Manipulation Language(数据操作语言),用来对数据库中表的数据记录进行增、删、改操作。
三种基本操作:
- 添加数据:
INSERT - 修改数据:
UPDATE - 删除数据:
DELETE
下面以一张示例表为例说明语法(表结构:id、name 名称、age 年龄、gender 性别,示例数据:令狐冲 28 男、风清扬 68 男、东方不败 32 男)。
7.1 添加数据(INSERT)
-- 为表中所有字段按顺序赋值(值的顺序、个数需与表结构一致)
INSERT INTO 表名 VALUES (值1, 值2, 值3, ...);
-- 为指定字段赋值(未指定字段取默认值或 NULL)
INSERT INTO 表名 (字段1, 字段2, ...) VALUES (值1, 值2, ...);
-- 一次插入多行
INSERT INTO 表名 (字段1, 字段2, ...) VALUES (值1, 值2, ...), (值1, 值2, ...);示例(假设表名为 emp,字段为 id, name, age, gender):
INSERT INTO emp (id, name, age, gender) VALUES (1, '令狐冲', 28, '男');
INSERT INTO emp (id, name, age, gender) VALUES (2, '风清扬', 68, '男'), (3, '东方不败', 32, '男');7.2 修改数据(UPDATE)
-- 更新满足条件的记录(不加 WHERE 会更新整张表,慎用)
UPDATE 表名 SET 字段1 = 值1, 字段2 = 值2, ... [WHERE 条件];示例:
UPDATE emp SET age = 29 WHERE name = '令狐冲';
UPDATE emp SET gender = '女' WHERE id = 3;7.3 删除数据(DELETE)
-- 删除满足条件的记录(不加 WHERE 会删除表中所有记录,慎用)
DELETE FROM 表名 [WHERE 条件];示例:
DELETE FROM emp WHERE id = 3;
-- 删除表中全部记录(表结构仍在)
-- DELETE FROM emp;注意:
UPDATE和DELETE务必带好WHERE条件,避免误改、误删整表数据。
8. DQL - 数据查询语言
DQL(Data Query Language,数据查询语言)用来查询表中记录,核心语句是 SELECT。子句书写顺序与对应内容速记如下(与教材/幻灯片一致):
| 子句 | 对应内容 |
|---|---|
SELECT | 字段列表 |
FROM | 表名列表 |
WHERE | 条件列表 |
GROUP BY | 分组字段列表 |
HAVING | 分组后条件列表 |
ORDER BY | 排序字段列表 |
LIMIT | 分页参数 |
完整语法结构如下(书写顺序与执行顺序不同,需区分):
SELECT
字段列表
FROM
表名列表
WHERE
条件列表
GROUP BY
分组字段列表
HAVING
分组后条件列表
ORDER BY
排序字段列表
LIMIT
分页参数8.1 各子句说明
| 子句 | 说明 |
|---|---|
SELECT | 要查询的字段(可 * 表示全部,或列名、表达式、别名,可配合 DISTINCT 去重) |
FROM | 数据来源的表(可多表,逗号或 JOIN) |
WHERE | 行级过滤条件(在分组、聚合之前过滤) |
GROUP BY | 按哪些字段分组(常配合聚合函数使用) |
HAVING | 分组后的过滤条件(在分组、聚合之后过滤) |
ORDER BY | 排序字段及升序/降序(ASC / DESC) |
LIMIT | 分页:起始下标、条数(或只写条数,从第 1 条起) |
8.2 基本示例
-- 查询全部字段
SELECT * FROM emp;
-- 查询指定字段并起别名
SELECT id, name AS 姓名, age 年龄 FROM emp;
-- 去重查询:查询所有不重复的性别
SELECT DISTINCT gender FROM emp;
-- 使用 CASE 把 job 数字映射为中文职位名,并去重
SELECT DISTINCT
CASE job
WHEN 1 THEN '班主任'
WHEN 2 THEN '讲师'
WHEN 3 THEN '学工主管'
WHEN 4 THEN '教研主管'
WHEN 5 THEN '咨询师'
ELSE '未知'
END AS 职位
FROM emp;
-- 条件查询
SELECT * FROM emp WHERE age > 30;
SELECT * FROM emp WHERE name LIKE '令狐%' AND gender = '男';
-- 排序:按年龄降序
SELECT * FROM emp ORDER BY age DESC;
-- 分页:从第 0 条开始取 10 条(LIMIT 起始下标, 条数)
SELECT * FROM emp LIMIT 0, 10;
-- 等价写法:LIMIT 10 表示取前 10 条
SELECT * FROM emp LIMIT 10;8.3 分组与 HAVING
-- 按性别分组,统计每组人数
SELECT gender, COUNT(*) AS 人数 FROM emp GROUP BY gender;
-- 分组后只保留人数大于 1 的组
SELECT gender, COUNT(*) AS 人数 FROM emp GROUP BY gender HAVING COUNT(*) > 1;WHERE 与 HAVING 区别:WHERE 在分组前过滤行,HAVING 在分组后过滤组(可对聚合结果做条件)。
8.4 书写顺序与执行顺序
书写顺序:SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT
执行顺序:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
(先确定表、过滤行、分组、再选字段、排序、分页。)
8.5 多表查询与笛卡尔积
8.5.1 多表查询是什么?
多表查询:在一条 SELECT 语句中,同时从多张表中获取数据。
- 单表查询语法:
SELECT 字段列表 FROM 表名; - 多表查询基础写法:
SELECT 字段列表 FROM 表1, 表2;
例如:
-- 同时从 emp(员工表)和 dept(部门表)中查询
SELECT * FROM emp, dept;如果不加任何关联条件,查询结果会包含 emp 表的每一行 × dept 表的每一行的所有组合。
- 假设 emp 有 30 行,dept 有 6 行,则结果共有 30 × 6 = 180 行。
- 这就是典型的 笛卡尔积 现象。
8.5.2 什么是笛卡尔积?
笛卡尔积(Cartesian Product)是数学概念:给定两个集合 A 和 B,笛卡尔积表示 A 中每个元素与 B 中每个元素组合 的所有可能。
在 SQL 多表查询中:
FROM 表1, 表2且 没有关联条件 时,会得到“表 1 所有行 × 表 2 所有行”的所有组合。- 其中大部分组合在业务上是无效的(比如员工和部门完全随机匹配)。
示意图:

8.5.3 消除无效的笛卡尔积
在实际开发中,我们通常只关心“有业务关联的行”,例如只保留“员工所属部门匹配”的数据:
-- 通过 WHERE 条件关联 emp.dept_id 与 dept.id,消除无效组合
SELECT
e.id,
e.name,
d.name AS dept_name
FROM
emp e, dept d
WHERE
e.dept_id = d.id;- 只有满足
e.dept_id = d.id的行组合会保留,相当于只保留“有部门的员工”这部分有效数据。 - 这种写法本质上是内连接,也可以用
JOIN ... ON ...的语法来表达(后续连接查询小节再展开)。
示意图(只保留“能配对上的”组合):

记忆:多表查询时,一定要加好关联条件,避免产生大量无意义的笛卡尔积。
8.6 多表查询的分类(总览)
多表查询大致可以分为两大类:
- 连接查询(JOIN)
- 内连接:只保留两张表中能匹配上的行,可以理解为 A、B 的交集部分。
- 外连接:
- 左外连接(LEFT JOIN):保留左表的全部行,右表能匹配上的显示数据,匹配不上的补
NULL。 - 右外连接(RIGHT JOIN):保留右表的全部行,左表能匹配上的显示数据,匹配不上的补
NULL。
- 左外连接(LEFT JOIN):保留左表的全部行,右表能匹配上的显示数据,匹配不上的补
- 子查询(Subquery)
- 在一条
SELECT/UPDATE/DELETE语句的 某个位置再嵌套一条SELECT,内层查询结果作为外层的条件或数据来源。 - 例如:
WHERE id IN (SELECT emp_id FROM …)、FROM (SELECT ... ) t等。
- 在一条
8.7 内连接(INNER JOIN)
8.7.1 概念
内连接查询:查询两表或多表中交集部分数据,即能在所有参与表中成功匹配上的行。
在 SQL 中,内连接从语法上可以分为两种写法:
- 隐式内连接(旧式写法)
- 显式内连接(推荐写法)
8.7.2 隐式内连接语法
SELECT 字段列表
FROM 表1, 表2
WHERE 连接条件 ...;示例(员工与部门):
SELECT
e.id,
e.name,
d.name AS dept_name
FROM
emp e, dept d
WHERE
e.dept_id = d.id;写在
FROM后面的多个表,通过WHERE子句中的连接条件“配对”。只有满足条件的行组合会被保留,本质上就是内连接。
在多表查询中,通常会给表起别名来简化书写:
-- AS 可以省略,推荐为表起简短有意义的别名
SELECT
e.id,
e.name,
d.name AS dept_name
FROM
emp AS e, dept AS d
WHERE
e.dept_id = d.id
AND e.gender = 1
AND e.salary > 8000;- 常见写法:
表名 AS 别名或表名 别名,AS关键字可以省略。 - 注意:一旦为表起了别名,在本条 SQL 中就不能再使用原表名来限定字段,只能使用别名(例如只能写
e.name,不能再写emp.name)。
8.7.3 显式内连接语法(推荐)
SELECT 字段列表
FROM 表1 [INNER] JOIN 表2 ON 连接条件 ...和上面同样的查询,用显式内连接写法为:
SELECT
e.id,
e.name,
d.name AS dept_name
FROM
emp e
INNER JOIN dept d ON e.dept_id = d.id;
INNER关键字可以省略,JOIN默认就是内连接:
emp e JOIN dept d ON e.dept_id = d.id等价于emp e INNER JOIN dept d ON e.dept_id = d.id。
隐式内连接 vs 显式内连接:
- 语义上都是内连接,得到的结果集相同。
- 显式
JOIN ... ON ...写法结构更清晰,方便后续扩展为左外连接 / 右外连接等,实际开发中更推荐使用。 - 隐式写法把连接条件和筛选条件都堆在
WHERE中,SQL 稍微复杂就容易漏写某个连接条件,一旦漏掉就会退化为笛卡尔积,排查困难。 - 隐式写法对外连接支持也不友好,真正要写 LEFT / RIGHT JOIN 时,必须采用显式
JOIN ... ON ...语法才安全、清晰。
8.8 外连接(OUTER JOIN)
8.8.1 概念
外连接用于在保留一张表的全部行的同时,关联另一张表中能匹配上的数据。常见分为两种:
- 左外连接(LEFT [OUTER] JOIN)
- 右外连接(RIGHT [OUTER] JOIN)
记忆:内连接取“交集”,外连接是在交集的基础上额外保留一边的全部行。
8.8.2 左外连接(LEFT JOIN)
语法:
SELECT 字段列表
FROM 表1 LEFT [OUTER] JOIN 表2 ON 连接条件 ...;- 左外连接可以理解为:查询 表 1(左表)的所有数据,其中能在表 2 中匹配上的,显示表 2 字段;匹配不上的,表 2 字段为
NULL。
示例(查询所有员工及其所属部门,即便个别员工没有部门也要查出来):
SELECT
e.id,
e.name,
d.name AS dept_name
FROM
emp e
LEFT JOIN dept d ON e.dept_id = d.id;8.8.3 右外连接(RIGHT JOIN)
语法:
SELECT 字段列表
FROM 表1 RIGHT [OUTER] JOIN 表2 ON 连接条件 ...;- 右外连接可以理解为:查询 表 2(右表)的所有数据,其中能在表 1 中匹配上的,显示表 1 字段;匹配不上的,表 1 字段为
NULL。
示例(查询所有部门及其员工,即便部门下暂时没有员工也要查出来):
SELECT
e.id,
e.name,
d.name AS dept_name
FROM
emp e
RIGHT JOIN dept d ON e.dept_id = d.id;实战中,如果能调整书写顺序,通常只用 LEFT JOIN 就够了(把希望“全部保留”的那张表写在左边),这样 SQL 更统一。
8.9 子查询(Subquery)
8.9.1 什么是子查询?
在一条 SQL 语句中嵌套另一条 SELECT 语句,这种嵌套查询就叫做子查询(也称为“嵌套查询”)。
SELECT * FROM t1
WHERE column1 = (SELECT column1 FROM t2 WHERE ...);- 内层的
SELECT称为子查询(subquery)。 - 外层的语句可以是
SELECT/INSERT/UPDATE/DELETE中的任意一种,最常见的是SELECT。
8.9.2 子查询的常见分类
根据子查询返回结果的形状,通常可以分为四类:
- 标量子查询:子查询返回单个值(一行一列)。
- 常配合比较运算符使用,如:
=、>、<、>=、<=等。
- 常配合比较运算符使用,如:
- 列子查询:子查询返回一列多行。
- 常配合
IN / ANY / ALL等关键字使用。
- 常配合
- 行子查询:子查询返回一行多列。
- 常配合多列比较使用,如:
(col1, col2) = (SELECT col1, col2 FROM ...)。
- 常配合多列比较使用,如:
- 表子查询:子查询返回多行多列(一整张“临时表”)。
- 常出现在
FROM (子查询) AS 别名位置,把子查询结果当作一张临时表再去查询。
- 常出现在
子查询的要点是:先把需求拆分成若干清晰的小步骤,先分别写出每一步对应的 SQL,再把这些 SQL 逐步“嵌套合并”成一条完整的子查询语句。
8.9.3 典型示例:从拆分到合并
下面分别给出 4 类子查询的典型示例,按照“先拆解步骤 → 再合并为子查询”的思路展示。
A. 标量子查询示例
- 需求 1:查询最早入职的员工信息。
拆分步骤:
-- 第一步:先查出最早入职日期
SELECT MIN(entry_date)
FROM emp;
-- 第二步:再根据上一步结果,查对应员工
SELECT *
FROM emp
WHERE entry_date = '2000-01-01';合并为一条标量子查询:
SELECT *
FROM emp
WHERE entry_date = (
SELECT MIN(entry_date)
FROM emp
);- 需求 2:查询在“阮小五”入职之后入职的员工信息。
拆分步骤:
-- 第一步:查出“阮小五”的入职日期
SELECT entry_date
FROM emp
WHERE name = '阮小五';
-- 第二步:查出入职日期晚于上一步结果的员工
SELECT *
FROM emp
WHERE entry_date > '2015-01-01'; -- 示例日期合并为一条标量子查询:
SELECT *
FROM emp
WHERE entry_date > (
SELECT entry_date
FROM emp
WHERE name = '阮小五'
);B. 列子查询示例
需求:查询“教研部”和“咨询部”的所有员工信息。
拆分步骤:
-- 第一步:查出这两个部门的 id
SELECT id
FROM dept
WHERE name = '教研部'
OR name = '咨询部';
-- 第二步:用已知的部门 id 查员工
SELECT *
FROM emp
WHERE dept_id IN (3, 2); -- 假设教研部 id=2,咨询部 id=3合并为一条列子查询:
SELECT *
FROM emp
WHERE dept_id IN (
SELECT id
FROM dept
WHERE name = '教研部'
OR name = '咨询部'
);C. 行子查询示例
需求:查询与“李忠”的薪资和职位都相同的员工信息。
拆分步骤:
-- 第一步:查出“李忠”的薪资和职位
SELECT salary, job
FROM emp
WHERE name = '李忠';
-- 第二步:用查到的组合值去匹配其它员工
SELECT *
FROM emp
WHERE (salary, job) = (5000, 5); -- 示例值合并为一条行子查询:
SELECT *
FROM emp
WHERE (salary, job) = (
SELECT salary, job
FROM emp
WHERE name = '李忠'
);D. 表子查询示例
需求:获取每个部门中薪资最高的员工信息。
拆分步骤:
-- 第一步:先按部门统计每个部门的最高薪资
SELECT dept_id, MAX(salary) AS max_salary
FROM emp
GROUP BY dept_id;
-- 第二步:再用第一步结果和 emp 表做连接,取出完整员工信息
SELECT *
FROM emp e,
(SELECT dept_id, MAX(salary) AS max_salary
FROM emp
GROUP BY dept_id) temp
WHERE e.dept_id = temp.dept_id
AND e.salary = temp.max_salary;这里 (SELECT dept_id, MAX(salary) ...) temp 就是一个表子查询,返回“多行多列”的临时表 temp,再与 emp 进行多表查询。
9. 事务(Transaction)
9.1 什么是事务?
事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有操作作为一个整体一起向系统提交或撤销:要么这些操作同时成功,要么同时失败。
例如“添加员工”业务中包含两个操作:
- 向
emp表插入员工基本信息。 - 向
emp_expr表插入该员工的工作经历。
这两个操作应视为一个整体:要么都成功(提交),要么都失败(回滚),不能出现“只插了员工、没插工作经历”或反过来。
默认行为:MySQL 的事务是自动提交的,即执行一条 DML 语句后,MySQL 会立即隐式提交事务。若要手动控制“多步操作一起提交或一起回滚”,需要先开启事务,再在合适时机执行 提交 或 回滚。
9.2 事务控制的三个步骤
事务控制主要三步:
| 步骤 | 说明 | 常用 SQL |
|---|---|---|
| 开启事务 | 在这组操作执行之前开启,后续 DML 暂不提交 | START TRANSACTION; / BEGIN; |
| 提交事务 | 所有操作都执行成功时,将本事务的修改持久化 | COMMIT; |
| 回滚事务 | 本组操作中有任一步失败时,撤销本事务的修改 | ROLLBACK; |
9.3 示例:添加员工业务的事务管理
将“添加员工 + 工作经历”放在同一事务中:
-- 开启事务
START TRANSACTION;
-- 或:BEGIN;
-- 1. 保存员工基本信息
INSERT INTO emp VALUES (39, 'Tom', '123456', '汤姆', 1, '13300001111', 1, 4000, '1.jpg', '2023-11-01', 1, NOW(), NOW());
-- 2. 保存员工的工作经历信息
INSERT INTO emp_expr(emp_id, begin, end, company, job)
VALUES (39, '2019-01-01', '2020-01-01', '百度', '开发'),
(39, '2020-01-10', '2022-02-01', '阿里', '架构');
-- 全部成功则提交
COMMIT;
-- 若有任一步失败,则回滚(撤销本事务内的所有修改)
-- ROLLBACK;- 若两条
INSERT都成功,执行COMMIT后数据真正写入数据库。 - 若其中任一条失败(如主键冲突、外键约束等),应执行
ROLLBACK,本事务内的修改全部撤销。
9.4 常见事务场景
事务管理的场景非常多,例如:
- 银行转账:A 账户扣款 + B 账户加款,必须同时成功或同时失败。
- 下单扣减库存:创建订单 + 扣减商品库存,要么都成功,要么都回滚,避免超卖或订单与库存不一致。
- 添加员工(如上):员工基本信息 + 工作经历等多表插入,作为一个业务单元用事务保证一致性。
9.5 事务四大特性
面试题:事务有哪些特性?
- 原子性(Atomicity):事务是不可分割的最小单元,要么全部成功,要么全部失败。
- 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
- 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
- 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
事务的四大特性简称为:ACID。

进一步理解:
- 原子性(Atomicity):事务包装的一组 SQL 是一个不可分割的工作单元,事务中的操作要么全部成功,要么全部失败。
- 一致性(Consistency):一个事务完成之后数据都必须处于一致性状态。
- 如果事务成功完成,那么数据库的所有变化将生效。
- 如果事务执行出现错误,那么数据库的所有变化将会被回滚(撤销),返回到原始状态。
- 隔离性(Isolation):多个用户并发访问数据库时,一个用户的事务不能被其他用户的事务干扰,多个并发的事务之间要相互隔离。
- 一个事务的成功或失败对其他事务没有影响。
- 持久性(Durability):一个事务一旦被提交或回滚,它对数据库的改变将是永久性的,哪怕数据库发生异常,重启之后数据依然存在。