【注】学习了一天学会数据库这个教程,少部分参考了部分评论区的笔记。

如何登录数据库服务器

如何直接在cmd直接使用数据库

网上的方法是:

使用ctrl+R输入services.msc找到对应的mysql进程,然后在属性中找到它的位置,把它加入用户变量中即可使用,注意使用cmd的时候要使用管理者身份,但是即使如此好像也没法解决。

如何登录数据库服务器
mysql -uroot -p

MySQL常用数据类型

  1. 数值

    注意各类数据类型的范围。按照大小选择类型。

  2. 日期时间

    时间也有格式,去网上查就行

  3. 字符串

    同理字符串也是看范围,直接去网上找就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;

小结:

  1. 建表添加约束
  2. 使用alter ..add添加
  3. alter modify的方式进行添加
  4. 使用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
);

传了值就不会使用默认值。

外键约束

  1. 涉及到两个表(父表,子表)
  2. 主表、副表
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一个列中的字段内容作为数据,输入到副表之中。

  1. 主表classes没有的数据,在副表是不可以使用的。
  2. 副表已经写好的数据,引用了主表的一个字段,在主表是不能删除的,要不然也会出现第一条的情况。

数据库的三大设计范式

第一范式

数据表中的所有字段都是不可分割的原子值

字段值还可以继续拆分的就不满足第一范式。

比如address:中国天津市津南区海河教育园xxx

如果字段设置为国家、省 城市、区、具体位置,就算第一范式。

<u>范式的设计是要和实际项目开发相匹配,不一定是越详细越好,也不是越粗糙越好。</u>

第二范式

必须是满足第一范式的前提下,

第二范式要求:

  1. 除主键外的每一列都必须完全依赖主键。
  2. 如果出现不完全依赖,那么只可能发生在符合主键的情况下(这是不符合第二范式的)
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中如何控制事务

  1. MySQL是默认开启事务的
select @@autocommit
  1. 默认事务开启的时候,当我们执行一个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这个用户
  1. 如何撤销这个效果呢?
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

所以这个性质叫做持久性。

总结

  1. 自动提交

    • 查看自动提交状态:SELECT @@AUTOCOMMIT
    • 设置自动提交状态:SET AUTOCOMMIT = 0
  2. 手动提交

    @@AUTOCOMMIT = 0 时,使用 COMMIT 命令提交事务。

  3. 事务回滚

    @@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了。
事务开启
  1. 修改默认提交 set autocommit = 0
  2. begin
  3. start transaction
事务手动提交

commit

事务手动回滚

rollback

事务的隔离性

事务的隔离性可分为四种 ( 性能从低到高 )

  1. READ UNCOMMITTED ( 读取未提交 )

    如果有多个事务,那么任意事务都可以看见其他事务的未提交数据

    通俗的说是 如果有事务a和事务b,如果a没有提交,b照样能看到a操作的结果

  2. READ COMMITTED ( 读取已提交 )

    只能读取到其他事务已经提交的数据

  3. REPEATABLE READ ( 可被重复读 )

    如果有多个连接都开启了事务,那么事务之间不能共享数据记录,否则只能共享已提交的记录。

  4. 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 结束它所处的事务,或者出现等待超时。

串行化的问题是,性能特差

隔离级别和安全特性成反比。


喵喵狂吠
6 声望5 粉丝

假发消费者,计科小学生。