MySQL 学习笔记
SQL 语法
SQL 通用语法:
- 单行或多行书写,以分号结尾。
- 可用空格或缩进增加可读性。
- MySQL 数据库的 SQL 语句不区分大小写,关键字规范使用大写。
- 注释:
- 单行:
#
或--
- 多行:
/**/
- 单行:
SQL 分类
分类 | 全称 | 说明 |
---|---|---|
DDL | Data Definition Language | 数据定义语言,用来定义数据库对象(数据库,表,字段) |
DML | Data Manipulation Language | 数据操作语言,用来对数据库表中的数据进行增删改 |
DQL | Data Query Language | 数据查询语言,用来查询数据库中表的记录 |
DCL | Data Control Language | 数据控制语言,用来创建数据库用户、控制数据库的访问权限 |
DDL
数据定义语言,用来定义数据库对象(数据库,表,字段)
数据库操作
查询
查询所有数据库:SHOW DATABASES;
查询当前数据库:SELECT DATABASE();
创建
CREATE DATABASE[IF NOT EXISTS] 数据库名[ DEFAULT CHARSET 字符集][ COLLATE 排序规则];
字符集 utf8 只支持 3 个字节,utf8mb4 支持 4 字节。
删除
DROP DATABASE [IF EXISTS]数据库名;
使用
USE 数据库名
表操作-查询
查询当前数据库所有表:SHOW TABLES;
查询表结构:DESC 表名;
,默认不显示建立表时字段的注释信息。
查询指定表详细信息的建表语句:SHOW CREATE TABLE 表名;
表操作-创建
1 | create table tb_user( |
表操作-数据类型
MySQL 中数据类型主要分为三类:数值类型、字符串类型、日期时间类型。
数值类型
DECIMAL
类型需要指定 精度(有效位数) 和 标度(小数位数)。如:123.45,精度为 5,标度为 2。
示例:
无符号小整数:age TINYINT UNSIGNED
指定长度浮点数:score DOUBLE(4,1)
,代表精度为 4,标度为 1。
字符串类型
示例:
CHAR
和 VARCHAR
使用时需要有一个参数,即 CHAR(10)
,参数指当前字符串能够存储的最大长度。指定 10,即最多能存储十个字符。
定长字符串 CHAR
(性能好) 即使存一个字符也会占用 10 个字符空间,未占用字符空间会用空格补齐。也就是默认大小 10 字节已被分配。
不定长字符串 VARCHAR
(性能较差)会根据所存储的内容计算占用多少字节空间,存储 1 字节就占用 1 字节。动态分配 10 字节,最高 10 字节。
日期时间类型
表操作-修改
添加字段:ALTER TABLE 表名 ADD 字段名类型(长度)[COMMENT注释][约束];
修改某字段数据类型:ALTER TABLE 表名 MODIFY 字段名 新数据类型(长度);
修改字段名和字段类型:ALTER TABLE 表名 CHANGE 旧字段名 新字段名 类型(长度) [COMMENT注释][约束];
删除字段:ALTER TABLE 表名 DROP 字段名;
修改表名:ALTER TABLE 表名 RENAME TO 新表名;
删除表:DROP TABLE [IF EXISTS] 表名;
删除指定表,并重新创建该表:TRUNCATE TABLE 表名;
删除表时,表中的数据会被全部删除。
DML
数据操作语言,用来对数据库表中的数据进行增删改。
主要:添加(INSERT)、修改(UPDATE)、删除(DELETE)
添加数据
给指定字段添加数据(只添加一条数据):INSERT INTO 表名(字段名1,字段名2,...)VALUES(值1,值2,.);
给全部字段添加数据(只添加一条数据):INSERT INTO 表名 VALUES(值1, 值2,..);
批量添加数据:
INSERT INTO 表名(字段名1,字段名2,...)VALUES(值1,值2,..),(值1,值2,...),(值1,值2,..);
或 INSERT INTO 表名 VALUES(值1,值2,...),(值1,值2,..),(值1,值2,..);
注意点:
插入数据时,指定的字段顺序需要与值的顺序是一一对应的。
字符串和日期型数据应该包含在引号中。
插入的数据大小,应该在字段的规定范围内。
修改数据
修改表中数据:UPDATE 表名 SET 字段名1=值1,字段名2=值2,..[WHERE 条件;
修改语句的条件可以有,也可以没有,如果没有条件(没有指定哪个字段),则会修改整张表的所有数据。
删除数据
删除表中数据:DELETE FROM 表名 [WHERE 条件];
DELETE
语句的条件可以有,也可以没有,如果没有条件,则会删除整张表的所有数据。
DELETE
语句不能删除某一个字段的值(可以使用UPDATE
)。
DQL
DQL英文全称是 DataQueryLanguage(数据查询语言),数据查询语言,用来查询数据库中表的记录。
DQL 语句语法结构(也是编写顺序):
- SELECT:字段列表。
- FROM:表名列表。
- WHERE:条件列表。
- GROUP BY:分组字段列表。
- HAVING:分组后条件列表。
- ORDER BY:排序字段列表。
- LIMIT:分页参数。
DQL 语句执行顺序:
- FROM:表名列表。
- WHERE:条件列表。
- GROUP BY:分组字段列表。
- HAVING:分组后条件列表。
- SELECT:字段列表。
- ORDER BY:排序字段列表。
- LIMIT:分页参数。
基本查询
查询多个字段
查询指定字段:
SELECT 字段1,字段2,字段3 FROM 表名;
查询表中所有数据:
SELECT * FROM 表名
设置别名(AS 可以省略,也就是字段后直接加别名)
SELECT 字段1 [AS 别名1],字段2[AS 别名2] FROM 表名;
可以给表名也起别名,如SELECT 字段 FROM 表名 别名 WHERE 别名.age > 10;
,表别名用点获取表的字段。去除重复记录(比如筛选用户来自哪些省份,使用
DISTINCT
则多次出现的省份只会出现一次)SELECT DISTINCT 字段列表 FROM 表名;
条件查询
SELECT 字段列表 FROM 表名 WHERE 条件列表;
注意点:
查询字段值(不)为 NULL 的语句:SELECT age FROM tb_user WHERE IS NULL;
SELECT age FROM tb_user WHERE IS NOT NULL;
BETWEEN...AND...
(闭区间):这里 BETWEEN
后接最小值,AND
后接最大值。
IN(...)
:类似 OR
的作用。
LIKE 占位符:
select * from tb_user where name like '__';
,like 后是两个下划线,代表匹配两个字符,也就是 name 字段是两个字符的值。select * from tb_user where name like '%m';
,name 字段值最后为 m 的值,前面是什么或者几个字符无所谓。
聚合函数
将一列数据作为一个整体,进行纵向计算。
常见聚合函数:
函数 | 功能 |
---|---|
count | 统计数量 |
max | 最大值 |
min | 最小值 |
avg | 平均值 |
sum | 求和 |
语法:SELECT 聚合函数(字段列表) FROM 表名;
注意点:NULL 值不参与聚合函数计算,如 使用 count 函数,则不会将对应字段的空值算上。
统计整张表的行数量:SELECT count(*) FROM tb_user;
条件查询+聚合函数求和:SELECT sum(age) FROM tb_user WHERE id > 0;
分组查询
语法:SELECT 字段列表 FROM 表名[WHERE条件]GROUP BY 分组字段名[HAVING 分组后过滤条件];
按性别分组,并统计各性别人数:
按性别分组统计年龄小于19岁的女性数量:
where 和 having 区别:
- 执行时机不同:where是分组之前进行过滤,不满足where条件,不参与分组;而having是分组之后对结果进行过滤。
- 判断条件不同:where不能对聚合函数进行判断,而having可以。
注意点:
- 执行顺序:where -> 聚合函数 -> having
- 分组之后,查询的字段一般为聚合函数和分组字段,查询其他字段无任何意义。
排序查询
语法:SELECT 字段列表 FROM 表名 ORDER BY 字段1 排序方式1,字段2 排序方式2;
排序方式:ASC(升序)、DESC(降序)。升序排序时,ASC 可以省略。
如果是多字段排序,当第一个字段值相同时,才会根据第二个字段进行排序。
分页查询
语法:SELECT 字段列表 FROM 表名 LIMIT 起始索引,查询记录数;
,在前面各个语法最后。
- 起始索引从0开始,
起始索引=(查询页码-1)* 每页显示记录数
。 - 分页查询是数据库的方言,不同的数据库有不同的实现,MySQL中是LIMIT。
- 如果查询的是第一页数据,起始索引可以省略,直接简写为
limit 10
。
DCL
DCL英文全称是DataControlLanguage(数据控制语言),用来管理数据库用户、控制数据库的访问权限。
管理用户
查询用户
USE mysql;
,SELECT * FROM user;
创建用户
CREATE USER '用户名'@'主机名' IDENTIFIED BY '密码';
CREATE USER '用户名'@'%' IDENTIFIED BY '密码';
这里的%
指的是可以可以任意主机访问该数据库,如果是 localhost 则该用户只允许本地访问数据库。修改用户密码
ALTER USER '用户名'@'主机名' IDENTIFIED WITH mysql_native_password BY '新密码';
删除用户
DROP USER '用户名'@'主机名';
权限控制
常用权限如下:
查询权限
SHOW GRANTS FOR '用户名'@'主机名';
授予权限
GRANT 权限列表 ON 数据库名.表名 TO '用户名'@'主机名';
,如果数据库名.表名
是*.*
则代表所有数据库。撤销权限
REVOKE 权限列表 ON 数据库名.表名 FROM '用户名'@'主机名';
注意点:
多个权限之间,使用逗号分隔
授权时,数据库名和表名可以使用
*
进行通配,代表所有。
函数
使用语法:SELECT 函数(参数);
字符串函数
使用示例:
update tb_user set id=lpad(id,3,'0');
,需要注意,id 这一列需要是字符串类型,如 VARCHAR(3)
。
这个语句试图将
id
列的每个值转换为字符串,并在其左侧填充零直到其总长度为3。如果id
的值本身就是一个长度大于或等于3的数字,那么LPAD()
函数将不会填充任何零,因为原有的长度已经满足或超过了指定的长度。此外,如果id
是一个整数类型的列,MySQL可能会尝试将结果转换回整数,这样填充的零就会被去掉。
数值函数
使用示例:
select ROUND(3.45,2);
本来就是两位小数,结果为 3.45。如果是 select ROUND(3.456,2);
,结果是 3.46。
生成六位随机数示例:
SELECT lpad(ROUND(RAND()*1000000,0),6,'0');
日期函数
使用示例:
select DAY(NOW());
select DATE_ADD(now(), INTERVAL 10 YEAR);
select datediff('2024-05-10', '2024-05-06');
:注意,这里是前者减后者
流程函数
在 SQL 语句实现条件筛选,提高语句效率。
使用示例:
SELECT IFNULL(null, 'default')
:此时第一个值为 null,因此返回 default。如果第一个值是 ''
空字符串,那么也不是 null,也就是会返回空字符串。
1 | select |
1 | select |
约束
约束是作用于表中字段上的规则,用于限制存储在表中的数据。
目的:保证数据库中数据的正确、有效性和完整性。
分类:
约束 | 描述 | 关键字 |
---|---|---|
非空约束 | 限制该字段的数据不能为null | NOT NULL |
唯一约束 | 保证该字段的所有数据都是唯一、不重复的 | UNIQUE |
主键约束 | 主键是一行数据的唯一标识,要求非空且唯一 | PRIMARY KEY |
默认约束 | 保存数据时,如果未指定该字段的值,则采用默认值 | DEFAULT |
检查约束(8.0.16版本之后) | 保证字段值满足某一个条件 | CHECK |
外键约束 | 用来让两张表的数据之间建立连接,保证数据的一致性和完整性 | FOREIGN KEY |
注意点:约束是作用于表中字段上的,可以在创建表/修改表的时候添加约束。
创建带有约束的表
1 | create table userdata( |
AUTO_INCREMENT
:第一个值为 1,然后自增。新增的数据 id 字段总会比已有最大 id 字段值大 1。使用 insert into
语句插入时,如果不符合约束条件,虽然数据不会被插入,但是 id 值会增加,导致之后插入新的有效值的 id 相对更大。
假设你有一个名为 users
的表,其中 id
是自动递增的主键。
- 当前
AUTO_INCREMENT
的值是 1。 - 你尝试插入一行数据,但由于某种约束条件(比如唯一性约束)没有被满足,插入操作失败。例如,你尝试插入一个已经存在的电子邮件地址。
- 尽管插入操作失败,
AUTO_INCREMENT
的值仍然会增加到 2。 - 接着你尝试插入另一个新用户,这次满足所有约束条件,插入操作成功。但是,这个新用户的
id
值将是 2,而不是你预期的 1。
这种现象通常被称为 “自动递增间隙” 或 “auto-increment gap”。在某些情况下,这可能会导致 id
值变得不连续,出现间隙。最好插入数据之前先验证数据是否符合所有约束条件,确保只有当所有条件满足时才尝试插入。
1 | insert into userdata(name, age, status, gender) values('Tom', 18, '1', '男'),('Marry', 20, '0', '女'); # 名字不能重复了,也不能为 null,被约束了。 |
外键约束
外键用来让两张表的数据之间建立连接,从而保证数据的一致性和完整性。
具有外键的表称为 子表,外键所关联的表称为 父表,也成为 主表 或 从表。
注意:目前上述的两张表,在数据库层面,并未建立外键关联,所以是无法保证数据的一致性和完整性的。
建立外键关联
创建表时添加外键:
1 | CREATE TABLE 表名( |
创建表后添加外键:
ALTER TABLE 表名 ADD CONSTRAINT 外键名称 FOREIGN KEY (外键字段名)REFERENCES 主表(主表列名);
,外键字段名也就是子表的字段。
示例:
alter table emp add constraint fk_emp_dept_id foreign key (dept_id) references dept(id);
上述命令解释为:emp 表建立了名为 fk_emp_dept_id 的外键,emp 表的 dept_id 列与 dept 表的 id 列建立外键关联。
命令的作用:当要删除主表的数据时,如果存在从表有与主表的外键关联,则不允许删除。
删除外键
ALTER TABLE 表名 DROP FOREIGN KEY 外键名称;
外键删除/更新行为规则
默认行为就是
NO ACTION/RESTRICT
CASCADE:在外键更新时,也会同步更新子表。如主表中修改了 id 为 2,从表关联了这个 id,则从表也会与主表同步修改为 2。如果主表中删除了对应的外键 id,那么从表相应的 id 整行也会被删除。
ALTER TABLE 表名 ADD CONSTRAINT 外键名称 FOREIGN KEY(外键字段)REFERENCES 主表名(主表字段名)ON UPDATE CASCADE ON DELETE CASCADE;
SET NULL:在外键删除时,会将子表中的关联值置为 NULL。
ALTER TABLE 表名 ADD CONSTRAINT 外键名称 FOREIGN KEY(外键字段)REFERENCES 主表名(主表字段名)ON UPDATE SET NULL ON DELETE SET NULL;
可以在创建表时指定外键删除/更新规则:
1 | CREATE TABLE child_table ( |
多表查询
项目开发中,在进行数据库表结构设计时,会根据业务需求及业务模块之间的关系,分析并设计表结构,由于业务之间相互关联,所以各个表结构之间也存在着各种联系,基本上分为三种:一对多(多对一)、多对多、一对一
一对多(多对一):
例如:部门和员工的关系,一个部门对应多个员工,一个员工对应一个部门。
实现:在多的一方建立外键,指向一的一方的主键。
多对多:
例如:学生和课程的关系。一个学生可以选修多门课程,一门课程亦可以供多个学生选择。
实现:建立第三张中间表,中间表至少包含两个外键,分别关联两方主键。
一对一:
例如:用户与用户详情的关系(原本存放在一张表中,此时拆分为两张表,并通过外键关联)。一对一关系,多用于单表拆分,将一张表的基础字段放在一张表中,其他详情字段放在另一张表中,以提升操作效率。
实现:任意一方加入外键,关联另外一方的主键,并且设置外键为唯一的(UNIQUE)。
多表查询概述
从多张表中查询数据。以下内容示例如上述 一对多 的图中的表。
笛卡尔积:笛卡尔乘积是指在数学中,两个集合A集合和B集合的所有组合情况。(在多表查询时,需要消除无效的笛卡尔积,也就是消除重复的无效数据。如一个员工仅对应一个部门,此时使用
where
指定员工的部门字段与部门的id
字段相同 )
示例:select * from emp,dept where emp.dept_id = dept.id;
,emp(员工表) 和 dept(部门表) 是两张表,条件是员工的部门 id 需要等于部门表的 id。否则,会出现笛卡尔积(也就是所有组合)。
以下为 emp 表:
以下为 dept 表:
多表查询分类
连接查询
- 内连接:相当于查询A、B交集部分数据。
- 外连接:
- 左外连接:查询左表所有数据,以及两张表交集部分数据
- 右外连接:查询右表所有数据,以及两张表交集部分数据
- 自连接:当前表与自身的连接查询,自连接必须使用表别名
子查询
内连接查询
相当于查询A、B交集部分数据。不能查出 null 值的信息。
内连接分为:隐式内连接 和 显式内连接,仅是语法不同。
注意:如果给表名起了别名,则不能再用原表名加 .
的形式拿到字段,只能用别名加 .
的方式。
隐式内连接:
语法:SELECT 字段列表 FROM 表1,表2 WHERE 条件;
示例:查询每一个员工的姓名,及关联的部门的名称(隐式内连接实现)。
select emp.name , dept.name from emp , dept where emp.dept_id = dept.id ;
,如果 emp.dept_id 是 null,则查不出,需用外连接。
起别名:select e.name,d.name from emp e , dept d where e.dept_id = d.id;
,起别名后 where 后不能用 emp.dept_id 指定字段名。
显式内连接:
语法:SELECT 字段列表 FROM 表1[INNER]JOIN 表2 ON 连接条件;
示例:查询每一个员工的姓名,及关联的部门的名称(显式内连接实现)。
select e.name, d.name from emp e inner join dept d on e.dept_id = d.id;
起别名:select e.name,d.namee from emp e join dept d on e.dept_id = d.id;
,inner 可以省略。
外连接查询
分为 左外连接 和 右外连接。
左外连接:
相当于查询表1(左表)的所有数据包含表1和表2交集部分的数据。也就是完全包含左表的数据(包括 null)。以左表为主。
常用,因为右外连接也可以改为左外连接,可相互替换。
语法:SELECT 字段列表 FROM 表1 LEFT [OUTER]JOIN 表2 ON 条件;
示例:emp 表的所有数据,和对应的部门信息(左外连接)。
select e.*,d.name from emp e left outer join dept d on e.dept_id = d.id;
,outer 可省略。
右外连接:
相当于查询表2(右表)的所有数据包含表1和表2交集部分的数据。也就是完全包含右表的数据(包括 null)。以右表为主。
语法:SELECT 字段列表 FROM 表1 RIGHT [OUTER]JOIN 表2 ON 条件;
示例:查询 dept 表的所有数据,和对应的员工信息(右外连接)。
select d.*, e.* from emp e right outer join dept d on e.dept_id = d.id;
用左外连接替换,也就是把右表改为左表:select d.*,e.* from dept d left outer join emp e on e.dept_id = d.id;
自连接查询
可以用内连接(不包括 null),也可以用外连接(完整数据,包括 null)。
把当前表看作两张表。
语法:SELECT 字段列表 FROM 表A 别名A JOIN 表A 别名B ON 条件...;
,通过给同一表起两个别名,看作两张表,可以使用内连接也可以使用外连接。
示例:查询员工(emp.name)及其所属领导的名字(emp.managerid)。
select a.name ,b.name from emp a, emp b where a.managerid = b.id;
,使用内连接。
示例:查询所有员工emp及其领导的名字emp,如果员工没有领导,也需要查询出来。
select a.name '员工',b.name '领导' from emp a left join emp b on a.managerid = b.id;
,使用外连接。
联合查询
对于
union [all]
查询,就是把多次查询的结果合并起来,形成一个新的查询结果集。
语法:
1 | SELECT 字段列表 FROM 表A ... |
对于联合查询的多张表的列数必须保持一致,字段类型也需要保持一致。
union all
会将全部的数据直接合并在一起,union
会对合并之后的数据(也就是针对于两个条件都满足的数据)去重。
示例:薪资低于5000的员工,和年龄大于50岁的员工全部查询出来。
1 | select * from emp where salary < 5000 |
子查询
SQL语句中嵌套 SELECT 语句,称为嵌套查询,又称子查询。
语法:SELECT * FROM t1 WHERE column1 = (SELECT column1 FROM t2);
子查询外部的语句可以是INSERT/UPDATE/DELETE/SELECT的任何一个。
根据子查询结果不同,分为:
标量子查询 (子查询结果为单个值)
列子查询(子查询结果为一列)
行子查询(子查询结果为一行)
表子查询(子查询结果为多行多列)
根据子查询位置,分为:WHERE之后、FROM之后、SELECT之后。
标量子查询
子查询返回的结果是单个值(数字、字符串、日期等),最简单的形式,这种子查询成为标量子查询。
常用的操作符:
= <> > >= < <=
,<>
是不等于。
示例:查询“销售部”的所有员工信息。
- 不使用子查询:
- 查询 “销售部” 部门ID:
select id from dept where name='销售部';
,结果为 4。 - 据销售部部门ID,查询员工信息:
select * from emp where dept_id = 4;
- 查询 “销售部” 部门ID:
- 使用子查询(将上述两条语句合并):
select * from emp where dept_id =(select id from dept where name = '销售部');
示例:查询在“方东白”入职之后的员工信息。
select * from emp where entrydate > (select entrydate from emp where name ='方东白');
列子查询
子查询返回的结果是一列(可以是多行),这种子查询称为列子查询。
常用的操作符:IN、NOT IN、ANY、SOME、ALL
操作符 | 描述 |
---|---|
IN | 在指定的集合范围之内,多选一 |
NOT IN | 不在指定的集合范围之内 |
ANY | 子查询返回列表中,有任意一个满足即可 |
SOME | 与ANY等同,使用SOME的地方都可以使用ANY |
ALL | 子查询返回列表的所有值都必须满足 |
示例:查询“销售部”和“市场部”的所有员工信息。
- 不使用列子查询:
- 查询“销售部”和“市场部”的部门ID:
select id from dept where name ='销售部' or name = '市场部';
,得到的是(2,4)
- 据部门ID,查询员工信息:
select * from emp where dept_id in (2,4);
- 查询“销售部”和“市场部”的部门ID:
- 使用列子查询:
select * from emp where dept_id in (select id from dept where name='销售部' or name = '市场部')
示例:查询比财务部所有人工资都高的员工信息。
select * from emp where salary > all (select salary from emp where dept_id = (select id from dept where name = '财务部');
,all 表示子查询得到的结果都需要满足小于 salary。这条语句实现了三个查询,最右侧是查询财务部部门 id,接着中间通过部门 id 查询该部门所有 salary,最后进行条件查询。
示例:查询比研发部其中任意一人工资高的员工信息。
select * from emp where salary > any ( select salary from emp where dept_id = (select id from dept where name = '研发部') );
,any 代表子表查询得到的值都满足条件。
行子查询
子查询返回的结果是一行(可以是多列),这种子查询称为行子查询。
常用操作符:
=, <>, IN, NOT IN
示例:查询与”张无忌”的薪资及直属领导相同的员工信息。
不使用行子查询
查询“张无忌”的薪资及直属领导:
select salary,managerid from emp where name = '张无忌';
,得到12500,1
查询与“张无忌”的薪资及直属领导相同的员工信息:
select * from emp where salary = 12500, managerid = 1;
可以替换为:
select * from emp where (salary,managerid) = (12500,1);
,类似元组形式。
使用行子查询:
select * from emp where (salary,managerid) = (select salary, managerid from emp where name = '张无忌');
,行子查询返回 (12500,1),然后与条件比较。这里子查询返回的是单行数据,则可以用=
,多行数据需要用IN
。
表子查询
子查询返回的结果是多行多列,这种子查询称为表子查询。
常用操作符:
IN
示例:查询与“鹿杖客”“宋远桥”的职位和薪资相同的员工信息。
表子查询:select * from emp where(job,salary) in ( select job,salary from emp where name ='鹿杖客' or name ='宋远桥' );
,这里子查询返回的是多行多列的表,所以用 IN
。
示例:查询入职日期是”2006-01-01”之后的员工信息,及其部门信息。
- 入职日期是”2006-01-01”之后的员工信息:
select * from emp where entrydate > '2006-01-01';
- 查询这部分员工,对应的部门信息:
select e.*,d.* from (select * from emp where entrydate >'2006-01-01') e left join dept d on e.dept_id = d.id ;
,这里将 1 查询返回的表作为另一张表,在返回的表中进行再次查询,又因为要查询出 e.dept_id 为 null 的数据,所以用 左外连接。
多表查询练习
事务
事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
示例(转账操作实现):
事务操作
修改事务的提交方式
查看/设置事务提交方式:SELECT @@autocommit;
,如果是 1,则是自动提交;0 则是手动提交。SET @@autocommit=0;
,设置为手动提交。
提交事务:COMMIT;
,提交操作之前,可以有/执行多个 SQL 语句。
回滚事务:ROLLBACK;
开启事务
开启事务:START TRANSACTION
或 BEGIN
,命令的作用是代表要手动控制事务。
提交事务:COMMIT;
回滚事务:ROLLBACK;
事务四大特性(ACID)
- 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
- 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
- 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
- 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
并发事务问题
脏读
事务 A 还没有 commit ,但是这时候事务 B 进行查询,获取到了事务 A update 但是没有 commit 的数据。这就是脏读。
不可重复读
事务 A 的第一步查询了 id 为 1 的数据,这时事务 B 更新了 id 为 1 的数据并 commit 了,接着事务 A 再次查询了 id 为 1 的数据,但是两次查询发现了 id 为 1 的数据不一致,这就是不可重复读。
也就是在相同事务中,如在事务 A 中,重复读取 id 为 1 的数据出现不一致就是不可重复读。而在事务 A 提交后,再次查询 id 为 1 的数据,此时才能查到事务 B 提交修改的 id 为 1 的数据,这就是没有出现不可重复读(事务隔离级别Repeatable Read
)。
幻读
首先事务 A 查询 id 为 1 的数据,发现数据库中没有,但这时事务 B 向数据库 commit 了 id 为 1 的数据,接着事务 A 因为之前没有查询到 id 为 1 的数据因此进行插入 id 为 1 的数据,但是插入失败了,因为事务 B 已经插入了,导致主键冲突。事务 A 又解决了不可重复读的问题,但是事务 A 第三步又进行查询 id 为 1 的数据,又发现好像没有数据,这就是幻读。
在解决了不可重复读的问题后,事务 A 向数据库中查询 id 为 1 的数据,没有查询到,但是事务 B 实际 insert 了 id 为 1 的数据并且 commit 了,这时候事务 A 插入数据又提示主键重复,但实际查询 id 为 1 的数据查询不到,因为事务 A 没有 commit 结束当前事务(因为解决了不可重复读),所以查询不到。这就出现了幻读。
事务的隔离级别
解决并发事务问题。
下图打勾的代表事务的隔离级别所存在的并发问题(事务的隔离级别从上到下,从低到高):
MySQL 默认隔离级别是 Repeatable Read
。Read uncommitted
性能最好但并发安全性差,Serializable
性能最差但并发安全性好。
查看事务隔离级别:SELECT @@TRANSACTION_ISOLATION;
设置事务隔离级别:SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE};
,SESSION 指的是当前客户端所有会话窗口有效,GLOBAL 是对所有客户端会话窗口有效。
当设置为 Serializable 事务级别时,两个事务同时操作同一数据库时,如 A 和 B 事务,A 查询了 id 为 1 的数据,然后 B 插入了 id 为 1 的数据,此时 B 会被阻塞,会等到 A commit,也就是 A 结束事务后,B 才能操作。