【注】学习了一天学会数据库这个教程,少部分参考了部分评论区的笔记。
如何登录数据库服务器
如何直接在cmd直接使用数据库
网上的方法是:
使用ctrl+R输入services.msc找到对应的mysql进程,然后在属性中找到它的位置,把它加入用户变量中即可使用,注意使用cmd的时候要使用管理者身份,但是即使如此好像也没法解决。
如何登录数据库服务器
mysql -uroot -p
MySQL常用数据类型
- 数值
注意各类数据类型的范围。按照大小选择类型。
- 日期时间
时间也有格式,去网上查就行
- 字符串
同理字符串也是看范围,直接去网上找就ok
总而言之,数值按照大小选择类型,时间日期和字符串按照格式选择类型。
简单的数据库操作
如何查询数据库中所有数据库
show database
如何选中某一个数据库进行操作
use <some database>
看到database changed就行。
查询语句
select * from <table> where id =<sth>
退出数据库
一定要有分号
exit;
如何在数据库服务器中创建数据库
create database test;
要使用就再使用use这样一个命令
如果要展示这样一个数据表,那么使用:
show tables;
如何创建一个数据表
create table <sth>(
name varchar(20),
owner varchar(20),
<name> <datatype> <length>,
);
查看创建好的数据表的结构
describe <sth>;
如何往数据表中插入记录
insert into <table>
values('a','b',<sth>);
插入到某个table中,对应不同的列,插入的时候必须要对应类型,否则会报错。
如何删除数据
delete from <table> where <col> = sth
相当于查找,查找到之后把这个地方的位置删除掉。
如何修改数据
update <table> set <col>=<sth> where <col'>=<sth'>;
小结一下:增删改查
select xx where xx=xx;
insert into xx values(xx,xx,xx..)
delete from xx where xx= xx
update xx set xx=xx where xx=xx
MySQL键表约束
主键约束
唯一确定一张表中的一条记录,通过给某个字段添加约束,使得该字段不重复,且不为空
create table user(
id int primary key,
name varchar(20)
);
primary key就是主键约束的命令。
如果多次插入相同的主键的东西,就会产生Duplicate Key错误。
主键不能为空。
还可以有另一种写法:
create table user(
id int,
name varchar(20),
primary key(id, name)
);
说明这两个都是主键,只要复合主键中有一个不一样就可以了,逻辑是或的关系,同样的复合主键也不能重复,不可以为空。
<u>小结:主键不为空且不能重复,是可以唯一确认一条约束。</u>
问题:
如果创建一个表的时候忘了创建主键约束了应该怎么办?
alter table user add primary key(id);
如何删除主键:
alter table user drop primary key;
修改主键约束:
alter table user modify id int primary key;
自增约束
create table user(
id inr primary key auto_increment
);
其中auto_increment就是自增约束。
和主键约束结合在一起使用。
唯一约束
约束修饰的字段不可以重复:
直接添加:
name varchar(20) unique
unique(name,id)
后面更改添加:
create table user(
id int,
name varchar(20)
);
alter table user add unique(name);
add unique是对应的命令,唯一约束可以为空,命令格式和primary key一样,同样是复合的情况下只要组合在一起不重复即可。
删除的时候有点不一样,要用drop index,其他还是一样是unique的语句。
alter table xx drop index xx;
小结:
- 建表添加约束
- 使用alter ..add添加
- alter modify的方式进行添加
- 使用alter...drop的方式进行删除
非空约束
修饰的字段不能为空
create table user(
name varchar(20) not null
);
那么在NULL列中显示的属性为NO
那么insert还有一种做法:
insert into <table> (name) values ('Tom');
只要前括号和后面括号内的内容对应即可。
默认约束
就是当插入字段值的时候,如果没有传值,就会使用默认值。
create table user(
id int,
name varchar(20),
age int default 10
);
传了值就不会使用默认值。
外键约束
- 涉及到两个表(父表,子表)
- 主表、副表
create table classes(
id int primary key,
name varchar(20)
);
create table student(
id int primary key,
name varchar(20),
class_id int,
foreign key(class_id) reference classes(id)
);
最后一句的意思是,在student这个表中的class_id对应的是classes这个表中的id字段。
就相当于使用classes一个列中的字段内容作为数据,输入到副表之中。
- 主表classes没有的数据,在副表是不可以使用的。
- 副表已经写好的数据,引用了主表的一个字段,在主表是不能删除的,要不然也会出现第一条的情况。
数据库的三大设计范式
第一范式
数据表中的所有字段都是不可分割的原子值
字段值还可以继续拆分的就不满足第一范式。
比如address:中国天津市津南区海河教育园xxx
如果字段设置为国家、省 城市、区、具体位置,就算第一范式。
<u>范式的设计是要和实际项目开发相匹配,不一定是越详细越好,也不是越粗糙越好。</u>
第二范式
必须是满足第一范式的前提下,
第二范式要求:
- 除主键外的每一列都必须完全依赖主键。
- 如果出现不完全依赖,那么只可能发生在符合主键的情况下(这是不符合第二范式的)
create tabe my_order(
product_id int,
customer_id int,
product_name varchar(20),
customer_name varchar(20),
primary key(product_id,customer_id)
);
那么product_name只和product_id有关,所以只依赖于主键的部分字段有关,这样是不满足第二范式的,所以要拆表满足第二范式,分为主表和副表。
变成:myorder product customer 然后使用外键连接把myorder连接product和customer。
第三范式
必须先满足第二范式,除开主键列以外的其他列之外不能有传递关系。
关于查询练习
建表
学生表:Student
学号
姓名
性别
出生年月日
班级
create table student(
sno varchar(20) primary key,
sname varchar(20) not null,
sgender varchar(10) not null,
sbirthday datetime,
class varchar(20)
);
教师表:Teacher
教师编号
教师名字
教室性别
出生年月日
职称
create table teacher(
tno varchar(20) primary key,
tname varchar(20) not null,
tgender varchar(10) not null,
tbirthday datetime,
prof varchar(20) not null,
depart varchar not null
);
课程表:Course
课程号
课程名称
教室编号
create table Course(
cno varchar(20) primary key,
cname varchar(20) not null,
tno primary (20) not null,
foreign key (tno) reference teacher(tno)
);
成绩表:Score
学号
课程号
成绩
create TABLE Score(
sno VARCHAR(20)not null,
cno varchar(20)not null,
degree decimal,
FOREIGN key(sno) REFERENCES student(sno),
foreign key(cno) REFERENCES Course(cno),
primary key(sno,cno)
);
添加数据
查询练习
基本的查询练习
-- 初级查询练习
-- 查询所有记录
select *from student;
-- 查询指定字段
select sname from student
-- 排重 distinct
select distinct depart from teacher
-- 查询区间,使用where和between
select * from score where degree BETWEEN 60 and 80
select * from score where degree > 60 and degree <80
-- 查询score中或的关系
select * from score where degree in(85,86,88)
-- 不同字段中或的关系
select * from student where class='95031' or sgender='女'
-- 升序,降序asc desc,默认升序
select * from student order by class asc
select * from score order by cno asc,degree desc
-- 计数
select count(*)from student where class ='95031'
-- 最高级分析
-- 最后一句话的理解,首先前半段是一样的,我想要最后输出sno和scn,所以select这两个,条件是where后面的,那么后面的应该用自然语言描述为分数为最大的那个,所以是degree=最大的那个,那么最后一步就是写出最大的那个是什么了,也就是select max(degree) from score,合起来就是这句话
select sno ,cno from score where degree =(select max(degree) from score)
-- 找出前几个,并且排序了的结果,limit 第几个开始,查了多少条
select sno,cno,degree from score order by degree asc limit 0,1
运用函数的查询
查询平均成绩
使用avg函数进行计算,avg括号内填要求和平均的字段
select AVG(degree)from score where cno='3-105'
如何按类求平均成绩呢,也就是说,比如有a,b,c三门课,能不能一块自动输出所有课程的平均成绩呢?使用group by 分组
select avg(degree) from score group by cno
查询多条件的平均成绩
group by是把某个数据列进行分组
-- 题目是选择有2个以上同学选择并且以3开头的课程号的课程平均分
-- 有三个不同的cno
-- where 筛选行,having筛选分组(当group by之后使用having)
select cno,avg(degree) from score group by cno
having count(cno)>=2 and cno like'3%'
多表查询
要求把几个表中不同的属性相对应输出:
-- 查询所有学生的sname cno 和 degree列
-- 这样太初级,而且只能一个一个查,不能一起查出来
select sno,sname from student
select sno,cno,degree from score
-- 多表联查 保证第一student的sname和第二个表得cno和degree对上,select的时候混着写就可以了,where的时候直接使用属性就可以
select sname,cno ,degree from student,score
where student.sno=score.sno
第二个练习:
-- 查询所有学生的sno cname和degree
-- 前提要有共同列相对,这个共同列就是where要写的东西
select sno,cname,degree from course,score
where score.cno=course.cno
如果需要三表联查怎么办
-- 三表查询 查询所有学生的sname cname 和degree列,两两联系即可
select sname,cname,degree from student,course,score where student.sno=score.sno and course.cno=score.cno
在此之中使用别名怎么办,用来表示更多的东西
select sname,cname,degree,student.sno as ssno,score.sno,course.cno as ccno from student,course,score where student.sno=score.sno and course.cno=score.cno
比如这样,这样可以用来表示更多东西,但是在实质上还是一样的,只是给取了一个别名罢了。
再练习一个
-- 查询选修3-105课程成绩高于109号同学的所有3-105成绩的记录
-- 首先查询degree满足这个条件的同学
select degree from score where sno='109' and sno='3-105'
-- 把这个结果作为条件,再塞到另一个语句中就完成了
select * from score where degree>(select degree from score where sno='109' and sno='3-105')
-- 查询和学号109 101 同年出生的所有学生的sno sname和sbirthday列
-- 和上面的思路一致,先把这些条件有关的东西选出来
select year(sbirthday) from student where sno in (108,101)
-- 注意这个条件是判断存在于这个集合中,所以要用in
select * from student where year(sbirthday) in (select year(sbirthday) from student where sno in (108,101))
如果是多层嵌套的查询,那就一步步实现:
-- 查询张旭教室任课的学生成绩
-- 第一步找到张旭任课学生
select tno from teacher where tname = '张旭'
-- 第二步 找到这些学生所上的课
select cno from course where tno = (select tno from teacher where tname = '张旭')
-- 第三步 找到这些学生这些课的成绩
select * from score where cno = (select cno from course where tno = (select tno from teacher where tname = '张旭'))
Another example: find a course ,which contains more than 5 students.
select cno from score group by cno having count(*)>5
select tno from course where cno = (select cno from score group by cno having count(*)>5)
select * from teacher where tno = (select tno from course where cno = (select cno from score group by cno having count(*)>5))
使用关键字的查询
in查询
-- 找到课程号为95031或95033课程中的学生
select * from student where class in('95031','95033')
where查询
-- 查询成绩大于85的内容
select * from score where degree>85;
子查询
-- 查询出计算机系老师所教课程的所有成绩信息
select * from teacher where depart = '计算机系'
select * from course where tno in (select tno from teacher where depart = '计算机系')
select * from score where cno in (select cno from course where tno in (select tno from teacher where depart = '计算机系'))
值得注意的是对于条件是in还是=,关键是条件是一个集合还是一个值
union和not in的使用
not in是指不在里面的意思,作为条件使用,union是取并集,在应用这个union的时候,需要把两个要并的语句分别写好,然后再加个union即可
注意,as是取别名,union是取并集
-- find out teachers whose profession is not in the set of professions of the other department.
select * from teacher where depart = '计算机系' and prof not in (select prof from teacher where depart = '电子工程系')
union
select * from teacher where depart = '电子工程系' and prof not in (select prof from teacher where depart = '计算机系');
any的使用
表示只要大于其中至少一个 那么就用>any(),any就是找到其中一个即可
-- 找到3-105中成绩至少大于3-245的成绩的所有信息
select * from score where cno='3-105' and degree >any(select degree from score where cno='3-245')
all表示所有
-- 找到3-105中成绩大于所有3-245的成绩的所有信息
select * from score where cno='3-105' and degree >all(select degree from score where cno='3-245')
同理使用上面的语句,然后改成all就可以了
使用别名在一张表中操作
-- 查询成绩比该课程平均成绩低的同学的成绩表
select * from score a where degree <(select avg(degree) from score b where a.cno =b.cno)
这里可以把score这张表看作是有两张一样的表(但是其实就是一张表),只不过给他们赋予别名a和b,然后我们从这样的表a里挑出想要的数据,条件使用到表b,那么在表b中就是找到成绩比平均成绩要低的表,而且要把a和b表相关联,所以要写出a.cno= b.cno
条件加分组筛选
-- 查询至少有两个男生的班号
select * from student
select class from student where sgender = '男' group by class having count(*)>1
模糊查询
查询不姓王的同学
select * from student where sname not like '王%'
进行一定的时间运算
使用到year()提取里面的datetime中的年份,然后使用year(now())找到现在的年份,并且进行运算
-- 计算所有同学的年龄
select sname,year(now())-year(sbirthday) as age from student
找出年份最大最小的同学(min、max的使用)
select sbirthday from student order by sbirthday
select max(sbirthday),min(sbirthday) from student
注意对于年份而言,年份越久远代表越小(归根结底是按照数字排序的,而不是距离现在的时间)
多字段排序
比如按照年份和班级多字段排序
select * from student order by class,birthday
按等级查询
假设建了一个表叫做grade
CREATE TABLE grade (
low INT(3),
upp INT(3),
grade char(1)
);
INSERT INTO grade VALUES (90, 100, 'A');
INSERT INTO grade VALUES (80, 89, 'B');
INSERT INTO grade VALUES (70, 79, 'C');
INSERT INTO grade VALUES (60, 69, 'D');
INSERT INTO grade VALUES (0, 59, 'E');
现在想要查找原来的分数映射到对应的grade之中应该怎么做呢?
select sno,cno,grade from score,grade where degree between low and up
思路就是使用between,直接对列操作,把约束在上下限之间的成绩作为条件,然后通过前面的select grade把对应的映射输出
四种连接查询
内连接:inner join 或者join
外连接:①左连接 left join或者left outer join②外连接 right join或者right outer join
完全外连接 full join或者full outer join
练习:
创建两个表先:
CREATE DATABASE testJoin;
CREATE TABLE person (
id INT,
name VARCHAR(20),
cardId INT
);
CREATE TABLE card (
id INT,
name VARCHAR(20)
);
内连接
-- 这里实质是有一个外键关系的,但是并不是严格外键的,所以inner join语句断句应该是select*from,person inner join card,on card.id=cardId,第一个是要什么,第二个是谁和谁合并,on后面是条件。
-- 内联查询其实就是两个表中的数据,通过某个字段相等,查询出相关记录数据
select * from person inner join card on card.id = cardId
1 张三 1 1 饭卡
2 李四 3 3 农行卡
左外连接
-- 左外连接
-- 会把左边表所有数据取出来,如果右边表中数据如果有相等,那么显示出来,如果没有,那么就补充null
select * from person left join card on person.cardId = card.id
1 张三 1 1 饭卡
2 李四 3 3 农行卡
3 王五 6
右外连接
-- 右外连接
-- 会把右边表所有数据取出来,如果左边表中数据如果有相等,那么显示出来,如果没有,那么就补充null
select * from person right join card on person.cardId = card.id
全外连接
MySQL是不支持全外连接的
-- MySQL 不支持这种语法的全外连接
-- SELECT * FROM person FULL JOIN card on person.cardId = card.id;
-- 出现错误:
-- ERROR 1054 (42S22): Unknown column 'person.cardId' in 'on clause'
-- MySQL全连接语法,使用 UNION 将两张表合并在一起。
SELECT * FROM person LEFT JOIN card on person.cardId = card.id
UNION
SELECT * FROM person RIGHT JOIN card on person.cardId = card.id;
-- 其实全连接可以通过这样实现
+------+--------+--------+------+-----------+
| id | name | cardId | id | name |
+------+--------+--------+------+-----------+
| 1 | 张三 | 1 | 1 | 饭卡 |
| 2 | 李四 | 3 | 3 | 农行卡 |
| 3 | 王五 | 6 | NULL | NULL |
| NULL | NULL | NULL | 2 | 建行卡 |
| NULL | NULL | NULL | 4 | 工商卡 |
| NULL | NULL | NULL | 5 | 邮政卡 |
+------+--------+--------+------+-----------+
小结
内连接是:左右交集
外连接:左外连接是左边都有,右边有的有,有的没有,右外连接以此类推
全外连接是完全都有
事务
事务是用来做什么
MySQL中,事务是一个最小的不可分割的工作单元,事务是能够保证一个业务的完整性。
-- a -> -100
UPDATE user set money = money - 100 WHERE name = 'a';
-- b -> +100
UPDATE user set money = money + 100 WHERE name = 'b';
在实际项目中,假设只有一条 SQL 语句执行成功,而另外一条执行失败了,就会出现数据前后不一致。
因此,在执行多条有关联 SQL 语句时,事务可能会要求这些 SQL 语句要么同时执行成功,要么就都执行失败。
MySQL中如何控制事务
- MySQL是默认开启事务的
select @@autocommit
- 默认事务开启的时候,当我们执行一个SQL语句的时候,效果会立刻体现出来且不能回滚(回滚的意思是撤销SQL语句执行效果)
CREATE DATABASE bank;
USE bank;
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(20),
money INT
);
INSERT INTO user VALUES (1, 'a', 1000);
ROLLBACK
SELECT * FROM user;
-- 依然存在着a这个用户
- 如何撤销这个效果呢?
set autocommit = 0;
这样自动提交就设置为0了,如此一来再实现:
CREATE DATABASE bank;
USE bank;
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(20),
money INT
);
INSERT INTO user VALUES (2, 'b', 1000);
ROLLBACK
SELECT * FROM user;
这样这个b用户就不再存在了
那加入再一次插入数据,手动提交数据之后,撤销仍然是没有用的,手动提交的命令是:
commit
所以这个性质叫做持久性。
总结
-
自动提交
- 查看自动提交状态:
SELECT @@AUTOCOMMIT
; - 设置自动提交状态:
SET AUTOCOMMIT = 0
。
- 查看自动提交状态:
-
手动提交
@@AUTOCOMMIT = 0
时,使用COMMIT
命令提交事务。 -
事务回滚
@@AUTOCOMMIT = 0
时,使用ROLLBACK
命令回滚事务。这时假设在转账时发生了意外,就可以使用
ROLLBACK
回滚到最后一次提交的状态:-- 假设转账发生了意外,需要回滚。 ROLLBACK; SELECT * FROM user; +----+------+-------+ | id | name | money | +----+------+-------+ | 1 | a | 1000 | | 2 | b | 1000 | +----+------+-------+
这时我们又回到了发生意外之前的状态,也就是说,事务给我们提供了一个可以反悔的机会。假设数据没有发生意外,这时可以手动将数据真正提交到数据表中:
COMMIT
手动设置事务
事务的默认提交被开启 ( @@AUTOCOMMIT = 1
) 后,此时就不能使用事务回滚了。但是我们还可以手动开启一个事务处理事件,使其可以发生回滚:
-- 使用 BEGIN 或者 START TRANSACTION 手动开启一个事务
-- START TRANSACTION;
BEGIN;
UPDATE user set money = money - 100 WHERE name = 'a';
UPDATE user set money = money + 100 WHERE name = 'b';
-- 由于手动开启的事务没有开启自动提交,
-- 此时发生变化的数据仍然是被保存在一张临时表中。
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
+----+------+-------+
-- 测试回滚
ROLLBACK;
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 1000 |
| 2 | b | 1000 |
+----+------+-------+
仍然使用 COMMIT
提交数据,提交后无法再发生本次事务的回滚。
BEGIN;
UPDATE user set money = money - 100 WHERE name = 'a';
UPDATE user set money = money + 100 WHERE name = 'b';
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
+----+------+-------+
-- 提交数据
COMMIT;
-- 测试回滚(无效,因为表的数据已经被提交)
ROLLBACK;
所以,小结如下:
在begin或者start transaction是可以回滚的,如果想要真实生效,那么就commit,这个效果是和autocommit=0
效果是一样的
事务的 ACID 特征与使用
事务的四大特征
- A 原子性:事务是最小的单位,不可以再分割;
- C 一致性:要求同一事务中的 SQL 语句,必须保证同时成功或者失败;
- I 隔离性:事务1 和 事务2 之间是具有隔离性的;
-
D 持久性:事务一旦结束 (
COMMIT
) ,就不可以再返回了 (ROLLBACK
) 。同理,一旦rollback,就不能再commit了。
事务开启
- 修改默认提交
set autocommit = 0
begin
start transaction
事务手动提交
commit
事务手动回滚
rollback
事务的隔离性
事务的隔离性可分为四种 ( 性能从低到高 ) :
-
READ UNCOMMITTED ( 读取未提交 )
如果有多个事务,那么任意事务都可以看见其他事务的未提交数据。
通俗的说是 如果有事务a和事务b,如果a没有提交,b照样能看到a操作的结果
-
READ COMMITTED ( 读取已提交 )
只能读取到其他事务已经提交的数据。
-
REPEATABLE READ ( 可被重复读 )
如果有多个连接都开启了事务,那么事务之间不能共享数据记录,否则只能共享已提交的记录。
-
SERIALIZABLE ( 串行化 )
所有的事务都会按照固定顺序执行,执行完一个事务后再继续执行下一个事务的写入操作。
查看当前数据库的默认隔离级别:
-- MySQL 8.x, GLOBAL 表示系统级别,不加表示会话级别。
-- 语句是:
-- 系统级别的
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
-- 会话级别的
SELECT @@TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| REPEATABLE-READ | -- MySQL的默认隔离级别,可以重复读。
+--------------------------------+
-- MySQL 5.x
SELECT @@GLOBAL.TX_ISOLATION;
SELECT @@TX_ISOLATION;
修改隔离级别:
-- 设置系统隔离级别,LEVEL 后面表示要设置的隔离级别 (READ UNCOMMITTED)。
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 查询系统隔离级别,发现已经被修改。
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| READ-UNCOMMITTED |
+--------------------------------+
脏读
测试 READ UNCOMMITTED ( 读取未提交 ) 的隔离性:
INSERT INTO user VALUES (3, '小明', 1000);
INSERT INTO user VALUES (4, '淘宝店', 1000);
SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 1000 |
| 4 | 淘宝店 | 1000 |
+----+-----------+-------+
-- 开启一个事务操作数据
-- 假设小明在淘宝店买了一双800块钱的鞋子:
START TRANSACTION;
UPDATE user SET money = money - 800 WHERE name = '小明';
UPDATE user SET money = money + 800 WHERE name = '淘宝店';
-- 然后淘宝店在另一方查询结果,发现钱已到账。
SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 200 |
| 4 | 淘宝店 | 1800 |
+----+-----------+-------+
由于小明的转账是在新开启的事务上进行操作的,而该操作的结果是可以被其他事务(另一方的淘宝店)看见的,因此淘宝店的查询结果是正确的,淘宝店确认到账。但就在这时,如果小明在它所处的事务上又执行了 ROLLBACK
命令,会发生什么?
-- 小明所处的事务
ROLLBACK;
-- 此时无论对方是谁,如果再去查询结果就会发现:
SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 1000 |
| 4 | 淘宝店 | 1000 |
+----+-----------+-------+
这就是所谓的脏读,一个事务读取到另外一个事务还未提交的数据。这在实际开发中是不允许出现的。(可能误认为是已经完成了的,但是其实这个是提交到临时表上的内容),那么这个人还是有可能把这个未提交的数据回滚的。
读取已提交
把隔离级别设置为 READ COMMITTED :
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| READ-COMMITTED |
+--------------------------------+
这样,再有新的事务连接进来时,它们就只能查询到已经提交过的事务数据了。但是对于当前事务来说,它们看到的还是未提交的数据,例如:
-- 正在操作数据事务(当前事务)
START TRANSACTION;
UPDATE user SET money = money - 800 WHERE name = '小明';
UPDATE user SET money = money + 800 WHERE name = '淘宝店';
-- 虽然隔离级别被设置为了 READ COMMITTED,但在当前事务中,
-- 它看到的仍然是数据表中临时改变数据,而不是真正提交过的数据。
SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 200 |
| 4 | 淘宝店 | 1800 |
+----+-----------+-------+
-- 假设此时在远程开启了一个新事务,连接到数据库。
$ mysql -u root -p12345612
-- 此时远程连接查询到的数据只能是已经提交过的
SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 1000 |
| 4 | 淘宝店 | 1000 |
+----+-----------+-------+
但是这样还有问题,那就是假设一个事务在操作数据时,其他事务干扰了这个事务的数据。也就是说,如果业务流量很大的时候,每时每刻这个表都是在改变的,但是手上原来的表还停留在上一时刻,这一时刻和下一时刻的计算是对不上的,例如:
-- 小张在查询数据的时候发现:
SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 200 |
| 4 | 淘宝店 | 1800 |
+----+-----------+-------+
-- 在小张求表的 money 平均值之前,小王做了一个操作:
START TRANSACTION;
INSERT INTO user VALUES (5, 'c', 100);
COMMIT;
-- 此时表的真实数据是:
SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 1000 |
| 4 | 淘宝店 | 1000 |
| 5 | c | 100 |
+----+-----------+-------+
-- 这时小张再求平均值的时候,就会出现计算不相符合的情况:
SELECT AVG(money) FROM user;
+------------+
| AVG(money) |
+------------+
| 820.0000 |
+------------+
虽然 READ COMMITTED 让我们只能读取到其他事务已经提交的数据,但还是会出现问题,就是在读取同一个表的数据时,可能会发生前后不一致的情况。这被称为<u>不可重复读现象</u> ( READ COMMITTED ) 。
幻读
将隔离级别设置为 REPEATABLE READ ( 可被重复读取 ) :
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| REPEATABLE-READ |
+--------------------------------+
测试 REPEATABLE READ ,假设在两个不同的连接上分别执行 START TRANSACTION
:
-- 小张 - 成都
START TRANSACTION;
INSERT INTO user VALUES (6, 'd', 1000);
-- 小王 - 北京
START TRANSACTION;
-- 小张 - 成都
COMMIT;
当前事务开启后,没提交之前,查询不到,提交后可以被查询到。但是,在提交之前其他事务被开启了,那么在这条事务线上,就不会查询到当前有操作事务的连接。相当于开辟出一条单独的线程。
无论小张是否执行过 COMMIT
,在小王这边,都不会查询到小张的事务记录,而是只会查询到自己所处事务的记录:
SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 1000 |
| 4 | 淘宝店 | 1000 |
| 5 | c | 100 |
+----+-----------+-------+
这是因为小王在此之前开启了一个新的事务 ( START TRANSACTION
) *,那么*在他的这条新事务的线上,跟其他事务是没有联系的,也就是说,此时如果其他事务正在操作数据,它是不知道的。
然而事实是,在真实的数据表中,小张已经插入了一条数据。但是小王此时并不知道,也插入了同一条数据,会发生什么呢?
INSERT INTO user VALUES (6, 'd', 1000);
-- ERROR 1062 (23000): Duplicate entry '6' for key 'PRIMARY'
报错了,操作被告知已存在主键为 6
的字段。这种现象也被称为幻读,一个事务提交的数据,不能被其他事务读取到。
串行化
顾名思义,就是所有事务的写入操作全都是串行化的。什么意思?把隔离级别修改成 SERIALIZABLE :
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| SERIALIZABLE |
+--------------------------------+
还是拿小张和小王来举例:
-- 小张 - 成都
START TRANSACTION;
-- 小王 - 北京
START TRANSACTION;
-- 开启事务之前先查询表,准备操作数据。
SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 1000 |
| 4 | 淘宝店 | 1000 |
| 5 | c | 100 |
| 6 | d | 1000 |
+----+-----------+-------+
-- 发现没有 7 号王小花,于是插入一条数据:
INSERT INTO user VALUES (7, '王小花', 1000);
此时会发生什么呢?由于现在的隔离级别是 SERIALIZABLE ( 串行化 ) ,串行化的意思就是:假设把所有的事务都放在一个串行的队列中,那么所有的事务都会按照固定顺序执行,执行完一个事务后再继续执行下一个事务的写入操作 ( 这意味着队列中同时只能执行一个事务的写入操作 ) 。
根据这个解释,小王在插入数据时,会出现等待状态,直到小张执行 COMMIT
结束它所处的事务,或者出现等待超时。
串行化的问题是,性能特差
隔离级别和安全特性成反比。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。