MySQL专题
0.准备sql
设计一张员工信息表,要求如下:
编号(纯数字)
员工工号 (字符串类型,长度不超过10位)
员工姓名(字符串类型,长度不超过10位)
性别(男/女,存储一个汉字)
年龄(正常人年龄,不可能存储负数)
身份证号(二代身份证号均为18位,身份证中有X这样的字符)
入职时间(取值年月日即可)
对应的建表语句如下:
drop table if exists emp;
create table emp(
id int comment '编号',
workno varchar(10) comment '工号',
name varchar(10) comment '姓名',
gender char(1) comment '性别',
age tinyint unsigned comment '年龄',
idcard char(18) comment '身份证号',
workaddress varchar(50) comment '工作地址',
entrydate date comment '入职时间'
)comment '员工表';
INSERT INTO emp (id, workno, name, gender, age, idcard, workaddress, entrydate) VALUES (1, '1', '柳岩666', '女', 20, '123456789012345678', '北京', '2000-01-01');
INSERT INTO emp (id, workno, name, gender, age, idcard, workaddress, entrydate) VALUES (2, '2', '张无忌', '男', 18, '123456789012345670', '北京', '2005-09-01');
INSERT INTO emp (id, workno, name, gender, age, idcard, workaddress, entrydate) VALUES (3, '3', '韦一笑', '男', 38, '123456789712345670', '上海', '2005-08-01');
INSERT INTO emp (id, workno, name, gender, age, idcard, workaddress, entrydate) VALUES (4, '4', '赵敏', '女', 18, '123456757123845670', '北京', '2009-12-01');
INSERT INTO emp (id, workno, name, gender, age, idcard, workaddress, entrydate) VALUES (5, '5', '小昭', '女', 16, '123456769012345678', '上海', '2007-07-01');
INSERT INTO emp (id, workno, name, gender, age, idcard, workaddress, entrydate) VALUES (6, '6', '杨逍', '男', 28, '12345678931234567X', '北京', '2006-01-01');
INSERT INTO emp (id, workno, name, gender, age, idcard, workaddress, entrydate) VALUES (7, '7', '范瑶', '男', 40, '123456789212345670', '北京', '2005-05-01');
INSERT INTO emp (id, workno, name, gender, age, idcard, workaddress, entrydate) VALUES (8, '8', '黛绮丝', '女', 38, '123456157123645670', '天津', '2015-05-01');
INSERT INTO emp (id, workno, name, gender, age, idcard, workaddress, entrydate) VALUES (9, '9', '范凉凉', '女', 45, '123156789012345678', '北京', '2010-04-01');
INSERT INTO emp (id, workno, name, gender, age, idcard, workaddress, entrydate) VALUES (10, '10', '陈友谅', '男', 53, '123456789012345670', '上海', '2011-01-01');
INSERT INTO emp (id, workno, name, gender, age, idcard, workaddress, entrydate) VALUES (11, '11', '张士诚', '男', 55, '123567897123465670', '江苏', '2015-05-01');
INSERT INTO emp (id, workno, name, gender, age, idcard, workaddress, entrydate) VALUES (12, '12', '常遇春', '男', 32, '123446757152345670', '北京', '2004-02-01');
INSERT INTO emp (id, workno, name, gender, age, idcard, workaddress, entrydate) VALUES (13, '13', '张三丰', '男', 88, '123656789012345678', '江苏', '2020-11-01');
INSERT INTO emp (id, workno, name, gender, age, idcard, workaddress, entrydate) VALUES (14, '14', '灭绝', '女', 65, '123456719012345670', '西安', '2019-05-01');
INSERT INTO emp (id, workno, name, gender, age, idcard, workaddress, entrydate) VALUES (15, '15', '胡青牛', '男', 70, '12345674971234567X', '西安', '2018-04-01');
INSERT INTO emp (id, workno, name, gender, age, idcard, workaddress, entrydate) VALUES (16, '16', '周芷若', '女', 18, null, '北京', '2012-06-01');
1.关于sql关键字的 执行顺序
我们要来说明的是DQL语句在执行时的执行顺序,也就是先执行那一部分,后执行那一部分。
![](https://i-blog.csdnimg.cn/blog_migrate/c5289762169ff45a26f6460c7f35aa68.png)
验证:
查询年龄大于15的员工姓名、年龄,并根据年龄进行升序排序。
select name , age from emp where age > 15 order by age asc;
![](https://i-blog.csdnimg.cn/blog_migrate/db9641dcd9218884c989e38a1e47e6e9.png)
在查询时,我们给emp表起一个别名 e,然后在select 及 where中使用该别名。
select e.name , e.age from emp e where e.age > 15 order by age asc;
执行上述SQL语句后,我们看到依然可以正常的查询到结果,此时就说明: from 先执行, 然后 where 和 select 执行。那 where 和 select 到底哪个先执行呢?
此时,此时我们可以给select后面的字段起别名,然后在 where 中使用这个别名,然后看看是否可以执行成功。
select e.name ename , e.age eage from emp e where eage > 15 order by age asc;
执行上述SQL报错了:
![](https://i-blog.csdnimg.cn/blog_migrate/1a1384fb2e3997ae01812fc860fdece9.png)
由此我们可以得出结论: from 先执行,然后执行 where , 再执行select 。
接下来,我们再执行如下SQL语句,查看执行效果:
select e.name ename , e.age eage from emp e where e.age > 15 order by eage asc;
结果执行成功。 那么也就验证了: order by 是在select 语句之后执行的。
综上所述,我们可以看到DQL语句的执行顺序为: from ... where ... group by ... having ... select ... order by ... limit ...
2. 函数
函数 是指一段可以直接被另一段程序调用的程序或代码。 也就意味着,这一段程序或代码在MySQL中已经给我们提供了,我们要做的就是在合适的业务场景调用对应的函数完成对应的业务需求即可。 那么,函数到底在哪儿使用呢?
我们先来看两个场景:
![](https://i-blog.csdnimg.cn/blog_migrate/e4ab4d5f212e161a693fdbcff599a364.png)
![](https://i-blog.csdnimg.cn/blog_migrate/41339c3bb50fd9d244d47b724dcc5860.png)
1). 在企业的OA或其他的人力系统中,经常会提供的有这样一个功能,每一个员工登录上来之后都能够看到当前员工入职的天数。 而在数据库中,存储的都是入职日期,如 2000-11-12,那如果快速计算出天数呢?
2). 在做报表这类的业务需求中,我们要展示出学员的分数等级分布。而在数据库中,存储的是学生的分数值,如98/75,如何快速判定分数的等级呢?
其实,上述的这一类的需求呢,我们通过MySQL中的函数都可以很方便的实现 。
MySQL中的函数主要分为以下四类: 字符串函数、数值函数、日期函数、流程函数。
2.1 字符串函数
MySQL中内置了很多字符串函数,常用的几个如下:
函数 | 功能 |
CONCAT(S1,S2,...Sn) | 字符串拼接,将S1,S2,... Sn拼接成一个字符串 |
LOWER(str) | 将字符串str全部转为小写 |
UPPER(str) | 将字符串str全部转为大写 |
LPAD(str,n,pad) | 左填充,用字符串pad对str的左边进行填充,达到n个字符串长度 |
RPAD(str,n,pad) | 右填充,用字符串pad对str的右边进行填充,达到n个字符串长度 |
TRIM(str) | 去掉字符串头部和尾部的空格(中间的不可以去除) |
SUBSTRING(str,start,len) | 返回从字符串str从start位置起的len个长度的字符串,从1开始 |
演示如下:
A. concat : 字符串拼接
select concat('Hello' , ' MySQL');
![](https://i-blog.csdnimg.cn/blog_migrate/8a093dd05f645ef55e2d87d609bdf9ea.png)
B. lower : 全部转小写
select lower('HELLO');
![](https://i-blog.csdnimg.cn/blog_migrate/66df3e5300fb2a13aabe14853ab3b120.png)
C. upper : 全部转大写
select upper('Hello');
![](https://i-blog.csdnimg.cn/blog_migrate/c3393e48048b7c93baf8a7a097306ebb.png)
D. lpad : 左填充
select lpad('hello', 8, '-');
![](https://i-blog.csdnimg.cn/blog_migrate/f29da0c6fa9498129b85b99fc5d378c3.png)
E. rpad : 右填充
select rpad('hello', 8, '-');
![](https://i-blog.csdnimg.cn/blog_migrate/b3a496a13670538f1063801fe0687fb0.png)
F. trim : 去除空格
select trim(' Hello MySQL ');
![](https://i-blog.csdnimg.cn/blog_migrate/95f09891c1feabcb5cda24887a7eebfd.png)
G. substring : 截取子字符串
select substring('Hello MySQL',1,5);
![](https://i-blog.csdnimg.cn/blog_migrate/087d665270ec3af27d312dfbb76e0af8.png)
案例:
![](https://i-blog.csdnimg.cn/blog_migrate/17364a984710f42db184968055f86ca6.png)
由于业务需求变更,企业员工的工号,统一为5位数,目前不足5位数的全部在前面补0。比如: 1号员工的工号应该为00001。
update emp set workno = lpad(workno, 5, '0');
处理完毕后, 具体的数据为:
![](https://i-blog.csdnimg.cn/blog_migrate/8c7f1e9f3da2c2ac17c91f11a3e994f9.png)
2.2 数值函数
常见的数值函数如下:
函数 | 功能 |
CEIL(x) | 向上取整 |
FLOOR(x) | 向下取整 |
MOD(x,y) | 返回x/y的模 |
RAND() | 返回0~1内的随机数 |
ROUND(x,y) | 求参数x的四舍五入的值,保留y位小数 |
演示如下:
A. ceil:向上取整
select ceil(1.1);
![](https://i-blog.csdnimg.cn/blog_migrate/3b5cf091fb9948dda0ecb70da2591b15.png)
B. floor:向下取整
select floor(1.9);
![](https://i-blog.csdnimg.cn/blog_migrate/dd16b8cccd335c6e8056f1a04aef30cb.png)
C. mod:取模
select mod(7,4);
![](https://i-blog.csdnimg.cn/blog_migrate/bb901c70ca4de4b509f66d283cb48150.png)
D. rand:获取随机数
select rand();
![](https://i-blog.csdnimg.cn/blog_migrate/307ea0040bce3989f3fa1d5f2fb4ab2d.png)
E. round:四舍五入
select round(2.344,2);
![](https://i-blog.csdnimg.cn/blog_migrate/686ebd1b569637896f3d99b3dcbec694.png)
案例:
通过数据库的函数,生成一个六位数的随机验证码。
思路: 获取随机数可以通过rand()函数,但是获取出来的随机数是在0-1之间的,所以可以在其基础上乘以1000000,然后舍弃小数部分,如果长度不足6位,补0
select lpad(round(rand()*1000000 , 0), 6, '0');
![](https://i-blog.csdnimg.cn/blog_migrate/f960d21d7eba86b848e8e658eea0a058.png)
2.3 日期函数
常见的日期函数如下:
函数 | 功能 |
CURDATE() | 返回当前日期 |
CURTIME() | 返回当前时间 |
NOW() | 返回当前日期和时间 |
YEAR(date) | 获取指定date的年份 |
MONTH(date) | 获取指定date的月份 |
DAY(date) | 获取指定date的日期 |
DATE_ADD(date, INTERVAL expr type) | 返回一个日期/时间值加上一个时间间隔expr后的时间值 |
DATEDIFF(date1,date2) | 返回起始时间date1 和 结束时间date2之间的天数 |
演示如下:
A. curdate:当前日期
select curdate();
![](https://i-blog.csdnimg.cn/blog_migrate/3840770e1f5947353669ba2bf9d9ebb5.png)
B. curtime:当前时间
select curtime();
![](https://i-blog.csdnimg.cn/blog_migrate/b15384fb44bc4fd4e7dd173aca015270.png)
C. now:当前日期和时间
select now();
![](https://i-blog.csdnimg.cn/blog_migrate/d4a2ec6a18d181a3e02efdb04ee1b988.png)
D. YEAR , MONTH , DAY:当前年、月、日
select YEAR(now());
select MONTH(now());
select DAY(now());
-- 都查
SELECT YEAR(NOW()) year ,MONTH(NOW()) month, DAY(NOW()) day
![](https://i-blog.csdnimg.cn/blog_migrate/bc88dea32b3cd27baba601576eb5d9e2.png)
E. date_add:增加指定的时间间隔,70年后的今天的日期
select date_add(now(), INTERVAL 70 YEAR );
![](https://i-blog.csdnimg.cn/blog_migrate/a9af9442e85051a84847905b87c40f55.png)
F. datediff:获取两个日期相差的天数
select datediff('2021-12-01', '2021-10-01');
![](https://i-blog.csdnimg.cn/blog_migrate/3f87ec1c10766c93eeaed92d37541fef.png)
案例:
查询所有员工的入职天数,并根据入职天数倒序排序。
思路: 入职天数,就是通过当前日期 - 入职日期,所以需要使用datediff函数来完成。
select name, datediff(curdate(), entrydate) as 'entrydays' from emp order by entrydays desc;
![](https://i-blog.csdnimg.cn/blog_migrate/8b0ea5389f257c1084ef0a0904ce4298.png)
2.4 流程函数
流程函数也是很常用的一类函数,可以在SQL语句中实现条件筛选,从而提高语句的效率。
函数 | 功能 |
IF(value , t , f) | 如果value为true,则返回t,否则返回f |
IFNULL(value1 , value2) | 如果value1不为空,返回value1,否则返回value2 |
CASE WHEN [ val1 ] THEN [res1] ... ELSE [ default ] END | 如果val1为true,返回res1,... 否则返回default默认值 |
CASE [ expr ] WHEN [ val1 ] THEN [res1] ... ELSE [ default ] END | 如果expr的值等于val1,返回res1,... 否则返回default默认值 |
演示如下:
A. if ,如果参数1=true的话,那么则返回ok字符,否则返回Error字符
select if(false, 'Ok', 'Error');
![](https://i-blog.csdnimg.cn/blog_migrate/6d99dd0d36cfd000e7babcb502d6326a.png)
B: ifnull,如果参数1不等于空(null),则返回参数1,否则返回参数2
select ifnull('Ok','Default');
select ifnull('','Default');
select ifnull(null,'Default');
-- 一条sql
select IFNULL('ok','Default') as param1, IFNULL('','Default') as param2, IFNULL(null,'Default') as param3
![](https://i-blog.csdnimg.cn/blog_migrate/dc96cd791fbbeb7d574006206a5e99ce.png)
C. case when then else end
需求: 查询emp表的员工姓名和工作地址 (北京/上海 ----> 一线城市 , 其他 ----> 二线城市)
-- 第一种做法:
SELECT name,
(CASE workaddress
WHEN '北京' THEN
'一线城市'
WHEN '上海' THEN
'一线城市'
ELSE
'二线城市'
END
) as '工作地址'
FROM emp;
--第二种做法
SELECT name,
(CASE
WHEN workaddress='北京' THEN
'一线城市'
WHEN workaddress='上海' THEN
'一线城市'
ELSE
'二线城市'
END
) as '工作地址'
FROM emp;
![](https://i-blog.csdnimg.cn/blog_migrate/5986159bb465b9a22028ff80e2b1ef94.png)
案例:
查询出每个学生对应的id,name及每科的学习情况(比如 85分以上优秀,60分以上及格,否则不及格)
create table score(
id int comment 'ID',
name varchar(20) comment '姓名',
math int comment '数学',
english int comment '英语',
chinese int comment '语文'
) comment '学员成绩表';
insert into score(id, name, math, english, chinese) VALUES (1, 'Tom', 67, 88, 95 ), (2, 'Rose' , 23, 66, 90),(3, 'Jack', 56, 98, 76);
具体的SQL语句如下:
SELECT id ,name,
(CASE
WHEN math>=85 THEN
'优秀'
WHEN math>=60 THEN
'及格'
ELSE
'不及格'
END
) as '数学',
(CASE
WHEN english>=85 THEN
'优秀'
WHEN english>=60 THEN
'及格'
ELSE
'不及格'
END
) as '英语',
(CASE
WHEN chinese>=85 THEN
'优秀'
WHEN chinese>=60 THEN
'及格'
ELSE
'不及格'
END
) as '语文'
FROM score
MySQL的常见函数我们学习完了,那接下来,我们就来分析一下,在前面讲到的两个函数的案例场景,思考一下需要用到什么样的函数来实现?
1). 数据库中,存储的是入职日期,如 2000-01-01,如何快速计算出入职天数呢? --------> 答案: datediff
2). 数据库中,存储的是学生的分数值,如98、75,如何快速判定分数的等级呢? ----------> 答案: case ... when ...
3. 多表查询
我们之前在讲解SQL语句的时候,讲解了DQL语句,也就是数据查询语句,但是之前讲解的查询都是单表查询,而本章节我们要学习的则是多表查询操作,主要从以下几个方面进行讲解。
3.1 多表关系
项目开发中,在进行数据库表结构设计时,会根据业务需求及业务模块之间的关系,分析并设计表结构,由于业务之间相互关联,所以各个表结构之间也存在着各种联系,基本上分为三种:
一对多(多对一)
多对多
一对一
3.1.1 一对多
案例: 部门 与 员工的关系
关系: 一个部门对应多个员工,一个员工对应一个部门
实现: 在多的一方建立外键,指向一的一方的主键
![](https://i-blog.csdnimg.cn/blog_migrate/6576bc182e2aa6018399a3864b990483.png)
3.1.2 多对多
案例: 学生 与 课程的关系
关系: 一个学生可以选修多门课程,一门课程也可以供多个学生选择
实现: 建立第三张中间表,中间表至少包含两个外键,分别关联两方主键
![](https://i-blog.csdnimg.cn/blog_migrate/ba8bafb26a4fb003416d5d1fbabfd6b6.png)
对应的SQL脚本:
create table student(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '姓名',
no varchar(10) comment '学号'
) comment '学生表';
insert into student values (null, '黛绮丝', '2000100101'),(null, '谢逊', '2000100102'),(null, '殷天正', '2000100103'),(null, '韦一笑', '2000100104');
create table course(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '课程名称'
) comment '课程表';
insert into course values (null, 'Java'), (null, 'PHP'), (null , 'MySQL') , (null, 'Hadoop');
create table student_course(
id int auto_increment comment '主键' primary key,
studentid int not null comment '学生ID',
courseid int not null comment '课程ID',
constraint fk_courseid foreign key (courseid) references course (id),
constraint fk_studentid foreign key (studentid) references student (id)
)comment '学生课程中间表';
insert into student_course values (null,1,1),(null,1,2),(null,1,3),(null,2,2),(null,2,3),(null,3,4);
3.1.3 一对一
案例: 用户 与 用户详情的关系
关系: 一对一关系,多用于单表拆分,将一张表的基础字段放在一张表中,其他详情字段放在另一张表中,以提升操作效率
实现: 在任意一方加入外键,关联另外一方的主键,并且设置外键为唯一的(UNIQUE)
![](https://i-blog.csdnimg.cn/blog_migrate/2b2663663b274e5692d851665ff4c8ca.png)
对应的SQL脚本:
create table tb_user(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '姓名',
age int comment '年龄',
gender char(1) comment '1: 男 , 2: 女',
phone char(11) comment '手机号'
) comment '用户基本信息表';
create table tb_user_edu(
id int auto_increment primary key comment '主键ID',
degree varchar(20) comment '学历',
major varchar(50) comment '专业',
primaryschool varchar(50) comment '小学',
middleschool varchar(50) comment '中学',
university varchar(50) comment '大学',
userid int unique comment '用户ID',
constraint fk_userid foreign key (userid) references tb_user(id)
) comment '用户教育信息表';
insert into tb_user(id, name, age, gender, phone) values
(null,'黄渤',45,'1','18800001111'),
(null,'冰冰',35,'2','18800002222'),
(null,'码云',55,'1','18800008888'),
(null,'李彦宏',50,'1','18800009999');
insert into tb_user_edu(id, degree, major, primaryschool, middleschool, university, userid) values
(null,'本科','舞蹈','静安区第一小学','静安区第一中学','北京舞蹈学院',1),
(null,'硕士','表演','朝阳区第一小学','朝阳区第一中学','北京电影学院',2),
(null,'本科','英语','杭州市第一小学','杭州市第一中学','杭州师范大学',3),
(null,'本科','应用数学','阳泉第一小学','阳泉区第一中学','清华大学',4);
3.2 多表查询概述
3.2.1 数据准备
1). 删除之前 emp, dept表的测试数据
2). 执行如下脚本,创建emp表与dept表并插入测试数据
-- 创建dept表,并插入数据
create table dept(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '部门名称'
)comment '部门表';
INSERT INTO dept (id, name) VALUES (1, '研发部'), (2, '市场部'),(3, '财务部'), (4, '销售部'), (5, '总经办'), (6, '人事部');
-- 创建emp表,并插入数据
create table emp(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '姓名',
age int comment '年龄',
job varchar(20) comment '职位',
salary int comment '薪资',
entrydate date comment '入职时间',
managerid int comment '直属领导ID',
dept_id int comment '部门ID'
)comment '员工表';
-- 添加外键
alter table emp add constraint fk_emp_dept_id foreign key (dept_id) references dept(id);
INSERT INTO emp (id, name, age, job,salary, entrydate, managerid, dept_id) VALUES
(1, '金庸', 66, '总裁',20000, '2000-01-01', null,5),
(2, '张无忌', 20, '项目经理',12500, '2005-12-05', 1,1),
(3, '杨逍', 33, '开发', 8400,'2000-11-03', 2,1),
(4, '韦一笑', 48, '开发',11000, '2002-02-05', 2,1),
(5, '常遇春', 43, '开发',10500, '2004-09-07', 3,1),
(6, '小昭', 19, '程序员鼓励师',6600, '2004-10-12', 2,1),
(7, '灭绝', 60, '财务总监',8500, '2002-09-12', 1,3),
(8, '周芷若', 19, '会计',48000, '2006-06-02', 7,3),
(9, '丁敏君', 23, '出纳',5250, '2009-05-13', 7,3),
(10, '赵敏', 20, '市场部总监',12500, '2004-10-12', 1,2),
(11, '鹿杖客', 56, '职员',3750, '2006-10-03', 10,2),
(12, '鹤笔翁', 19, '职员',3750, '2007-05-09', 10,2),
(13, '方东白', 19, '职员',5500, '2009-02-12', 10,2),
(14, '张三丰', 88, '销售总监',14000, '2004-10-12', 1,4),
(15, '俞莲舟', 38, '销售',4600, '2004-10-12', 14,4),
(16, '宋远桥', 40, '销售',4600, '2004-10-12', 14,4),
(17, '陈友谅', 42, null,2000, '2011-10-12', 1,null);
dept表共6条记录,emp表共17条记录。
3.2.2 概述
多表查询就是指从多张表中查询数据。
原来查询单表数据,执行的SQL形式为:select * from emp;
那么我们要执行多表查询,就只需要使用逗号分隔多张表即可,如: select * from emp , dept ; 具体的执行结果如下:
![](https://i-blog.csdnimg.cn/blog_migrate/845a0872653b85d0e5afc2d4eaadf77b.png)
此时,我们看到查询结果中包含了大量的结果集,总共102条记录,而这其实就是员工表emp所有的记录(17) 与 部门表dept所有记录(6) 的所有组合情况,这种现象称之为笛卡尔积。接下来,就来简单介绍下笛卡尔积。
笛卡尔积: 笛卡尔乘积是指在数学中,两个集合A集合 和 B集合的所有组合情况。
而在多表查询中,我们是需要消除无效的笛卡尔积的,只保留两张表关联部分的数据。
![](https://i-blog.csdnimg.cn/blog_migrate/1fc5fa6d443821099bd93831190e10d6.png)
![](https://i-blog.csdnimg.cn/blog_migrate/1b5e3e87e7f2330792f038e9d7bc1811.png)
在SQL语句中,如何来去除无效的笛卡尔积呢? 我们可以给多表查询加上连接查询的条件即可。
select * from emp , dept where emp.dept_id = dept.id;
![](https://i-blog.csdnimg.cn/blog_migrate/79d983426976272226214b5fd047b3dd.png)
而由于id为17的员工,没有dept_id字段值,所以在多表查询时,根据连接查询的条件并没有查询到。
3.2.3 分类
连接查询
内连接:相当于查询A、B交集部分数据
外连接:
左外连接:查询左表所有数据,以及两张表交集部分数据
右外连接:查询右表所有数据,以及两张表交集部分数据
自连接:当前表与自身的连接查询,自连接必须使用表别名
子查询
3.3 内连接
![](https://i-blog.csdnimg.cn/blog_migrate/4610c6c823e91dc5d8fb9804fe6ce945.png)
内连接查询的是两张表交集部分的数据。(也就是绿色部分的数据)
内连接的语法分为两种: 隐式内连接、显式内连接。先来学习一下具体的语法结构。
1). 隐式内连接
SELECT 字段列表 FROM 表1 , 表2 WHERE 条件 ... ;
2). 显式内连接
SELECT 字段列表 FROM 表1 [ INNER ] JOIN 表2 ON 连接条件 ... ;
案例:
A. 查询每一个员工的姓名 , 及关联的部门的名称 (隐式内连接实现)
表结构: emp , dept
连接条件: emp.dept_id = dept.id
select emp.name , dept.name from emp , dept where emp.dept_id = dept.id ;
-- 为每一张表起别名,简化SQL编写
select e.name,d.name from emp e , dept d where e.dept_id = d.id;
B. 查询每一个员工的姓名 , 及关联的部门的名称 (显式内连接实现) --- INNER JOIN ... ON ...
表结构: emp , dept
连接条件: emp.dept_id = dept.id
select e.name, d.name from emp e inner join dept d on e.dept_id = d.id;
-- 为每一张表起别名,简化SQL编写
select e.name, d.name from emp e join dept d on e.dept_id = d.id;
表的别名:
①. tablea as 别名1 , tableb as 别名2 ;
②. tablea 别名1 , tableb 别名2 ;
注意事项:
一旦为表起了别名,就不能再使用表名来指定对应的字段了,此时只能够使用别名来指定字段。
3.4 外连接
![](https://i-blog.csdnimg.cn/blog_migrate/2b81d28b492c12b77af677ff800ef9c7.png)
外连接分为两种,分别是:左外连接 和 右外连接。具体的语法结构为:
1). 左外连接
SELECT 字段列表 FROM 表1 LEFT [ OUTER ] JOIN 表2 ON 条件 ... ;
左外连接相当于查询表1(左表)的所有数据,当然也包含表1和表2交集部分的数据。
2). 右外连接
SELECT 字段列表 FROM 表1 RIGHT [ OUTER ] JOIN 表2 ON 条件 ... ;
右外连接相当于查询表2(右表)的所有数据,当然也包含表1和表2交集部分的数据。
案例:
A. 查询emp表的所有数据, 和对应的部门信息
由于需求中提到,要查询emp的所有数据,所以是不能内连接查询的,需要考虑使用外连接查询。
表结构: emp, dept
连接条件: emp.dept_id = dept.id
select e.*, d.name from emp e left outer join dept d on e.dept_id = d.id;
select e.*, d.name from emp e left join dept d on e.dept_id = d.id;
B. 查询dept表的所有数据, 和对应的员工信息(右外连接)
由于需求中提到,要查询dept表的所有数据,所以是不能内连接查询的,需要考虑使用外连接查询。
表结构: emp, dept
连接条件: emp.dept_id = dept.id
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;
注意事项:
左外连接和右外连接是可以相互替换的,只需要调整在连接查询时SQL中,表结构的先后顺序就可以了。而我们在日常开发使用时,更偏向于左外连接。
3.5 自连接
3.5.1 自连接查询
自连接查询,顾名思义,就是自己连接自己,也就是把一张表连接查询多次。我们先来学习一下自连接的查询语法:
SELECT 字段列表 FROM 表A 别名A JOIN 表A 别名B ON 条件 ... ;
而对于自连接查询,可以是内连接查询,也可以是外连接查询。
案例:
A. 查询员工 及其 所属领导的名字
表结构: emp
select a.name , b.name from emp a , emp b where a.managerid = b.id;
B. 查询所有员工 emp 及其领导的名字 emp , 如果员工没有领导, 也需要查询出来
表结构: emp a , emp b
select a.name '员工', b.name '领导' from emp a left join emp b on a.managerid = b.id;
注意事项:
在自连接查询中,必须要为表起别名,要不然我们不清楚所指定的条件、返回的字段,到底是哪一张表的字段。
3.5.2 联合查询
对于union查询,就是把多次查询的结果合并起来,形成一个新的查询结果集。
SELECT 字段列表 FROM 表A ...
UNION [ ALL ]
SELECT 字段列表 FROM 表B ....;
对于联合查询的多张表的列数必须保持一致,字段类型也需要保持一致。
union all 会将全部的数据直接合并在一起,union 会对合并之后的数据去重。
案例:
A. 将薪资低于 5000 的员工 , 和 年龄大于 50 岁的员工全部查询出来.
当前对于这个需求,我们可以直接使用多条件查询,使用逻辑运算符 or 连接即可。 那这里呢,我们也可以通过union/union all来联合查询.
select * from emp where salary < 5000
union all
select * from emp where age > 50;
![](https://i-blog.csdnimg.cn/blog_migrate/41a6524ff552b3ea4ea5202f1c596eff.png)
union all查询出来的结果,仅仅进行简单的合并,并未去重。
select * from emp where salary < 5000
union
select * from emp where age > 50;
![](https://i-blog.csdnimg.cn/blog_migrate/f8aa6ff6515f93036cd9951996d7e796.png)
union 联合查询,会对查询出来的结果进行去重处理。
注意:
如果多条查询语句查询出来的结果,字段数量不一致,在进行union/union all联合查询时,将会报错。如:
![](https://i-blog.csdnimg.cn/blog_migrate/ba29853b3a98fa17423252af6a4a90a9.png)
3.6 子查询
3.6.1 概述
1). 概念
SQL语句中嵌套SELECT语句,称为嵌套查询,又称子查询。
SELECT * FROM t1 WHERE column1 = ( SELECT column1 FROM t2 );
子查询外部的语句可以是INSERT / UPDATE / DELETE / SELECT 的任何一个。
2). 分类
根据子查询结果不同,分为:
A. 标量子查询(子查询结果为单个值)
B. 列子查询(子查询结果为一列)
C. 行子查询(子查询结果为一行)
D. 表子查询(子查询结果为多行多列)
根据子查询位置,分为:
A. WHERE之后
B. FROM之后
C. SELECT之后
3.6.2 标量子查询
子查询返回的结果是单个值(数字、字符串、日期等),最简单的形式,这种子查询称为标量子查询。
常用的操作符:= <> > >= < <=
案例:
A. 查询 "销售部" 的所有员工信息
完成这个需求时,我们可以将需求分解为两步:
①. 查询 "销售部" 部门ID
select id from dept where name = '销售部';
②. 根据 "销售部" 部门ID, 查询员工信息
select * from emp where dept_id = (select id from dept where name = '销售部');
B. 查询在 "方东白" 入职之后的员工信息
完成这个需求时,我们可以将需求分解为两步:
①. 查询 方东白 的入职日期
select entrydate from emp where name = '方东白';
②. 查询指定入职日期之后入职的员工信息
select * from emp where entrydate > (select entrydate from emp where name = '方东白');
3.6.3 列子查询
子查询返回的结果是一列(可以是多行),这种子查询称为列子查询。
常用的操作符:IN 、NOT IN 、 ANY 、SOME 、 ALL
操作符 | 描述 |
IN | 在指定的集合范围之内,多选一 |
NOT IN | 不在指定的集合范围之内 |
ANY | 子查询返回列表中,有任意一个满足即可 |
SOME | 与ANY等同,使用SOME的地方都可以使用ANY |
ALL | 子查询返回列表的所有值都必须满足 |
案例:
A. 查询 "销售部" 和 "市场部" 的所有员工信息
分解为以下两步:
①. 查询 "销售部" 和 "市场部" 的部门ID
select id from dept where name = '销售部' or name = '市场部';
②. 根据部门ID, 查询员工信息
select * from emp where dept_id in (select id from dept where name = '销售部' or name = '市场部');
B. 查询比 财务部 所有人工资都高的员工信息
分解为以下两步:
①. 查询所有 财务部 人员工资
select id from dept where name = '财务部';
select salary from emp where dept_id = (select id from dept where name = '财务部');
②. 比 财务部 所有人工资都高的员工信息
select * from emp where salary > all ( select salary from emp where dept_id = (select id from dept where name = '财务部') );
C. 查询比研发部其中任意一人工资高的员工信息
分解为以下两步:
①. 查询研发部所有人工资
select salary from emp where dept_id = (select id from dept where name = '研发部');
②. 比研发部其中任意一人工资高的员工信息
select * from emp where salary > any ( select salary from emp where dept_id = (select id from dept where name = '研发部') );
3.6.4 行子查询
子查询返回的结果是一行(可以是多列),这种子查询称为行子查询。
常用的操作符:= 、<> 、IN 、NOT IN
案例:
A. 查询与 "张无忌" 的薪资及直属领导相同的员工信息 ;
这个需求同样可以拆解为两步进行:
①. 查询 "张无忌" 的薪资及直属领导
select salary, managerid from emp where name = '张无忌';
②. 查询与 "张无忌" 的薪资及直属领导相同的员工信息 ;
select * from emp where (salary,managerid) = (select salary, managerid from emp where name = '张无忌');
3.6.5 表子查询
子查询返回的结果是多行多列,这种子查询称为表子查询。
常用的操作符:IN
案例:
A. 查询与 "鹿杖客" , "宋远桥" 的职位和薪资相同的员工信息
分解为两步执行:
①. 查询 "鹿杖客" , "宋远桥" 的职位和薪资
select job, salary from emp where name = '鹿杖客' or name = '宋远桥';
②. 查询与 "鹿杖客" , "宋远桥" 的职位和薪资相同的员工信息
select * from emp where (job,salary) in ( select job, salary from emp where name = '鹿杖客' or name = '宋远桥' );
B. 查询入职日期是 "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 ;
3.7 多表查询案例
数据环境准备:
create table salgrade(
grade int,
losal int,
hisal int
) comment '薪资等级表';
insert into salgrade values (1,0,3000);
insert into salgrade values (2,3001,5000);
insert into salgrade values (3,5001,8000);
insert into salgrade values (4,8001,10000);
insert into salgrade values (5,10001,15000);
insert into salgrade values (6,15001,20000);
insert into salgrade values (7,20001,25000);
insert into salgrade values (8,25001,30000);
在这个案例中,我们主要运用上面所讲解的多表查询的语法,完成以下的12个需求即可,而这里主要涉及到的表就三张:emp员工表、dept部门表、salgrade薪资等级表 。
1). 查询员工的姓名、年龄、职位、部门信息 (隐式内连接)
表: emp , dept
连接条件: emp.dept_id = dept.id
select e.name , e.age , e.job , d.name from emp e , dept d where e.dept_id = d.id;
2). 查询年龄小于30岁的员工的姓名、年龄、职位、部门信息(显式内连接)
表: emp , dept
连接条件: emp.dept_id = dept.id
select e.name , e.age , e.job , d.name from emp e inner join dept d on e.dept_id = d.id where e.age < 30;
3). 查询拥有员工的部门ID、部门名称
表: emp , dept
连接条件: emp.dept_id = dept.id
select distinct d.id , d.name from emp e , dept d where e.dept_id = d.id;
4). 查询所有年龄大于40岁的员工, 及其归属的部门名称; 如果员工没有分配部门, 也需要展示出来(外连接)
表: emp , dept
连接条件: emp.dept_id = dept.id
select e.*, d.name from emp e left join dept d on e.dept_id = d.id where e.age > 40 ;
5). 查询所有员工的工资等级
表: emp , salgrade
连接条件 : emp.salary >= salgrade.losal and emp.salary <= salgrade.hisal
-- 方式一
select e.* , s.grade , s.losal, s.hisal from emp e , salgrade s where e.salary >= s.losal and e.salary <= s.hisal;
-- 方式二
select e.* , s.grade , s.losal, s.hisal from emp e , salgrade s where e.salary between s.losal and s.hisal;
6). 查询 "研发部" 所有员工的信息及 工资等级
表: emp , salgrade , dept
连接条件 : emp.salary between salgrade.losal and salgrade.hisal , emp.dept_id = dept.id
查询条件 : dept.name = '研发部'
select e.* , s.grade from emp e , dept d , salgrade s where e.dept_id = d.id and ( e.salary between s.losal and s.hisal ) and d.name = '研发部';
7). 查询 "研发部" 员工的平均工资
表: emp , dept
连接条件 : emp.dept_id = dept.id
select avg(e.salary) from emp e, dept d where e.dept_id = d.id and d.name = '研发部';
8). 查询工资比 "灭绝" 高的员工信息。
①. 查询 "灭绝" 的薪资
select salary from emp where name = '灭绝';
②. 查询比她工资高的员工数据
select * from emp where salary > ( select salary from emp where name = '灭绝' );
9). 查询比平均薪资高的员工信息
①. 查询员工的平均薪资
select avg(salary) from emp;
②. 查询比平均薪资高的员工信息
select * from emp where salary > ( select avg(salary) from emp );
10). 查询低于本部门平均工资的员工信息
①. 查询指定部门平均薪资
select avg(e1.salary) from emp e1 where e1.dept_id = 1;
select avg(e1.salary) from emp e1 where e1.dept_id = 2;
②. 查询低于本部门平均工资的员工信息
select * from emp e2 where e2.salary < ( select avg(e1.salary) from emp e1 where e1.dept_id = e2.dept_id );
11). 查询所有的部门信息, 并统计部门的员工人数
select d.id, d.name , ( select count(*) from emp e where e.dept_id = d.id ) '人数' from dept d;
12). 查询所有学生的选课情况, 展示出学生名称, 学号, 课程名称
表: student , course , student_course
连接条件: student.id = student_course.studentid , course.id = student_course.courseid
select s.name , s.no , c.name from student s , student_course sc , course c where s.id = sc.studentid and sc.courseid = c.id ;
备注: 以上需求的实现方式可能会很多, SQL写法也有很多,只要能满足我们的需求,查询出符合条件的记录即可。
3.8 行转列和列转行查询
行列转换在做报表分析时还是经常会遇到的sql问题
转换的样式:
![](https://i-blog.csdnimg.cn/blog_migrate/841ac429abee51301074bfe79e72f32b.png)
行转列的互相转换
具体的实现:
假如我们有下表:
![](https://i-blog.csdnimg.cn/blog_migrate/2f157546abeb916c344df1444d1406ec.png)
1、创建表sql
CREATE TABLE `student-sss` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(10) DEFAULT NULL COMMENT '姓名',
`subject` varchar(10) DEFAULT NULL COMMENT '科目',
`score` varchar(10) DEFAULT NULL COMMENT '分数',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='学生表';
INSERT INTO `student-sss` VALUES(1,'小明','语文',96),(2,'小明','数学',98),(3,'小明','英语',95),(4,'大花','语文',92),(5,'大花','数学',96),(6,'大花','英语',98)
2、方式一:使用case...when...then进行行转列
SELECT name,
SUM(CASE `subject` WHEN '语文' THEN score ELSE 0 END ) as '语文',
SUM(CASE `subject` WHEN '数学' THEN score ELSE 0 END ) as '数学',
SUM(CASE `subject` WHEN '英语' THEN score ELSE 0 END ) as '英语'
FROM `student-sss`
GROUP BY name
![](https://i-blog.csdnimg.cn/blog_migrate/584a18aea0f28f915c9db038db60e829.png)
3、方式二:使用IF() 进行行转列
SELECT name,
SUM(IF(`subject`='语文',score,0)) as '语文',
SUM(IF(`subject`='数学',score,0)) as '数学',
SUM(IF(`subject`='英语',score,0)) as '英语'
FROM `student-sss`
GROUP BY name
![](https://i-blog.csdnimg.cn/blog_migrate/94df248a5dd28726f451e8e2cee13259.png)
注意点:
(1)SUM() 是为了能够使用GROUP BY根据name进行分组,因为每一个name对应的subject="语文"的记录只有一条,所以SUM() 的值就等于对应那一条记录的score的值。
假如name=‘小明’ and subject=‘语文’ 的记录有两条,则此时SUM() 的值将会是这两条记录的和,同理,使用Max()的值将会是这两条记录里面值最大的一个。但是正常情况下,一个user对应一个subject只有一个分数,因此可以使用SUM()、MAX()、MIN()、AVG()等聚合函数都可以达到行转列的效果。
(2)IF(subject=‘语文’,score,0) 作为条件,即对所有subject='语文’的记录的score字段进行SUM()、MAX()、MIN()、AVG()操作,如果score没有值则默认为0。
列转行的互相转换
具体的实现:
假如我们有下表:
![](https://i-blog.csdnimg.cn/blog_migrate/f90d7cc091b86018b63e0eb0d574afdc.png)
1、创建表sql
CREATE TABLE `student-ccc` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(10) DEFAULT NULL COMMENT '姓名',
`score-yu` VARCHAR(4) DEFAULT NULL COMMENT '语文分数',
`score-shu` VARCHAR(4) DEFAULT NULL COMMENT '数学分数',
`score-ying` VARCHAR(4) DEFAULT NULL COMMENT '英语分数',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='学生表';
INSERT INTO `student-ccc` VALUES(1,'小明','96','98','95'),(2,'大花','92','96','98')
2、列转行sql实现
SELECT id,name,'语文' as SUBJECT,`score-yu` as score FROM `student-ccc`
UNION ALL
SELECT id,name,'数学' as SUBJECT,`score-shu` as score FROM `student-ccc`
UNION ALL
SELECT id,name,'英语' as SUBJECT,`score-ying` as score FROM `student-ccc`
ORDER BY id
![](https://i-blog.csdnimg.cn/blog_migrate/3611083ece48d2f61d627e914f80ddc7.png)
4. 事务
4.1 事务简介
事务 是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
就比如: 张三给李四转账1000块钱,张三银行账户的钱减少1000,而李四银行账户的钱要增加1000。 这一组操作就必须在一个事务的范围内,要么都成功,要么都失败。
正常情况: 转账这个操作, 需要分为以下这么三步来完成 , 三步完成之后, 张三减少1000, 而李四增加1000, 转账成功 :
![](https://i-blog.csdnimg.cn/blog_migrate/07964ff9d707f59309359adc292c3467.png)
![](https://i-blog.csdnimg.cn/blog_migrate/2715ee4009f2805b71e25190ebfb2280.png)
异常情况: 转账这个操作, 也是分为以下这么三步来完成 , 在执行第三步是报错了, 这样就导致张三减少1000块钱, 而李四的金额没变, 这样就造成了数据的不一致, 就出现问题了。
![](https://i-blog.csdnimg.cn/blog_migrate/c13f8051918d8d79f688ab9e3e9a9f22.png)
![](https://i-blog.csdnimg.cn/blog_migrate/ee9190ca8da47d50053c50eabd19e353.png)
为了解决上述的问题,就需要通过数据的事务来完成,我们只需要在业务逻辑执行之前开启事务,执行完毕后提交事务。如果执行过程中报错,则回滚事务,把数据恢复到事务开始之前的状态。
注意: 默认MySQL的事务是自动提交的,也就是说,当执行完一条DML语句时,MySQL会立即隐式的提交事务。
4.2 事务操作
数据准备:
drop table if exists account;
create table account(
id int primary key AUTO_INCREMENT comment 'ID',
name varchar(10) comment '姓名',
money double(10,2) comment '余额'
) comment '账户表';
insert into account(name, money) VALUES ('张三',2000), ('李四',2000);
4.2.1 未控制事务
1). 测试正常情况
-- 1. 查询张三余额
select * from account where name = '张三';
-- 2. 张三的余额减少1000
update account set money = money - 1000 where name = '张三';
-- 3. 李四的余额增加1000
update account set money = money + 1000 where name = '李四';
测试完毕之后检查数据的状态, 可以看到数据操作前后是一致的。
![](https://i-blog.csdnimg.cn/blog_migrate/8c4ac5d88ac1312f623d56ab3ecd5d7c.png)
2). 测试异常情况
-- 1. 查询张三余额
select * from account where name = '张三';
-- 2. 张三的余额减少1000
update account set money = money - 1000 where name = '张三';
出错了....
-- 3. 李四的余额增加1000
update account set money = money + 1000 where name = '李四';
我们把数据都恢复到2000, 然后再次一次性执行上述的SQL语句(出错了.... 这句话不符合SQL语法,执行就会报错),检查最终的数据情况, 发现数据在操作前后不一致了。
![](https://i-blog.csdnimg.cn/blog_migrate/58bea0fae722e6ee4e35a92d73267858.png)
l
4.2.2 控制事务一
1). 查看/设置事务提交方式
SELECT @@autocommit ;
SET @@autocommit = 0 ;
2). 提交事务
COMMIT;
3). 回滚事务
ROLLBACK;
注意:上述的这种方式,我们是修改了事务的自动提交行为, 把默认的自动提交修改为了手动提交, 此时我们执行的DML语句都不会提交, 需要手动的执行commit进行提交。
@@autocommit=1表示自动提交事务,把值设置成0的话表示手动提交事务
4.2.3 控制事务二
1). 开启事务
START TRANSACTION 或 BEGIN ;
2). 提交事务
COMMIT;
3). 回滚事务
ROLLBACK;
转账案例:
-- 开启事务
start transaction
-- 1. 查询张三余额
select * from account where name = '张三';
-- 2. 张三的余额减少1000
update account set money = money - 1000 where name = '张三';
-- 3. 李四的余额增加1000
update account set money = money + 1000 where name = '李四';
-- 如果正常执行完毕, 则提交事务
commit;
-- 如果执行过程中报错, 则回滚事务
-- rollback;
4.3 事务四大特性
原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
![](https://i-blog.csdnimg.cn/blog_migrate/8586088bcfc481a94facfb0a91fee2de.png)
一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
![](https://i-blog.csdnimg.cn/blog_migrate/965780519df2ef85ccc99a88a7189ac9.png)
持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
![](https://i-blog.csdnimg.cn/blog_migrate/3b0d1429896fed3b2d40d35b6d89d496.png)
上述就是事务的四大特性,简称ACID。
4.4 并发事务问题
1). 脏读:一个事务读到另外一个事务还没有提交的数据。
![](https://i-blog.csdnimg.cn/blog_migrate/8748104394b89c6cfc6b62fd586dcade.png)
比如B读取到了A未提交的数据。
2). 不可重复读:一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。
![](https://i-blog.csdnimg.cn/blog_migrate/1ffef0c4f46520bb40fc49abfd5c486f.png)
事务A两次读取同一条记录,但是读取到的数据却是不一样的。
3). 幻读:一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在,好像出现了 "幻影"。
演示:
第一步,事务A去数据库查询id=1的数据,发现没有数据,但是此时事务B新增了id=1的数据并提交了,此时数据库中有了id=1的数据
![](https://i-blog.csdnimg.cn/blog_migrate/c93ce2c9a2b4cc6bd2c480fd0a186ccd.png)
第二步,事务A再次去新增id=1的数据,但是发现已经添加不进去了,主键冲突异常
![](https://i-blog.csdnimg.cn/blog_migrate/fe70155ea551062c27980ec0a816a530.png)
第三步,事务A再次去查询id=1的数据,但是还是没有查询到id=1的数据,此时就像出现了幻觉一样,称之为幻读
![](https://i-blog.csdnimg.cn/blog_migrate/94869656d2a876b80fad2bd5ebfd81c4.png)
4.5 事务隔离级别
为了解决并发事务所引发的问题,在数据库中引入了事务隔离级别。主要有以下几种:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
Read uncommitted【读未提交】 | √ | √ | √ |
Read committed【读已提交】 | × | √ | √ |
Repeatable Read(默认)【可重复读】 | × | × | √ |
Serializable【串行化】 | × | × | × |
注意:事务隔离级别越高,数据越安全,但是性能越低。
PS:mysql的默认隔离级别是Repeatable Read,但是如果数据库是oracle 的是Read committed
4.5.1 查看和设置隔离级别
1). 查看事务隔离级别
mysql8的方式
SELECT @@TRANSACTION_ISOLATION;
mysql5.7的方式
SELECT @@tx_isolation
![](https://i-blog.csdnimg.cn/blog_migrate/6fcbd147fa97cebaea35aa3edf6cf1c9.png)
2). 设置事务隔离级别
SET [ SESSION | GLOBAL ] TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE }
SESSION表示窗口级别,而GLOBAL是表示全局级别
演示设置读未提交级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
查看隔离级别
![](https://i-blog.csdnimg.cn/blog_migrate/1a7ec43e63dc5e8a3ce42c4bb92258ef.png)
改回默认隔离级别[可重复读]
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
查看隔离级别
![](https://i-blog.csdnimg.cn/blog_migrate/5eca282a5c3cdd851806a17dc6689b9f.png)
4.5.2 利用隔离级别解决并发事务
1 准备工作
本次采用黑窗口的方式来分别模拟两个事务操作
准备两个事务窗口
![](https://i-blog.csdnimg.cn/blog_migrate/efeabb7c2171146f6b9a76a80ef971f3.png)
两个窗口都设置手动提交事务
![](https://i-blog.csdnimg.cn/blog_migrate/121cd127d3b461e4bd03d6eedff60d73.png)
2 演示Read uncommitted看是否会出现脏读问题:
![](https://i-blog.csdnimg.cn/blog_migrate/5bb7cf7bd5323aa23a90e326e0e44384.png)
注意:从图上看到发现事务1中查看账户的时候张三账户少了1000元,但是这个是由事务2中的修改操作来的,但是这个事务并没有提交。所以导致出现了脏读问题。
注意:最后每个窗口都要执行以下commit;提交事务。
3 演示Read committed 看是否能出现脏读问题
![](https://i-blog.csdnimg.cn/blog_migrate/d472bf72d2887c4fdbf3ad077fc5ce59.png)
注意:两个事务操作从1-7步骤发现,当事务2没有提交之前,事务1中查询的张三账户余额并没有发生改变,这说明读已提交隔离级别控制了脏读问题,当事务2提交了之后,事务1中张三的余额才会发生变化。
注意:最后每个窗口都要执行以下commit;提交事务。
4 演示Read commited看是否出现 不可重复读问题
![](https://i-blog.csdnimg.cn/blog_migrate/844acad4cf2317b8bd695de1fe880ea6.png)
从步骤中,5和7发现两次查询出现了不一样的结果,发现会出现不可重复的问题
注意:最后每个窗口都要执行以下commit;提交事务。
5 演示Repeatable Read 看是否出现不可重复读问题
![](https://i-blog.csdnimg.cn/blog_migrate/f8626e7c75c00c5fab6b109e1e76c191.png)
注意:从图上发现,当事务2,执行6后,在事务1上第7步的时候发现查询的结果还是一样的,事务2上的步骤8执行之后,事务1上的步骤9后还是结果一样的,只有当步骤10执行之后,查询的数据才会更新账户余额,发现事务隔离级别可重复读确实解决了不可重复读的问题。
注意:最后每个窗口都要执行以下commit;提交事务。
6 演示Repeatable Read 看是否出现幻读问题
![](https://i-blog.csdnimg.cn/blog_migrate/142a028b27dee05d7d7fc2c18dfae76f.png)
从图上看发现事务1中步骤5中没有发现该数据,但是在事务2中执行了步骤6和7之后,在事务1中执行步骤8的时候发现主键冲突,但是在事务1中再次查询id=3的数据发现还是没有数据,此时就出现了幻读问题。
注意:最后每个窗口都要执行以下commit;提交事务。
7 演示Serializable 看是否出现幻读问题
第一步,
![](https://i-blog.csdnimg.cn/blog_migrate/f2ef0f48643fc31820ed97d2416f3ea3.png)
第二步,当事务1中新增数据并提交该事务之后,事务2报主键冲突异常
![](https://i-blog.csdnimg.cn/blog_migrate/dd63646976b68fa72e5d274a0e0f0ecd.png)
注意:该隔离级别发现,不会出现幻读了,因为,当一个事务1中正在执行的时候,另外一个事务2是处于等待阻塞状态的,只有当事务1提交之后,事务2才会继续执行。
5. 存储引擎
5.1 MySQL体系结构
![](https://i-blog.csdnimg.cn/blog_migrate/c5f2619af33a83aee6d2c5e7511d341a.png)
1). 连接层
最上层是一些客户端和链接服务,包含本地sock 通信和大多数基于客户端/服务端工具实现的类似于 TCP/IP的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于SSL的安全链接。服务器也会为安全接入的每个客户端验证它所具有的操作权限。
2). 服务层
第二层架构主要完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化,部分内置函数的执行。所有跨存储引擎的功能也在这一层实现,如 过程、函数等。在该层,服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定表的查询的顺序,是否利用索引等, 最后生成相应的执行操作。如果是select语句,服务器还会查询内部的缓存,如果缓存空间足够大,这样在解决大量读操作的环境中能够很好的提升系统的性能。
3). 引擎层
存储引擎层, 存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API和存储引擎进行通信。不同的存储引擎具有不同的功能,这样我们可以根据自己的需要,来选取合适的存储引擎。数据库中的索引是在存储引擎层实现的。
4). 存储层
数据存储层, 主要是将数据(如: redolog、undolog、数据、索引、二进制日志、错误日志、查询日志、慢查询日志等)存储在文件系统之上,并完成与存储引擎的交互。
和其他数据库相比,MySQL有点与众不同,它的架构可以在多种不同场景中应用并发挥良好作用。主要体现在存储引擎上,插件式的存储引擎架构,将查询处理和其他的系统任务以及数据的存储提取分离。这种架构可以根据业务的需求和实际需要选择合适的存储引擎。
1.2 存储引擎介绍
![](https://i-blog.csdnimg.cn/blog_migrate/e980576af434556cef94a3cc58cd32ec.png)
大家可能没有听说过存储引擎,但是一定听过引擎这个词,引擎就是发动机,是一个机器的核心组件。比如,对于舰载机、直升机、火箭来说,他们都有各自的引擎,是他们最为核心的组件。而我们在选择引擎的时候,需要在合适的场景,选择合适的存储引擎,就像在直升机上,我们不能选择舰载机的引擎一样。
而对于存储引擎,也是一样,他是mysql数据库的核心,我们也需要在合适的场景选择合适的存储引擎。接下来就来介绍一下存储引擎。
存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式 。存储引擎是基于表的,而不是基于库的,所以存储引擎也可被称为表类型。我们可以在创建表的时候,来指定选择的存储引擎,如果没有指定将自动选择默认的存储引擎。mysql5.5版本后默认是innoDB存储引擎。
1). 建表时指定存储引擎
CREATE TABLE 表名(
字段1 字段1类型 [ COMMENT 字段1注释 ] ,
......
字段n 字段n类型 [COMMENT 字段n注释 ]
) ENGINE = INNODB [ COMMENT 表注释 ] ;
2). 查询当前数据库支持的存储引擎
show engines;
示例演示:
A. 查询建表语句 --- 默认存储引擎: InnoDB
show create table account;
![](https://i-blog.csdnimg.cn/blog_migrate/e7458be0dda88a862d29fa3a1b712e6d.png)
我们可以看到,创建表时,即使我们没有指定存储疫情,数据库也会自动选择默认的存储引擎。
B. 查询当前数据库支持的存储引擎
show engines ;
![](https://i-blog.csdnimg.cn/blog_migrate/269a4e89528593f554ed7f3806daca5d.png)
C. 创建表 my_myisam , 并指定MyISAM存储引擎
create table my_myisam(
id int,
name varchar(10)
) engine = MyISAM ;
D. 创建表 my_memory , 指定Memory存储引擎
create table my_memory(
id int,
name varchar(10)
) engine = Memory ;
1.3 存储引擎特点
上面我们介绍了什么是存储引擎,以及如何在建表时如何指定存储引擎,接下来我们就来介绍下来上面重点提到的三种存储引擎 InnoDB、MyISAM、Memory的特点。
1.3.1 InnoDB
1). 介绍
InnoDB是一种兼顾高可靠性和高性能的通用存储引擎,在 MySQL 5.5 之后,InnoDB是默认的 MySQL 存储引擎。
2). 特点
DML操作遵循ACID模型,支持事务;
行级锁,提高并发访问性能;
支持外键FOREIGN KEY约束,保证数据的完整性和正确性;
3). 文件
xxx.ibd:xxx代表的是表名,innoDB引擎的每张表都会对应这样一个表空间文件,存储该表的表结构(frm-早期的 、sdi-新版的)、数据和索引。
参数:innodb_file_per_table
show variables like 'innodb_file_per_table';
![](https://i-blog.csdnimg.cn/blog_migrate/b458a1d4d1458a61ed0e37b93425fda9.png)
如果该参数开启,代表对于InnoDB引擎的表,每一张表都对应一个ibd文件。 我们直接打开MySQL的数据存放目录: C:\ProgramData\MySQL\MySQL Server 8.0\Data , 这个目录下有很多文件夹,不同的文件夹代表不同的数据库,我们直接打开itcast文件夹。
![](https://i-blog.csdnimg.cn/blog_migrate/46732fbd9636b1af95ba70d22037b0c0.png)
可以看到里面有很多的ibd文件,每一个ibd文件就对应一张表,比如:我们有一张表 account,就有这样的一个account.ibd文件,而在这个ibd文件中不仅存放表结构、数据,还会存放该表对应的索引信息。 而该文件是基于二进制存储的,不能直接基于记事本打开,我们可以使用mysql提供的一个指令 ibd2sdi ,通过该指令就可以从ibd文件中提取sdi信息,而sdi数据字典信息中就包含该表的表结构。
![](https://i-blog.csdnimg.cn/blog_migrate/2924d36a2ae0a29f72fb2b3a9b074cb4.png)
4). 逻辑存储结构
![](https://i-blog.csdnimg.cn/blog_migrate/7d1d497dc6d7b485738882245468a78f.png)
表空间 : InnoDB存储引擎逻辑结构的最高层,ibd文件其实就是表空间文件,在表空间中可以包含多个Segment段。
段 : 表空间是由各个段组成的, 常见的段有数据段、索引段、回滚段等。InnoDB中对于段的管理,都是引擎自身完成,不需要人为对其控制,一个段中包含多个区。
区 : 区是表空间的单元结构,每个区的大小为1M。 默认情况下, InnoDB存储引擎页大小为16K, 即一个区中一共有64个连续的页。
页 : 页是组成区的最小单元,页也是InnoDB 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB。为了保证页的连续性,InnoDB 存储引擎每次从磁盘申请 4-5 个区。
行 : InnoDB 存储引擎是面向行的,也就是说数据是按行进行存放的,在每一行中除了定义表时所指定的字段以外,还包含两个隐藏字段(后面会详细介绍)。
1.3.2 MyISAM
1). 介绍
MyISAM是MySQL早期的默认存储引擎。
2). 特点
不支持事务,不支持外键
支持表锁,不支持行锁
访问速度快
3). 文件
xxx.sdi:存储表结构信息
xxx.MYD: 存储数据
xxx.MYI: 存储索引
![](https://i-blog.csdnimg.cn/blog_migrate/6f45efa9634f7dedea2aae1616cae7f0.png)
1.3.3 Memory
1). 介绍
Memory引擎的表数据时存储在内存中的,由于受到硬件问题、或断电问题的影响,只能将这些表作为临时表或缓存使用。
2). 特点
内存存放
hash索引(默认)
3).文件
xxx.sdi:存储表结构信息
1.3.4 区别及特点
特点 | InnoDB | MyISAM | Memory |
存储限制 | 64TB | 有 | 有 |
事务安全 | 支持 | - | - |
锁机制 | 行锁 | 表锁 | 表锁 |
B+tree索引 | 支持 | 支持 | 支持 |
Hash索引 | - | - | 支持 |
全文索引 | 支持(5.6版本之后) | 支持 | - |
空间使用 | 高 | 低 | N/A |
内存使用 | 高 | 低 | 中等 |
批量插入速度 | 低 | 高 | 高 |
支持外键 | 支持 | - | - |
面试题:
InnoDB引擎与MyISAM引擎的区别 ?
①. InnoDB引擎, 支持事务, 而MyISAM不支持。
②. InnoDB引擎, 支持行锁和表锁, 而MyISAM仅支持表锁, 不支持行锁。
③. InnoDB引擎, 支持外键, 而MyISAM是不支持的。
主要是上述三点区别,当然也可以从索引结构、存储限制等方面,更加深入的回答,具体参考如下官方文档:
https://dev.mysql.com/doc/refman/8.0/en/innodb-introduction.html
https://dev.mysql.com/doc/refman/8.0/en/myisam-storage-engine.html
1.4 存储引擎选择
在选择存储引擎时,应该根据应用系统的特点选择合适的存储引擎。对于复杂的应用系统,还可以根据实际情况选择多种存储引擎进行组合。
InnoDB: 是Mysql的默认存储引擎,支持事务、外键。如果应用对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,数据操作除了插入和查询之外,还包含很多的更新、删除操作,那么InnoDB存储引擎是比较合适的选择。
MyISAM : 如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完整性、并发性要求不是很高,那么选择这个存储引擎是非常合适的。
MEMORY:将所有数据保存在内存中,访问速度快,通常用于临时表及缓存。MEMORY的缺陷就是对表的大小有限制,太大的表无法缓存在内存中,而且无法保障数据的安全性。