MySQL基础篇
首先,sql语句的执行顺序是这样的:
from——join——where——group by——having——select——order by——limit
前言:MySQL服务的登录和退出
方式一:通过MySQL自带的客户端,只限于root用户
方式二:通过Windows命令行:
登录:mysql -u用户名 -p密码
退出:exit或Ctrl+C
在介绍MySQL的四种语句之前,先说一说其他SQL语句:
show databases;
#查看都有哪些数据库
desc 表名;
#查看表的结构
或者
show columns from 表名
show tables;
#先选定好数据库,查看这个数据库中有哪些表
1、DQL数据查询语言
1.1 常见函数介绍
1.1.1数学函数
①round()四舍五入
SELECT ROUND(-1.12);
#结果:-1
②ceil()向上取整
SELECT CEIL(2.1);
#结果:3
③floor()向下取整
SELECT floor(1.9);
#结果:1
④truncate(double1,int2)保留几位小数,不是四舍五入,是直接截取
SELECT TRUNCATE(1.9922,1);
#1.9
⑤rand()获取0-1之间的随机数
SELECT RAND();
⑥bin()将10进制的数转化为2进制
SELECT BIN(100);
#1100100
⑦ASCII(str)获取str左侧第一个字符的ascii
SELECT ASCII('dasd');
#100
1.1.2字符串函数(*注:MySQL字符串的索引从1开始,与Java不同 *)
①substr() 有四种重载形式
重载1、substr(str,pos)截取从指定位置pos(postion)之后的所有字符
SELECT substr('asdasfasaf',2);
#结果:sdasfasaf
重载2、substr(str from pos)截取str从pos之后的所以字符
SELECT SUBSTR('asfas' FROM 3);
#fas
重载3、substr(str,pos,len)截取从pos开始后长为len的字符
SELECT SUBSTR('asfasasf',2,3);
#sfa
重载4、substr(str from pos for len)截取str从pos开始后面长为len的字符
SELECT SUBSTR('asfasfas' FROM 2 FOR 3);
#sfa
重载2与1效果一样、4与3效果一样
②rpad(str,len,padstr)、lpad(str,len,padstr)
在str的右边\左边填充padstr至总长度为len
SELECT RPAD('赵展鹏',5,'ab');
#赵展鹏ab
③instr(str1,str2)获取子串str2在父串str1中的起始索引位置
SELECT INSTR('aasfaasga','f');
#4
④replace(str1,str2,str3)在str1中将str2替换成str3
SELECT REPLACE('asdasdad','as','b');
#bdbdad
⑤reverse(str)反转str
SELECT REVERSE('赵展鹏');
#鹏展赵
⑥space(N)返回N个空格
SELECT CONCAT(SPACE(2),'asda',SPACE(2),'asdas');
# asda asdas
⑦trim()去前后空格
SELECT LENGTH(TRIM(' a '));
#1,若不适用trim长度为9
1.1.3日期函数
①now()返回当前的日期和时间
SELECT NOW();
#2022-05-16 00:30:25
②curdare()返回当前的日期
SELECT CURDATE();
#2022-05-16
③curtime()返回当前的时间
SELECT CURTIME();
#00:32:55
④datediff(expr1,expr2)返回expr2与expr1之间的天数差
SELECT DATEDIFF('2022-05-16','2022-02-11');
#94
⑤DAYNAME(date)返回日期date对应的是星期几
SELECT DAYNAME('2022-05-16');
#Monday
⑥DATE_FORMAT(date,format)根据format的格式确定date的形式,format的格式如下:
格式符 | 功能 |
---|---|
%Y | 四位的年份 |
%y | 两位的年份 |
%m | 月份(01,02,03…12) |
%c | 月份(1,2,3…12) |
%d | 日(01,02…) |
%H | 小时(24小时制) |
%h | 小时(12小时制) |
%i | 分钟 |
%s | 秒钟 |
SELECT DATE_FORMAT(NOW(),'%Y-%m-%d-%H:%i:%s');
#2022-05-16-00-48-11
1.2连接查询
连接查询是从两个关系的笛卡尔积中选择符合条件的元组
sql99语法的写法:
select 查询列表
from 表名 【别名】
连接类型 join 表名 【别名】
on 连接条件
【where】
【having】
【group by】
【order by】
分类:
①内连接:inner
等值连接、非等值连接、自连接、
②外连接:
左外:left 【outer】只保留左边的表中的悬浮元组
右外:right 【outer】只保留右边表中的悬浮元组
全外:full 【outer】内连接+表1中有表2中没有的+表2中有表1中没有的
③交叉连接:cross
1.2.1内连接
①等值连接
有三张表:departments(department_id,department_name,manager_id,location_id)
employees(employee_id,manager_id,department_id)
jobs(job_id,job_title)
locations(location_id,city)
#查询员工名、部门名
SELECT last_name,department_name
FROM departments d
INNER JOIN employees e
ON e.`department_id` = d.`department_id`;
#查询名字中包含e的员工名和工种名
SELECT last_name AS '员工名',job_title AS '工种名'
FROM employees e
INNER JOIN jobs j
ON e.`job_id` = j.`job_id`
WHERE last_name LIKE '%e%';
#查询部门个数>3的城市名和部门个数
SELECT city,COUNT(*)
FROM locations l
INNER JOIN departments d
ON l.`location_id` = d.`location_id`
GROUP BY city
HAVING COUNT(*)>3;
#查询哪个部门的部门员工个数>3的部门名和员工个数,并按个数降序
SELECT department_name,COUNT(*) AS '员工个数'
FROM departments d
INNER JOIN employees e
ON d.`department_id` = e.`department_id`
GROUP BY department_name
HAVING COUNT(*)>3
ORDER BY COUNT(*) DESC;
② 非等值连接
#查询每个工资级别的个数
SELECT grade_level,COUNT(*)
FROM job_grades j
JOIN employees e
ON e.`salary` BETWEEN j.`lowest_sal` AND j.`highest_sal`
GROUP BY grade_level;
③自连接
#查询员工的名字和上级的名称
SELECT e.`last_name` '员工名',m.`last_name` '领导名'
FROM employees e
JOIN employees m
ON e. `manager_id`= m.employee_id;
1.2.1外连接
应用场景:用于一个表有而另一个表没有
特点:
1、外连接的查询结果为主表中的所有记录
如果主表中有满足条件的,这时候就是内连接
如果从表中没有满足条件的,显示null
2、左外连接:left左边的是主表
右外连接:right右边的主表
1.3子查询
子查询又叫嵌套查询,在查询结果上可以分为
①标量子查询:结果只有一行一列
②列子查询:结果有一列多行
③行子查询:结果有一行多列
④表子查询
在SQL语句的结构上可以分为
①select后的子查询
②from后的子查询
③where/having后的子查询
④exists后的子查询
1.3.1标量子查询
1.3.2列子查询
#案例1、返回location_id是1400或1700的部门中的所有员工姓名
SELECT last_name
FROM employees
WHERE department_id IN(
SELECT department_id
FROM departments
WHERE location_id IN (1400,1700)
);
外层查询的IN不能用=代替,=只能用于比较1对1的关系,而子查询结果是一列多行
1.3.3行子查询
1.4带有EXIST的子查询
当exists内层查询结果非空时,外层的where子句返回真值,执行sql语句。
exists的子查询一般选择*,因为子查询不返回数据,只返回真假值
1.5相关子查询
基本特点:执行依赖于外部查询。多数情况下是子查询的WHERE子句中引用了外部查询的表。
执行过程:
(1)从外层查询中取出一个元组,将元组相关列的值传给内层查询。
(2)执行内层查询,得到子查询操作的值。
(3)外查询根据子查询返回的结果或结果集得到满足条件的行。
(4)然后外层查询取出下一个元组重复做步骤1-3,直到外层的元组全部处理完毕。
因此,相关子查询时一个类似于循环的过程。
现在要找出每个学生超过他所有选修课程平均成绩的课程号:
select Sno, Cno from score x where degree >= (select avg(degree)
from score y
where x.Sno = y.Sno)
内层查询是求一个学生所有选修课程平均成绩的,至于是哪个学生的平均成绩要看参数x.sno的值,而该值是与父查询相关的,因此这类内层查询条件依赖外层查询称为相关子查询。
2、DDL数据库定义语言
2.1库的管理
2.1.1库的创建(create)
CREATE DATABASE IF NOT EXISTS books;
2.1.2库的修改(alter)
ALTER DATABASE books CHARACTER SET gbk;
2.1.3库的删除(drop)
DROP DATABASE IF EXISTS books;
2.2表的管理
2.2.1.1表的创建
一般格式:
create table 表名(
字段名1 类型 约束,
字段名2 类型 约束,
…
);
CREATE TABLE
2.2.1.2约束
2.2.1.2.1约束的类型
创建表时会在字段上添加相应的约束,Mysql有六大常见的约束
①NOT NULL 非空约束
②PRIMARY KEY 主键约束
一个表中主键唯一且非空,但是主键的某个属性可以不唯一
③UNIQUE 唯一约束
④CHECK检查约束
⑤FOREIGN KEY 外键约束
(①只支持表级约束②外码必须是另一个关系里的一个key)
⑥DEFAULT 默认约束
同时约束分为吧表级约束和列级约束,其中外键约束只能为表级约束,非空约束、默认约束只能列级约束
2.2.1.2.2创建表时添加约束
CREATE TABLE stuinfo(
id INT PRIMARY KEY,#主键
stuName VARCHAR(20) NOT NULL,#非空约束
seat INT UNIQUE,#唯一约束
age INT DEFAULT 18,#默认约束
majorID INT,
FOREIGN KEY (majorID)REFERENCES major(id)#表级约束
);
2.2.1.2.3修改表时添加约束
列级约束:ALTER TABLE 表名 MODIFY COLUMN 列名 类型 约束类型
表级约束:ALTER TABLE 表名 ADD 约束(列名)
2.2.1.2.3修改表时删除约束
①删除默认或非空约束:
alter table 表名 modify column 列名 类型;
②删除唯一约束:
alter table 表名 drop index 列名;
③删除主键约束:
alter table 表名 drop primary key;
2.2.2表的修改
都是在alter table 表名 的基础上+
添加列:add column 列名
删除列:drop column 列名
改表名:rename to 新表名
改列名:change column 旧列名 新列名 类型
改列类型:modify column 列名 新类型
2.2.3表的删除
DROP table IF EXISTS books;
3、DML数据库操作语言
4、TCL事务控制语言
4.1事务的概念及属性
4.2事务的创建
4.3事务的隔离级别
如果同时运行多个事务,且这些事务访问的是同一个数据库的数据,如果没有必要的隔离机制,就会导致各种并发问题:
脏读:对于两个事务T1 T2,T1已经读取了T2更新但还没有提交的字段,之后若T2回滚,那T1读取的内容就是无效的
不可重复读:若T1读取了一个字段,然后T2更新了这个字段,当T1再次读取这个字段时,值就不同了
幻读:若T1从一个表中读取了一个字段,然后T2在该表中(插入/删除)了一些新的行,之后T1再读取同一个表,数据与之前的不同了
丢失修改 :若事务T1 T2同时读数据库的同一数据并修改,T2的修改结果破坏了T1的修改结果,导致T1修改的数据丢失
5、其他
5.1存储过程
5.2函数
5.3标识列
5.4视图
5.5变量
5.6流程控制结构
5.7游标
MySQL高级篇
前言:MySQL8 Linux下的安装与卸载
安装
1、先将MySQL8的5个安装包放到opt目录下
2、centos7下检查MySQL依赖
①检查/tmp临时目录权限
chmod -R 777 /tmp
②安装前,检查依赖
rpm -qa| grep libaio
如果存在libaio包,结果如下
rpm -qa|grep net-tools
如果存在net-tools包,结果如下
3、正式安装(按照顺序安装)
①mysql-community-common-8.0.25-1.el7.x86_64.rpm
②mysql-community-client-plugins-8.0.25-1.el7.x86_64.rpm
③mysql-community-libs-8.0.25-1.el7.x86_64.rpm
④mysql-community-client-8.0.25-1.el7.x86_64.rpm
⑤mysql-community-server-8.0.25-1.el7.x86_64.rpm
安装完之后可以查看MySQL版本
4、服务初始化
mysqld --initialize --user=mysql
5、查看root用户登录密码
cat /var/log/mysqld.log
6、查看mysql服务是否已经启动
systemctl status mysqld
7、启动MySQL服务
systemctl start mysqld
8、查看MySQL服务是否开机时自启动
enable表示自启动
如果不是enable可以设置为enable
systemctl enable mysqld.serviec
卸载
一、字符集的修改与底层原理的说明
1、查看默认字符集
show variables like '%character%';
MySQL5.7
MySQL8.0
2、建表、建库语句中可以查看默认使用的字符集
8.0建表、建库语句显示字符集为 utf8mb4
5.7建表、建库语句显示字符集为 latin1
因此对于MySQL5.7而言,其建表默认字符集为latin1,不能向表中插入中文数据
3、修改字符集(MySQL5.7需要,MySQL8.0不需要)
①首先找到Linux下MySQL的配置文件**(/etc/my.cnf)**
vim /etc/my.cnf
将“character_set_sever=utf8” 添加到mysqld下
然后重启服务 systemctl restart mysqld
修改字符集前,已经创建好的库、表 依然使用latin1字符集,还是不可以向表中插入中文数据;
修改字符集后影响的是新创建的数据库;
旧数据库下的建表字符集和数据库的字符集一样,即latin1。
4、更改已有的数据库和表的字符集
alter database dbtest1 character set 'utf8';
alter table emp1 character set 'utf8';
5、各个级别的字符集
数据库共有四个级别的字符集和比较规则
- 服务器级别
- 数据库级别
- 表级别
- 列级别
最高的为服务器级别,最低的为列级别
①默认情况下,高级别的字符集决定低级别的字符集
因此上面我们修改的字符集时character_set_sever=utf8,修改的是最高级别的字符集。
②创建时,各级也可以使用指定的字符集
(比较规则在下面介绍)
- 数据库级别
create database character_set_gbk character set gbk
[collate 比较规则名];
- 表级别
create table 表名 character set 字符集名
[collate 比较规则名]
6、字符集与比较规则
1、uft8(utf8mb3)与utf8mb4
字符集表示一个字符需要1-4个字节,但常用的字符1-3个字节就可以表示;utf8/utf8mb3只能表示1-3个字节的字符,即表示那些常用的字符;而utf8mb4能表示1-4个字节的字符。若存储一些emoji表情,必须使用utf8mb4字符集
2、比较规则
①查看所有的字符集
show charset;
字符集名称、描述、默认比较规则、描述一个字符最多需要几个字节
后缀 | 描述 |
---|---|
_as | 区分重音 |
_ai | 不区分重音 |
_ci | 不区分大小写 |
_cs | 区分大小写 |
所以utf8与utf8mb4的比较规则为不区分大小写、
②常用的操作
- 查看gbk字符集的比较规则
show collation like 'gbk%';
2、查看服务器|数据库的字符集和比较规则
show variables like '%_server';
show variables like '%_database';
结果如下
3、请求到响应过程中字符集的变化
一般情况下,客户端使用的字符集与操作系统一致,Windows为gbk字符集,Linux为utf8字符集
客户端发送的请求经客户端编码后发送给服务器,服务器使用character_set_client字符集解码(其实默认情况下客户端就是使用character_set_client编码的,但有些客户端也会改变编码字符集;服务器始终会认为客户端使用的是character_set_client编码)说的有点啰嗦了…因此客户端使用的字符集要和character_set_client一致;
服务器响应客户端的请求时会使用character_set_result编码,将请求返回给服务器,因此客户端字符集也要和character_set_result一致
总而言之,character_set_client、character_set_connection、character_set_result三者的值设置为一样的最好
7、书写规范
(1)、一般的大小写规则
Windows下sql语句不区分大小写,但在linux下:
- 关键字(select、from、where…)、函数名不区分大小写
- 数据库名、表名、表的别名、变量名区分大小写
- 列名不区分大小写
(2)、linux下大小写规则设置
MySQL5.7:
找到MySQL的配置文件my.cnf,在[mysqld]下加入一行lower_case_table_name=1,然后重启mysql服务
但在MySQL8.0下,不建议修改
二、MySQL的数据目录
1、MySQL8的主要目录结构
查看MySQL8的目录结构
find / -name mysql
1.1 数据库文件路径
其中,
/var/lib/mysql
是MySQL数据文件的存放路径
数据目录: MySQL服务器启动时会到文件系统某个目录下加载一些文件,之后在运行过程中产生的数据也会保存到某些文件中,这个目录就叫数据目录;
数据目录对应一个系统变量datadir,我们可以查看这个变量
show variables like 'datadir';
可以看出,数据目录就是数据库文件存放路径
1.2相关命令目录
/usr/bin/mysql
/usr/sbin
1.3配置文件目录
etc/my.cnf
2、数据库和文件系统的关系
2.1系统数据库
show databases;
-
mysql
核心数据库,存储mysql用户和权限信息,还有一些存储过程、日志信息、时区信息…
eg:
-
information_schema
-
performance_schema
保存mysql运行过程中一些状态信息 -
sys
2.2数据库在文件系统中的表示
- InnoDB存储引擎(默认)
①在MySQL5.5版本下,数据库的数据存放在系统表空间中(ibdata1文件)
数据库只保存表结构信息
②在MySQL5.7下,一张表对应两个数据库文件,分别是 .frm和.ibd
.frm存储表结构,
.ibd存储表数据,又叫独立表空间
.opt文件是什么?
存放本数据库的一些相关配置——字符集、比较规则…
③系统表空间和独立表空间的设置
innodb_file_per_table=0
#0:使用系统表空间
#1:使用独立表空间
④MySQL8下,没有frm结构,表结构和表数据都存放在独立表空间ibd文件中
——何以验证“表结构和表数据都存放在独立表空间ibd文件中”?
——需要解析ibd文件:
在ibd文件存储目录下,执行命令:
ibd2sdi --dump-file=emp1.txt emp1.txt
解析完成之后可以看到生成了一个txt文件,查看这个文件,里面即包含了表结构和表数据
- MyISAM存储引擎(默认)
在MySQL5.7和5.5下,一表对应三个文件:
.frm、.MYD、.MYI
InnoDB存储引擎将数据和索引一起存储在.ibd文件下,而MyISAM将数据和索引分开存储在.MYD和.MYI文件下
在MySQL8.0下
一表对应三个文件:
.sdi、.MYD、.MYI
.sdi就相当于.frm
我们知道视图是一个虚拟表,只存放表结构信息,不存放表数据,因此视图只有一个.frm文件
三、用户与权限管理
1、用户管理
1.1创建用户
首先必须得有创建用户的权限,这里我们使用root用户,也就是超级管理员。创建用户语法如下:
create user '用户名'@'主机名' identified by '密码';
每创建一个用户,mysql.user表下就会多一条用户信息:
1.2修改用户
修改用户修改表user中字段user的值,例如:将用户名zzp1修改为xtczzp1
update user set user='xtczzp1' where user ='zzp1';
修改之后要刷新权限
flush privileges;
新创建的用户权限很小,
1.3删除用户
drop user 用户名;
1.4设置用户密码
1.4.1设置当前用户的密码
用于root用户和普通用户修改自己的密码
写法1:alter user
alter user user() identified by 'new_password';
写法2:set
set 密码 = '密码';
注意:
mysql用户的密码存放在mysql数据库下user表中,字段authentication_string为用户密码
是经过加密处理后再存放的
1.4.2设置其他用户的密码
root用户可以修改其他用户的密码
方式1:ALTER
alter user 用户名 identified by 密码;
方式2:SET
set password for '用户名'='密码'
1.5密码管理
1.5.1密码过期
密码过期后仍可以登录数据库服务器,但无法进行查询了,必须重置新密码才能正常使用
(1)手动设置立马过期
alter user '用户名' password expire;
(2)手动设置指定时间过期1:全局设置
MySQL使用default_password_lifetime系统变量建立全局密码过期策略;
①default_password_lifetime=0表示禁用密码自动过期
②default_password_lifetime=N表示每隔N天,密码自动过期
方式1:
set persist default_password_lifetime=N;
方式2:
在mysql配置文件my.cnf中设置:
[mysqld]
default_password_lifetime=N
(3)手动设置指定时间过期2:单独设置
在创建用户create user和修改用户alter user时可以单独设置密码过期策略
create user '用户名' password expire interval N day;
create user '用户名' password expire never
1.5.2密码重用限制
密码重用限制基于密码更改的数量和密码使用的时间
密码更改的数量 (password_history) 是指新密码不能和最近几个密码一样
密码使用的时(password_reuse_interval)是指新密码不能和指定时间内使用过的密码一样
手动设置密码重用方式1:全局
①SQL语句:
set persist password_histoy=6
#设置新密码不能与最近使用过的6个密码相同
set persist password_reuse_interval=365
#设置新密码不能与最近一年使用的密码相同
②配置文件:
[mysqld]
persist password_histoy=6
1.5.3 密码强度评估
2、权限管理
2.1授予权限
给用户授权有两种方式:
- 直接给用户授权
- 把角色赋予用户给用户授权
初始状态下新用户权限很小
给用户‘zhang3’授予一定权限后:
grant select,insert,update on dbtest1.emp1 to 'zhang3';
给张3赋予全部权限
grant all privileges on *.* to 'zhang3';
注意:
①这里全部权限不包括“授权操作”,如果想要用户zhang3也有授权操作,则需要在授权语句后加‘with grant option’
grant all privileges on *.* to zhang3 with grant option;
②给用户授权后,该用户必须退出重新登录才有效
2.2查看权限
查看自己的权限
show grants;
root用户查看某个用户的权限
show grants for '用户名'@'主机名';
2.3收回权限
revoke 权限1,权限2… on 数据库.表 from 用户名@主机名
2.4权限表
MySQL服务器会根据权限表的内容为用户赋予相应的权限,这些权限表存放在mysql数据库中,分别是:
2.4.1user表(主键:host、user)
显示已经创建的用户的权限
2.4.2db表(主键 host、db、user)
对于指定的数据库,用户有哪些权限
2.4.3tables_priv表
对于指定数据库下的指定表,用户有哪些权限
2.4.4colunms_priv表
与上面同理
2.4.5proc_priv表
2.5访问控制
2.5.1 连接核实阶段
当用户登录mysql服务器后,MySQL服务器会根据用户客户端提供的user、host、authentication_string与user表中的这个三个字段匹配;匹配成功:表示用户登录成功
2.5.2请求核实阶段
当用户连接到服务器后,开始sql语句的具体操作时,服务器会检查该操作用户是否有权限来执行,具体步骤如下:
3、角色管理
MySQL8.0之后引入的功能。角色就是权限的集合
3.1创建角色
create role 'manager'@'%';
3.2给角色授权
grant select,updata on dbtest1.* to 'manager';
3.3查看角色的权限
show grants for 'manager';
3.4回收角色的权限
rovke update on dbtest1.* from 'manager'
3.5删除角色
drop role 'manager';
3.6给用户赋予角色
grant 'role' to 'user';
一个用户可以拥有多个角色
3.7激活角色
MySQL创建完角色之后,默认是没有被激活的,也就是不能使用
set default role all to 'user';
为用户‘user’激活他所拥有的全部角色
激活以后用户需要重新登陆
3.8撤销用户的角色
revoke role from user
四、配置文件使用
五、MySQL逻辑架构
1、逻辑架构
1.1第1层:连接层
- MySQL验证用户提供的账号密码,如果不对会收到一个Access denied for user的错误
- 账号密码通过之后,mysql从权限表中核实用户的权限
1.2第2层:服务层(sql层)
- SQL Interface 接收客户端发送的sql命令,并返回相应的查询结果
- Parser 查询解析器:对sql语句进行语法分析、语义分析,并生成语法树
- Optimizer 查询优化器:确定sql语句的执行路径,生成一个执行计划
- Caches&Buffers 查询缓存组件:
1.3第3层:引擎层
真正的负责MySQL中数据的存储和提取
1.4存储层
数据库的文件存放
linux下数据库文件存放路径为/var/lib/mysql
2、SQL执行流程
客户端连接到MySQL服务器之后执行sql命令时,①看查询缓存,如果sql命令命中查询缓存,则直接返回给客户端查询结果
②没有命中查询缓存时,查询解析器(parser)会对sql语句的词法、语法分析
词法分析就是识别sql语句中每个单词是什么,比如"SELECT departmen_id FROM employees WHERE salary=1"中,SELECT ,FROM ,WHERE 是关键字,departmen_id 是字段名,employees 是表名…这些含义都要经过词法分析得出。
语法分析就是判断sql语句是否符合语法规则
如果sql语句正确,则会生成一个语法树
经过解析器后,服务器就知道这条sql语句具体是干什么的了,但是怎么干还要经过查询优化器
③查询优化器(optimizer) :一条查询语句可以有多种执行方式,优化器的作用就是找到最好的执行方式
优化分为两个阶段:
- 逻辑优化: 等价改变sql语句使sql查询更高效
- 物理优化:通过索引和表连接方式的技术进行优化
优化之后生成一个sql执行计划
3、SQL8执行原理
1.1开启profiling
默认情况下profiling是关闭的,开启后可以让监控sql执行时所使用资源的情况
仅对当前会话有效
1.2 show profiles
show profiles可以显示出当前会话所有sql语句执行消耗的时间
1.3 show profile
show profile for query id可以查看某一次sql语句具体的执行情况
六、存储引擎
存储引擎就是存储数据、建立索引、更新查询数据等技术的实现方式,是基于表的,而不是基于库;存储引擎,也可被称为表类型
1、查看存储引擎
2、设置系统默认的存储引擎
2.1、查看默认存储引擎
2.2、修改默认存储引擎
建表时系统默认使用InnoDB存储引擎
set default_storage_engine=myisam;
或者修改配置文件
default_storage_engine=myisam
修改完之后要重启服务器
systemctl restart mysqld
这样建表时就会使用myisam存储引擎
2.3、设置表的存储引擎
create table emp1(
)engine=myisam;
2.4、修改表的存储引擎
alter table emp1 engine=myisam;
4、引擎介绍
4.1、InnoDB:支持外键的事务存储引擎
- InnoDB可以确保事务能完整提交和回滚
- 如果对表有频繁的更新、删除操作,则使用InnoDB存储引擎
- InnoDB不仅缓存索引而且也缓存真实的数据,因此对内存要求较高;而MyISAM只缓存索引
- 数据文件结构:
.frm存储数据结构(8.0中统一存放在.ibd中)
.ibd独立表空间 存储数据和索引
4.2、MyISAM:非事务存储引擎
MyISAM是5.5之前默认的存储引擎,不支持事务、行级锁、外键,不支持事务因此崩溃后无法恢复;
- 以select、insert为主的应用或者对事务完整性没有要求的应用,访问速度很快
- 对数据统计有额外的常数存储,count(*)效率非常高
- 数据文件结构
.MYD存储数据
.MDI存储索引
.frm存储表结构
4.3、Archive:用于数据存档
- 仅支持插入、查询两种操作
- 拥有很好的压缩机制,使用zlib压缩库,同样的数据量下,Archive表比InnoDB表和MyISAM表都小很多
- 使用Archive创建表时,同时也会创建以表名开头的文件,文件扩展名为.arz
- Archive适合日志和数据采集,插入速度很块——适合存储大量的历史记录的数据
4.4、CSV引擎:可用于数据交换机制
创建CSV表时,服务器会创建一个以表名开头,扩展名为.csv的文件,该文件可以作为MySQL表来处理,也可以使用文本编辑器读取
使用wps打开csv数据文件
注意:使用csv引擎必须设置字段为非空约束
4.5、 Memory引擎:置于内存的表
七、索引的数据结构
1、为什么使用索引
索引是帮助MySQL高效获取数据的数据结构,就好比一本书的目录;建立索引可以减少磁盘I/O的次数,索引是在存储引擎中实现的,不同的存储引擎,索引可能不同(为什么要使用索引??使用索引是使用B+树检索数据,不使用索引怎样检索数据???)
2、索引的优点
- 减少磁盘I/O的次数
- 对于有依赖关系的父表和子表联合查询时,可以提高查询速度
- 建立唯一索引,可以保证数据的唯一性
- 使用分组和排序查询时,可以减少分组和排序的时间,降低cpu消耗
3、索引的缺点
- 创建和维护索引都需要耗费时间
- 索引也需要单独存储在磁盘上
- 索引会使更新表的速度降低——当向表中增加删除数据时,索引也需要动态维护
因此,更新数据之前可以先删除索引,更新完之后再创建索引
4、B+树
需要强调一点的是,索引是一种基于存储引擎的数据结构,不同的存储引擎索引可能不同。下面介绍InnoDB的索引——B+树
1、首先来说,B+树不仅仅存放真实的数据,还有其他的一些信息:
如上图,c1,c2,c3为具体字段的数据;
record_type表示记录的类型,
- 0:数据项
- 1:目录项
- 2:最大项
- 3:最小项
真实的B+树是这样的,它有如下特点:
- 每一个结点就是一个数据页,每一个数据页最多存放16KB数据
- 数据(用户记录)只在叶子结点中存放
- 页内的数据项之间使用单链表链接,同层次的数据页之间使用双向链表链接
- 非叶子结点存放叶子结点的主键最小值和页码信息,非叶子结点也叫做目录项(数据项)
- B+树一般不会超过4层:假设叶子结点每个数据页存放100条用户记录,每个目录页(非叶子结点)存放1000条数据,那么2层的B+树就可以存放1000×100条用户记录;3层的B+树可以存放1000×1000×100条用户记录
把具有这些特征的B+树称之为聚簇索引
B+树可以明显减少服务器与磁盘IO的次数,例如:想要查找c1=20的记录,只需要进行3次IO即可
5、聚簇索引、二级索引
①、聚簇索引
聚簇索引是一种数据的存储方式,“聚簇”表示数据行和相邻的键值聚簇的存储在一起,即“索引即数据、数据即索引”
InnoDB即采用这样的B+树,所以Innodb底层有“.ibd”文件,存放数据和索引。这种聚簇索引不需要用户显示的去创建,因为InnoDB会自动为用户创建聚簇索引
- 优点
由于数据记录是按照主键排好序的,因此对于主键的排序查找和范围查找速度快 - 缺点
基于聚簇索引是按照主键顺序排列并且数据项之间是单链表链接- 插入数据时可能会导致页分裂,为此一般我们都定义主键为自增长列
- 更新主键可能会导致大量的数据移动;为此我们一般定义为主键不可更新
- 限制
- MySQL数据库只有InnoDB支持聚簇索引
- MySQL的表只能有一个聚簇索引,一般是为表的主键建立聚簇索引
- 如果没有定义主键,InnoDB会在非空唯一字段上建立聚簇索引
②、二级索引(辅助索引、非聚簇索引)
聚簇索引只能在搜索条件是主键的值时发挥作用,当搜索条件是非主键字段时,如何做?
——可以在其他字段上建立二级索引
二级索引的数据项与聚簇索引的数据项不同,聚簇索引的数据项存储了完整的数据记录,而二级索引数据项只存储该字段的值和对应主键的值,并不存储完整的用户记录
——如果要是存储完整的数据,那么每在一个字段上建立一个二级索引时就要存储所有的数据,这对磁盘空间的开销是巨大的
当查询
select * from emp1 where c2=2;
先通过二级索引找到c2=20的数据项,同时该数据项也记录了主键c1=20,然后根据c1=20 回表,通过聚簇索引,找到全部的记录;
注意 :聚簇索引是按照主键顺序排列的,因此可以通过二分查找找到符合用户条件的数据
③、联合索引
联合索引是非聚簇索引的一种,就是同时为多个字段建立非聚簇索引
6、InnoDB的B+树索引的注意事项
(1)根页面位置永远不变
-
最开始表中没有数据的时候,根结点页面也没有东西
-
当用户向表中添加数据的时候,首先向根结点页面内添加数据
-
根结点空间用尽时,根结点将记录全部复制到一个新页中
-
然后对这个新页进行页分裂操作
也就是说,可以理解为“添加数据是根结点产生其他子结点的过程”,之前为了方便理解,我们演示B+树生成的过程是“子结点向上抽取生成父结点”
(2)数据页内数据项具有唯一性
如果二级索引的目录项只是记录每个叶子结点的最小值和页号的话,会出现以下问题
如果现在向B+树中插入数据c1=9,c2=1的话,就不能判断是向页4中插入还是页5中插入,原因是目录页的数据项不唯一——存放的页4和页5的c2最小值都是1,因此这个二级索引是不严谨的。严谨的二级索引目录页应该存放每一个叶子结点的最小值和主键值,由于主键唯一,所以目录页的数据项也唯一
小结:①InnoDB表不应该设置过长的字段为主键,因为InnoDB二级索引数据页中不论是数据项还是目录项都需要存放主键值
②非单调字段作为主键也不是一个好主意,B+树是有序的,非单调字段作为主键插入数据时,B+树为了维持有序需要不断调整,因此一般使用自增长列作为主键
7、MyISAM中索引的方案
InnoDB中数据即索引——聚簇索引的B+树中即存储了索引也存储了完整的用户记录(.ibd文件)
MyISAM虽然也采用B+树,但却将索引和数据分开存储,因此MyISAM中没有聚簇索引;索引结构如下图所示
- MyISAM将数据按照用户插入的顺序存放到数据文件中,该文件扩展名为.MYD,这些数据没有按照主键大小排序,因此不能通过二分法查找
- 而MyISAM的索引单独存放在.MYI文件中
- MyISAM的B+树与InnoDB的B+树不同的是:MyISAM的叶子结点只存放索引字段+对应数据记录的地址
- 因此在MyISAM搜索符合条件的数据时,必须要进行一次回表操作
- 上图是为主键col1建立的索引,我们也可以为col2,col3…等其他字段建立索引,不同的是主键col1的值唯一,而col2,col3的值可以重复
八、InnoDB的数据存储结构
1、磁盘与内存交互的基本数据单位:页
页就是B+树中的每一个结点,页与页之间通过双向链表相连,页中的数据记录按照主键值按顺序生成一个单向链表。
问:我们知道,单链表查找增删快,查找慢。如果要查找的数据定位到页c中,而页c中有1000条数据记录,那么该如何查找
答:每个数据页都会为里面的数据生成一个页目录,该页目录用数组实现,当查找页中具体某条数据时,就可以通过二分法查找了
InnoDB将数据存放在页中,页的默认大小为16KB。在InnoDB数据库中,不论读一行数据还是读多行数据,都是将这些数据所在的页加载到内存中,即:页是磁盘与内存交互的基本数据单位
我们可以通过命令查看页的大小
2、页的上层结构
- 区是页的上一层结构,一个区默认存储64个连续的页,即一个区的大小为16*64=1MB
- 段是区的上一层结构,段是数据库的分配单位——不同的数据库对象以不同形式的段存在。比如:创建一张表时会创建一个表段,创建一个索引时会创建一个索引段
- 表空间是段的上一层结构
3、页的内部结构
一个完整的页内部结构如下:
1.1第一部分:文件头(File Header)和文件尾(File Trailer)
-
File Header(38字节)
- File_Page_OFFSET(4字节)
记录页号的信息,一个页号可以唯一定位一个页 - File_Page_TYPE(2字节)
该页的类型
- File_Page_PREV(4字节)和FIL_PAGE_NEXT(4字节)
这两个变量记录着本页的上一页和下一页的页号。我们知道页与页之间通过双向链表链接,在逻辑上是连续的。
- FILE_PAGE_SPACE_OR_CHECKSUN (4字节)
表示当前页面的校验和,校验和是一个数值,在文件头和文件尾都有。一个数据页对应两个相同校验和:文件头的校验和、文件尾的校验和;且每当改变数据页中的数据时,校验和会发生变化,即校验和可以唯一标识一个数据页中的数据
当内存中的数据发生变化需要同步到磁盘上时,先同步文件头,依次向后同步,最后同步文件尾;如果同步到一半发生故障了,那么,文件头的校验和已经改变为新的校验和,而文件尾的校验和还是原来的校验和。因此,可以通过文件头和文件尾的校验和是否相同来判断数据同步是否有问题 - FILE_PAGE_LSN(8字节)
页面被最后修改时对应的日志序列的位置
- File_Page_OFFSET(4字节)
-
File_Trailer(8字节)
- FILE_PAGE_SPACE_OR_CHECKSUM(4字节)
文件尾部的校验和,与文件头的校验和一起校验页的完整性 - FILE_PAGE_LSN(8字节)
与文件头的LSN值一起校验页的完整性
- FILE_PAGE_SPACE_OR_CHECKSUM(4字节)
1.2第二部分:User Records(用户记录)、Free Space(空闲空间)、Infimum+Supremum(最大记录和最小记录)
- User Records(用户记录)
* - Free Space空闲空间
- 添加数据会占用Free Space的空间,存放数据的部分即为User Records。当数据越来越多,Free Space空间被占满了时,也就意味着这个数据页用完了,再添加数据就需要申请新的页
- 添加数据会占用Free Space的空间,存放数据的部分即为User Records。当数据越来越多,Free Space空间被占满了时,也就意味着这个数据页用完了,再添加数据就需要申请新的页
- Infimum+Supremum最小记录和最大记录(26字节)
最大记录和最小记录是指按照主键大小比较,最大记录和最小记录分别由记录头信息的5个字节和8字节的固定写法.这两个记录不是我们用户定义的记录,因此不存放在user records中(可以理解为最大记录和最小记录不存放数据,只是记录user records中真实的最大数据和最小数据的地址)
头信息在下面compact行格式中会讲
1.3第三部分:Page Directory(页目录)、Page Header(页头)
- Page Directory(页目录)
- 为什么要有页目录?
在页中数据以单向链表方式存储,链表增删快但检索数据很慢,页目录可以理解为就是一个数组,因此专门给页做一个页目录,就可以通过二分法的方式检索页内的数据了 - 页目录是如何支持二分法的?
- 1.将所有的记录(不包括已经删除的记录)分成几个组
- 2.第一组只有一个记录:最小记录
- 3.最后一组会有1-8条记录,包含最大记录
- 4.其余的组包含4-8条记录
其中页目录存放每组最后一个记录(也就是每组最大的记录)的地址偏移量,页目录中每个地址偏移量也被称为槽,每个槽指向了每组最后一个记录
- 为什么要有页目录?
- Page Header(页头)
4、Innodb的行格式(记录格式)
1.1指定行格式的语法
查看表的行格式
show table status like '表名'\G;
修改表的行格式
alter table 表名 row_format=compact;
1.2COMPACT行格式
①变长字段长度列表(2字节)
MySQL中支持一些长度可变的字段,例如varchar、text、bolb…,可变字段存储的数据长度并不是固定的,因此在compact行格式中,将这些字段数据占用的字节数存放在开头部位;这些长度值按照列的逆序存放。例如:以上举例的第一行数据在compact行格式中可变字段长度列表为 06 04 08
②NULL值列表(1字节)
Compact会把可以为null或者为null的列表统一标注出来
二进制值为1时表示该列为null
二进制值为0时表示该列不为null
但是null值列表会自动跳过主键列和not null约束的列
例如,上面的举例中第二行数据在compact行格式null值列表中为 110(逆序存储、自动跳过not null字段)
③记录头信息
- delet_mask
该属性标记当前一行记录是否已经被删除
值为0表示没有被删除
值为1表示被删除了
被删除的记录只是被打了一个删除标记,其实并没有被真实从磁盘上删除,因为从磁盘上删除后其他记录需要重新排列,导致新能消耗。所有被删除的记录会形成一个垃圾链表,这个链表占用的空间为可重用空间 - min_rec_mask
- record_type
- 0 用户记录
- 1 非叶子结点的记录
- 2 最小记录
- 3 最大记录
- heap_no
记录当前记录在页中的位置,用户自己的记录heap_no从2开始;MySQL还会默认提供两个虚拟记录分别是“最大记录和最小记录” - n_owned
页目录中每组最后一条记录的头信息会记录该组一共有多少条记录,作为n_owned字段的值 - next_record
该属性就是所谓的"指向下一条记录",它表示从当前记录到下一条记录的地址偏移量.比如:当前记录的next_record值为32,表示从当前记录的地址向后移动32个字节就是下一条记录了
④真实的数据
真实的数据除了用户自己定义的列之外,还有三个隐藏列
- row_id 行id,唯一标识一条记录
我们知道,Innodb会自动为主键列创建聚簇索引,如果没有主键会默认使用unique列创建,但如果unique也没有,就会使用隐藏列row_id创建聚簇索引 - transaction_id 事务id
- roll_pointer 回滚指针
⑤验证compact的行格式
第一行数据:
第一行数据在ibd文件中16进制表示形式为
- 红色03 02 01:可变字段长度列表
- 绿色 00:null值列表
- 粉色 00 00 10 00 2c: 头信息
- 蓝色 00 00 00 2b 68 00: 隐藏列row_id
- 红色 00 00 00 00 06 05: 隐藏列transaction_id
- 紫色 80 00 00 00 32 01 10: 隐藏列roll_pointer
- 黄色61:真实的数据a
- 浅紫色62 62:真实数据bb
- 橙色 62 62 20 20 20 20 20 20 20 20:真实数据bb(由于col3为固定长度10的字段,所以不足的部分自动填充)
- 蓝色 63 63 63:真实数据ccc
1.3Dynamic和Compressed行格式
Dynamic是MySQL8的默认行格式,(Dynamic和Compressed)与Compact不同的是在处理行溢出上
1、行溢出
我们知道,一个页最多存储16KB的数据,也就是16*1024=16384字节数据,但对于一个varchar最多就可以存放65535字节数据,因此可能就会存在一个数据页连一条记录也存放不了的现象
不仅是varchar会存在行溢出的现象,其他可变长度字段blog、text也会存在行溢出
不同的行格式处理行溢出的方式不同
- Compact和Reduntant
存放前768字节的数据和溢出页的地址,可以通过这个地址找到其余的数据,其余的数据进行分页存储
- Compressed和Dynamic
采用完全行溢出方式,在数据页中只存放20字节的溢出页的地址,真实的数据都存放在溢出页中- Compressed与Dynamic不同的是,Compressed会对其数据进行zlib压缩存储
九、索引的创建和设计原则
1.1索引的分类
按照物理实现方式分:聚集索引和非聚集索引
按照功能分:普通索引、单一索引、主键索引、全文索引
按照字段个数分:单列索引、联合索引
- 普通索引
不加任何限制条件,只是为了提交查询效率;可以创建在任何数据类型中 - 唯一索引
- 主键索引
InnoDB中主键索引也叫聚簇索引,InnoDB会自动为表建立主键索引 - 单列索引
在单个字段上建立的索引。可以是唯一索引、主键索引、普通索引… - 联合索引
- 全文索引
1.2索引的创建
建表时创建索引
首先,可以通过"show index from 表"查看指定的表中的索引;索引的创建分为①建表时就创建索引②建表后添加索引两种方式
1、隐式创建
Mysql会自动为主键约束、唯一约束、外键约束创建相应的索引
2、显式创建
- 唯一索引
create table emp1(
字段1 类型,
字段2 类型,
字段3 类型
...
unique index [索引名](字段)
);
- 联合索引
create table emp1(
字段1 类型,
字段2 类型,
字段3 类型
...
unique index [索引名](字段1,字段2...)
);
注意:
①对于联合索引,字段1写在了最前面,因此b+树中先按照字段1升序排列,如果两条记录的字段1的值相同时,再按照字段2的值升序排列…
②索引名可有可无,如果没有索引名,默认使用字段名作为索引名
- 主键索引、外键索引
主键索引和外键索引是通过约束自动添加的
在已有的表上创建索引
create index 索引名 on 表名(字段名);
或者
alter table 表名 add index 索引名(字段名)
1.3索引的删除
上面介绍创建索引时可以用ALTER TABLE 表名 ADD INDEX 索引名(字段) 或者CREATE INDEX 索引名 ON 表名(字段)
同样,删除索引与之对应:①ALTER TABLE 表名 DROP INDEX 索引名②DROP INDEX 索引名
ON 表名
1.4MySQL8索引新特性
1、支持降序索引
MySQL8开始真正的支持降序索引。在之间的版本使用的是升序索引,假设需要将索引字段a降序排列,需要进行反向扫描,这会降低数据库的性能
create table ts1(a int,
b int,
index idx_a_b(a,b desc));
创建联合索引:a升序,b降序
2、支持隐藏索引
1.5索引设计原则
首先,sql语句的执行顺序是这样的:
from——join——where——group by——having——select——order by——limit
1、适合建立索引的情况
①唯一性约束的字段
唯一约束的字段具有唯一性,一旦找到这条数据便可停止检索
②频繁作为where查询条件的字段
③频繁被GROUP BY和ORDER BY的字段
为什么?
我们字段b+的中的单链表是按照顺序存放的;
而GROUP BY和ORDER BY都需要排序,为这些字段排序后GROUP BY和ORDER BY时可以不用再排序了
当查询语句中同时出现GROUP BY和ORDER BY时,要建立联合索引,并且联合索引中字段顺序是先写GROUP BY的字段再写ORDER BY的字段(为什么??)
对于使用order by的查询,最好使用索引覆盖,并且order by要配合limit使用,否则索引会失效(下面排序查询优化有具体说明)
④UPDATE和DELETE的WHERE查询条件字段
当我们进行更新和删除操作的时候,一定是得先找到这条数据,然后才能执行更新或者删除,因此要为UPDATE和DELETE的WHERE查询条件字段建立索引。并且,如果修改的字段是非索引字段,效率会更高,因为更改后不需要维护索引
⑤DISTINCT字段需要创建索引(为什么??)
⑥使用类型小的字段创建索引
(compact行格式中如何体现不同的数据类型??)
类型小就是说该类型的数据取值范围小。比如,能用int不用bigint…
原因
索引也需要占用磁盘空间,数据类型小就说明一个数据页可以存放更多条数据,那这样B+树的层次就可能降低,从而减少磁盘IO次数
⑦使用字符串前缀创建索引
如果要问字符串字段创建索引,并且这个字段的字符串很长,那么
- 索引中存字符串需要很大的磁盘空间
- 字符串比较时也要花费更多的时间
因此我们可以取部分字符串的前缀创建索引,如果两个字符串的前缀相同,那么根据主键值回表聚簇索引从而查询到完整的字符串的值。这样即节约空间又节省字符串的比较时间
在Alibaba《Java开发手册》中明确指出:在varchar字段上建立索引时必须指明索引长度,没有必要为全字段建立索引
alter table employees add index(last_name(5));
或者
create index 索引名 on employees(last_name(5));
⑧区分度高(散列性高)的列适合作为索引
例如:某张表的gender列,易知该列只有两个值:男、女;如果为该字段建立索引,由于存在大量重复数据值,故无法快速定位一条记录,这也就违背了我们建立索引的初衷
⑨使用最频繁的列放在联合索引的左侧
为什么??我们知道,创建联合索引时,谁写在前面,就先按照谁排序。比如s1写在前面,s2在后面,那么创建索引时先按照s1升序,当s1的值相同时再排s2的值。联合索引的好处在于一条sql语句可以同时使用多个索引,且不需要多次创建索引。但是如果你把联合索引当单列索引来使用,那只能当最左边字段的单列索引,而不能当后面字段的单列索引
⑩联合索引优于单列索引
如果需要为多个字段建立索引,那么最好建立联合索引,且使用频繁的列放在联合索引左侧
2、限制索引的数目
一张表中一般索引不超过6个,因为:
- 索引也需要占用磁盘空间
- 当我们INSERT、UPDATE、DELETE数据时,也需要维护索引
- 我们知道优化器会生成一个最好的执行计划,如果索引很多,优化器生成执行计划的时间也会很长
3、不适合创建索引
①在where中使用不到的字段
②数据量小的表中不要创建索引
③大量重复数据的列上不要创建索引
④避免对经常更新表创建过多索引
⑤无序的值不适合作为索引
我们知道索引都是按照升序或者降序排好序的,无序的值作为索引需要频繁的比较,甚至还会页分裂
⑥避免冗余索引
idx_name属于冗余索引了,当只有name字段作为where的筛选条件时,一定会使用联合索引,根本不会使用idx_name索引;而且,当birthday或者phone_number作为where筛选条件时,联合索引也可以起作用
⑦避免重复索引
当某一字段为唯一或者非空约束时,那么不需要再为该字段添加索引了
十、性能分析工具
1、查看系统性能参数
show status like '参数';
有如下参数:
connections:连接mysql服务器的次数
uptime:mysql服务器的上线时间
slow_queries:慢查询次数
Innodb_rows_insert:insert操作插入的次数
Innodb_rows_delete:delete操作的次数
Innodb_rows_update:update操作的次数
Innodb_rows_read:select操作的次数
2、统计sql的查询成本
通过查看当前会话的last_query_cost变量查看查询成本,该成本对应的是sql语句读取的页的数量
- 位置决定效率:如果要读取的数据页就在缓冲池中,效率比从内存、磁盘读取数据页要高很多
- 批量决定效率:如果对磁盘中某一数据页随机读取,效率很低,而如果采用顺序读取的方式,批量读取数据页,效率就会提升 (顺序读取即读取物理位置连续的数据页,随机读取即读取物理位置随机的数据页)
综上:①经常使用的数据要放到缓冲池中②充分利用磁盘的吞吐能力,批量读取数据
3、定位执行慢的sql:慢查询日志
MySQL中存在慢查询日志,用来记录超过时间阈值的sql语句(帮助我们找到执行时间过长的sql语句),该阈值为long_query_time的值,默认值为10s。
默认情况下,MySQL数据库没有开启慢查询日志,如果不需要调优,不需要开启慢查询日志
1、开启慢查询日志(临时)
- 1、开启慢查询日志
默认情况下慢查询日志是没有开启的
set global slow_query_log=‘on’;
注意:要设置全局变量的值,必须加global
开启之后slow_query_log的值变为ON
slow_query_log_file是慢查询日志文件
- 2、设置时间阈值
默认情况下,时间阈值是10s
修改:
注意:变量long_query_time即是全局变量也是会话变量,因此修改时需要全部修改
以上临时修改当重启mysql服务器以后失效
2、开启慢查询日志(永久)
通过修改配置文件的方式永久性修改查询日志
[mysqld]
slow_query_log=ON;
slow_query_log_file= /var/lib/mysql/xtczp1-slow.log
long_query_time=3
3、慢查询日志分析工具:mysqldunpslow
查看mysqldumpslow工具的用法:
mysqldumpslow工具可以定位到具体哪一条sql语句执行耗时
mysqldumpslow的参数说明:
-a:显示具体的字符串或数值,不用S或N代替
-s:排序方式(t:按照查询时间排序;c:按照执行次数排序;r:按照结果行数排序;l:按锁定时间排序)
-t:显示指定数量的结果
使用示例
可结合上述参数进行使用,比如如下使用示例
-
示例1:返回执行次数最高的top3
执行命令:mysqldumpslow -s c -t 5 /var/lib/mysql/xtczp1
-slow.log -
示例2: 返回结果行数最多的top3
执行命令:mysqldumpslow -s r -t 3 /var/lib/mysql/xtczp1
-slow.log -
示例3: 返回执行时间最长的top3
执行命令:mysqldumpslow -s t -t 3 /var/lib/mysql/xtczp1
-slow.log -
示例4: 返回执行时间最长的top3(字符和数字不使用N和S替代)
执行命令:mysqldumpslow -s t -a -t 3 /var/lib/mysql/xtczp1
-slow.log
4、关闭慢查询日志
跟开启一样,有临时和永久两种方式
- 临时
set global slow_query_log=‘off’
关闭慢查询日志之后,重启mysql服务器,就会看到时间阈值long_query_time恢复到了默认值
- 永久
略
5、删除慢查询日志文件
4、查看sql语句执行成本:show profiling
见第五章第三节
5、分析查询语句:EXPLAIN
EXPLAIN可以查看某条sql语句的具体执行计划
下面我们来学习explain中各列的具体作用
1、table
这条sql涉及的表名。涉及几个表,explain就会给出几条记录
2、id
- 每一个select对应一个唯一的id
EXPLAIN SELECT * FROM employees e INNER JOIN departments d ON
e.department_id
=d.department_id
;
这里只有一个查询,所以只有一个id
当有多个select时:
EXPLAIN SELECT * FROM employees WHERE salary >( SELECT salary
FROM employees WHERE last_name = ‘Abel’ );
注意:有时候优化器会将用户写的sql语句优化:将子查询改为连接查询…这样虽然有多个select,但还是只有一个id(下面介绍show warnings时会具体说):
查询优化器可能对涉及子查询的查询语句进行重写,转变为多表查询的操作
EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key2 FROM s2 WHERE
common_field = ‘a’);
- union
union操作是取两表的并集,需要去重,这时,会生成临时表
EXPLAIN SELECT * FROM employees WHERE department_id>90 UNION SELECT *
FROM employees WHERE email LIKE ‘%a%’;
但对于union all来说,不需要去重,也就不会生成临时表
总结:①一个id号码,对应一个查询
②id号码越大,优先级越高,越先执行
3、select_type
一条sql语句可能有多条select语句,每一条select语句都看作一个小的查询,而这个小的查询的类型就是select_type属性的值。
- SIMPLE
不包含union和子查询的select的小查询都是simple - PRIMARY
- 对于union来说,union左边的select是primary;右边的select是union;
UNION需要去重,去重时会生成临时表,这个临时表的select_type=union result
对于union all来说 - 对于子查询来说,外层查询是primary。
外层select是primary,-
如果子查询不能转化为对应的‘semi-join’的形式(多表查询),且该查询是不相关子查询,那么该子查询就为‘SUBQUERY’
-
如果子查询不能转化为对应的‘semi-join’的形式(多表查询),但该子查询是相关子查询,那么该子查询就为‘DEPENDENT SUBQUERY’
注意:'DEPENDENT SUBQUERY’的子查询会被执行多次
-
- 在包含UNION或UNION ALL 的大查询中,如果小查询都依赖于外层查询,那除了最左边的小查询之外,其余的小查询都是DEPENDENT UNION
- 对于union来说,union左边的select是primary;右边的select是union;
explain select * from s1
where key1 in (select key1 from s2 where key1='a' union select key1 from s1 where key1='b')
注意:优化器会重构,将上面的sql变为相关子查询
- DERIVED
对于包含派生表的子查询(from后面的子查询),其select_type=‘DERIVED’ - MATERIALIZED(为什么??)
4、type(★)
我们知道,每一条sql语句都会有一个对应的执行计划,表示MySQL执行查询时的访问方法,其中type的值就是访问方法
- system
表中只有一条数据,并且该表使用的存储引擎的统计数据是精确的,比如MYISAM、MEMORY(统计数据是精确的是指“有额外一个变量记录了这个表中有多少条数据”) - const
当我们根据主键或唯一二级索引列与常数进行等值匹配,对该表的访问方法就是const - eq_ref
在连接查询时,当被驱动表通过主键或者唯一索引列等值匹配的方式进行访问,则对被驱动表的访问方法就是’eq_ref’
explain select * from s1 inner join s2 on s1.id=s2.id;
我们知道,连接查询时驱动表取一条数据与被驱动表连接。因此对驱动表的访问是ALL;由于连接条件是id相等,所以对被驱动表的访问是eq_ref
- ref
当通过普通的二级索引与常量进行等值匹配时,那么对该表的访问方法就是ref
CREATE INDEX m_i ON employees(manager_id);
EXPLAIN SELECT * FROM employees WHERE manager_id=100;
- ref_or_null
当通过普通二级索引列与常量进行等值匹配时,如果该索引列的值可以是null,则对该表的访问方法就是’ref_or_null’
EXPLAIN SELECT * FROM employees
WHERE last_name='asd' OR last_name IS NULL;
- index_merge
某些查询语句下,单列索引可以合并为联合索引
explain select * from s1 where key1 ='a' or key3='a';
但如果将上面的语句or改为and(为什么??)
- range
如果通过索引对某表进行范围查找,那么对该表的访问方法就是’range‘
- unique_subquery
我们知道,优化器可以将in子查询转换为exists子查询。
如果优化器将in子查询转换为exists子查询,并且子查询可以使用到主键进行等值匹配的话,那么子查询的执行计划就是unique_subquery
explain select * from s1
where key2 in
(select id from s2 where s1.key1=s2.key1)
or key3 = 'a';
key2与主键id进行等值匹配
- index
CREATE INDEX d_m_index ON employees(department_id,manager_id);
EXPLAIN SELECT department_id FROM employees WHERE manager_id=205;
虽然筛选条件manager_id在联合索引后面,按理说使用不上这个联合索引,但由于要查询的字段也是联合索引的一部分,那么该联合索引就可以用,且对该表的访问方法为index
注意:要查询的字段必须是联合索引的一部分才可以。 如果上面的查询改为
CREATE INDEX d_m_index ON employees(department_id,manager_id);
EXPLAIN SELECT department_id,last_name FROM employees WHERE manager_id=205;
这个联合索引就用不上了
- ALL
全表遍历
5、possible_keys和key
可能用到的索引和实际使用的索引
一般来说,只要这个字段上有索引,那么就可能用到这个索引;但并不是possible_keys越多越好——因为优化器需要从possible_keys中选择最优的key作为执行计划。
6、key_len(★)
使用到的索引的长度。衡量是否充分利用上了索引,值越大越好,一般来说对联合索引更有参考意义。
CREATE INDEX d_m_index ON employees(department_id,manager_id);
EXPLAIN SELECT last_name FROM employees WHERE department_id=90;
如果将上述查询语句的筛选条件再加上‘AND manager_id=’
CREATE INDEX d_m_index ON employees(department_id,manager_id);
EXPLAIN SELECT last_name FROM employees WHERE department_id=90 AND manager_id=100;
联合索引底层记录的是department_id、manager_id,第一种情况只使用了一个字段的索引,第二种情况使用的是两个字段的索引,更精确
7、rows(★)
预估要读取的记录条数,值越小越好
(对于连接查询,两个表的rows为什么是那些数字??)
8、filtered
某表经过搜索条件过滤后,剩余记录条数与rows的百分比;对于单表查询来说,filtered的值没有什么意义。对于连接查询来说,,驱动表的filtered的值决定了被驱动表要执行的次数(rows*filtered);值越大越好
预估要读取s1表9895条记录,符合where条件的有10%,因此连接时s1表去除989条记录(为什么??sql语句执行顺序不是先join再where吗?)
9、ref
10、Extra(★)
- No table used
没有from子句
select 1;
-
Impossible where
当where判断条件永远为false时,提示Impossible where -
Using where
- 当使用全表扫描(没有索引)执行对某表的扫描时
,并且where子句中有对该表的搜索条件,此时extra的值为Using where - 当使用索引对某表执行查询时,并且where子句中除了有索引列的搜索条件以外,还有其他非索引列
- 当使用全表扫描(没有索引)执行对某表的扫描时
-
No matching min/max row
当查询列表有聚集函数max min时, 但没有符合where子句的查询条件时,会提示No matching min/max row
如果有符合where查询条件的记录,会提示Select table optimized away -
Using index
如果我们where查询条件使用到了某个索引列,而查询列表不需要回表操作,即使用索引覆盖。这种情况下就会提示Using index
注意这种情况:employees上有一个联合索引(department_id,manager_id),当查询列表是department_id,筛选条件是manager_id的时候,这时候会用到这个联合索引,因为查询列表和搜索条件构成了联合索引,MySQL就会遍历整个联合索引来查询查询列表,不需要回表操作 -
Using index condition
select * from s1 where key1>‘z’ and key1 like ‘%b’;
对于这个查询来说,key1>'z'
可以使用到key1的索引,而key1 like '%a'
却使用不到索引。在之前的mysql版本中,会这样执行这个查询语句:
①通过二级找到key1>'z’的key1
②根据符合条件的key1的主键值回表
③在聚簇索引中找符合key1 like '%a'
的记录
上述执行计划可以优化:
①通过二级找到key1>'z’的key1
②先不着急回表,只对符合key1 like '%a'
的记录回表。
这种优化叫做索引条件下推,在查询语句中如果使用了索引下推,就会提示Using index condition
- join buffer(在下面会有详细介绍)
在连接查询时,当被驱动表不能有效利用索引加快访问速度,MySQL一般会为其分配一块名叫join buffer 的内存来加快查询速度 - Not exists
在左外连接查询中,如果查询条件是被驱动表某个列的值为null,而这个列又不允许存储null值,那就会提示not exists - Zero limit
表示不打算从表中读取任何记录
select .........limit 0;
- Using union
在有些情况下,MySQL可能会使用索引合并的方式执行查询,如
select * from s1 where key1=‘a’ or key3 = ‘a’;
key1 key3上都有索引,又只能使用一个索引,所以MySQL会合并这两个索引。这种情况下就会提示Using union
- Using filesort
如果排序字段没有建立着索引,那排序时需要在内存或者磁盘上进行,这种排序方式称为文件排序 - Using temporary
在有些查询语句(比如去重、排序…)中,MySQL可能会借助一些临时表来完成。对于DISTINCT、GROUP BY、UNION等查询语句,如果不能有效利用索引完成查询,MySQL会建立内部的临时表来执行查询
11、Explain小结
①explain不能显示MySQL执行查询时所做的优化工作,只能显示出优化结果
②explain也不能显示触发器、存储过程、或用户自定义的函数对查询的影响
③explain统计的部分信息(rows)时估算的,并非精确值
6、EXPLIAN的进一步使用
1、show warnings
我们知道,有时候查询优化器会重写我们的sql语句。这时候可以在explain之后紧接着使用show warnings查看查询优化器重构之后真正执行的sql语句
例如,对于explain的id字段,子查询可能被重写为连接查询,这时候id就只有一个,可以通过show warnings查看底层真正执行的sql
2、EXPLAIN的三种格式
- 传统格式
- JSON格式
使用格式:在explain和真正的查询语句之间加上format=json
;json格式是输出信息最详尽的一种格式,它包含了一种查询计划成本信息 - TREE格式
7、MySQL监控分析视图 sys_schema
- 索引情况
#查看冗余索引
select * from sys.schema_redundant_indexs
#查看未使用过的索引
select * from sys.schema_unused_indexs;
十一、索引优化与查询优化
索引优化的角度:
- 索引失效、没有充分利用索引——建立索引
- 关联查询太多的join——SQL优化
- 服务器调优和各个参数的配置——调整my.cnf
- 数据过多——分库分表
SQL优化分为物理优化
和逻辑优化
物理优化
是通过索引和表连接方式进行
逻辑优化
是通过SQL语句的等价变换来完成
1、数据准备(MySQL8.0)
1、建表
两个表:学生表50万条数据、班级表1万条数据
mysql> create table class(
-> id int(11) not null auto_increment,
-> className varchar(30) default null,
-> address varchar(40) default null,
-> monitor int null,
-> primary key(id)
-> )engine=innodb auto_increment=1 default charset=utf8; Query OK, 0 rows affected, 2 warnings (1.22 sec)
mysql> create table student(
-> id int(11) not null auto_increment,
-> stuno int not null,
-> name varchar(20) default null,
-> age int(3) default null,
-> classId int(11) default null,
-> primary key(id)
-> )engine=innodb auto_increment=1 default charset=utf8; Query OK, 0 rows affected, 4 warnings (0.14 sec)
2、创建函数
首先要设置允许创建函数
set global log_bin_trust_function_creators=1;
创建产生随机字符串的函数
mysql> delimiter // mysql> create function ran_string(n int) returns
varchar(255)
-> begin
-> declare chars_str varchar(100) default ‘abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ’;
-> declare return_str varchar(255) default ‘’ ;
-> declare i int default 0;
-> while i<n do
-> set return_str =concat(return_str,substring(chars_str,floor(1+rand()*52)) -> set return_str
=concat(return_str,substring(chars_str,floor(1+rand()*52),1));
-> set i =i+1;
-> end while;
-> return return_str;
-> end // Query OK, 0 rows affected (0.72 sec)
产生班级随机编号的函数
mysql> delimiter // mysql> create function rand_num(from_num
int,to_num int) returns int(11)
-> begin
-> declare i int default 0;
-> set i = floor(from_num+rand()*(to_num-from_num));
-> return i;
-> end // Query OK, 0 rows affected, 1 warning (0.11 sec)
往stu表中插入数据的存储过程
mysql> delimiter // mysql> create procedure insert_stu(start
int,max_num int)
-> begin
-> declare i int default 0;
-> set autocommit =0;
-> repeat
-> set i =i+1;
-> insert into student(stuno,name,age,classId) values ((start+i),rand_string(6),rand_num(1,50),rand_num(1,1000));
-> until i =max_num
-> end repeat;
-> commit;
-> end// Query OK, 0 rows affected (0.33 sec)
往class表插入数据的存储过程
mysql> delimiter //
mysql> create procedure insert_class(max_num int)
-> begin
-> declare i int default 0;
-> set autocommit=0;
-> repeat
-> set i =i+1;
-> insert into class(classname,address,monitor) values (rand_string(8),rand_string(10),rand_num(1,100000));
-> until i = max_num
-> end repeat;
-> commit;
-> end //
Query OK, 0 rows affected (0.11 sec)
2、索引查询优化
下面介绍索引失效的情况,在编写SQL语句时要尽量避免
1、全值匹配最好
这里说的全值匹配指的是,查询条件就是联合索引中的全部字段
像上面这种查询条件全值匹配联合索引是最好的。但有些情况下,查询条件只能部分匹配联合索引
注意:如果将and改为or,虽然有联合索引,但也使用不到索引
除非这时候再在classId上加一个单列索引
2、最左前缀原则(where中没有涉及联合索引最左侧字段)
- 联合索引的书写顺序有严格的要求。我们知道,创建联合索引时,谁写在前面,就先按照谁排序。比如s1写在前面,s2在后面,那么创建索引时先按照s1升序,当s1的值相同时再排s2的值。联合索引的好处在于一条sql语句可以同时使用多个索引,且不需要多次创建索引。但是如果你把联合索引当单列索引来使用,那只能当最左边字段的单列索引,而不能当后面字段的单列索引。
- 使用联合索引时,查询条件必须按照索引字段的顺序写,一旦跳过某个字段,改字段后面的字段便不能使用联合索引了
3、主键递增原则
主键上有聚簇索引,最好将主键设置为递增(autoincrement),否则插入数据时底层B+树要进行页分裂
4、筛选条件中(有函数作用于索引字段、某些运算作用于索引字段、类型转换作用于索引字段)这都会导致这个索引失效 。
5、范围条件右边的索引列也会失效
查询条件中classId为范围查找,因此联合索引中classId右边的字段不能使用索引
根据索引长度可知,索引只作用到了classId字段就终止了,可以做如下修改:将进行范围查找的索引字段写在联合索引的最右边
注意:对查询优化器来说,查询列表先写谁都一样,但联合索引要遵循最左前缀原则
因此,实际开发中,金额、日期一般都是范围查询,所以将范围查询的字段放在联合索引最右边
6、不等于(!=、<>)也会使索引失效
7、关于is null和is not null
- 如果字段允许为空,则is null会使用索引,is not null不使用索引
- 如果字段有非空约束,is null和is not null都会使索引失效.因为如果字段为非空,对于is null查询条件来说,这就是一个Impossible where 的查询;对于is not null来说,无异于全表检索。索引,is null 和is not null都会使索引失效。
同样not like也无法使用索引
8、like左模糊查询索引失效
9、OR前后存在非索引列索引失效
从执行成本方面考虑,name上有索引,如果使用这个索引,那需要回表操作,找到name=‘aaa’的记录,由于or是取并集,对于age,还需要全表检索。这样一来执行成本就是回表+全表检索;还不如不使用索引直接进行全表扫描效率高
10、数据库和表使用统一字符集(utf8mb4/utf8mb3)
11、总结
- 在创建联合索引时,尽量将过滤性(区别度高)的字段写在前面
- 在使用联合索引时,where子句中尽量多的包含联合索引中的字段
- 出现范围查询时,把该字段放在索引顺序的最后面
3、关联查询优化
连接查询可以理解为就是嵌套循环:外层取一条数据,跟内层所有数据比较.直到外层表遍历结束
- 对于两表都没有索引的情况
- 左外连接左边的表的驱动表,右边的表是被驱动表
- 右外连接右边的表是驱动表,左边的表是被驱动表
- 内连接中一般数据量小的表是驱动表,数据量大表为被驱动表(为什么??)
- 两表中只有一个表有索引
- 对于左外和右外连接,优化器有权选择驱动表和被驱动表,而不仅仅是 左外连接左边的表的驱动表、右外连接右边的表是驱动表
原因也很简单:驱动表无论如何都要全表扫描,故加不加索引影响不大;当被驱动表没有索引时要全表扫描,有索引时只需要通过索引检索book.card=type.card
的记录即可。因此,有索引的表作为被驱动表 - 对于内连接,同理,有索引的表作为被驱动表
- 对于左外和右外连接,优化器有权选择驱动表和被驱动表,而不仅仅是 左外连接左边的表的驱动表、右外连接右边的表是驱动表
- 两表都有索引
同理于两表都没有索引的情况
因此,在连接查询中,如果两表都没有索引或者都有索引,要用小表驱动大表;如果只有一个表有索引,要用没有索引的表驱动有索引的表
4、JOIN底层原理
一般来说左外连接左边的表是驱动表,右外连接右边的是驱动表。但我们知道,优化器可能会重写我们的sql语句,比如可以将外连接改写为内连接。所以,左外连接右边的表也可能是驱动表。
1、Simple Nested-Loop Join
简单循环嵌套连接。当AB表都没有索引时,AB表的简单循环嵌套连接为:A中读取一条数据,把B加载到内存中,全表遍历B表,找到符合连接条件的B表数据
开销 | SNLJ |
---|---|
驱动表扫描次数 | 1 |
被驱动表扫描次数 | A |
读取的记录数 | A+A*B |
JOIN比较次数 | A*B |
回表读取的记录数 | 0 |
2、Index Nested-Loop Join
索引嵌套循环连接。
Index Nested-Loop Join主要是为减少被驱动表匹配的次数
根据要查询的字段以及索引的类型判断要不要回表
开销 | INLJ |
---|---|
驱动表扫描次数 | 1 |
被驱动表扫描次数 | 0 |
读取的记录数 | A+B(matched) |
回表读取的记录数 | B(matched)(if possible) |
3、Block Nested-Loop Join
索引嵌套循环是最好的情况。但事实情况往往是被驱动表没有索引,
,那就用不上索引嵌套循环了,但Simple Nested-Loop Join成本太高。因此引入Block Nested-Loop Join。
首先需要明确:扫描全表其实就是将全表从磁盘全部加载进内存,但由于内存限制,有时候并不能将一张表全部加载进内存:有时候扫描表前面的记录时,后面的记录还在磁盘上;当扫描到后面的记录时,由于内存限制,前面扫描过的记录得从内存释放掉。因此,对于粗暴的SNLJ算法,需要加载很多次被驱动表,而且还伴随着从内存释放,这里的磁盘IO成本非常高
Block Nested-Loop Join可以减少对被驱动表的访问次数,从而减少磁盘的IO操作。
- SNLJ算法的磁盘IO为什么很大?
- 解释:驱动表每次只拿一条记录与被驱动表匹配。,对于驱动表的每一条记录,都需要进行“进内存、出内存”反复的操作。
BNLJ算法不再是逐条获取驱动表的数据了,而是将驱动表中的一块数据批量加载到join buffer缓冲区中,然后驱动表批量与被驱动表比较。比如,驱动表A有100条记录,每次加载到join buffer的记录为10条,因此,只需要扫描10次被驱动表就可以完成比较。而SNLJ算法需要扫描被驱动表100次。
但BNLJ算法没有解决的是JOIN比较的次数
通过show variables like '%optimizer_switch%'
可以产看block_nested_loop
是默认开启状态
通过show variables like '%join_buffer%'
可以查看join_buffer缓冲区大小默认为256K
连接查询总结:
-
INLJ 优于 BNLJ 优于 SNLJ
-
一、通过减少驱动表循环匹配的次数
- ①.永远让小的结果集驱动大的结果集
- 结果集是什么?
比如100条数据的A表与1000条数据的B表连接,A表并不一定的小的结果集,因为如果B表经过某些筛选之后,只剩50条数据了,那B就是小的结果集 - 什么结果集才算是小的结果集?
小的度量单位:行数每行的大小.
我们知道驱动表可以批量加载进join_buffer中,join_buffer_size大小是固定的,每行数据占内存越小,join_buffer中就可以加载越多的数据,从而减少驱动表循环匹配的次数
- 结果集是什么?
- ②.增加join_buffer_size的大小
- ③.减少驱动表不必要的字段查询。join_buffer中不仅加载了连接条件涉及的字段,还加载了查询列表,如果查询列表很占内存,那join_buffer中就缓存不了几条记录(要拒绝查询列表*)例如:
select t1.b,t2.* from t1 straight_join t2 on t1.b=t2.b where t2.b<=100 推荐
select t1.b,t2.* from t2 straight_join t1 on t1.b=t2.b where t2.b<=100 不推荐 - ①.永远让小的结果集驱动大的结果集
-
二、减少被驱动表循环匹配的次数
- ①为被驱动表连接列添加索引
- ②以上②③④通过join_buffer减少驱动表循环次数,因而也减少了被驱动表的循环次数
4、Hash Join
MySQL8.0.20弃用BNLJ算法,从8.0.18版本开始使用hash join
5、子查询优化
子查询效率不高的原因:
①MySQL需要为子查询建立临时表,然后从临时表中查询符合外层查询条件的记录,查询完毕后,再撤销这些临时表
②子查询生成的临时表不存在索引
因此在MySQL中尽量使用连接查询代替子查询。例如:查询班长的信息
select * from student where student.stuno in( select monitor from
class where monitor is not null);
优化器将子查询改写成了多表连接查询
6、排序优化
在MySQL中支持两种排序方式:index排序和filesort排序
通常情况下,index排序优于filesort排序。要避免出现filesort排序的情况,就需要在排序字段上加索引。下面介绍排序查询中的优化
①、排序查询不limit索引失效
原因:age上有一个二级索引,查询列表是*,因此对于从二级索引找到的每一个age,都需要回表操作来查询到*,因此不如不适用索引直接进行全表遍历
当然,如果索引覆盖的话,尽管不limit,也可以使用到索引
加上limit之后,我只需要进行limit限制次数的回表即可,全部回表,这时候使用索引成本低
关键在于执行成本哪个高哪个低,显然回表操作成本很高
②、order by顺序错误索引失效
联合索引age_classId_stuno_index(age,classId,stuno)
③order by方向反索引失效
联合索引age_classId_stuno_index(age,classId,stuno)
要么都升序都么都降序
④、无过滤,索引失效
两个联合索引
age_classId_stuno_index(age,classId,stuno)
age_classId_name_index (age,classId,name)
explain select * from student where age=45 order by classId;
这时候使用了age_classId_stuno_index(age,classId,stuno)这个联合索引,并且我们发现这个索引只作用到了age字段。原因:筛选条件where age =45肯定是要使用联合索引的,经过筛选之后剩下的数据对于优化器使用索引来说不如直接回表成低,所以联合索引不再作用于classId上
⑤、关于filesort和index排序
一般来说index排序性能要优于filesort。但也有例外
经过筛选条件age=30 and stuno<101000
后,剩下的记录对cpu而言已经很少,直接进行filesort即可
结论:当出现【where范围查找】和【order by、group by时】,首先观察范围查找过滤的数据量,如果where范围查找能过滤掉大部分的数据,剩下的数据通过filesort也无可厚非,这样的话优先把索引加在范围查找的字段上
⑥、GROUP BY优化
- group by使用索引的原则与order by一致
- where的效率要高于having,尽量把筛选条件写在where中
- order by、group by、distinct这些语句比较耗费cpu资源。包含这些查询的语句,where条件过滤后的结果集尽可能保持在1000行以内,否则sql会很慢
⑦、分页查询优化
select * from student limit 2000000,10;
对这条查询语句来说,我需要先排序前200万条数据,然后再取2000000-200010的数据,小耗巨资办小事
优化一、
先在索引上完成排序分页操作,然后根据主键关联回原表查询所需要的其他列内容
select * from student t,(select id from student order by id limit 2000000,10) a where t.id=a.id;
优化二、
如果主键是递增列的话,可以把limit分页操作转换为主键的范围查找
select * from student where id >2000000 limit 10;
7、覆盖索引
索引覆盖是指要查询的列表包含于联合索引(联合索引+主键)之中。
所有的优化都是基于成本考虑的,上面提到的所有的优化都不是绝对的,优化器也会帮我们做出成本低的决策。
上面提到<>会导致索引失效
,这是因为通常情况下,<>会面对大量数据,并且如果查询列表写的不好(如查询*),就会面临大量回表操作,这样使用索引还不如直接进行全表遍历。
然而,如果使用索引覆盖,<>就会使用索引,原因是不需要回表
覆盖索引的利弊:
- 避免回表
- 将随机IO变成顺序IO(二级索引中的数据是连续的或存放在同一个区中的,但回表之后的数据可能就不是连续的了)
- 如果是要建立冗余索引来支持覆盖索引时就需要权衡利弊了
8、索引条件下推(ICP)
索引下推在上面有详细介绍,总结为一句话就是:原本这个字段的索引已经失效了,但出于低成本的考虑,索引会继续向下使用这个索引。
索引条件下推的几种案例:
由以上几个案例可知索引下推的特点:
- 望文生义可知,索引下推是指在联合索引中在原本失效的索引字段上,索引继续向下作用到这个字段。因此,索引下推一般用于联合索引中
- 如果查询条件都能使索引不失效,那么不会使用到索引下推;索引下推一般针对于索引失效的字段(<>、左模糊查询…)索引继续向下作用于这个字段
- 索引下推增加了使用索引检索的次数,从而减少了回表操作。因此对于那些不需要回表(使用聚簇索引或索引覆盖都不需要回表)的查询,不涉及索引下推
9、其他优化策略
①、count(*)与count(具体字段)的效率
- MYISAM统计数据是精确的,count(*)的复杂度为O(1);
- InnodB需要全表扫描,采用循环+计数的方式完成统计,复杂度为O(n)
- 在InnoDB下,如果要使用count(具体字段),尽量采用二级索引而不要采用主键字段。如果这个字段有多个二级索引,系统会自动选择key_len小的二级索引。(count不需要读取数据,只需要统计数据)
②、SELECT(*)
- 无法使用覆盖索引
- MySQL解析语句时,会通过数据字典将 * 按顺序解析为所有的列名
③、LIMIT1
对于全表扫描的sql,如果确定结果只有一条,那么就可以使用limit1,当找到这条数据的时候就不需要继续扫描 了。另外,如果要扫描的字段已经有唯一约束了,就不需要加limit 1了
④、EXISTS和IN
select * from a where cc in (select cc from b);
select * from a where exists(select cc from b where b.cc=a.cc);
10、淘宝主键的设计
1、自增ID
自增id只是站在了数据库角度考虑问题,而没有站在业务角度。自增id除了满足递增、唯一性,剩下的几乎全是问题
-
可靠性不高
存在自增id回溯问题 -
安全性不高
通过ID号,很容易就知道了用户数量的多少 -
性能差
自增id需要在服务器端生成,性能差 -
交互多
-
局部唯一性
自增id是局部唯一的,而不是全局唯一的
2、业务字段做主键
选择卡号
卡号虽然满足主键的条件。但实际情况中,卡号可能为空。如某个人退卡后,这张卡不可能销毁,而是给了其他用户重复使用,这就会导致驴唇不对马嘴的情况。
选择身份证号或电话号
手机号也存在被运营商回收然后给别人使用的情况;
身份证号涉及个人隐私,如果强制要求用户填写身份证号,会赶跑很多客户
3、淘宝的主键设计
淘宝的订单ID=时间+去重字段+用户ID的后6位
4、推荐的主键设计
对于非核心业务
,可以选择自增id
对于核心业务
,主键至少是全局唯一且单调递增的。
可以选择一种UUID作为主键,这里不详细介绍UUID是啥,只需要知道UUID被设计为在空间和时间上全球唯一的数字
除了使用UUID作为主键,还可以自行设计主键。
例如,可以使用“用户的尾号+机房信息”…
十二、数据库设计
超键
:唯一标识元组的属性集。
候选键
:如果超键不包括多余属性,那这个超键就是候选键
主键
:从候选键中选一个作为主键
主属性
:候选键中的某一属性
非主属性
:候选键不包括的属性
外键
:如果属性T不是表1的主键,而是另一个表2的主键,那这个属性就是表1的外键。
1、第一范式
表中每一个字段必须具有原子性
,即每个字段都是不可再拆分
的.
属性的原子性具有主观性,是否要拆分要根据业务需求。如下表:
姓名 | 地址 |
---|---|
地址要不要拆分成具体的省、市、区要根据业务需求 |
2、第二范式
非主属性完全依赖
任何一个候选码。
例如,在这个表中(球员编号,比赛编号,姓名,年龄,比赛时间,比赛场地)
主键为(球员编号,比赛编号),很显然这张表不符合2NF。会存在一下问题:
- 数据冗余:一个球员可以参加m场比赛,这时候比赛时间和比赛场地就是m个重复数据;一场比赛又可以由多名球员参加,比赛编号,比赛时间,比赛场地又是重复的
- 更新异常:如果调整了比赛时间或场地,那需要大量更新数据
- 插入异常:如果现在已经确定好一场比赛了,但还不确定参赛球员,那着时候就没法插入数据
- 删除异常:如果我想删除球员编号,那么该编号的比赛信息也会被删除
3、第三范式
非主属性不能传递依赖
于候选码,即要求非主属性之间完全相互独立
2NF消除的是非主属性对候选码的部份依赖,3NF消除的是非主属性对候选码的传递依赖
范式优缺点:
- 优点:降低数据冗余
- 缺点:可能会降低查询效率.数据冗余越低,查询的时候可能就越需要关联多张表,有时还会使索引失效。
4、反范式化
1、概念
MySQL的三大范式设计会使查询时产生大量连接查询,降低查询效率,有时候我们可以增加冗余字段
进行反范式化设计来提高查询效率
2、反范式设计举例
举例1:在employees表和departments表中,我们经常会遇到这种场景:查询某个员工属于哪个部门。在第三范式下该查询需要连接查询:
select last_name,department_name
from employees e
inner join departments d
on e.department_id=d.department_id
对于这种常见的查询,我们可以直接在employees表中添加一个冗余字段:department_name
举例2:
对于①商品流水表(流水编号,商品编号,数量,价格,金额,交易日期)②商品信息表(商品编号,条码,商品名,规格,售价)
如果我们需要频繁访问某一商品的流水账,那不如直接在商品流水表中添加冗余字段商品名
3、反范式设计缺点
反范式化的缺点:
- 增大了空间开销
- 对于增加的冗余字段,要修改数据必须同时修改。
- 数据量小的时候,反范式设计意义不大
4、反范式设计的应用场景
- 增加的冗余字段不需要频繁修改
- 这个冗余字段在查询的时候不可或缺
5、ER模型
数据库的设计是牵一发而动全身。为了防止设计好数据库之后大型修改,我们在设计数据库之前就要看到数据库全貌
1、ER模型的三要素
- 实体
分为强实体
和弱实体
;强实体是不依赖于其他实体的实体 - 属性
- 联系
- 一对一:人和身份证信息
- 一对多:一个班级有多个学生,但一个学生只属于一个班级
- 多对多:一个供货商可以对多个超市供货,一个超市也可以从多个供货商取货
2、ER模型转化成数据表
转化原则:
- 一个实体转化成一个数据表一个多对多的关系,也转化成一个数据表
- 一对一或者一对多的关系,通常用外键表示
比如(用户—1—发表—n—评论)ER模型中,在n端实体中加上用户实体的主键userID来作为外键 - 一个多对多的联系,也转化成一个数据表
比如在(订单—n—订单详情—n—商品)ER模型中,订单和商品属于多对多的联系。那这样需要把联系订单详情
转化成一个实体,这个实体包括订单和商品的主键
6、数据表的设计原则
- 数据表越少越好,联系越简单越好
- 表的字段越少越好
- 联合主键的字段数越少越好
因为无论是聚簇索引还是二级索引,都需要存储主键
7、PowerDesigner的使用
十三、数据库调优其他策略
数据库调优目标:
*
十四、事务
1、事务的基本概念
事务是用户定义的一个数据库操作序列,使数据从一个状态变到另一个状态。这些操作要么全做,要么全不做,是一个不可分割的工作单位
只有Innoddb是支持事务的
2、事务的ACID特性
①原子性(atomicity)
指的是事务是一个不可分割的工作单位,要么全部提交,要么全部回滚
②一致性(consistency)
指的是事务执行的结果必须使数据库从一个一致性状态变到另一个一致性状态。一致性状态可以理解为符合语义的状态
或理解为符合预定约束的状态
比如,从a账号转账100元到b账户,a账户要-100,b账户+100,但如果由于某种原因,导致b账户没有+100。那这个操作之后,数据库就处于不一致的状态。
再比如,如果将数据库某张表姓名字段加唯一约束,如果一个事务提交或回滚之后姓名不唯一了,那执行这个操作之后数据也就处于不一致状态
③隔离性(isolation)
隔离性是指一个事务的执行不能被其他并发执行的事务干扰。
④持久性(durability)
一个事务一旦提交,它对数据库的改变就是永久的、不可撤销的。
3、事务的状态
①活动的
一个事务正在
对数据执行操作
②部分提交的
首先明确:事务的操作首先是在内存中进行的,当执行完所有操作之后需要刷盘。
部分提交状态指事务执行完最后一个操作时,还没有刷盘,就说该事务处于部分提交状态
③提交的
当处于部分提交状态的事务将操作刷盘后,该事务就处于提交状态了
④失败的
当处于活动的或部分提交的事务,如果遇到了某些错误而无法继续执行时,该事务就会处于失败状态
⑤终止的
处于失败状态的数据库,为了保证一致性
,需要撤销之前对数据库的操作,即回滚
。回滚之后的事务,就处于终止状态
4、显式事务与隐式事务
一个完整的事务由以下操作构成:
开启事务->DML操作->结束事务(commit或rollback)
-
显式事务
- 开启事务使用:
start tansaction
或begin
。
二者的区别在于start tanscation后可以跟read only(只读)或read write(读写)或with consistent snapshot(一致性读) - 保存点:savepoint
在一个事务中可以设置多个保存点,事务回滚时不一定非要回到最初状态,也可以回到设置的保存点处;回到保存带你处以后再决定是commit还是rollback
savepoint 保存点名称
- 开启事务使用:
-
隐式事务
如果我们不使用显示事务,那么一条SQL语句即为一个事务,语句执行完之后会自动提交。
默认状态下autocommit是打开的
可以把autocommit值设置为false,然后尽管是隐士事务,也不会自动提交了,必须手动提交事务(只针对DML有效,对DDL无效)
另外,在autocommit为true的状态下,使用start transaction或begin开启事务,那DML操作就不会自动提交
5、强制隐式提交
对于隐式提交事务,如果设置了autocommit为false,便不再自动提交。这是主要针对DML语句的,下面这几种情况,即使设置了autocommit为false,系统也会自动提交事务
①数据库定义语言DDL
数据库对象(库、表、视图、存储过程)的create alter drop
②有关用户与权限的操作
③事务控制或锁定的语句
如果开启一个事务start tansaction,执行完SQL后又开启了另一个事务,那上面的事务也会自动提交
④
6、事务演示
事务的分类:
-
扁平事务
是最简单和最常用的一种,由begin开始 commit结束 -
带有保存点的扁平事务
-
链事务
一个链事务由多个子事务链式组成,在提交一个事务时不需要释放数据库对象,
-
嵌套事务
-
分布式事务
①保存点savepoint
现有表
开启事务并更新数据
START TRANSACTION;
UPDATE affair SET money=money-100 WHERE NAME = '赵占平';
这时设置一个保存点s1,然后再更新数据
SAVEPOINT s1;
UPDATE affair SET money=money-100 WHERE NAME = '赵占平';
然后让事务回滚到s1保存点处
注意:①回滚到保存点并没有终止事务,回滚到保存点以后还需要提交或者回滚事务才算终止;②保存点的设置与执行在物理和逻辑上应是一致的,即:SQL语句的执行顺序和书写顺序应保持一样
②提交与回滚
- 案例1
先执行一个事务
BEGIN;
INSERT INTO aaa VALUE('zzp');
COMMIT;
再执行一个事务
BEGIN;
INSERT INTO aaa VALUE('zzp1');
INSERT INTO aaa VALUE('zzp1');
COMMIT;
注意:由于这些事务都是显式开启的,因此不涉及autocommit
由于第二个事务会执行失败,执行回滚后,事务会回到最后一次commit的状态。即,表中数据只有zzp1
- 案例2:
先执行一个事务
BEGIN;
INSERT INTO aaa VALUE('zzp');
COMMIT;
再执行这两个操作语句:
INSERT INTO aaa VALUE('zzp1');
INSERT INTO aaa VALUE('zzp1');
显然第二条语句会报错。如果此时回滚,表中数据会有zzp和zzp1.因为第二个事务INSERT INTO aaa VALUE('zzp1');
不是显式开启的,默认会自动提交。因此回滚后会回到这里
- 案例3:
先执行一个事务
BEGIN;
INSERT INTO aaa VALUE('zzp');
COMMIT;
再执行这两个操作语句:
set @completion_type=1;#开启链式事务
INSERT INTO aaa VALUE('zzp1');
INSERT INTO aaa VALUE('zzp1');
默认情况下,显式事务需要begin或start transaction开启,如果没有begin或start transaction,就默认是隐士事务(如案例2);但开启链式事务后(set @completion_type=1),即使第二个事务不写begin或start transaction(第一个事务得写),也是显示的事务
③Innodb和Myisam
MyIsam不支持事务,也就是可以理解为每一条sql语句都是隐式事务,不需要开启,也不需要提交和回滚
7、事务的隔离级别
1、数据库并发问题
①脏写
两个事务A B,事务A修改了另一个事务B修改但还未提交的数据
②脏读
两个事务A B,事务A读取了,事务B更新但还没提交的数据
③不可重复读
两个事务A B,当事务A读取了一个字段,事务B更新了这个字段的值,事务A再读取这个字段时,前后两次值不一样了
④幻读
两个事务A B,事务A通过某筛选条件读取了一些数据,然后事务B又插入了一些数据,此时A再通过相同筛选条件读取数据时,读到的数据就不一样了
2、数据库四种隔离级别
数据库四种隔离级别,按照问题的严重性排序:脏写>脏读>不可重复读>幻读。数据库的隔离级别越高,并发性越低。要兼顾隔离级别和并发性
这四种隔离级别都解决了脏写。REPEATABLE READ是MySQL默认隔离级别,Oracle只支持READ COMMITED和SERIALIZABLE,READ COMMITED是Oracle默认隔离级别
3、查看MySQL的隔离级别
show variables like 'transaction_isolation'
还可以设置MySQL的隔离级别:
set [global|session] transation isolation level 隔离级别
隔离级别就是上述四种
global和session都只在内存中生效,当重启MySQL服务器以后,就失效了。session只对当前会话的后续事务有效,global对当前会话之后的所有会话有效
4、隔离级别的演示
表在这
①脏读演示
先将事务的隔离级别改为read uncommitted
显示开启一个事务,将张三改为50,不提交
在另一个事务中查看表数据,读出了脏数据
②不可重复读演示演示
先将事务的隔离级别改为read committed
开启一个事务a,读取张三的账户为100
然后另一个事务b修改张三的账户为50
然后事务a再读取张三的账户,就变了50,产生了不可重复读现象
③幻读演示
先设置隔离级别为可重复读
开启一个事务a,查询账户为50的用户信息
在事务b中插入一行新的数据
事务a再次查询账户为50的用户信息时仍然只要张三
十五,MySQL事务日志
我们知道,事务具有A(原子性)C(一致性)I(隔离性)D(持久性)。
- 事务的隔离性是通过锁机制实现的
- 一致性、原子性、持久性是通过redo日志为undo日志实现的。
- redo日志又叫重做日志,redo日志是存储引擎层生成的日志,记录的是物理级别的修改操作,比如哪一个数据页、偏移量…进行了…修改操作
- undo日志又叫回滚日志,undo日志也是存储引擎层生成的日志,记录的是逻辑级别的操作,比如一条insert语句在undo日志中对应的记录为delete语句
1、redo日志
Innodb中数据页是基本数据单位,访问一个数据页时,要先把这个数据页加载到内存的buffer pool中,然后对这个数据页中所有的操作都在内存中执行,内存中的脏数据(脏数据就是内存中改了但还没刷新到磁盘上的数据)会以一定频率刷新到磁盘上,只有刷新到磁盘上时,数据库的一致性才能保证。
1.1为什么需要redo日志
如果一个事务已经提交,内存中数据已经修改,在刷盘之前如果服务器宕机,就无法再刷盘了。破坏了事务的一致性。
一个简单的解决方案是:在事务提交之前把所有的修改都刷新到磁盘上。这样粗暴的方法有很大弊端:
如果我只修改了很小数据量的数据,那我刷盘时也需要将整个数据页加载到内存,这样就小题大做了
因此,我们提供使用redo日志解决问题:把对数据库的操作先写入redo日志中。Innodb采用的时WAL技术,即先写日志再刷盘。只有日志写入成功时,事务才算提交成功
1.2 redo日志的优点
- 降低了刷盘频率
- redo日志占用空间非常小
存储表空间ID,页号,偏移量所需的存储空间很小,刷盘很快
1.3 redo日志的特点
- redo日志是顺序写入磁盘的,是顺序IO,效率比随机IO快
- 事务执行过程中,redo日志不断更新
有一个操作redo就会记录一个操作
1.4 redo日志的组成和执行流程
redo日志由两部分组成:
redo log buffer(重做日志缓冲)
保存在内存中,MySQL服务器启动时就会向系统申请一片连续的重做日志缓冲空间。可以通过innodb_log_buffer_size
查看缓冲的大小
redo log file(重做日志文件)
保存在磁盘中,可以通过查看MySQL数据文件(var/lib/mysql)看到两个redo重做日志文件:ib_logfile0
、ib_logfile1
redo日志的执行流程:
对于一条sql语句操作,①首先把磁盘中的数据加载到内存中 ②然后在内存中执行sql操作并在redo log buffer中记录相应的的修改 ③事务commit后,把redo log buffer的内容写入redo log file ④将内存中的数据以一定频率刷新到磁盘中
注意:commit前,更新redo log buffer;commit后根据redo log buffer更新redo log file
1.5 redo日志的刷盘策略
我们知道redo日志有两个文件 redo_log_buffer 和 redo_log_file,前者是内存中的,后者是磁盘中的。
sql操作首先在redo_log_buffer中记录,然后再刷盘到redo_log_file中。我们关心的是刷盘何时进行,毕竟只有刷盘到redo_log_file中,事务的持久性才能保证。
注意:刷盘不是redo_log_buffer直接刷到redo_log_file中,而是redo_log_buffer先刷到文件系统缓存(page cache)。
中,InnoDB后台有一个线程:每隔1秒就会把redo log buffer中的内容写入文件缓存系统(page cache)中。
当事务commit后,Innodb支持三种刷盘策略,可以通过innodb_flush_log_at_trx_commit
参数值来确定(这个参数值只会影响redo log buffer ->page cache这一过程,redo log buffer是在执行sql时实时记录的):
-
innodb_flush_log_at_trx_commit =0
:每次事务提交时不进行刷盘操作,即:redo log buffer->page cache->redo log file完全由后台进程决定
-
innodb_flush_log_at_trx_commit =1
:默认值。每次事务提交时同步进行刷盘操作(redo log buffer->page cache->redo log file)
这种情况是最安全的,但性能是最差的 -
innodb_flush_log_at_trx_commit =2
:每次事务提交时只把redo log buffer中的内容写入page cache中。至于page cache何时写入redo log file由os决定
此时mysql服务器宕机也不影响,但os宕机会有影响
1.6 redo log file
1、相关参数
innodb_log_group_home_dir
:redo log文件所在的路径。默认值为./
,意思是redo 日志默认存放在数据库数据目录下(var/lib/mysql)
,默认情况下有两个 redo log file文件:ib_logfile0
、ib_logfile1
。innodb_log_files_in_group
:redo log file文件的个数,默认情况下有两个。- innodb_flush_log_at_trx_commit:刷盘策略
Innodb_log_file_size
:单个redo log file文件的大小。默认值为48MB
2、日志文件组
innodb的日志文件redo log file有多个,构成日志文件组:ib_logfile0、ib_logfile1、ib_logfile2…写入日志文件时先从ib_logfile0开始写入,这个日志文件写满了就写下一个日志文件,依次类推。当最后一个日志文件也写满时,最开始写入的日志文件会被覆盖。
3、checkpoint与write pos
- write pos:write pos扫过的地方表示已经写入redo日志
- checkpoint:checkpoint扫过的地方表示内存的脏数据已经刷新到磁盘
如果进行redo日志文件中数据的覆盖?checkpoint扫过的地方都可以覆盖
十六、锁
1、MySQL读写并发问题
再来回顾一下数据库的并发问题:
- 脏写:两个事务都在写,事务a修改了事务b修改但未提交的事务
- 脏读:两个事务一读一写,事务a读取了事务b修改但未提交的事务
- 不可重复读:两个事务一读一写,事务a读取了一个记录,事务b修改了这个记录,事务a再读这个记录时前后两次结果不一样了
- 幻读:两个事务一读一写:事务a读了一些记录,事务b插入了一些记录,事务a再读时就多了一些记录。
接下来我们讨论MySQL4中隔离级别是如何解决4大并发问题的。
并发事务访问相同的记录可以分为以下三种情况
①读-读
并发事务之间没有什么影响,不会产生并发问题
②写-写(脏写的解决方案)
可能会存在脏写问题。任何一种隔离级别都不允许脏写发生,原因是多个事务并发访问同一记录时需要排队执行
——把记录和锁相关联,而这个锁是事务生成的,因此锁的记录,可以被锁生成的事务写操作,其他事务需要排队等待执行
事务T1提交后,就会释放锁,以便让其他事务操作记录
③读-写或写读(其他并发问题的解决方案)
这种情况下可能存在 脏读、不可重复读、幻读。如何解决?
方案一:读使用MVCC,写加锁
对于写操作
,也就是增删改的操作,必须是对数据库的最新版本进行操作。对于读操作
;读操作
的是生成的一个ReadView
,通过ReadView找到符合条件的历史版本;查询语句只能读到Read View之前已提交的事务所做的更改
普通的读操作在READ COMMITTED和REPEATABLE READ隔离级别下会使用到MVCC读取记录
- 在READ COMMITTED隔离级别下:一个事务每次执行的select都会生成一个Read View,这样可以保证读到的数据都是已经提交的数据,这只解决脏读,还存在不可重复读问题
- 在REPEATABLE READ隔离级别下:一个事务在执行过程中只有第一次的select操作才会生成一个ReadView,这样即使其他事务更新(不可重复度)或插入(幻读)了一些数据,我也读不到,解决了不可重复读和幻读问题
对于MVCC,读历史版本记录和改记录的最新版本并不冲突,可以同时进行,提高了数据库的性能
方案二:读写都加锁
2、锁的不同分类
①按照对数据的操作类型分:共享锁、排他锁
我们知道,并发事务的读读操作并不会引起并发问题,并发写-写和读-写会引起一些并发问题;我们的解决方案是使用MVCC
或者加锁
。MVCC下章具体介绍。因此我们这里重点介绍使用加锁
的方式解决数据库并发问题。
- 共享锁(shared lock)也叫读锁、S锁,可以在读操作上加这个锁,不能在写操作加共享锁(写操作一定是排他锁)
- 排他锁(exclusive lock)也叫写锁、X锁,既可以在写操作上加也可以在读操作上加。
读操作
读操作排他锁和共享锁都可以加
#读操作加共享锁
select.... lock in share mode
#或者
select....for share#(8.0新特性)
#读操作加排他锁
select.... for update
Mysql8新特性:在5.7版本之前,如果事务获取不到锁就会一直等待直到innodb_lock_wait_timeout
超时。在8.0中,可以在for share或for update后加nowait
、skip lock
跳过等待或者跳过锁定的数据
读操作加锁是针对具体数据而言的,即:读哪些数据为哪些数据加锁
写操作
写操作只能加排他锁。写操作即为 INSERT、DELETE、UPDATE
- INSERT:通过一种
隐式锁
的结构来保护插入这条记录在事务提交之前不能被其他事务访问 - DELETE:定位B+树中这条记录的位置,然后给这条记录加排他锁,再执行删除操作。可以把定位待删除的记录堪称
加X锁的读操作
- UPDATE:分三种情况
- ①没有修改主键值,且被更新的列所占的存储空间前后未变化
先在B+树中定位这条记录,然后为这条记录加X锁,最后在原位置进行修改。可以把定位待修改的记录堪称加X锁的读操作
- ②没有修改键值,但更新的字段所占的存储空间变了。先在B+树中定位这条记录,然后为这条记录加X锁,把该记录彻底删掉,然后插入一条新记录。定位待修改的记录可以看成
加X锁的读操作
,然后插入新记录有INSERT
操作提供隐式锁
来保护 - ③修改主键值
先DELETE旧记录再INSERT新记录。加锁方式按照DELETER和INSERT规则进行
- ①没有修改主键值,且被更新的列所占的存储空间前后未变化
②从数据操作的粒度分:表级锁、页级锁、行锁
锁的粒度大,如表级锁,会影响数据库的高并发;锁的粒度小,如行级锁,但管理锁需要耗费资源,数据库的性能会收到影响
1、表锁
表锁是MySQL最基本的锁策略,不依赖于存储引擎,并且是开销最小的策略。表锁会锁定整张表,并发率受到影响。
①表锁之X锁、S锁
对于增删改查语句,InnoDB不会自动为表添加表锁。对于MYISAM存储引擎,select操作之前会给涉及的表加读锁,在增删改之前会给涉及的表加写锁;
一般情况下,不会使用Innodb提供的表级别的S锁和X锁,但用户可以手动设置表的X锁和S锁:
lock tables t read;
lock tables t write;
但要尽量避免使用表级别的X锁和S锁,因为Innodb支持更厉害的行级锁
②表锁之意向锁(intention lock)
Innodb是支持行级锁的,如果我们给某行数据加上了排他锁,数据库会给更大一级的空间,比如数据页或数据表,自动加一个意向排他锁,告诉其他事务这个数据页或数据表已经事务加排他锁了,当其他事务再想获取排他锁时,就会直接处于阻塞状态,而不会去筛查每一行数据,看看有没有锁
- 如果事务想获取某些记录的共享锁,就需要在数据表上添加意向共享锁
- 如果事务想获取某些记录的排他锁,就需要在数据表上添加意向排他锁
注意:意向锁是由存储引擎自动维护的,是用户在添加了某些记录的排他锁或共享锁之后自动添加的,不需要用户手动添加意向锁
需要注意的是:意向锁之间是相互兼容的;不同的事务可以为不同的记录行添加锁,此时数据表会有很多意向锁,意向锁之间的兼容的。
这里的排他锁和共享锁是表级。
在这里插入代码片
总之:
- 意向锁是一种不与行级锁冲突的表级锁
- 意向锁的存在表明Innodb支持多粒度锁并存
- 意向锁暗示某个事物正在某些行持有锁
2、Innodb行锁
①记录锁
记录锁也为分为X记录锁和S记录锁,对于增删改操作,要操作哪些记录,就会为哪些记录自动添加记录锁。
② 间隙锁
Innodb在REAPETABLE READ
隔离级别下是可以解决幻读
问题的,可以采用MVCC
或者加锁
的方式.
对于加锁方式而言,这个锁就是间隙锁
,例如,对于一张表
③临键锁
兼具记录锁与间隙锁:既能锁住一条记录又能锁住一个区间的记录。临建锁是在innodb存储引擎下、repeatable read隔离级别下的一种锁。下面来演示临建锁。
开启一个事务,并为[1,5)加X锁(这就是一种临建锁)
开启另一个事务
这个事务只能修改5的数据而不能修改1的数据了。注意:如果是另一个事务要读取数据的话,是不受限制的,因为读读操作不会引发并发问题
④插入意向锁
3、页锁
页锁的开销介于表锁和行锁之间,会存在死锁的现象,并发性也一般。注意:每个层次的锁的数量都是有限制的,当数量超过了这个限制时,就会进行锁升级,比如,行锁升级为表锁,这样做的好处就是降低了锁占用的空间,同时也降低了并发性
③从对待锁的态度分为:乐观锁和悲观锁
悲观锁和乐观锁是一种设计思想,并不是真正的锁。
悲观锁使用数据库自身的锁机制实现;乐观锁通过程序实现
1、悲观锁
悲观锁总是假设最坏的情况,认为自己每次操作数据时都会有其他事务也来操作数据,从而引发并发问题,因此悲观锁的思想是:每次操作数据时都为这些数据加锁(就是我们上面说的一些锁)。悲观锁是通过数据库的锁机制实现的。
例如:在商品秒杀场景中
两个事务同时减库存,会出现超卖的情况。使用悲观锁的思想解决问题:在线程A查询库存时,就为数据加X锁,在之后的所有操作中(生成订单、减库存),都只允许线程A读写,这样就避免了其他事务的干扰。前提:需要将线程A的sql操作放在一个事务中
注意:select.....for update
操作会把扫描过的行都加上锁,因此必须保证要查找的字段有索引,避免全表扫描
2、乐观锁
乐观锁认为数据库的并发操作不是总会发生,因此,每次操作数据前不给数据加锁。乐观锁的思想是:更新数据的时候判断一下在此期间有无其他人更新这个数据。采用版本号机制
或CAS机制
实现。
- 版本号机制
在表中设计一个版本号字段version
,对数据库操作之前先读取version的值,操作之后更新version的值:update.....set version = version+1 where version =version
,如果更新不成功表示在此期间有事务对数据已经进行了修改,version已经变了。如果更新成功表示在此期间没有其他事务的影响 - CAS机制
④其他锁之:全局锁、死锁
- 全局锁
全局锁就是将整个数据库实例加锁,让整个数据库处于只读状态。应用场景:做数据库的逻辑备份
flush tables with read lock;
- 死锁
两个事务都需要持有对方的锁
举例1:
举例2:
A给B转账100元,同时B给A也转账100元,可能导致死锁
begin;#事务1
update account set balance = balance -100 where name =‘A’;
update account set balance = balance +100 where name =‘B’;
begin;#事务2
update account set balance = balance -100 where name =‘B’;
update account set balance = balance +100 where name =‘A’;
分析死锁:事务1先给A加锁,事务2先给B加锁,然后事务1给B加锁(不会成功,阻塞),事务2给A加锁(不会成功,阻塞),产生了死锁;
死锁产生的条件:两个及以上的事务加锁顺序不一致
。例如案例2中,事务1先给A加锁,事务2先给B加锁,这两个事务加锁顺序不一致,导致了事务1(或事务2)再加锁时,事务2(或事务1)占用资源。如果两个事务都先给A加锁(即不管谁给谁转账,都先操作A账户),再给B加锁,就能避免死锁
如何处理死锁
方式1:等待直到超时
设置时间阈值(innodb_lock_wait_timeout
),处于死锁中的事务超过这个时间
方式2:死锁检测
方式1太被动,innodb提供了wait_for graph
死锁检测算法,每当加锁不成功阻塞时,都会立即触发这个算法
wait_for graph算法依赖于以上两张链表:
T2载row1上有x锁,T1在row2上有s锁,T4在row2上有s锁;这时,T1 T2 T3再去申请锁,那必然需要等待,由此画出wait-for graph图
死锁检测原理:当wait_for graph有向图里产生环,证明存在死锁
一旦检测到死锁,Innodb回滚undo量最小的事务,让其他事务继续执行。
这种方式也有缺点:当有100个线程同时操作同一行数据的时候,发生死锁时要产生wait_for graph图就要检测100*100次
如何避免死锁
- 合理设计索引,使得sql尽可能少的锁定数据
- 调整sql执行的逻辑顺序,避免update、delete长时间持有锁的sql在事务前面
- 把大的事务拆分成小的事务,小的事务也尽量缩短锁定资源的时间
- 并发量比较高的系统中,不要显示加锁
3、锁的内存结构和监控策略
内存结构
Innodb锁的内存结构如下
结构解析:
-
1、锁所在的事务信息
不论是表锁还是行锁,锁都是在事务执行过程中产生的。这个记录就是记录了哪个事务产生了这个锁。这里只是一个指针,通过该指针可以找到关于事务的更多信息 -
2、索引信息
对于行锁,加锁的记录是属于哪个指针的。也是一个指针来记录 -
3、表锁/行锁信息
- 表锁:记录对哪个表加锁
- 行锁:
- Space ID:所在的表空间
- Page number:记录所在的页号
- n_bits:
-
4、type_mode
- lock_mode锁的模式:
- lock_is:共享意向锁
- lock_ix:独占意向锁
- lock_S:共享锁
- lock_x:独占锁
- lock_auto_inc:自增锁
lock_is lock_ix lock_auto_inc都是表级锁,lock_s
和lock_x既可以是表级锁,也可以是行级锁
- lock_type锁的类型
- lock_table 表级锁
- lock_rec 行级锁
- rec_lock_type行锁的具体类型,当lock_type为lock_rec时, rec_lock_type可以取得值为:
- lock_ordinary 临键锁
- lock_gap 间隙锁
- lock_rec_not_gap 记录锁
- lock_insert_intention 插入意向锁
- is_waiting:这个属性在type_mode第9个比特位的位置,当第9个比特位为1时, is_waiting为true,也就是当前事务没有获取到锁,需要等待;当为0时, is_waiting为false,也就是当前事务获取到了锁
- lock_mode锁的模式:
锁监控
对于MySQL8.0来说,锁的监控可以通过
information_schema.innodb_trx
:查看事务的锁情况,只能看到阻塞的事务的锁performance_schema.data_locks
:查看事务的锁情况,可以看到所有的事务的锁情况performance_schema.data_lock_waits
:同information_schema.innodb_trx
十七、MVCC
1、MVCC概述
对于读写并发操作,一种是读写操作都加锁;另一种就是采用MVCC。MVCC是通过数据行的多个版本管理来实现数据库的并发控制。说白了,就是为了查询一些正在被另一个事务更新的行,并且可以看到他们被更新之前的值,而且还不用等待更新事务释放锁。这就提高了数据库的并发性能。
- 快照读与当前读
快照读又叫一致性读,读取的是记录的历史版本,不加锁的简单select都属于快照读;当前读读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录加锁。加锁的select,或对数据的增删改操作都会进行当前读
注意:快照读的前提是隔离级别不能是串行化,否则快照读会退化为当前读
2、复习
2.1再谈隔离级别
sql标准中隔离级别解决的并发问题如下:
但在MySQL默认隔离级别RR下,也解决了幻读问题!RR隔离级别下,MySQL的MVCC机制通过乐观锁的方式解决了不可重复读和幻读
2.2隐藏字段、Undo Log版本链
- 隐藏字段
MySQL的行格式中有三个隐藏字段:row_id、trx_id、roll_pointer。row_id与聚簇索引有关,与MVCC无关。- trx_id:每一个事务对聚簇索引进行改动时,事务id都会赋值给trx_id,事务id是递增的
- 对聚簇索引进行改动时,改动之前的记录(旧版本记录)都会写入undo日志中,这个隐藏字段roll_pointer就相当于一个指针,指向旧版本记录
每次对记录更新后,都会将旧值放到undo日志中,随着更新次数的增加,所有的旧版本都会被roll_pointer属性串联成一个链表,我们称之为版本链
,版本链的头节点就是最新的记录。每个版本还包含生成该版本的事务id
3、MVCC实现原理:ReadView
3.1什么是ReadView
我们知道,一条记录可能有多个历史版本,这些历史版本保存在undo log中,通过roll_pointer指针连接成链表,那如果一个事务想查询这个行记录,需要读哪个版本呢?这时就用到了ReadView,它帮我们解决了行的可见性问题
ReadView是事务在使用MVCC机制时产生的读视图。当事务启动时,会生成数据库系统当前的一个快照,
3.2设计思路
MVCC只在RR和RC隔离级别下工作。在RU隔离级别下,可以读到未提交的数据,所有直接进行当前读;在串行化隔离级别下,要求访问记录必须加锁;在RR和RC隔离级别下,读到的都是已经提交的数据,核心问题在于,在已提交的版本链中,要把哪个版本的记录展示给事务。
creator_trx_id
: 创建这个ReadView的事务ID。注意:只有增删改才会为事务分配ID,单纯的Select事务ID都为0trx_ids
:生成ReadView时当前系统中活跃的(还没提交的) 读写事务的事务ID列表。up_limi_id
活跃的事务中,最小的事务IDlow_limit_id
表示生成ReadView时系统应该分配给下一个事务的ID值。
3.3ReadView规则
假设系统中正在活跃的id为trx2 trx3 trx5 trx8
当一个事务访问该条记录时,首先从记录的最新版本开始,
- 如果被访问版本的trx_id与ReadView中creator_trx_id相同,表示事务自己更新了记录然后自己访问,所有该版本可以被事务访问到
- 如果被访问的版本的trx_id小于up_limit_id,表示该版本的事务在当前事务生成ReadView之前已经提交了,所以该事务可以访问该版本
- 如果被访问版本的trx_id介于up_limit_id与low_limit_id之间
- 如果被访问版本的trx_id在trx_ids列表中,表明产生该版本的事务还没提交,该版本不可以被其他事务访问
- 如果被访问版本的trx_id不在trx_ids列表中,表明产生该版本的事务已经提交了,该版本可以被其他事务访问
- 如果被访问版本的trx_id大于等于limit_trx_id,表明生成该版本的事务在当前事务开启之后才开启,该版本不能被访问。
3.4MVCC整体操作流程
当我们查询一条记录时:
- 首先获取事务自己的版本号,也就是事务id,trx_id
- 获取事务自己生成的ReadView
- 查询得到的数据,然后使用ReadView比较规则
- 如果不符合规则,就需要从Undo Log中获取历史快照,
- 最后返回符合ReadView规则的数据
MVCC是通过Undo Log+ReadView进行数据的读取,Undo Log保存历史数据,ReadView帮我们判断当前版本的数据是否可见
4、ReadView在RR和RC隔离级别下的演示
①RC
每次读取数据之前,都生成一个ReadView,即第一次读取数据时,有一个ReadView,第二次读取数据之前,其他事务修改了记录,那第二次读取数据时生成的ReadView与第一次就不同了,因此在RC隔离级别下存在不可重复读问题
②RR
只在第一次执行查询语句时生成一个ReadView,之后该事务的所有查询不再生成ReadView,而是重用这个第一次生成的ReadView,这就解决了不可重复读和幻读问题
十八、日志
1、MySQL支持的日志
-
慢查询日志
记录执行时间超过long_query_time的所有查询 -
通用查询日志
记录所有连接的起始终止时间,以及用户所有的操作。 -
错误日志
记录MySQL服务器启动、运行、或停止时出现的问题 -
二进制日志
记录所有更改数据
的语句 -
中继日志
-
数据定义语句日志
记录数据定于语句执行的元数据操作
除二进制日志以外,其他日志都是文本文件,默认情况下,日志文件位于MySQL数据目录下
2、通用查询日志
查看通用查询日志状态
SHOW VARIABLES LIKE '%general%';
默认情况下,general_log是关闭的,需要手动开启;可以直接SET GLOBAL general_log=ON;
,也可以在配置文件中永久性开启.
通用查询日志会记录所有的用户操作,文件会很大,应该定期删除/刷新
3、错误日志
查看错误日志的存储路径
SHOW VARIABLES LIKE 'log_err%';
4、二进制日志(bin log)
bin log全名为binary log,它记录了数据库所有执行的DDL、DML等更新事件的语句,不包含查询语句(select、show操作),如果也包含了查询操作,那就无异于通用查询日志了
bin log的应用场景:
- 数据恢复
- 数据复制
4.1 查看默认情况
sql8下,bin log是默认开启的
show variables like ‘%log_bin%’
参数:
log_bin_basename
:bin log日志文件名
log_bin_index
:bin log文件的索引文件,管理了所有的binlog文件的目录
log_bin_trust_function_creator
:限制存储过程
4.2日志参数设置
4.3查看日志
MySQL创建二进制文件时,会先创建以‘filename’为名称、‘.index’后缀的索引文件,然后再创建以’filename‘为名称的,’.00001‘为后缀的日志文件,MySQL服务器重启一次,’.00001‘为后缀的日志文件就会增加一个。
可以通过show binary logs
查看当前二进制日志的列表和大小。
binlog是二进制文件,无法直接查看,需要借助mysqlbinlog命令工具:
mysqlbinlog -v “var/lib/mysql/binlog.00001”
这种办法读取的内容比较多,我们还可以指定要读取的内容
show binlog events [in ‘log_name’] [from pos] [limit [offset,] row_count]
例如:
4.4使用日志实现数据恢复
MySQL如果启用binlog,就可以通过mysqlbinlog工具从指定时间或指定positon恢复数据,写法如下:
/usr/bin/mysqlbinlog --start-position=156 --stop-position=1373 --database=dbtest1 /var/lib/mysql/binlog.000030 | /usr/bin/mysql -uroot -pabc123ABC@ -v dbtest1;
- filename是日志文件名
- option
- start-date和stop date:指定恢复数据的起始时间和结束时间(通过查看bin log文件可以获取时间)
- start-position和stop-position:指定恢复数据的开始位置和结束位置(通过show events可以获取position值)
4.5删除二进制文件
purge master logs
删除指定部分的二进制文件,reset master
删除所有的二进制文件
1、purge master logs
purge master logs to ‘日志文件名’
purge master logs before ‘指定日期’
4.6二进制日志的写入机制
事务开始时,先把日志写到binlog cache中,事务提交时,再把binlog cache写道binlog file中,一个事务的binlog不能被拆开,无论这个事务多大,操作系统都要为其分配一个binlog cache。可以通过binlog_cache_size
控制binlog_cache大小。binlog的刷盘策略如下,类似于rodo log file
write 和 fsync的时机可由参数sync_binlog
控制。sync_binlog
的默认值为0,表示每次事务提交时都write,但何时fsync由os决定,此时如果os宕机,数据就丢失了。sync_binlog
值为1时,表示每次事务提交都会write和fsync
4.7binlog和redolog对比
- redo log是
物理日志
,记录的是“在哪个数据页上做了哪些修改”,由Innodb存储引擎层产生 - binlog是
逻辑日志
,记录的是slq语句的原始逻辑,如“更新id=2字段c值+1”
4.8两阶段提交
在事务执行过程中,redo log实时写入,而bin log只有在事务在提交时才写入,二者写入时机不一样。
由于二者写入时机不一样,因此就会产生redo log已经写入了,但服务器出现了故障导致bin log没有写入。而从机是参考binlog更新数据的,这就导致了主从机数据不一致。
为了解决以上不一致问题,Innodb使用两阶段提交方案:将redo log的写入分为prepare和commit两个阶段
两阶段提交解决bin log和redo log不一致的原理
5、中继日志(relay log)
中继日志只存在于主从服务器架构的从服务器上。当主服务器进行数据更新时,主服务器的binlog日志会记录这些修改,从服务器为了保持和主服务器的一致,会读取主服务器的binlog文件,然后将读取的内容写到自己的日志文件中,这个日志文件就是中继日志
,然后从服务器根据中继日志的内容更新从服务器的数据
中继日志的文件名为 从服务器名 -relay-bin.序号
十九、主从复制
一般应用对于数据库而言,都是读多写少
,也就是说数据库读取数据的压力比较大,有一个思路是采用数据库集群的方案:做主从架构、进行读写分离。
读写分离虽然可以提高数据库的并发性,但提高并发性我们最应该先考虑的是SQL优化
,其次是缓存策略
,最后才是主从架构和读写分离
1主从复制的原理
从机slave会读取主机的binlog进行数据同步
三个线程
主库二进制日志转储线程(binlog dump thread)
:主库将binlog日志文件发送给从库,主库读取事件时,会在binlog上加锁,读取完之后,释放锁
从库I/O线程
从库I/O线程读取主库二进制转储线程发送的binlog更新的部分,并写入从库中继日志中
从库SQL线程
读取从库的中继日志,更新从库中的数据与主库保持一致
注意:不是所有的MySQL版本都默认开启binlog的
2主从复制的作用
读写分离、数据备份、具有高可用性
3、主从复制架构搭建
4、主从复制数据一致性问题
二十、数据库备份与恢复
1、物理备份与逻辑备份
物理备份
:转储数据库物理文件到某一目录,这种备份速度比较快,但占用空间大
逻辑备份
:备份sql语句
,在恢复的时候执行备份的sql语句实现数据库数据的重现
2、mysqldump实现逻辑备份
2.1备份一个数据库
将数据库的sql语句备份成一个文本文件。
mysqldump -uroot -p books>D:\DBMS\mysql-5.5.62-winx64-data\backup\books_b.sql
2.2 备份全部数据库
2.3 备份部分数据库
2.4 备份备份部分表
2.5 备份单表的部分数据
2.6排除某些表的备份
命令恢复数据
3、MySQL命令恢复数据
3.1恢复单库
注意:如果备份文件中包含了建库语句,则恢复时不需要指定数据库名称;如果备份文件中没有建库语句,则需要提前建好库,将备份文件中的数据恢复到这个库中;
4、物理备份:直接复制整个数据库
为了保证备份的一致性,备份前需要:
- 方式1:将服务器停止
- 方式2:备份前对相关表执行
flush tables with read lock
,同时flush tables。备份完毕之后unlock tables
5、物理恢复
6、表的导出与导入
6.1select…into outfile
MySQL中可以使用select…into outfile语句将表的内容导出成一个文本文件。但对于outfile有权限设置。
查看secure_file_priv的值
- 值为empty,表示不限制文件生成的位置
- 值为null,表示禁止导出
- 值为一个路径,表示导出的文本文件只能放在这个指定的目录或其子目录
7、数据库的迁移
将数据从一个计算机存储系统永久地传输到另一个计算机存储系统