MySQL高级篇
MySQL高级篇
重点:
第四章,尤其是sql执行流程
第五章(学完了索引以后再把这章看看)
索引的建立过程,从上而下,从下而上都要清楚
第六节
B树和B+数区别
B+树和哈希索引的区别 重点
一、Linux下MySQL的安装与使用
一、安装前说明
1、Linux系统及工具的准备
安装并启动好两台虚拟机:
1
CentOS 7
- 掌握克隆虚拟机的操作
- mac地址
- 主机名
- ip地址
- UUID
- 掌握克隆虚拟机的操作
安装有
Xshell
和Xftp
等访问CentOS系统的工具CentOS6和CentOS7在MySQL的使用中的区别
- 防火墙:6是iptables,7是firewalld
- 启动服务的命令:6是service,7是systemctl
2、查看是否安装过MySQL
- 如果你是用rpm安装, 检查一下RPM PACKAGE:
1 | rpm -qa | grep -i mysql # -i 忽略大小写 |
- 检查mysql service:
1 | systemctl status mysqld.service |
- 如果存在mysql-libs的旧版本包,显示如下:
- 如果不存在mysql-lib的版本,显示如下:
3、MySQL的卸载
①关闭 mysql 服务
1 | systemctl stop mysqld.service |
②查看当前 mysql 安装状况
1 | rpm -qa | grep -i mysql |
③卸载上述命令查询出的已安装程序
1 | yum remove mysql-xxx mysql-xxx mysql-xxx mysqk-xxxx |
务必卸载干净,反复执行rpm -qa | grep -i mysql
确认是否有卸载残留
④删除 mysql 相关文件
- 查找相关文件
1 | find / -name mysql |
- 删除上述命令查找出的相关文件
1 | rm -rf xxx |
⑤删除 my.cnf
二、MySQL的Linux版安装
1、MySQL的4大版本
MySQL Community Server 社区版本
,开源免费,自由下载,但不提供官方技术支持,适用于大多数普通用户。MySQL Enterprise Edition 企业版本
,需付费,不能在线下载,可以试用30天。提供了更多的功能和更完备的技术支持,更适合于对数据库的功能和可靠性要求较高的企业客户。MySQL Cluster 集群版
,开源免费。用于架设集群服务器,可将几个MySQL Server封装成一个Server。需要在社区版或企业版的基础上使用。MySQL Cluster CGE 高级集群版
,需付费。
官方还提供了 MySQL Workbench
(GUITOOL)一款专为MySQL设计的 ER/数据库建模工具
。它是著名的数据库设计工具DBDesigner4的继任者。MySQLWorkbench又分为两个版本,分别是 社区版
(MySQL Workbench OSS)、商用版
(MySQL WorkbenchSE)。
2、下载MySQL指定版本
①下载地址
②打开官网,点击DOWNLOADS
然后,点击 MySQL Community(GPL) Downloads
③点击 MySQL Community Server
④在General Availability(GA) Releases中选择适合的版本
如果安装Windows 系统下MySQL ,推荐下载 MSI安装程序
;点击 Go to Download Page
进行下载
即可
- Windows下的MySQL安装有两种安装程序
mysql-installer-web-community-8.0.25.0.msi
下载程序大小:2.4M;安装时需要联网安装组件。mysql-installer-community-8.0.25.0.msi
下载程序大小:435.7M;安装时离线安装即可。推荐。
⑤Linux系统下安装MySQL的几种方式
Linux系统下安装软件的常用三种
方式:
- 方式1:
rpm命令
使用rpm命令安装扩展名为”.rpm
“的软件包。
.rpm包的一般格式: - 方式2:
yum命令
需联网,从互联网获取
的yum源,直接使用yum命令安装。 - 方式3:
编译安装源码包
针对tar.gz
这样的压缩格式,要用tar命令
来解压;如果是其它压缩格式,就使用其它命令。
- Linux系统下安装MySQL,官方给出多种安装方式
安装方式 | 特点 |
---|---|
rpm | 安装简单,灵活性差,无法灵活选择版本、升级 |
rpm repository | 安装包极小,版本安装简单灵活,升级方便,需要联网安装 |
通用二进制包 | 安装比较复杂,灵活性高,平台通用性好 |
源码包 | 安装最复杂,时间长,参数设置灵活,性能好 |
这里不能直接选择CentOS 7系统的版本,所以选择与之对应的 Red Hat Enterprise Linux
https://downloads.mysql.com/archives/community/
直接点Download下载RPM Bundle全量包。包括了所有下面的组件。不需要一个一个下载了。
⑥下载的tar包,用压缩工具打开
解压后rpm安装包 (红框为抽取出来的安装包)
3、CentOS7下检查MySQL依赖
①检查/tmp临时目录权限(必不可少)
由于mysql安装过程中,会通过mysql用户在/tmp目录下新建tmp_db文件,所以请给/tmp较大的权限。执行 :
1 | chmod -R 777 /tmp |
②安装前,检查依赖
1 | rpm -qa|grep libaio |
- 如果存在
libaio
包如下:
1 | rpm -qa|grep net-tools |
如果存在
net-tools
包如下:如果不存在需要到centos安装盘里进行rpm安装。安装linux如果带图形化界面,这些都是安装好的。
4、CentOS7下MySQL安装过程
①将安装程序拷贝到/opt目录下
在mysql的安装文件目录下执行:(必须按照顺序执行
)
1 | rpm -ivh mysql-community-common-8.0.25-1.el7.x86_64.rpm |
- 注意: 如在检查工作时,没有检查mysql依赖环境在安装mysql-community-server会报错
rpm
是Redhat Package Manage缩写,通过RPM的管理,用户可以把源代码包装成以rpm为扩展名的文件形式,易于安装。-i
, –install 安装软件包-v
, –verbose 提供更多的详细信息输出-h
, –hash 软件包安装的时候列出哈希标记 (和 -v 一起使用效果更好),展示进度条
②安装过程截图
安装过程中可能的报错信息
一个命令:
yum remove mysql-libs
解决,清除之前安装过的依赖即可
③查看MySQL版本
执行如下命令,如果成功表示安装mysql成功。类似java -version如果打出版本等信息
1 | mysql --version |
执行如下命令,查看是否安装成功。需要增加 -i 不用去区分大小写,否则搜索不到。
1 | rpm -qa|grep -i mysql |
④ 服务的初始化
为了保证数据库目录与文件的所有者为 mysql 登录用户,如果你是以 root 身份运行 mysql 服务,需要执
行下面的命令初始化:
1 | mysqld --initialize --user=mysql |
说明:
–initialize 选项默认以“安全”模式来初始化,则会为 root 用户生成一个密码并将 该密码标记为过期
,登录后你需要设置一个新的密码。生成的 临时密码
会往日志中记录一份。
查看密码:
1 | cat /var/log/mysqld.log |
root@localhost: 后面就是初始化的密码
⑤启动MySQL,查看状态
1 | 加不加.service后缀都可以 |
mysqld
这个可执行文件就代表着MySQL
服务器程序,运行这个可执行文件就可以直接启动一个服务器进程。
查看进程:
1 | ps -ef | grep -i mysql |
⑥查看MySQL服务是否自启动
1 | systemctl list-unit-files|grep mysqld.service |
默认是enabled
。
- 如不是enabled可以运行如下命令设置自启动
1 | systemctl enable mysqld.service |
- 如果希望不进行自启动,运行如下命令设置
1 | systemctl disable mysqld.service |
三、MySQL登录
1、首次登录
通过 mysql -hlocalhost -P3306 -uroot -p
进行登录,在Enter password:录入初始化密码
2、修改密码
- 因为初始化密码默认是过期的,所以查看数据库会报错
- 修改密码:
1 | ALTER USER 'root'@'localhost' IDENTIFIED BY 'new_password'; |
- 5.7版本之后(不含5.7),mysql加入了全新的密码安全机制。设置新密码太简单会报错。
- 改为更复杂的密码规则之后,设置成功,可以正常使用数据库了
3、设置远程登录
①当前问题
在用SQLyog或Navicat中配置远程连接Mysql数据库时遇到如下报错信息,这是由于Mysql配置了不支持远程连接引起的。
②确认网络
1.在远程机器上使用ping ip地址 保证网络畅通
2.在远程机器上使用telnet命令 保证端口号开放 访问
1 | telnet ip地址 端口号 |
拓展: telnet命令开启
:
③关闭防火墙或开放端口
方式一:关闭防火墙
- CentOS6 :
1 | service iptables stop |
- CentOS7
1 | systemctl start firewalld.service |
方式二:开放端口
- 查看开放的端口号
1 | firewall-cmd --list-all |
- 设置开放的端口号
1 | firewall-cmd --add-service=http --permanent |
- 重启防火墙
1 | firewall-cmd --reload |
④Linux下修改配置
在Linux系统MySQL下测试:
1 | use mysql; |
可以看到root用户的当前主机配置信息为localhost
。
- 修改Host为通配符%
Host列指定了允许用户登录所使用的IP,比如user=root Host=192.168.1.1。这里的意思就是说root用户只能通过192.168.1.1的客户端去访问。 user=root Host=localhost,表示只能通过本机客户端去访问。而 %是个通配符
,如果Host=192.168.1.%,那么就表示只要是IP地址前缀为“192.168.1.”的客户端都可以连接。如果Host=%
,表示所有IP
都有连接权限。
注意:在生产环境下不能为了省事将host设置为%,这样做会存在安全问题,具体的设置可以根据生产环境的IP进行设置。
1 | update user set host = '%' where user ='root'; |
Host设置了“%”后便可以允许远程访问。
Host修改完成后记得执行flush privileges使配置立即生效
1 | flush privileges; |
⑤测试
- 如果是
MySQL5.7
版本,接下来就可以使用SQLyog或者Navicat成功连接至MySQL了。 - 如果是
MySQL8.0
版本,连接时还会出现如下问题
配置新连接报错:错误号码 2058,分析是 mysql 密码加密方法变了。
解决方法:Linux下 mysql -u root -p 登录你的 mysql 数据库,然后 执行这条SQL:
1 | ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'abc123'; |
然后在重新配置SQLyog的连接,则可连接成功了,OK。
四、MySQL8的密码强度评估(了解)
1、MySQL不同版本设置密码(可能出现)
- MySQL5.7中:成功
1 | alter user 'root' identified by 'abcd1234'; |
- MySQL8.0中:失败
1 | alter user 'root' identified by 'abcd1234'; # HelloWorld_123 |
2、MySQL8之前的安全策略
在MySQL 8.0之前,MySQL使用的是validate_password插件检测、验证账号密码强度,保障账号的安全性。
安装/启用插件方式1:在参数文件my.cnf中添加参数
1 | [mysqld] |
说明1: plugin library中的validate_password文件名的后缀名根据平台不同有所差异。 对于Unix和Unix-like系统而言,它的文件后缀名是.so,对于Windows系统而言,它的文件后缀名是.dll。
说明2: 修改参数后必须重启MySQL服务才能生效。
说明3: 参数FORCE_PLUS_PERMANENT是为了防止插件在MySQL运行时的时候被卸载。当你卸载插件时就会报错。如下所示。
1 | SELECT PLUGIN_NAME, PLUGIN_LIBRARY, PLUGIN_STATUS, LOAD_OPTION |
安装/启用插件方式2:运行时命令安装(推荐)
1 | INSTALL PLUGIN validate_password SONAME 'validate_password.so'; |
此方法也会注册到元数据,也就是mysql.plugin表中,所以不用担心MySQL重启后插件会失效。
3、MySQL8的安全策略
①validate_password说明
MySQL 8.0,引入了服务器组件(Components)这个特性,validate_password插件已用服务器组件重新实现。8.0.25版本的数据库中,默认自动安装validate_password组件。
未安装插件前,执行如下两个指令
,执行效果:
1 | show variables like 'validate_password%'; |
安装插件后,执行如下两个指令
,执行效果:
1 | SELECT * FROM mysql.component; |
关于validate_password
组件对应的系统变量说明:
选项 | 默认值 | 参数描述 |
---|---|---|
validate_password_check_user_name | ON | 设置为ON的时候表示能将密码设置成当前用户名。 |
validate_password_dictionary_file | 用于检查密码的字典文件的路径名,默认为空 | |
validate_password_length |
8 | 密码的最小长度,也就是说密码长度必须大于或等于8 |
validate_password_mixed_case_count | 1 | 如果密码策略是中等或更强的,validate_password要求密码具有的小写和大写字符的最小数量。对于给定的这个值密码必须有那么多小写字符和那么多大写字符。 |
validate_password_number_count | 1 | 密码必须包含的数字个数 |
validate_password_policy |
MEDIUM | 密码强度检验等级,可以使用数值0、1、2或相应的符号值LOW、MEDIUM、STRONG来指定。 0/LOW :只检查长度。1/MEDIUM :检查长度、数字、大小写、特殊字符。 2/STRONG :检查长度、数字、大小写、特殊字符、字典文件。 |
validate_password_special_char_count | 1 | 密码必须包含的特殊字符个数 |
提示:
组件和插件的默认值可能有所不同。
例如,MySQL 5.7. validate_password_check_user_name的默认值为OFF。
②修改安全策略
修改密码验证安全强度
1 | SET GLOBAL validate_password_policy=LOW; |
此外,还可以修改密码中字符的长度
1 | set global validate_password_length=1; |
③密码强度测试
如果你创建密码是遇到“Your password does not satisfy the current policy requirements”,可以通过函数组件去检测密码是否满足条件: 0-100。当评估在100时就是说明使用上了最基本的规则:大写+小写+特殊字符+数字组成的8位以上密码
1 | SELECT VALIDATE_PASSWORD_STRENGTH('medium'); |
注意:如果没有安装validate_password组件或插件的话,那么这个函数永远都返回0。 关于密码复杂度对应的密码复杂度策略。如下表格所示:
Password Test | Return Value |
---|---|
Length < 4 | 0 |
Length ≥ 4 and < validate_password.length | 25 |
Satisfies policy 1 (LOW) | 50 |
Satisfies policy 2 (MEDIUM) | 75 |
Satisfies policy 3 (STRONG) | 100 |
3、卸载插件、组件(了解)
卸载插件
1 | UNINSTALL PLUGIN validate_password; |
卸载组件
1 | UNINSTALL COMPONENT 'file://component_validate_password'; |
五、字符集的相关操作
1、修改MySQL5.7字符集
①修改步骤
在MySQL 8.0版本之前,默认字符集为latin1
,utf8字符集指向的是utf8mb3
。网站开发人员在数据库设计的时候往往会将编码修改为utf8字符集。如果遗忘修改默认的编码,就会出现乱码的问题。从MySQL8.0开始,数据库的默认编码将改为 utf8mb4
,从而避免上述乱码的问题。
操作1:查看默认使用的字符集
1 | show variables like 'character%'; |
- MySQL8.0中执行:
- MySQL5.7中执行:
MySQL 5.7 默认的客户端和服务器都用了latin1
,不支持中文,保存中文会报错。MySQL5.7截图如下:
在MySQL5.7中添加中文数据时,报错:
因为默认情况下,创建表使用的是latin1
。如下:
操作2:修改字符集
1 | vim /etc/my.cnf |
在MySQL5.7或之前的版本中,在文件最后加上中文字符集配置:
1 | character_set_server=utf8 |
操作3:重新启动MySQL服务
1 | systemctl restart mysqld |
但是原库、原表的设定不会发生变化,参数修改只对新建的数据库生效。
②已有库&表字符集的变更
MySQL5.7版本中,以前创建的库,创建的表字符集还是latin1。
修改已创建数据库的字符集
1 | alter database dbtest1 character set 'utf8'; |
修改已创建数据表的字符集
1 | alter table t_emp convert to character set 'utf8'; |
注意:但是原有的数据如果是用非’utf8’编码的话,数据本身编码不会发生改变。已有数据需要导
出或删除,然后重新插入。
2、各级别的字符集
MySQL有4个级别的字符集和比较规则,分别是:
- 服务器级别
- 数据库级别
- 表级别
- 列级别
执行如下SQL语句:
1 | show variables like 'character%'; |
character_set_server:服务器级别的字符集
character_set_database:当前数据库的字符集
character_set_client:服务器解码请求时使用的字符集
character_set_connection:服务器处理请求时会把请求字符串从character_set_client转为character_set_connection
character_set_results:服务器向客户端返回数据时使用的字符集
①服务器级别
character_set_server
:服务器级别的字符集。
我们可以在启动服务器程序时通过启动选项或者在服务器程序运行过程中使用 SET 语句修改这两个变量的值。比如我们可以在配置文件中这样写:
1 | [server] |
当服务器启动的时候读取这个配置文件后这两个系统变量的值便修改了。
②数据库级别
character_set_database
:当前数据库的字符集
我们在创建和修改数据库的时候可以指定该数据库的字符集和比较规则,具体语法如下:
1 | CREATE DATABASE 数据库名 |
③表级别
我们也可以在创建和修改表的时候指定表的字符集和比较规则,语法如下:
1 | CREATE TABLE 表名 (列的信息) |
如果创建和修改表的语句中没有指明字符集和比较规则,将使用该表所在数据库的字符集和比较规则作为该表的字符集和比较规则。
④列级别
对于存储字符串的列,同一个表中的不同的列也可以有不同的字符集和比较规则。我们在创建和修改列定义的时候可以指定该列的字符集和比较规则,语法如下:
1 | CREATE TABLE 表名( |
对于某个列来说,如果在创建和修改的语句中没有指明字符集和比较规则,将使用该列所在表的字符集和比较规则作为该列的字符集和比较规则。
提示
在转换列的字符集时需要注意,如果转换前列中存储的数据不能用转换后的字符集进行表示会发生
错误。
比方说原先列使用的字符集是utf8,列中存储了一些汉字,现在把列的字符集转换为ascii的
话就会出错,因为ascii字符集并不能表示汉字字符。
⑤小结
我们介绍的这4个级别字符集和比较规则的联系如下:
- 如果
创建或修改列
时没有显式的指定字符集和比较规则,则该列默认用表的
字符集和比较规则 - 如果
创建表时
没有显式的指定字符集和比较规则,则该表 `默认用数据库的 字符集和比较规则 - 如果
创建数据库时
没有显式的指定字符集和比较规则,则该数据库默认用服务器的
字符集和比较规则
知道了这些规则之后,对于给定的表,我们应该知道它的各个列的字符集和比较规则是什么,从而根据
这个列的类型来确定存储数据时每个列的实际数据占用的存储空间大小了。比方说我们向表 t 中插入一
条记录
1 | INSERT INTO t(col) VALUES('我们'); |
首先列 col
使用的字符集是 gbk
,一个字符'我'
在 gbk 中的编码为 0xCED2
,占用两个字节,两个字符的实际数据就占用4个字节。如果把该列的字符集修改为 utf8
的话,这两个字符就实际占用6个字节
3、字符集与比较规则(了解)
①utf8 与 utf8mb4
utf8
字符集表示一个字符需要使用1~4个字节,但是我们常用的一些字符使用1~3个字节就可以表示
了。而字符集表示一个字符所用的最大字节长度,在某些方面会影响系统的存储和性能,所以设计
MySQL的设计者偷偷的定义了两个概念:
utf8mb3
:阉割过的utf8
字符集,只使用1~3个字节表示字符。utf8mb4
:正宗的utf8
字符集,使用1~4个字节表示字符。
②比较规则
上表中,MySQL版本一共支持41种字符集,其中的 Default collation
列表示这种字符集中一种默认的比较规则,里面包含着该比较规则主要作用于哪种语言,比如utf8_polish_ci
表示以波兰语的规则比较, utf8_spanish_ci
是以西班牙语的规则比较, utf8_general_ci
是一种通用的比较规则。
后缀表示该比较规则是否区分语言中的重音、大小写。具体如下:
后缀 | 英文释义 | 描述 |
---|---|---|
_ai | accent insensitive | 不区分重音 |
_as | accent sensitive | 区分重音 |
_ci | case insensitive | 不区分大小写 |
_cs | case sensitive | 区分大小写 |
_bin | binary | 以二进制方式比较 |
最后一列 Maxlen
,它代表该种字符集表示一个字符最多需要几个字节。
常用操作1:
1 | #查看GBK字符集的比较规则 |
常用操作2:
1 | #查看服务器的字符集和比较规则 |
常用操作3:
1 | #查看表的字符集 |
4、请求到响应过程中字符集的变化
系统变量 | 描述 |
---|---|
character_set_client |
服务器解码请求时使用的字符集 |
character_set_connection |
服务器处理请求时会把请求字符串从character_set_client 转为 character_set_connection |
character_set_results |
服务器向客户端返回数据时使用的字符集 |
这几个系统变量在我的计算机上的默认值如下(不同操作系统的默认值可能不同):
为了体现出字符集在请求处理过程中的变化,我们这里特意修改一个系统变量的值:
1 | set character_set_connection = gbk; |
现在假设我们客户端发送的请求是下边这个字符串:
1 | SELECT * FROM t WHERE s = '我'; |
为了方便大家理解这个过程,我们只分析字符 ‘我’ 在这个过程中字符集的转换。现在看一下在请求从发送到结果返回过程中字符集的变化:
客户端发送请求所使用的字符集
一般情况下客户端所使用的字符集和当前操作系统一致,不同操作系统使用的字符集可能不一
样,如下:
- 类
Unix
系统使用的是utf8
Windows
使用的是gbk
当客户端使用的是utf8
字符集,字符'我'
在发送给服务器的请求中的字节形式就是:0xE68891
- 类
提示
如果你使用的是可视化工具,比如navicat之类的,这些工具可能会使用自定义的字符集来编码发送到服务器的字符串,而不采用操作系统默认的字符集(所以在学习的时候还是尽量用命令行窗口)。
- 服务器接收到客户端发送来的请求其实是一串二进制的字节,它会认为这串字节采用的字符集是
character_set_client
,然后把这串字节转换为character_set_connection
字符集编码的字符。
由于我的计算机上character_set_client
的值是utf8
,首先会按照utf8
字符集对字节串0xE68891 进行解码,得到的字符串就是'我'
,然后按照character_set_connection
代表的字符集,也就是gbk
进行编码,得到的结果就是字节串0xCED2
。 - 因为表
t
的列col
采用的是gbk
字符集,与character_set_connection
一致,所以直接到列中找字节值为0xCED2
的记录,最后找到了一条记录。
提示
如果某个列使用的字符集和character_set_connection代表的字符集不一致的话,还需要进行一次字符集转换。
- 上一步骤找到的记录中的
col
列其实是一个字节串0xCED2
,col
列是采用gbk
进行编码的,所以首先会将这个字节串使用gbk
进行解码,得到字符串 ‘我’ ,然后再把这个字符串使用character_set_results
代表的字符集,也就是utf8
进行编码,得到了新的字节串:0xE68891
,然后发送给客户端。 - 由于客户端是用的字符集是
utf8
,所以可以顺利的将0xE68891
解释成字符我
,从而显示到我们的显示器上,所以我们人类也读懂了返回的结果。
总结图示如下:
六、SQL大小写规范
1、Windows和Linux平台区别
在 SQL 中,关键字和函数名是不用区分字母大小写的,比如 SELECT、WHERE、ORDER、GROUP BY 等关键字,以及 ABS、MOD、ROUND、MAX 等函数名。
不过在 SQL 中,你还是要确定大小写的规范,因为在 Linux 和 Windows 环境下,你可能会遇到不同的大小写问题。 windows系统默认大小写不敏感
,但是 linux系统是大小写敏感的 `。
通过如下命令查看:
1 | SHOW VARIABLES LIKE '%lower_case_table_names%' |
- Windows系统下:
- Linux系统下:
- lower_case_table_names参数值的设置:
默认为0,大小写敏感
。- 设置1,大小写不敏感。创建的表,数据库都是以小写形式存放在磁盘上,对于sql语句都是转换为小写对表和数据库进行查找。
- 设置2,创建的表和数据库依据语句上格式存放,凡是查找都是转换为小写进行。
- 两个平台上SQL大小写的区别具体来说:
MySQL在Linux下数据库名、表名、列名、别名大小写规则是这样的:
1、数据库名、表名、表的别名、变量名是严格区分大小写的;
2、关键字、函数名称在 SQL 中不区分大小写;
3、列名(或字段名)与列的别名(或字段别名)在所有的情况下均是忽略大小写的;
MySQL在Windows的环境下全部不区分大小写
2、Linux下大小写规则设置
当想设置为大小写不敏感时,要在 my.cnf
这个配置文件 [mysqld] 中加入lower_case_table_names=1
,然后重启服务器。
- 但是要在重启数据库实例之前就需要将原来的数据库和表转换为小写,否则将找不到数据库名。
- 此参数适用于MySQL5.7。在MySQL 8下禁止在重新启动 MySQL 服务时将
lower_case_table_names
设置成不同于初始化 MySQL 服务时设置的lower_case_table_names
值。如果非要将MySQL8设置为大小写不敏感,具体步骤为:
1 | 1、停止MySQL服务 |
3、SQL编写建议
如果你的变量名命名规范没有统一,就可能产生错误。这里有一个有关命名规范的建议:
- 关键字和函数名称全部大写;
- 数据库名、表名、表别名、字段名、字段别名等全部小写;
- SQL 语句必须以分号结尾。
数据库名、表名和字段名在 Linux MySQL 环境下是区分大小写的,因此建议你统一这些字段的命名规则,比如全部采用小写的方式。
虽然关键字和函数名称在 SQL 中不区分大小写,也就是如果小写的话同样可以执行。但是同时将关键词和函数名称全部大写,以便于区分数据库名、表名、字段名。
七、sql_mode的合理设置
1、 宽松模式 vs 严格模式
宽松模式:
如果设置的是宽松模式,那么我们在插入数据的时候,即便是给了一个错误的数据,也可能会被接受,并且不报错。
举例
:我在创建一个表时,该表中有一个字段为name,给name设置的字段类型时char(10)
,如果我
在插入数据的时候,其中name这个字段对应的有一条数据的长度超过了10
,例如’1234567890abc’,超过了设定的字段长度10,那么不会报错,并且取前10个字符存上,也就是说你这个数据被存为
了’1234567890’,而’abc’就没有了。但是,我们给的这条数据是错误的,因为超过了字段长度,但是并没有报错,并且mysql自行处理并接受了,这就是宽松模式的效果。
应用场景
:通过设置sql mode为宽松模式,来保证大多数sql符合标准的sql语法,这样应用在不同数据
库之间进行 迁移
时,则不需要对业务sql 进行较大的修改。
严格模式:
出现上面宽松模式的错误,应该报错才对,所以MySQL5.7版本就将sql_mode默认值改为了严格模式。所以在生产等环境
中,我们必须采用的是严格模式,进而 开发、测试环境
的数据库也必须要设置,这样在开发测试阶段就可以发现问题。并且我们即便是用的MySQL5.6,也应该自行将其改为严格模式。开发经验
:MySQL等数据库总想把关于数据的所有操作都自己包揽下来,包括数据的校验,其实开发中,我们应该在自己 开发的项目程序级别将这些校验给做了
,虽然写项目的时候麻烦了一些步骤,但是这样做之后,我们在进行数据库迁移或者在项目的迁移时,就会方便很多。
改为严格模式
后可能会存在的问题
:
若设置模式中包含了 NO_ZERO_DATE
,那么MySQL数据库不允许插入零日期,插入零日期会抛出错误而不是警告。
例如,表中含字段TIMESTAMP列(如果未声明为NULL或显示DEFAULT子句)将自动分配DEFAULT ‘0000-00-00 00:00:00’(零时间戳),这显然是不满足sql_mode中的NO_ZERO_DATE而报错。
2、宽松模式再举例
宽松模式举例1:
1 | select * from employees group by department_id limit 10; |
宽松模式举例2:
设置 sql_mode 模式为 STRICT_TRANS_TABLES ,然后插入数据:
3、模式查看和设置
- 查看当前的sql_mode
1 | select @@session.sql_mode |
- 临时设置方式:设置当前窗口中设置sql_mode
1 | SET GLOBAL sql_mode = 'modes...'; #全局 |
举例:
1 | #改为严格模式。此方法只在当前会话中生效,关闭当前会话就不生效了。 |
- 永久设置方式:在/etc/my.cnf中配置sql_mode
在my.cnf文件(windows系统是my.ini文件),新增:
1 | [mysqld] |
然后 重启MySQL
。
当然生产环境上是禁止重启MySQL服务的,所以采用 临时设置方式 + 永久设置方式
来解决线上的问题,那么即便是有一天真的重启了MySQL服务,也会永久生效了。https://mp.csdn.net/edit)
二、MySQL的数据目录
一、MySQL的主要目录结构
查询mysql安装位置
1 | find / -name mysql |
安装好MySQL 8之后,我们查看如下的目录结构:
1、数据库文件的存放路径
MySQL数据库文件的存放路径:/var/lib/mysql/
1 | show variables like 'datadir'; |
从结果中可以看出,在我的计算机上MySQL的数据目录就是 /var/lib/mysql/
。
2、相关命令目录
相关命令目录:/usr/bin(mysqladmin、mysqlbinlog、mysqldump等命令)
和/usr/sbin
。
3、配置文件目录
配置文件目录:/usr/share/mysql-8.0(命令及配置文件)
,/etc/mysql(如my.cnf)
二、数据库和文件系统的关系
1、查看默认数据库
查看一下在我的计算机上当前有哪些数据库:
1 | SHOW DATABASES; |
可以看到有4个数据库是属于MySQL自带的系统数据库。
mysql
MySQL 系统自带的核心数据库,它存储了MySQL的用户账户和权限信息,一些存储过程、事件的定
义信息,一些运行过程中产生的日志信息,一些帮助信息以及时区信息等。information_schema
MySQL 系统自带的数据库,这个数据库保存着MySQL服务器维护的所有其他数据库的信息
,比如有
哪些表、哪些视图、哪些触发器、哪些列、哪些索引。这些信息并不是真实的用户数据,而是一些
描述性信息,有时候也称之为元数据
。在系统数据库information_schema
中提供了一些以innodb_sys
开头的表,用于表示内部系统表。
1 | USE information_schema; |
performance_schema
MySQL 系统自带的数据库,这个数据库里主要保存MySQL服务器运行过程中的一些状态信息,可以用来监控 MySQL 服务的各类性能指标
。包括统计最近执行了哪些语句,在执行过程的每个阶段都
花费了多长时间,内存的使用情况等信息。sys
MySQL 系统自带的数据库,这个数据库主要是通过视图
的形式把information_schema
和performance_schema
结合起来,帮助系统管理员和开发人员监控 MySQL 的技术性能。
2、 数据库在文件系统中的表示
看一下我的计算机上的数据目录下的内容:
1 | [root@achang mysql]# cd /var/lib/mysql |
这个数据目录下的文件和子目录比较多,除了 information_schema
这个系统数据库外,其他的数据库
在 数据目录
下都有对应的子目录。
以我的 temp
数据库为例,在MySQL5.7 中打开:
1 | [root@achang mysql]# cd ./temp |
在MySQL8.0中打开:
1 | [root@atguigu01 mysql]# cd ./temp |
3、表在文件系统中的表示
①InnoDB存储引擎模式
- 表结构
为了保存表结构,InnoDB
在 数据目录
下对应的数据库子目录下创建了一个专门用于 描述表结构的文
件 ,文件名是这样:表名.frm
比方说我们在 atguigu 数据库下创建一个名为 test 的表:
1 | USE atguigu; |
那在数据库 atguigu
对应的子目录下就会创建一个名为test.frm
的用于描述表结构的文件。.frm文件
的格式在不同的平台上都是相同的。这个后缀名为.frm是以 二进制格式
存储的,我们直接打开是乱码
的。
②表中数据和索引
- 系统表空间(system tablespace)
默认情况下,InnoDB会在数据目录下创建一个名为ibdata1
、大小为12M
的文件,这个文件就是对应
的系统表空间
在文件系统上的表示。怎么才12M?注意这个文件是自扩展文件
,当不够用的时候它会自
己增加文件大小。
当然,如果你想让系统表空间对应文件系统上多个实际文件,或者仅仅觉得原来的 ibdata1 这个文件名
难听,那可以在MySQL启动时配置对应的文件路径以及它们的大小,比如我们这样修改一下my.cnf 配置
文件:
1 | [server] |
- 独立表空间(file-per-table tablespace)
在MySQL5.6.6以及之后的版本中,InnoDB并不会默认的把各个表的数据存储到系统表空间中,而是为每一个表建立一个独立表空间
,也就是说我们创建了多少个表,就有多少个独立表空间。使用 独立表空间 来存储表数据的话,会在该表所属数据库对应的子目录下创建一个表示该独立表空间的文件,文件名和表名相同,只不过添加了一个.ibd
的扩展名而已,所以完整的文件名称长这样:表名.ibd
比如:我们使用了独立表空间
去存储atguigu
数据库下的 test 表的话,那么在该表所在数据库对应
的atguigu
目录下会为test
表创建这两个文件:
test.frm
test.ibd
其中test.ibd
文件就用来存储test
表中的数据和索引。
- 系统表空间与独立表空间的设置
我们可以自己指定使用 系统表空间 还是 独立表空间
来存储数据,这个功能由启动参数innodb_file_per_table
控制,比如说我们想刻意将表数据都存储到 系统表空间
时,可以在启动
MySQL服务器的时候这样配置:
1 | [server] |
默认情况:
1 | show variables like 'innodb_file_per_table'; |
- 其他类型的表空间
随着MySQL的发展,除了上述两种老牌表空间之外,现在还新提出了一些不同类型的表空间,比如通用表空间(general tablespace)、临时表空间(temporary tablespace)等。
②MyISAM存储引擎模式
- 表结构
在存储表结构方面,MyISAM
和InnoDB
一样,也是在数据目录
下对应的数据库子目录下创建了一个专门用于描述表结构的文件:表名.frm
- 表中数据和索引
在MyISAM中的索引全部都是二级索引
,该存储引擎的数据和索引是分开存放
的。所以在文件系统中也是使用不同的文件来存储数据文件和索引文件,同时表数据都存放在对应的数据库子目录下。假如test
表使用MyISAM存储引擎的话,那么在它所在数据库对应的atguigu
目录下会为test
表创建这三个文
件:
test.frm 存储表结构
test.MYD 存储数据 (MYData)
test.MYI 存储索引 (MYIndex)
举例:创建一个 MyISAM
表,使用ENGINE
选项显式指定引擎。因为InnoDB
是默认引擎。
1 | CREATE TABLE `student_myisam` ( |
4、小结
举例: 数据库a
,表b
。
1、如果表b采用 InnoDB ,data\a中会产生1个或者2个文件:
b.frm
:描述表结构文件,字段长度等- 如果采用
系统表空间
模式的,数据信息和索引信息都存储在ibdata1
中 - 如果采用
独立表空间
存储模式,data\a中还会产生b.ibd
文件(存储数据信息和索引信息)
此外:
① MySQL5.7 中会在data/a的目录下生成 db.opt
文件用于保存数据库的相关配置。比如:字符集、比较
规则。而MySQL8.0不再提供db.opt文件。
② MySQL8.0中不再单独提供b.frm,而是合并在b.ibd文件中。
2、如果表b采用 MyISAM
,data\a中会产生3个文件:
MySQL5.7 中:b.frm
:描述表结构文件,字段长度等。
MySQL8.0 中b.xxx.sdi
:描述表结构文件,字段长度等b.MYD
(MYData):数据信息文件,存储数据信息(如果采用独立表存储模式)b.MYI
(MYIndex):存放索引信息文件
##三、用户与权限管理
一、用户管理
1、登录MySQL服务器
启动MySQL服务后,可以通过mysql命令来登录MySQL服务器,命令如下:
1 | mysql –h hostname|hostIP –P port –u username –p DatabaseName –e "SQL语句" |
举例:
1 | mysql -uroot -p -hlocalhost -P3306 mysql -e "select host,user from user" |
2、创建用户
CREATE USER语句的基本语法形式如下:
1 | CREATE USER 用户名 [IDENTIFIED BY '密码'][,用户名 [IDENTIFIED BY '密码']]; |
- 用户名参数表示新建用户的账户,由
用户(User)
和主机名(Host)
构成; - “[ ]”表示可选,也就是说,可以指定用户登录时需要密码验证,也可以不指定密码验证,这样用户可以直接登录。不过,不指定密码的方式不安全,不推荐使用。如果指定密码值,这里需要使用IDENTIFIED BY指定明文密码值。
- CREATE USER语句可以同时创建多个用户。
举例:
1 | CREATE USER zhang3 IDENTIFIED BY '123123'; # 默认host是 % |
3、修改用户
修改用户名:
1 | UPDATE mysql.user SET USER='li4' WHERE USER='wang5'; |
4、删除用户
方式1:使用DROP方式删除(推荐)
使用DROP USER语句来删除用户时,必须用于DROP USER权限。DROP USER语句的基本语法形式
如下:
1 | DROP USER user[,user]…; |
举例:
1 | DROP USER li4 ; # 默认删除host为%的用户 |
方式2:使用DELETE方式删除
1 | DELETE FROM mysql.user WHERE Host=’hostname’ AND User=’username’; |
执行完DELETE命令后要使用FLUSH命令来使用户生效,命令如下:
1 | FLUSH PRIVILEGES; |
举例:
1 | DELETE FROM mysql.user WHERE Host='localhost' AND User='Emily'; |
注意:
不推荐
通过DELETE FROM USER u WHERE USER='li4'
进行删除,系统会有残留信息保留。而drop user命令会删除用户以及对应的权限,执行命令后你会发现mysql.user表和mysql.db表的相应记录都消失了。
5、设置当前用户密码
旧的写法
如下 :
1 | # 修改当前用户的密码:(MySQL5.7测试有效) |
这里介绍 推荐的写法
:
使用ALTER USER命令来修改当前用户密码
用户可以使用ALTER命令来修改自身密码,如下语句代表修改当前登录用户的密码。基本语法如下:1
ALTER USER USER() INDENTIFIED BY 'new_password';
使用set语句来修改
1 | SET PASSWORD='new_password'; |
该语句会自动将密码加密后再赋给当前用户
。
6、修改其它用户密码
使用ALTER语句来修改普通用户的密码
可以使用ALTER USER语句来修改普通用户的密码。基本语法形式如下:
1 | ALTER USER user [IDENTIFIED BY '新密码'] |
使用SET命令来修改普通用户的密码
使用root用户登录到MySQL服务器后,可以使用SET语句来修改普通用户的密码。SET语句的代码如下:
1 | SET PASSWORD FOR 'username'@'hostname'='new_password'; |
- 使用
UPDATE语句
修改普通用户的密码(不推荐
)
1 | UPDATE MySQL.user SET authentication_string=PASSWORD("123456") |
7、MySQL8密码管理(了解)
- 密码过期策略
在MySQL中,数据库管理员可以 手动设置
账号密码过期,也可以建立一个 自动
密码过期策略。
过期策略可以是 全局的
,也可以为 每个账号
设置单独的过期策略。
1 | ALTER USER user PASSWORD EXPIRE; |
练习:
1 | ALTER USER 'kangshifu'@'localhost' PASSWORD EXPIRE; |
方式①:使用SQL语句更改该变量的值并持久化
1 | SET PERSIST default_password_lifetime = 180; # 建立全局策略,设置密码每隔180天过期 |
方式②:配置文件my.cnf中进行维护
1 | [mysqld] |
手动设置指定时间过期方式2:单独设置
每个账号既可延用全局密码过期策略,也可单独设置策略。在 CREATE USER
和 ALTER USER
语句上加入 PASSWORD EXPIRE
选项可实现单独设置策略。下面是一些语句示例。
1 | #设置kangshifu账号密码每90天过期: |
2. 密码重用策略
手动设置密码重用方式1:全局
- 方式①:使用SQL
1
2SET PERSIST password_history = 6; #设置不能选择最近使用过的6个密码
SET PERSIST password_reuse_interval = 365; #设置不能选择最近一年内的密码- 方式②:my.cnf配置文件
1
2
3[mysqld]
password_history=6
password_reuse_interval=365手动设置密码重用方式2:单独设置
1 | #不能使用最近5个密码: |
二、权限管理
1、权限列表
MySQL到底都有哪些权限呢?
1 | mysql> show privileges; |
(1)
CREATE和DROP权限
,可以创建新的数据库和表,或删除(移掉)已有的数据库和表。如果将MySQL数据库中的DROP权限授予某用户,用户就可以删除MySQL访问权限保存的数据库。
(2)SELECT、INSERT、UPDATE和DELETE权限
允许在一个数据库现有的表上实施操作。
(3)SELECT权限
只有在它们真正从一个表中检索行时才被用到。
(4)INDEX权限
允许创建或删除索引,INDEX适用于已有的表。如果具有某个表的CREATE权限,就可以在CREATE TABLE语句中包括索引定义。
(5)ALTER权限
可以使用ALTER TABLE来更改表的结构和重新命名表。
(6)CREATE ROUTINE权限
用来创建保存的程序(函数和程序),ALTER ROUTINE权限用来更改和删除保存的程序,EXECUTE权限
用来执行保存的程序。
(7)GRANT权限
允许授权给其他用户,可用于数据库、表和保存的程序。
(8)FILE权限
使用户可以使用LOAD DATA INFILE和SELECT … INTO OUTFILE语句读或写服务器上的文件,任何被授予FILE权限的用户都能读或写MySQL服务器上的任何文件(说明用户可以读任何数据库目录下的文件,因为服务器可以访问这些文件)。
2、 授予权限的原则
权限控制主要是出于安全因素,因此需要遵循以下几个 经验原则
:
1、只授予能
满足需要的最小权限
,防止用户干坏事。比如用户只是需要查询,那就只给select权限就可以了,不要给用户赋予update、insert或者delete权限。
2、创建用户的时候限制用户的登录主机
,一般是限制成指定IP或者内网IP段。
3、为每个用户设置满足密码复杂度的密码
。
4、定期清理不需要的用户
,回收权限或者删除用户。
3、授予权限
给用户授权的方式有 2 种,分别是通过把 角色赋予用户给用户授权
和 直接给用户授权
。用户是数据库的使用者,我们可以通过给用户授予访问数据库中资源的权限,来控制使用者对数据库的访问,消除安全隐患。
授权命令:
1 | GRANT 权限1,权限2,…权限n ON 数据库名称.表名称 TO 用户名@用户地址 [IDENTIFIED BY ‘密码口令’]; |
- 该权限如果发现没有该用户,则会直接新建一个用户。
- 给li4用户用本地命令行方式,授予atguigudb这个库下的所有表的插删改查的权限。
1 | GRANT SELECT,INSERT,DELETE,UPDATE ON atguigudb.* TO li4@localhost ; |
- 授予通过网络方式登录的joe用户 ,对所有库所有表的全部权限,密码设为123。注意这里唯独不包括grant的权限
1 | GRANT ALL PRIVILEGES ON *.* TO joe@'%' IDENTIFIED BY '123'; |
我们在开发应用的时候,经常会遇到一种需求,就是要根据用户的不同,对数据进行横向和纵向的分组。
- 所谓横向的分组,就是指用户可以接触到的数据的范围,比如可以看到哪些表的数据;
- 所谓纵向的分组,就是指用户对接触到的数据能访问到什么程度,比如能看、能改,甚至是删除。
4、查看权限
- 查看
当前用户
权限
1 | SHOW GRANTS; |
- 查看某用户的全局权限
1 | SHOW GRANTS FOR 'user'@'主机地址' ; |
5、收回权限
收回权限就是取消已经赋予用户的某些权限。收回用户不必要的权限可以在一定程度上保证系统的安全性。MySQL中使用 REVOKE语句
取消用户的某些权限。使用REVOKE收回权限之后,用户账户的记录将从db、host、tables_priv和columns_priv表中删除,但是用户账户记录仍然在user表中保存(删除user表中的账户记录使用DROP USER语句)。
注意:在将用户账户从user表删除之前,应该收回相应用户的所有权限。
- 收回权限命令
1 | REVOKE 权限1,权限2,…权限n ON 数据库名称.表名称 FROM 用户名@用户地址; |
- 举例
1 | #收回全库全表的所有权限 |
- 注意: 须用户重新登录后才能生效
三、权限表
1、user表
user表是MySQL中最重要的一个权限表, 记录用户账号和权限信息
,有49个字段。如下图:
这些字段可以分成4类,分别是范围列(或用户列)、权限列、安全列和资源控制列。
- 范围列(或用户列)
- host : 表示连接类型
%
表示所有远程通过 TCP方式的连接IP 地址
如 (192.168.1.2、127.0.0.1) 通过制定ip地址进行的TCP方式的连接机器名
通过制定网络中的机器名进行的TCP方式的连接::1
IPv6的本地ip地址,等同于IPv4的 127.0.0.1localhost
本地方式通过命令行方式的连接 ,比如mysql -u xxx -p xxx 方式的连接。
- user : 表示用户名,同一用户通过不同方式链接的权限是不一样的。
- password : 密码
- 所有密码串通过 password(明文字符串) 生成的密文字符串。MySQL 8.0 在用户管理方面增加了角色管理,默认的密码加密方式也做了调整,由之前的
SHA1
改为了SHA2
,不可逆 。同时加上 MySQL 5.7 的禁用用户和用户过期的功能,MySQL 在用户管理方面的功能和安全性都较之前版本大大的增强了。 - mysql 5.7 及之后版本的密码保存到
authentication_string
字段中不再使用password 字
段。
- 所有密码串通过 password(明文字符串) 生成的密文字符串。MySQL 8.0 在用户管理方面增加了角色管理,默认的密码加密方式也做了调整,由之前的
- host : 表示连接类型
- 权限列
- Grant_priv字段
表示是否拥有GRANT权限 - Shutdown_priv字段
表示是否拥有停止MySQL服务的权限 - Super_priv字段
表示是否拥有超级权限 - Execute_priv字段
表示是否拥有EXECUTE权限。拥有EXECUTE权限,可以执行存储过程和函数。 - Select_priv , Insert_priv等
为该用户所拥有的权限。
- Grant_priv字段
- 安全列
- 安全列只有6个字段,其中两个是ssl相关的(ssl_type、ssl_cipher),用于
加密
;两个是x509相关的(x509_issuer、x509_subject),用于标识用户
;另外两个Plugin字段用于验证用户身份
的插件,该字段不能为空。如果该字段为空,服务器就使用内建授权验证机制验证用户身份。
- 安全列只有6个字段,其中两个是ssl相关的(ssl_type、ssl_cipher),用于
- 资源控制列
- 资源控制列的字段用来 限制用户使用的资源 ,包含4个字段,分别为:
- ①max_questions,用户每小时允许执行的查询操作次数;
- ②max_updates,用户每小时允许执行的更新操作次数;
- ③max_connections,用户每小时允许执行的连接操作次数;
- ④max_user_connections,用户允许同时建立的连接次数。
查看字段:
- 资源控制列的字段用来 限制用户使用的资源 ,包含4个字段,分别为:
1 | DESC mysql.user; |
查看用户, 以列的方式显示数据:
1 | SELECT * FROM mysql.user \G; |
查询特定字段:
1 | SELECT host,user,authentication_string,select_priv,insert_priv,drop_priv |
2、db表
使用DESCRIBE查看db表的基本结构:
1 | DESCRIBE mysql.db; |
- 用户列
db表用户列有3个字段,分别是Host、User、Db。这3个字段分别表示主机名、用户名和数据库名。表示从某个主机连接某个用户对某个数据库的操作权限,这3个字段的组合构成了db表的主键。 - 权限列
Create_routine_priv和Alter_routine_priv这两个字段决定用户是否具有创建和修改存储过程的权限。
3、tables_priv表和columns_priv表
tables_priv表用来 对表设置操作权限
,columns_priv表用来对表的 某一列设置权限
。tables_priv表和columns_priv表的结构分别如图:
1 | desc mysql.tables_priv; |
tables_priv表有8个字段,分别是Host、Db、User、Table_name、Grantor、Timestamp、Table_priv和
Column_priv,各个字段说明如下:
Host 、 Db 、 User 和 Table_name
四个字段分别表示主机名、数据库名、用户名和表名。- Grantor表示修改该记录的用户。
- Timestamp表示修改该记录的时间。
Table_priv
表示对象的操作权限。包括Select、Insert、Update、Delete、Create、Drop、Grant、References、Index和Alter。- Column_priv字段表示对表中的列的操作权限,包括Select、Insert、Update和References。
1 | desc mysql.columns_priv; |
4、procs_priv表
procs_priv表可以对 存储过程和存储函数设置操作权限
,表结构如图:
1 | desc mysql.procs_priv; |
四、访问控制(了解)
1、连接核实阶段
当用户试图连接MySQL服务器时,服务器基于用户的身份以及用户是否能提供正确的密码验证身份来确定接受或者拒绝连接。即客户端用户会在连接请求中提供用户名、主机地址、用户密码,MySQL服务器接收到用户请求后,会使用user表中的host、user和authentication_string这3个字段匹配客户端提供信息
。
服务器只有在user表记录的Host和User字段匹配客户端主机名和用户名,并且提供正确的密码时才接受
连接。如果连接核实没有通过,服务器就完全拒绝访问;否则,服务器接受连接,然后进入阶段2等待用户请求
。
2、请求核实阶段
一旦建立了连接,服务器就进入了访问控制的阶段2,也就是请求核实阶段。对此连接上进来的每个请求,服务器检查该请求要执行什么操作、是否有足够的权限来执行它,这正是需要授权表中的权限列发挥作用的地方。这些权限可以来自user、db、table_priv和column_priv表。
确认权限时,MySQL首先 检查user表
,如果指定的权限没有在user表中被授予,那么MySQL就会继续 检查db表
,db表是下一安全层级,其中的权限限定于数据库层级,在该层级的SELECT权限允许用户查看指定数据库的所有表中的数据;如果在该层级没有找到限定的权限,则MySQL继续 检查tables_priv表
以及 columns_priv表
,如果所有权限表都检查完毕,但还是没有找到允许的权限操作,MySQL将 返回错误信息 ,用户请求的操作不能执行,操作失败。
提示: MySQL通过向下层级的顺序(从user表到columns_priv表)检查权限表,但并不是所有的权限都要执行该过程。例如,一个用户登录到MySQL服务器之后只执行对MySQL的管理操作,此时只涉及管理权限,因此MySQL只检查user表。另外,如果请求的权限操作不被允许,MySQL也不会继续检查下一层级的表。
五、角色管理
1、角色的理解
引入角色的目的是 方便管理拥有相同权限的用户
。恰当的权限设定,可以确保数据的安全性,这是至关重要的
。
2、创建角色
创建角色使用 CREATE ROLE
语句,语法如下:
1 | CREATE ROLE 'role_name'[@'host_name'] [,'role_name'[@'host_name']]... |
角色名称的命名规则和用户名类似。如果 host_name省略,默认为% , role_name不可省略
,不可为空。
练习:我们现在需要创建一个经理的角色,就可以用下面的代码:
1 | CREATE ROLE 'manager'@'localhost'; |
3、给角色赋予权限
创建角色之后,默认这个角色是没有任何权限的,我们需要给角色授权。给角色授权的语法结构是:
1 | GRANT privileges ON table_name TO 'role_name'[@'host_name']; |
上述语句中privileges代表权限的名称,多个权限以逗号隔开。可使用SHOW语句查询权限名称,下图列出了部分权限列表。
1 | SHOW PRIVILEGES\G; |
练习1:
我们现在想给经理角色授予商品信息表、盘点表和应付账款表的只读权限,就可以用下面的代码来实现:
1 | GRANT SELECT ON demo.settlement TO 'manager'; |
4、查看角色的权限
赋予角色权限之后,我们可以通过 SHOW GRANTS 语句,来查看权限是否创建成功了:
1 | SHOW GRANTS FOR 'manager'; |
只要你创建了一个角色
,系统就会自动默认
给你一个“ USAGE
”权限,意思是 连接登录数据库的权限
。代码的最后三行代表了我们给角色“manager”赋予的权限,也就是对商品信息表、盘点表和应付账款表的只读权限。
结果显示,库管角色拥有商品信息表的只读权限和盘点表的增删改查权限。
5、回收角色的权限
角色授权后,可以对角色的权限进行维护,对权限进行添加或撤销。添加权限使用GRANT语句,与角色授权相同。撤销角色或角色权限使用REVOKE语句。
修改了角色的权限,会影响拥有该角色的账户的权限。
撤销角色权限的SQL语法如下:
1 | REVOKE privileges ON tablename FROM 'rolename'; |
练习1:撤销school_write角色的权限。
(1)使用如下语句撤销school_write角色的权限。
1 | REVOKE INSERT, UPDATE, DELETE ON school.* FROM 'school_write'; |
(2)撤销后使用SHOW语句查看school_write对应的权限,语句如下。
1 | SHOW GRANTS FOR 'school_write'; |
6、删除角色
当我们需要对业务重新整合的时候,可能就需要对之前创建的角色进行清理,删除一些不会再使用的角色。
删除角色的操作很简单,你只要掌握语法结构就行了。
1 | DROP ROLE role [,role2]... |
注意, 如果你删除了角色,那么用户也就失去了通过这个角色所获得的所有权限 。
练习:执行如下SQL删除角色school_read。
1 | DROP ROLE 'school_read'; |
7、给用户赋予角色
角色创建并授权后,要赋给用户并处于 激活状态
才能发挥作用。给用户添加角色可使用GRANT语句,语法形式如下:
1 | GRANT role [,role2,...] TO user [,user2,...]; |
在上述语句中,role代表角色,user代表用户。可将多个角色同时赋予多个用户,用逗号隔开即可。
练习:给kangshifu用户添加角色school_read权限。
(1)使用GRANT语句给kangshifu添加school_read权限,SQL语句如下
1 | GRANT 'school_read' TO 'kangshifu'@'localhost'; |
(2)添加完成后使用SHOW语句查看是否添加成功,SQL语句如下。
1 | SHOW GRANTS FOR 'kangshifu'@'localhost'; |
(3)使用kangshifu用户登录,然后查询当前角色,如果角色未激活,结果将显示NONE。SQL语句如下。
1 | SELECT CURRENT_ROLE(); |
8、激活角色
方式1:使用set default role 命令激活角色
举例:
1 | SET DEFAULT ROLE ALL TO 'kangshifu'@'localhost'; |
举例:使用 SET DEFAULT ROLE 为下面4个用户默认激活所有已拥有的角色如下:
1 | SET DEFAULT ROLE ALL TO |
方式2:将activate_all_roles_on_login设置为ON
- 默认情况:
1 | show variables like 'activate_all_roles_on_login'; |
- 设置:
1 | SET GLOBAL activate_all_roles_on_login=ON; |
这条 SQL 语句的意思是,对 所有角色永久激活
。运行这条语句之后,用户才真正拥有了赋予角色的所有权限。
9、撤销用户的角色
撤销用户角色的SQL语法如下:
1 | REVOKE role FROM user; |
练习:撤销kangshifu用户的school_read角色。
(1)撤销的SQL语句如下
1 | REVOKE 'school_read' FROM 'kangshifu'@'localhost'; |
(2)撤销后,执行如下查询语句,查看kangshifu用户的角色信息
1 | SHOW GRANTS FOR 'kangshifu'@'localhost'; |
执行发现,用户kangshifu之前的school_read角色已被撤销。
10、设置强制角色(mandatory role)
方式1:服务启动前设置
1 | [mysqld] |
方式2:运行时设置
1 | SET PERSIST mandatory_roles = 'role1,role2@localhost,r3@%.example.com'; #系统重启后仍然有效 |
四、逻辑架构
一、逻辑架构剖析
1、服务器处理客户端请求
那服务器进程对客户端进程发送的请求做了什么处理,才能产生最后的处理结果呢?这里以查询请求为例展示:
2、Connectors
3、第1层:连接层
系统(客户端)访问 MySQL
服务器前,做的第一件事就是建立 TCP
连接。
经过三次握手建立连接成功后, MySQL
服务器对 TCP
传输过来的账号密码做身份认证、权限获取。
- 用户名或密码不对,会收到一个Access denied for user错误,客户端程序结束执行
- 用户名密码认证通过,会从权限表查出账号拥有的权限与连接关联,之后的权限判断逻辑,都将依赖于此时读到的权限
TCP
连接收到请求后,必须要分配给一个线程专门与这个客户端的交互。所以还会有个线程池,去走后
面的流程。每一个连接从线程池中获取线程,省去了创建和销毁线程的开销。
4、 第2层:服务层
SQL Interface: SQL接口
- 接收用户的SQL命令,并且返回用户需要查询的结果。比如SELECT … FROM就是调用SQLInterface
- MySQL支持DML(数据操作语言)、DDL(数据定义语言)、存储过程、视图、触发器、自定义函数等多种SQL语言接口
Parser: 解析器
- 在解析器中对 SQL 语句进行
语法分析
、语义分析
。将SQL语句分解成数据结构,并将这个结构传递到后续步骤,以后SQL语句的传递和处理就是基于这个结构的。如果在分解构成中遇到错误,那么就说明这个SQL语句是不合理的。 - 在SQL命令传递到解析器的时候会被解析器验证和解析,并为其创建
语法树
,并根据数据字典丰富查询语法树,会验证
该客户端是否具有执行该查询的权限
。创建好语法树后,MySQL还会对SQl查询进行语法上的优化,进行查询重写。
- 在解析器中对 SQL 语句进行
Optimizer: 查询优化器
- SQL语句在语法解析之后、查询之前会使用查询优化器确定 SQL 语句的执行路径,生成一个
执行计划
。 - 这个执行计划表明应该
使用哪些索引
进行查询(全表检索还是使用索引检索),表之间的连接顺序如何,最后会按照执行计划中的步骤调用存储引擎提供的方法来真正的执行查询,并将查询结果返回给用户。 - 它使用“
选取-投影-连接
”策略进行查询。例如:
1
2SELECT id,name FROM student WHERE gender = '女';
1这个SELECT查询先根据WHERE语句进行
选取
,而不是将表全部查询出来以后再进行gender过滤。 这个SELECT查询先根据id和name进行属性投影
,而不是将属性全部取出以后再进行过滤,将这两个查询条件连接
起来生成最终查询结果。- SQL语句在语法解析之后、查询之前会使用查询优化器确定 SQL 语句的执行路径,生成一个
Caches & Buffers: 查询缓存组件
- MySQL内部维持着一些Cache和Buffer,比如Query Cache用来缓存一条SELECT语句的执行结果,如果能够在其中找到对应的查询结果,那么就不必再进行查询解析、优化和执行的整个过程了,直接将结果反馈给客户端。
- 这个缓存机制是由一系列小缓存组成的。比如表缓存,记录缓存,key缓存,权限缓存等 。
- 这个查询缓存可以在
不同客户端之间共享
。 - 从MySQL 5.7.20开始,不推荐使用查询缓存,并在
MySQL 8.0中删除
。
小故事:
如果我问你9+8×16-3×2×17的值是多少,你可能会用计算器去算一下,最终结果35。如果再问你一遍9+8×16-3×2×17的值是多少,你还用再傻呵呵的再算一遍吗?我们刚刚已经算过了,直接说答案就好了。
5、第3层:引擎层
插件式存储引擎层( Storage Engines),真正的负责了MySQL中数据的存储和提取,对物理服务器级别维护的底层数据执行操作,服务器通过API与存储引擎进行通信。不同的存储引擎具有的功能不同,这样我们可以根据自己的实际需要进行选取。
MySQL 8.0.25默认支持的存储引擎如下:
6、存储层
所有的数据,数据库、表的定义,表的每一行的内容,索引,都是存在 文件系统
上,以文件
的方式存在的,并完成与存储引擎的交互。当然有些存储引擎比如InnoDB,也支持不使用文件系统直接管理裸设备,但现代文件系统的实现使得这样做没有必要了。在文件系统之下,可以使用本地磁盘,可以使用DAS、NAS、SAN等各种存储系统。
7、小结
以上简化如下:
简化为三层结构:
- 连接层:客户端和服务器端建立连接,客户端发送 SQL 至服务器端;
- SQL 层(服务层):对 SQL 语句进行查询处理;与数据库文件的存储方式无关;
- 存储引擎层:与数据库文件打交道,负责数据的存储和读取。
二、SQL执行流程
1、MySQL 中的 SQL执行流程
MySQL的查询流程:
- 查询缓存:Server 如果在查询缓存中发现了这条 SQL 语句,就会直接将结果返回给客户端;如果没有,就进入到解析器阶段。需要说明的是,因为查询缓存往往效率不高,所以在 MySQL8.0 之后就抛弃了这个功能。
大多数情况查询缓存就是个鸡肋,为什么呢?
1 | SELECT employee_id,last_name FROM employees WHERE employee_id = 101; |
查询缓存是提前把查询结果缓存起来,这样下次不需要执行就可以直接拿到结果。需要说明的是,在MySQL 中的查询缓存,不是缓存查询计划,而是查询对应的结果。这就意味着查询匹配的 鲁棒性大大降低
,只有 相同的查询操作才会命中查询缓存
。两个查询请求在任何字符上的不同(例如:空格、注释、大小写),都会导致缓存不会命中。因此 MySQL 的 查询缓存命中率不高 。
同时,如果查询请求中包含某些系统函数、用户自定义变量和函数、一些系统表,如 mysql 、information_schema、 performance_schema 数据库中的表,那这个请求就不会被缓存。以某些系统函数举例,可能同样的函数的两次调用会产生不一样的结果,比如函数 NOW
,每次调用都会产生最新的当前时间,如果在一个查询请求中调用了这个函数,那即使查询请求的文本信息都一样,那不同时间的两次查询也应该得到不同的结果,如果在第一次查询时就缓存了,那第二次查询的时候直接使用第一次查询的结果就是错误的!
此外,既然是缓存,那就有它 缓存失效的时候
。MySQL的缓存系统会监测涉及到的每张表,只要该表的结构或者数据被修改,如对该表使用了INSERT 、 UPDATE 、 DELETE 、 TRUNCATE TABLE 、 ALTER TABLE 、 DROP TABLE 或 DROP DATABASE 语句
,那使用该表的所有高速缓存查询都将变为无效并从高速缓存中删除!对于 更新压力大的数据库
来说,查询缓存的命中率会非常低
。
- 解析器:在解析器中对 SQL 语句进行
语法分析
、语义分析
。
分析器先做“词法分析
”。
你输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。 MySQL 从你输入的”select”这个关键字识别出来,这是一个查询语
句。它也要把字符串“T”识别成“表名 T”,把字符串“ID”识别成“列 ID”。
接着,要做“语法分析
”。根据词法分析的结果,语法分析器(比如:Bison)会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法
。
select department_id,job_id,avg(salary) from employees group by department_id;
如果SQL语句正确,则会生成一个这样的语法树
: - 优化器:在优化器中会确定 SQL 语句的执行路径,比如是根据
全表检索
,还是根据索引检索
等。
举例:如下语句是执行两个表的 join:
1 | select * from test1 join test2 using(ID) |
- 方案1:可以先从表 test1 里面取出 name=’zhangwei’的记录的 ID 值,再根据 ID 值关联到表 test2
再判断 test2 里面 name的值是否等于 ‘mysql高级课程’。- 方案2:可以先从表 test2 里面取出 name=‘mysql高级课程’ 的记录的 ID 值,再根据 ID 值关联到 test1,再判断 test1 里面 name的值是否等于 zhangwei。这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。优化器阶段完成后,这个语句的执行方案就确定下来了,然后进入执行器阶段。
在查询优化器中,可以分为 逻辑查询
优化阶段和 物理查询
优化阶段
- 执行器:
截止到现在,还没有真正去读写真实的表,仅仅只是产出了一个执行计划。于是就进入了执行器阶段
。
在执行之前需要判断该用户是否具备权限
。如果没有,就会返回权限错误。如果具备权限,就执行 SQL查询并返回结果。在 MySQL8.0 以下的版本,如果设置了查询缓存,这时会将查询结果进行缓存。
1 | select * from test where id=1; |
比如:表 test 中,ID 字段没有索引,那么执行器的执行流程是这样的:
调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是1,如果不是则跳过,如果是则将这行存在结果集中;调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
至此,这个语句就执行完成了。对于有索引的表,执行的逻辑也差不多。
SQL 语句在 MySQL 中的流程是:SQL语句→查询缓存→解析器→优化器→执行器。
2、MySQL8中SQL执行原理
profiling 为记录你执行sql的记录
①确认profiling 是否开启
1 | select @@profiling; |
profiling=0 代表关闭,我们需要把 profiling 打开,即设置为 1:
1 | set profiling=1; |
②多次执行相同SQL查询
然后我们执行一个 SQL 查询(你可以执行任何一个 SQL 查询):
1 | select * from employees; |
③查看profiles
查看当前会话所产生的所有 profiles:
1 | show profiles; # 显示最近的几次查询 |
④查看profile
显示执行计划,查看程序的执行步骤:
1 | show profile; |
当然你也可以查询指定的 Query ID,比如:
1 | show profile for query 7; |
查询 SQL 的执行时间结果和上面是一样的。
此外,还可以查询更丰富的内容:
1 | show profile cpu,block io for query 6; |
1 | show profile cpu,block io for query 7; |
3、MySQL5.7中SQL执行原理
上述操作在MySQL5.7中测试,发现前后两次相同的sql语句,执行的查询过程仍然是相同的。
不是会使用缓存吗?这里我们需要 显式开启查询缓存模式
。在MySQL5.7中如下设置:
① 配置文件中开启查询缓存
在 /etc/my.cnf 中新增一行:
1 | query_cache_type=1 |
②重启mysql服务
1 | systemctl restart mysqld |
③开启查询执行计划
由于重启过服务,需要重新执行如下指令,开启profiling。
1 | set profiling=1; |
④执行语句两次
1 | select * from locations; |
⑤查看profiles
⑥查看profile
显示执行计划,查看程序的执行步骤:
1 | show profile for query 1; |
1 | show profile for query 2; |
结论不言而喻。执行编号2时,比执行编号1时少了很多信息,从截图中可以看出查询语句直接从缓存中获取数据。
4、SQL语法顺序
随着Mysql版本的更新换代,其优化器也在不断的升级,优化器会分析不同执行顺序产生的性能消耗不同而动态调整执行顺序。
需求:查询每个部门年龄高于20岁的人数且高于20岁人数不能少于2人,显示人数最多的第一名部门信息
下面是经常出现的查询顺序:
5、Oracle中的SQL执行流程(了解)
Oracle 中采用了共享池
来判断 SQL 语句是否存在缓存和执行计划,通过这一步骤我们可以知道应该采用硬解析还是软解析。
我们先来看下 SQL 在 Oracle 中的执行过程:
从上面这张图中可以看出,SQL 语句在 Oracle 中经历了以下的几个步骤。
1.语法检查:检查 SQL 拼写是否正确,如果不正确,Oracle 会报语法错误。
2.语义检查:检查 SQL 中的访问对象是否存在。比如我们在写 SELECT 语句的时候,列名写错了,系统就会提示错误。语法检查和语义检查的作用是保证 SQL 语句没有错误。
3.权限检查:看用户是否具备访问该数据的权限。
4.共享池检查:共享池(Shared Pool)是一块内存池,最主要的作用是缓存 SQL 语句和该语句的执行计划
。Oracle 通过检查共享池是否存在 SQL 语句的执行计划,来判断进行软解析,还是硬解析。那软解析和硬解析又该怎么理解呢?
在共享池中,Oracle 首先对 SQL 语句进行 Hash 运算
,然后根据 Hash 值在库缓存(Library Cache)中查找,如果 存在 SQL 语句的执行计划
,就直接拿来执行,直接进入“执行器”的环节,这就是 软解析
。
如果没有找到 SQL 语句和执行计划,Oracle 就需要创建解析树进行解析,生成执行计划,进入“优化器”这个步骤,这就是 硬解析
。
\5. 优化器:优化器中就是要进行硬解析,也就是决定怎么做,比如创建解析树,生成执行计划。
\6. 执行器:当有了解析树和执行计划之后,就知道了 SQL 该怎么被执行,这样就可以在执行器中执
行语句了。
共享池
是 Oracle 中的术语,包括了库缓存,数据字典缓冲区等。我们上面已经讲到了库缓存区,它主要
缓存 SQL 语句和执行计划。而 数据字典缓冲区
存储的是 Oracle 中的对象定义,比如表、视图、索引等对象。当对 SQL 语句进行解析的时候,如果需要相关的数据,会从数据字典缓冲区中提取。
库缓存 这一个步骤,决定了 SQL 语句是否需要进行硬解析。为了提升 SQL 的执行效率,我们应该尽量避免硬解析,因为在 SQL 的执行过程中,创建解析树,生成执行计划是很消耗资源的。
你可能会问,如何避免硬解析,尽量使用软解析呢?在 Oracle 中, 绑定变量
是它的一大特色。绑定变量就是在 SQL 语句中使用变量,通过不同的变量取值来改变 SQL 的执行结果。这样做的好处是能 提升软解析的可能性
,不足之处在于可能会导致生成的执行计划不够优化,因此是否需要绑定变量还需要视情况而定。
举个例子,我们可以使用下面的查询语句:
1 | select * from player where player_id = 10001; |
你也可以使用绑定变量,如:
1 | select * from player where player_id = :player_id; |
这两个查询语句的效率在 Oracle 中是完全不同的。
如果你在查询 player_id = 10001 之后,还会查询10002、10003 之类的数据,那么每一次查询都会创建一个新的查询解析。而第二种方式使用了绑定变量,那么在第一次查询之后,在共享池中就会存在这类查询的执行计划,也就是软解析。
因此,我们可以通过使用绑定变量来减少硬解析,减少 Oracle 的解析工作量
。但是这种方式也有缺点,使用动态 SQL 的方式,因为参数不同,会导致 SQL 的执行效率不同,同时 SQL 优化也会比较困难。
Oracle的架构图:
小结:
Oracle 和 MySQL 在进行 SQL 的查询上面有软件实现层面的差异。Oracle 提出了共享池的概念,通共享池来判断是进行软解析,还是硬解析。
三、数据库缓冲池(buffer pool)
InnoDB
存储引擎是以页为单位来管理存储空间的,我们进行的增删改查操作其实本质上都是在访问页
面(包括读页面、写页面、创建新页面等操作)。而磁盘 I/O 需要消耗的时间很多,而在内存中进行操
作,效率则会高很多,为了能让数据表或者索引中的数据随时被我们所用,DBMS 会申请 占用内存来作为数据缓冲池
,在真正访问页面之前,需要把在磁盘上的页缓存到内存中的 Buffer Pool
之后才可以访问。
这样做的好处是可以让磁盘活动最小化,从而 减少与磁盘直接进行 I/O 的时间
。
要知道,这种策略对提升 SQL 语句的查询性能来说至关重要。如果索引的数据在缓冲池里,那么访问的成本就会降低很多。
1、缓冲池 vs 查询缓存
缓冲池和查询缓存是一个东西吗?不是。
①缓冲池(Buffer Pool)
首先我们需要了解在 InnoDB 存储引擎中,缓冲池都包括了哪些。
在 InnoDB 存储引擎中有一部分数据会放到内存中,缓冲池则占了这部分内存的大部分,它用来存储各种数据的缓存,如下图所示:
从图中,你能看到 InnoDB 缓冲池包括了数据页、索引页、插入缓冲、锁信息、自适应 Hash 和数据字典信息等。
缓存池的重要性:
缓存原则:
“ 位置 * 频次 ”
这个原则,可以帮我们对 I/O 访问效率进行优化。
首先,位置决定效率,提供缓冲池就是为了在内存中可以直接访问数据。
其次,频次决定优先级顺序。因为缓冲池的大小是有限的,比如磁盘有 200G,但是内存只有 16G,缓冲池大小只有 1G,就无法将所有数据都加载到缓冲池里,这时就涉及到优先级顺序,会 优先对使用频次高的热数据进行加载
。
缓冲池的预读特性:
②查询缓存
那么什么是查询缓存呢?
查询缓存是提前把 查询结果缓存
起来,这样下次不需要执行就可以直接拿到结果。需要说明的是,在MySQL 中的查询缓存,不是缓存查询计划,而是查询对应的结果。
因为命中条件苛刻,而且只要数据表发生变化,查询缓存就会失效,因此命中率低。
2、缓冲池如何读取数据
缓冲池管理器会尽量将经常使用的数据保存起来,在数据库进行页面读操作的时候,首先会判断该页面是否在缓冲池中,如果存在就直接读取,如果不存在,就会通过内存或磁盘将页面存放到缓冲池中再进行读取。缓存在数据库中的结构和作用如下图所示:
如果我们执行 SQL 语句的时候更新了缓存池中的数据,那么这些数据会马上同步到磁盘上吗?
3、 查看/设置缓冲池的大小
如果你使用的是 InnoDB 存储引擎,可以通过查看 innodb_buffer_pool_size 变量来查看缓冲池的大小。命令如下:
1 | show variables like 'innodb_buffer_pool_size'; |
你能看到此时 InnoDB 的缓冲池大小只有 134217728/1024/1024=128MB。我们可以修改缓冲池大小,比如改为256MB,方法如下:
1 | set global innodb_buffer_pool_size = 268435456; |
或者配置文件
:
1 | [server] |
然后再来看下修改后的缓冲池大小,此时已成功修改成了 256 MB:
4、多个Buffer Pool实例(缓冲池)
Buffer Pool本质是InnoDB向操作系统申请的一块连续的内存空间,在多线程环境下,访问Buffer Pool中的数
据都需要加锁处理。在Buffer Pool特别大而且多线程并发访问特别高的情况下,单一的Buffer Pool可能会
影响请求的处理速度。所以在Buffer Pool特别大的时候,我们可以把它们拆分成若干个小的Buffer Pool,每
个Buffer Pool都称为一个实例,它们都是独立的,独立的去申请内存空间,独立的管理各种链表。所以在多线
程并发访问时并不会相互影响,从而提高并发处理能力。
我们可以在服务器启动的时候通过设置 innodb_buffer_pool_instances的值来修改Buffer Pool实例的个数比方说这样:
1 | [server] |
这样就表明我们要创建2个 Buffer Pool
实例。
我们看下如何查看缓冲池的个数,使用命令:
1 | show variables like 'innodb_buffer_pool_instances'; |
那每个 Buffer Pool
实例实际占多少内存空间呢?其实使用这个公式算出来的:
1 | innodb_buffer_pool_size/innodb_buffer_pool_instances |
也就是总共的大小除以实例的个数,结果就是每个Buffer Pool
实例占用的大小。
5、引申问题
Buffer Pool是MySQL内存结构中十分核心的一个组成,你可以先把它想象成一个黑盒子。
黑盒下的更新数据流程
当我们查询数据的时候,会先去Buffer Pool中查询。如果Buffer Pool中不存在,存储引擎会先将数据从磁盘
加载到Buffer Pool中,然后将数据返回给客户端;同理,当我们更新某个数据的时候,如果这个数据不存在于
BufferPool,同样会先数据加载进来,然后修改修改内存的数据。被修改过的数据会在之后统一刷入磁盘。
这个过程看似没啥问题,实则是有问题的。假设我们修改Buffer Pool中的数据成功,但是还没来得及将数据
刷入磁盘MySQL就挂了怎么办?按照上图的逻辑,此时更新之后的数据只存在于Buffer Pool中,如果此时
MySQL宕机了,这部分数据将会永久地丢失;再者,我更新到一半突然发生错误了,想要回滚到更新之前的版
本,该怎么办?连数据持久化的保证、事务回滚都做不到还谈什么崩溃恢复?
答案:Redo Log & Undo Log
##五、存储引擎
一、查看存储引擎
- 查看mysql提供什么存储引擎:
1 | show engines; |
显式如下:
1 | show engines \G; |
二、设置系统默认的存储引擎
- 查看默认的存储引擎:
1 | show variables like '%storage_engine%'; |
- 修改默认的存储引擎
如果在创建表的语句中没有显式指定表的存储引擎的话,那就会默认
使用InnoDB
作为表的存储引擎。
如果我们想改变表的默认存储引擎的话,可以这样写启动服务器的命令行:
1 | SET DEFAULT_STORAGE_ENGINE=MyISAM; |
或者修改my.cnf
文件:
1 | default-storage-engine=MyISAM |
三、设置表的存储引擎
存储引擎是负责对表中的数据进行提取和写入工作的,我们可以为 不同的表设置不同的存储引擎
,也就是
说不同的表可以有不同的物理存储结构,不同的提取和写入方式。
1、创建表时指定存储引擎
我们之前创建表的语句都没有指定表的存储引擎,那就会使用默认的存储引擎InnoDB
。如果我们想显式的指定一下表的存储引擎,那可以这么写:
1 | CREATE TABLE 表名( |
2、修改表的存储引擎
如果表已经建好了,我们也可以使用下边这个语句来修改表的存储引擎:
1 | ALTER TABLE 表名 ENGINE = 存储引擎名称; |
比如我们修改一下 engine_demo_table
表的存储引擎:
1 | mysql> ALTER TABLE engine_demo_table ENGINE = InnoDB; |
这时我们再查看一下engine_demo_table
的表结构:
1 | mysql> SHOW CREATE TABLE engine_demo_table\G |
四、引擎介绍
1、InnoDB 引擎:具备外键支持功能的事务存储引擎
MySQL从3.23.34a开始就包含InnoDB存储引擎。
大于等于5.5之后,默认采用InnoDB引擎
。InnoDB是MySQL的
默认事务型引擎
,它被设计用来处理大量的短期(short-lived)事务。可以确保事务的完整提交(Commit)和回滚(Rollback)。除了增加和查询外,还需要更新、删除操作,那么,应优先选择InnoDB存储引擎。
除非有非常特别的原因需要使用其他的存储引擎,否则应该优先考虑InnoDB引擎。
数据文件结构:
- 表名.frm 存储表结构(MySQL8.0时,合并在表名.ibd中)
- 表名.ibd 存储数据和索引
InnoDB是
为处理巨大数据量的最大性能设计
.- 在以前的版本中,字典数据以元数据文件、非事务表等来存储。现在这些元数据文件被删除了。比如:
.frm , .par , .trn , .isl , .db.opt
等都在MySQL8.0中不存在了。
- 在以前的版本中,字典数据以元数据文件、非事务表等来存储。现在这些元数据文件被删除了。比如:
对比MyISAM的存储引擎,
InnoDB写的处理效率差一些
,并且会占用更多的磁盘空间以保存数据和索引。MyISAM只缓存索引,不缓存真实数据;InnoDB不仅缓存索引还要缓存真实数据,
对内存要求较高
,而且内存大小对性能有决定性的影响。
2、MyISAM 引擎:主要的非事务处理存储引擎
- MyISAM提供了大量的特性,包括全文索引、压缩、空间函数(GIS)等,但MyISAM
不支持事务、行级锁、外键
,有一个毫无疑问的缺陷就是崩溃后无法安全恢复
。 5.5之前默认的存储引擎
- 优势是访问的
速度快
,对事务完整性没有要求或者以SELECT、INSERT为主的应用 - 针对数据统计有额外的常数存储。故而 count(*) 的查询效率很高
- 数据文件结构:
- 表名.frm 存储表结构
- 表名.MYD 存储数据 (MYData)
- 表名.MYI 存储索引 (MYIndex)
- 应用场景:只读应用或者以读和insert为主的业务
3、Archive 引擎:用于数据存档
archive是归档的意思,仅仅支持插入和查询 两种功能(行被插入后不能再修改)。
在MySQL5.5以后 支持索引 功能。
拥有很好的压缩机制,使用zlib压缩库,在记录请求的时候实时的进行压缩,经常被用来作为仓库使用。
创建ARCHIVE表时,存储引擎会创建名称以表名开头的文件。数据文件的扩展名为**.ARZ**。
根据英文的测试结论来看,同样数据量下,Archive表比MyISAM表要小大约75%,比支持事务处理的InnoDB表小大约83%。
ARCHIVE存储引擎采用了 行级锁。该ARCHIVE引擎支持 AUTO_INCREMENT列属性。AUTO_INCREMENT列可以
具有唯一索引或非唯一索引。尝试在任何其他列上创建索引会导致错误。
Archive表适合日志和数据采集(档案)类应用;适合存储大量的独立的作为历史记录的数据。拥有很高的插入速度,但是对查询的支持较差。
下表展示了ARCHIVE 存储引擎功能
4、Blackhole 引擎:丢弃写操作,读操作会返回空内容
- Blackhole引擎没有实现任何存储机制,它会丢弃所有插入的数据不做任何保存。
- 但服务器会记录Blackhole表的日志,所以可以用于复制数据到备库,或者简单地记录到日志。但这种应用方式会碰到很多问题,因此并不推荐。
5、CSV 引擎:存储数据时,以逗号分隔各个数据项
CSV引擎可以将 普通的CSV文件作为MySQL的表来处理,但不支持索引。
CSV引擎可以作为一种数据交换的机制,非常有用。
CSV存储的数据直接可以在操作系统里,用文本编辑器,或者excel读取。
对于数据的快速导入、导出是有明显优势的。
创建CSV表时,服务器会创建一个纯文本数据文件,其名称以表名开头并带有.CSV扩展名。当你将数据存储到表中时,存储引擎将其以逗号分隔值格式保存到数据文件中。
案例如下
1 | mysql> CREATE TABLE test (i INT NOT NULL, c CHAR(10) NOT NULL) ENGINE = CSV; |
创建CSV表还会创建相应的 元文件 ,用于存储表的状态
和 表中存在的行数
。此文件的名称与表的名称相同,后缀为CSM
。如图所示
如果检查test.CSV
通过执行上述语句创建的数据库目录中的文件,其内容使用Notepad++
打开如下:
1 | "1","record one" |
这种格式可以被Microsoft Excel
等电子表格应用程序读取,甚至写入。使用Microsoft Excel打开如图所示
6、Memory 引擎:置于内存的表
概述:
Memory采用的逻辑介质是内存
,响应速度很快
,但是当mysqld守护进程崩溃的时候数据会丢失
。另外,要求存储的数据是数据长度不变的格式,比如,Blob和Text类型的数据不可用(长度不固定的)。主要特征:
- Memory同时
支持哈希(HASH)索引
和B+树索引
。 - Memory表至少比MyISAM表要
快一个数量级
。 - MEMORY
表的大小是受到限制
的。表的大小主要取决于两个参数,分别是max_rows
和max_heap_table_size
。其中,max_rows可以在创建表时指定;max_heap_table_size的大小默认为16MB,可以按需要进行扩大。 - 数据文件与索引文件分开存储。
- 缺点:其数据易丢失,生命周期短。基于这个缺陷,选择MEMORY存储引擎时需要特别小心。
- Memory同时
使用Memory存储引擎的场景:
目标数据比较小
,而且非常频繁的进行访问
,在内存中存放数据,如果太大的数据会造成内存溢出
。可以通过参数max_heap_table_size
控制Memory表的大小,限制Memory表的最大的大小。- 如果
数据是临时的
,而且必须立即可用
得到,那么就可以放在内存中。
1
2
3
4
5
6| 1 | record one |
| 2 | record two |
+---+------------+
2 rows in set (0.00 sec)
"1","record one"
"2","record two"存储在Memory表中的数据如果突然间
丢失的话也没有太大的关系
。
7、Federated 引擎:访问远程表
Federated引擎是访问其他MySQL服务器的一个 代理
,尽管该引擎看起来提供了一种很好的 跨服务器的灵活性
,但也经常带来问题,因此 默认是禁用的
。
8、 Merge引擎:管理多个MyISAM表构成的表集合
9、NDB引擎:MySQL集群专用存储引擎
也叫做 NDB Cluster 存储引擎,主要用于MySQL Cluster 分布式集群
环境,类似于 Oracle 的 RAC 集群。
10、引擎对比
MySQL中同一个数据库,不同的表可以选择不同的存储引擎。如下表对常用存储引擎做出了对比。
特点 | MyISAM | InnoDB | MEMORY | MERGE | NDB |
---|---|---|---|---|---|
存储限制 | 有 | 64TB | 有 | 没有 | 有 |
事务 | 支持 | ||||
锁机制 | 表锁 ,即使操作一条记录也会锁住整个表,不适合高并发的操作 |
行锁 ,操作时只锁某一行,不对其它行有影响,适合高并发的操作 |
表锁 |
表锁 |
行锁 |
B树索引 | 支持 | 支持 | 支持 | 支持 | 支持 |
哈希索引 | 支持 | 支持 | |||
全文索引 | 支持 | ||||
集群索引 | 支持 | ||||
数据缓存 | 支持 | 支持 | 支持 | ||
索引缓存 | 只缓存索引,不缓存真实数据 | 不仅缓存索引还要缓存真实数据,对内存要求较高,而且内存大小对性能有决定性的影响 | 支持 | 支持 | 支持 |
数据可压缩 | 支持 | ||||
空间使用 | 低 | 高 | N/A | 低 | 低 |
内存使用 | 低 | 高 | 中等 | 低 | 高 |
批量插入的速度 | 高 | 低 | 高 | 高 | 高 |
支持外键 | 支持 |
五、 MyISAM和InnoDB的区别
MySQL5.5之前的默认存储引擎是MyISAM,5.5之后改为了InnoDB。
六、索引的数据结构 -mysql
索引的数据结构
一、为什么使用索引
假如给数据使用二叉树
这样的数据结构进行存储,可以通过更少的查询次数完成查询,如下图所示
对字段Col 2添加了索引,就相当于在硬盘上为Col 2维护了一个索引的数据结构,即这个二叉搜索树。二叉搜
索树的每个结点存储的是**(K,V)结构**,key是Col 2,value是该key所在行的文件指针(地址)。比如:该二叉搜
索树的根节点就是:(34,0x07)。现在对Col 2添加了索引,这时再去查找Col 2=89这条记录的时候会先去查
找该二叉搜索树(二叉树的遍历查找)。读34到内存,89>34;继续右侧数据,读89到内存,89 == 89;找到数
据返回。找到之后就根据当前结点的value快速定位到要查找的记录对应的地址。我们可以发现,只需要查
找两次就可以定位到记录的地址,查询速度就提高了。
这就是我们为什么要建索引,目的就是为了减少磁盘I/O的次数,加快查询速率。
二、索引及其优缺点
1、索引概述
MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构
。
索引的本质
:索引是数据结构。你可以简单理解为“排好序的快速查找数据结构”,满足特定查找算法。这些数据结构以某种方式指向数据, 这样就可以在这些数据结构的基础上实现 高级查找算法
。
索引是在存储引擎中实现的,因此每种存储引擎的索引不一定完全相同,并且每种存储引擎不一定支持所有索引类型。同时,存储引擎可以定义每个表的最大索引数和最大索引长度。所有存储引擎支持每个表至少16个索引,总索引长度至少为256字节。有些存储引擎支持更多的索引数和更大的索引长度。
2、 优点
(1)类似大学图书馆建书目索引,提高数据检索的效率,降低
数据库的IO成本
,这也是创建索引最主要的原因。
(2)通过创建唯一索引,可以保证数据库表中每一行数据的唯一性
。
(3)在实现数据的参考完整性方面,可以加速表和表之间的连接
。换句话说,对于有依赖关系的子表和父表联合查询时,可以提高查询速度
。
(4)在使用分组和排序子句进行数据查询时,可以显著 减少查询中分组和排序的时间 ,降低了CPU的消耗
。
3、缺点
(1)创建索引和维护索引要
耗费时间
,并且随着数据量的增加,所耗费的时间也会增加。
(2)索引需要占磁盘空间
,除了数据表占数据空间之外,每一个索引还要占一定的物理空间, 存储在磁盘上 ,如果有大量的索引,索引文件就可能比数据文件更快达到最大文件尺寸。
(3)虽然索引大大提高了查询速度,同时却会降低更新表的速度
。当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,这样就降低了数据的维护速度。
因此,选择使用索引时,需要综合考虑索引的优点和缺点。
三、InnoDB中索引的推演
1、索引之前的查找
先来看一个精确匹配的例子:
1 | SELECT [列名列表] FROM 表名 WHERE 列名 = xxx; |
①在一个页中的查找
假设目前表中的记录比较少,所有的记录都可以被存放到一个页中,在查找记录的时候可以根据搜索条件的不同,分为两种情况:
- 以主键为搜索条件
可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记
录。
- 以其他列作为搜索条件
因为在数据页中并没有对非主键列建立所谓的页目录,所以我们无法通过二分法快速定位相应的槽。这种情
况下只能从 最小记录 开始 依次遍历 单链表中的每条记录,然后对比每条记录是不是符合搜索条件。很显然,这种查找的效率是非常低的。
②在很多页中查找
大部分情况下我们表中存放的记录都是非常多的,需要好多的数据页来存储这些记录。在很多页中查找记录的话可以分为两个步骤:
- 定位到记录所在的页。
- 从所在的页内中查找相应的记录。
在没有索引的情况下,不论是根据主键列或者其他列的值进行查找,由于我们并不能快速的定位到记录
所在的页,所以只能 从第一个页
沿着 双向链表
一直往下找,在每一个页中根据我们上面的查找方式去查找指定的记录。因为要遍历所有的数据页,所以这种方式显然是 超级耗时
的。如果一个表有一亿条记录呢?此时 索引
应运而生。
2、设计索引
建一个表:
1 | mysql> CREATE TABLE index_demo( |
这个新建的index_demo
表中有2个INT类型的列,1个CHAR(1)类型的列,而且我们规定了c1列为主键,这个表使用 Compact
行格式来实际存储记录的。这里我们简化了index_demo表的行格式示意图:
我们只在示意图里展示记录的这几个部分:
record_type
:记录头信息的一项属性,表示记录的类型, 0 表示普通记录、 2 表示最小记录、 3 表示最大记录、 1 暂时还没用过,下面讲。next_record
:记录头信息的一项属性,表示下一条地址相对于本条记录的地址偏移量,我们用箭头来表明下一条记录是谁。各个列的值
:这里只记录在index_demo
表中的三个列,分别是c1 、 c2 和 c3
。其他信息
:除了上述3种信息以外的所有信息,包括其他隐藏列的值以及记录的额外信息。
将记录格式示意图的其他信息项暂时去掉并把它竖起来的效果就是这样:
把一些记录放到页里的示意图就是:
①一个简单的索引设计方案
我们在根据某个搜索条件查找一些记录时为什么要遍历所有的数据页呢?因为各个页中的记录并没有规
律,我们并不知道我们的搜索条件匹配哪些页中的记录,所以不得不依次遍历所有的数据页。所以如果我们 想快速的定位到需要查找的记录在哪些数据页
中该咋办?我们可以为快速定位记录所在的数据页而建立一个目录
,建这个目录必须完成下边这些事:
- 下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值。
- 给所有的页建立一个目录项。
所以我们为上边几个页做好的目录就像这样子:
以 页28
为例,它对应 目录项2
,这个目录项中包含着该页的页号 28
以及该页中用户记录的最小主键值 5
。我们只需要把几个目录项在物理存储器上连续存储(比如:数组),就可以实现根据主键值快速查找某条记录的功能了。比如:查找主键值为 20
的记录,具体查找过程分两步:
- 先从目录项中根据
二分法
快速确定出主键值为 20 的记录在目录项3
中(因为12 < 20 < 209
),它对应的页是页9
。 - 再根据前边说的在页中查找记录的方式去
页9
中定位具体的记录。
至此,针对数据页做的简易目录就搞定了。这个目录有一个别名,称为 索引
。
②InnoDB中的索引方案
迭代1次:目录项纪录的页
我们把前边使用到的目录项放到数据页中的样子就是这样:
从图中可以看出来,我们新分配了一个编号为30的页来专门存储目录项记录。这里再次强调 目录项记录
和普通的 用户记录
的不同点:
目录项记录
的record_type
值是1,而普通用户记录
的record_type
值是0。- 目录项记录只有
主键值和页的编号
两个列,而普通的用户记录的列是用户自己定义的,可能包含很多列
,另外还有InnoDB自己添加的隐藏列。 - 了解:记录头信息里还有一个叫
min_rec_mask
的属性,只有在存储目录项记录
的页中的主键值 - 最小的
目录项记录
的min_rec_mask
值为1
,其他别的记录的min_rec_mask
值都是0
。
相同点:两者用的是一样的数据页,都会为主键值生成 Page Directory
(页目录),从而在按照键
值进行查找时可以使用 二分法
来加快查询速度。
现在以查找主键为 20
的记录为例,根据某个主键值去查找记录的步骤就可以大致拆分成下边两步:
- 先到存储
目录项记录
的页,也就是页30中通过二分法
快速定位到对应目录项,因为12 < 20 < 209
,所以定位到对应的记录所在的页就是页9。 - 再到存储用户记录的页9中根据
二分法
快速定位到主键值为20
的用户记录。
迭代2次:多个目录项纪录的页
虽然说 目录项记录 中只存储主键值和对应的页号,比用户记录需要的存储空间小多了,但是不论怎么说一个页只
有16KB大小,能存放的目录项记录也是有限的,那如果表中的数据太多,以至于一个数据页不足以存放所有的
目录项记录,如何处理呢?
这里我们假设一个存储目录项记录的页最多只能存放4条目录项记录,所以如果此时我们再向上图中插入一条主键值为320的用户记录的话,那就需要分配一个新的存储目录项记录的页:
从图中可以看出,我们插入了一条主键值为320的用户记录之后需要两个新的数据页:
- 为存储该用户记录而新生成了
页31
。 - 因为原先存储目录项记录的
页30的容量已满
(我们前边假设只能存储4条目录项记录),所以不得不需要一个新的页32
来存放页31
对应的目录项。
现在因为存储目录项记录的页不止一个,所以如果我们想根据主键值查找一条用户记录大致需要3个步骤,以查找主键值为 20
的记录为例:
- 确定
目录项记录页
我们现在的存储目录项记录的页有两个,即页30
和页32
,又因为页30表示的目录项的主键值的范围是[1, 320)
,页32表示的目录项的主键值不小于320
,所以主键值为20
的记录对应的目录项记录在页30
中。 - 通过目录项记录页
确定用户记录真实所在的页
。
在一个存储目录项记录
的页中通过主键值定位一条目录项记录的方式说过了。 - 在真实存储用户记录的页中定位到具体的记录。
迭代3次:目录项记录页的目录页
问题来了,在这个查询步骤的第1步中我们需要定位存储目录项记录的页,但是这些页是不连续的,如果我们表中
的数据非常多则会产生很多存储目录项记录的页,那我们怎么根据主键值快速定位一个存储目录项记录的页呢?那就为这些存储目录项记录的页再生成一个更高级的目录,就像是一个多级目录一样,大目录里嵌套小目录,小目
录里才是实际的数据,所以现在各个页的示意图就是这样子:
如图,我们生成了一个存储更高级
目录项的页33
,这个页中的两条记录分别代表页30和页32,如果用户记录的主键值在 [1, 320)
之间,则到页30中查找更详细的目录项记录,如果主键值不小于320
的话,就到页32中查找更详细的目录项记录。
我们可以用下边这个图来描述它:
这个数据结构
,它的名称是 B+树
。
B+Tree
不论是存放用户记录的数据页,还是存放目录项记录的数据页,我们都把它们存放到B+树这个数据结构中了,所
以我们也称这些数据页为 节点。从图中可以看出,我们的实际用户记录其实都存放在B+树的最底层的节点上,这
些节点也被称为叶子节点,其余用来存放目录项的节点称为非叶子节点或者内节点,其中B+树最上边的那个节
点也称为 根节点。
一个B+树的节点其实可以分成好多层,规定最下边的那层,也就是存放我们用户记录
的那层为第 0
层,之后依次往上加。之前我们做了一个非常极端的假设:存放用户记录的页最多存放3条记录
,存放目录项记录的页 最多存放4条记录
。其实真实环境中一个页存放的记录数量是非常大的,假设所有存放用户记录的叶子节点代表的数据页可以存放100条用户记录
,所有存放目录项记录的内节点代表的数据页可以存放1000条目录项记录
,那么:
- 如果B+树只有1层,也就是只有1个用于存放用户记录的节点,最多能存放
100
条记录。 - 如果B+树有2层,最多能存放
1000×100=10,0000
条记录。 - 如果B+树有3层,最多能存放
1000×1000×100=1,0000,0000
条记录。 - 如果B+树有4层,最多能存放
1000×1000×1000×100=1000,0000,0000
条记录。相当多的记录!!!
你的表里能存放 100000000000
条记录吗?所以一般情况下,我们 用到的B+树都不会超过4层
,那我们通过主键值去查找某条记录最多只需要做4个页面内的查找(查找3个目录项页和一个用户记录页),又因为在每个页面内有所谓的 Page Directory
(页目录),所以在页面内也可以通过 二分法
实现快速定位记录。
③常见索引概念
索引按照物理实现方式,索引可以分为 2 种:聚簇(聚集)
和非聚簇(非聚集)
索引。我们也把非聚集索引称为二级索引或者辅助索引。
(1)聚簇索引
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式(所有的用户记录都存储在了叶子节点),也就是
所谓的索引即数据,数据即索引。
术语“聚簇”表示数据行和相邻的键值聚簇的存储在一起。
特点:
- 使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:
页内
的记录是按照主键的大小顺序排成一个单向链表
。- 各个存放
用户记录的页
也是根据页中用户记录的主键大小顺序排成一个双向链表
。 - 存放
目录项记录的页
分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表
。
- B+树的
叶子节点 存储的是完整的用户记录
。
所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。
优点:
数据访问更快
,因为聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非聚簇索引更快- 聚簇索引对于主键的
排序查找
和范围查找
速度非常快 - 按照聚簇索引排列顺序,查询显示一定范围数据的时候,由于数据都是紧密相连,数据库不用从多个数据块中提取数据,所以
节省了大量的io操作
。
缺点:
插入速度严重依赖于插入顺序
,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列为主键
-更新主键的代价很高
,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新二级索引访问需要两次索引查找
,第一次找到主键值,第二次根据主键值找到行数据
限制:
对于MySQL数据库目前只有InnoDB数据引擎支持聚簇索引,而MylSAM并不支持聚簇索弘。
由于数据物理存储排序方式只能有一种,所以每个MySQL的表只能有一个聚簇索引。一般情况下就是该表的主键。
如果没有定义主键,Innodb会选择非空的唯一索引代替。如果没有这样的索引,Innodb会隐式的定义一个主键来作为聚簇索引。
为了充分利用聚簇索引的聚簇的特性,所以innodb表的主键列尽量选用有序的顺序id,而不建议用无序的id,
比如UUID、MD5、HASH、字符串列作为主键无法保证数据的顺序增长。
(2)二级索引(辅助索引、非聚簇索引)
这个B+树和上边介绍的聚簇索引有几处不同
使用记录c2列的大小进行记录和页的排序,这包括三个方面的含义:
页内的记录是按照c2列的大小顺序排成一个 单向链表。
各个存放用户记录的页 也是根据页中记录的c2列大小顺序排成一个 双向链表。
存放 目录现记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的c2列大小顺序排成一个双向链表。
B+树的叶子节点存储的并不是完整的用户记录,而只是c2列+主键这两个列的值。
目录项记录中不再是主键+页号的搭配,而变成了c2列+页号的搭配。
所以如果我们现在想通过c2列的值查找某些记录的话就可以使用我们刚刚建好的这个B+树了。以查找c2列的值为4的记录为例,查找过程如下:
确定目录项记录页
根据根页面,也就是页44,可以快速定位到 目录项记录所在的页为页42(因为2<4<9)。
通过 目录项记录页确定用户记录真实所在的页。
在页42中可以快速定位到实际存储用户记录的页,但是由于c2列并没有唯一性约束,所以c2列值为4的
记录可能分布在多个数据页中,又因为2<4≤4,所以确定实际存储用户记录的页在页34和页35中。在真实存储用户记录的页中定位到具体的记录。
到页34和页35中定位到具体的记录。但是这个B+树的叶子节点中的记录只存储了c2和c1(也就是主键)两个列,所以我们必须再根据主键值
去聚簇索引中再查找一遍完整的用户记录。
概念:回表
我们根据这个以c2列大小排序的B+树只能确定我们要查找记录的主键值,所以如果我们想根据c2列的值查找到完整的用户记录的话,仍然需要到 聚簇索引
中再查一遍,这个过程称为 回表
。也就是根据c2列的值查询一条完整的用户记录需要使用到 2
棵B+树!
问题:为什么我们还需要一次 回表
操作呢?直接把完整的用户记录放到叶子节点不OK吗?
如果每一个都完整存放用户记录,那倘若100w的用户数据,每个都要完整记录,那不是有几个二级索引,就需要翻几倍去储存,加大了储存空间的开销
因为这种按照 非主键列 建立的B+树需要一次回表操作才可以定位到完整的用户记录,所以这种B+树也被称为二级索引(英文名 secondary index),或者辅助索引。由于我们使用的是c2列的大小作为B+树的排序规则,所以
我们也称这个B+树是为c2列建立的索引。|
非聚簇索引的存在不影响数据在聚簇索引中的组织,所以一张表可以有多个非聚簇索引。
小结:聚簇索引与非聚簇索引的原理不同,在使用上也有一些区别:
聚簇索引的叶子节点存储的就是我们的数据记录,非聚簇索引的叶子节点存储的是数据位置。非聚簇索引不
会影响数据表的物理存储顺序。一个表只能有一个聚簇索引,因为只能有一种排序存储的方式,但可以有多个非聚簇索引,也就是多个索引
目录提供数据检索。使用聚簇索引的时候,数据的**查询效率高,**但如果对数据进行插入,删除,更新等操作,效率会比非聚簇索
引低。
(3)联合索引
如图所示,我们需要注意以下几点:
- ·每条 目录项记录 都由c2、c3、页号这三个部分组成,各条记录先按照c2列的值进行排序,如果记录的c2列相同,则按照c3列的值进行排序。
- B+树 叶子节点处的用户记录由c2、c3和主键c1列组成。
我们也可以同时以多个列的大小作为排序规则,也就是同时为多个列建立索引
,比方说我们想让B+树按照 c2和c3列
的大小进行排序,这个包含两层含义:
- 先把各个记录和页按照c2列进行排序。
- 在记录的c2列相同的情况下,采用c3列进行排序
注意一点,以c2和c3列的大小为排序规则建立的B+树称为 联合索引
,本质上也是一个二级索引。它的意思与分别为c2和c3列分别建立索引的表述是不同的,不同点如下:
- 建立
联合索引
只会建立如上图一样的1棵B+树。 - 为c2和c3列分别建立索引会分别以c2和c3列的大小为排序规则建立2棵B+树。
④InnoDB的B+树索引的注意事项
#####1.根页面位置万年不动
我们前边介绍B+树索引的时候,为了大家理解上的方便,先把存储用户记录的叶子节点都画出来,然后接着画存
储目录项记录的内节点,实际上B+树的形成过程是这样的:
每当为某个表创建一个B+树索引(聚簇索引不是人为创建的,默认就有)的时候,都会为这个索引创建一个
根节点页面。最开始表中没有数据的时候,每个B+树索引对应的根节点中既没有用户记录,也没有目录项记
录。随后向表中插入用户记录时,先把用户记录存储到这个 根节点中。
当根节点中的可用空间用完时 继续插入记录,此时会将根节点中的所有记录复制到一个新分配的页,比如页
a中,然后对这个新页进行页分裂的操作,得到另一个新页,比如页b。这时新插入的记录根据键值(也就是
聚簇索引中的主键值,二级索引中对应的索引列的值)的大小就会被分配到页a或者页b中,而根节点便升
级为存储目录项记录的页。
这个过程特别注意的是:一个B+树索引的根节点自诞生之日起,便不会再移动。这样只要我们对某个表建立一个
索引,那么它的根节点的页号便会被记录到某个地方,然后凡是InnoDB存储引擎需要用到这个索引的时候,都会从那个固定的地方取出根节点的页号,从而来访问这个索引。
#####2.内节点中目录项记录的唯一性
#####3.一个页面最少存储2条记录
一个B+树只需要很少的层级就可以轻松存储数亿条记录,查询速度相当不错!这是因为B+树本质上就是一个大的
多层级目录,每经过一个目录时都会过滤掉许多无效的子目录,直到最后访问到存储真实数据的目录。那如果一
个大的目录中只存放一个子目录是个啥效果呢?那就是目录层级非常非常非常多,而且最后的那个存放真实数据
的目录中只能存放一条记录。费了半天劲只能存放一条真实的用户记录?所以InnoDB的一个数据页至少可以存放两条记录。
四、MyISAM中的索引方案
B树索引适用存储引擎如表所示:
即使多个存储引擎支持同一种类型的索引,但是他们的实现原理也是不同的。Innodb和MyISAM默认的索引是Btree索引;而Memory默认的索引是Hash索引。
MyISAM引擎使用 B+Tree
作为索引结构,叶子节点的data域存放
的是 数据记录的地址
。
1、MyISAM索引的原理
我们知道InnoDB中索引即数据,也就是聚簇索引的那棵B+树的叶子节点中已经把所有完整的用户记录都包含了,
而MyISAM的索引方案虽然也使用树形结构,但是却将索引和数据分开存储:
- 将表中的记录 按照记录的插入顺序 单独存储在一个文件中,称之为数据文件。这个文件并不划分为若干个数据页,有多少记录就往这个文件中塞多少记录就成了。由于在插入数据的时候并没有**刻意按照主键大小排序,**所以我们并不能在这些数据上使用二分法进行查找。
- 使用MyISAM存储引擎的表会把索引信息另外存储到一个称为 索引文件的另一个文件中。MyISAM会单独为表的主键创建一个索引,只不过在索引的叶子节点中存储的不是完整的用户记录,而是主键值 +数据记录地址的组合。
这里设表一共有三列,假设我们以Col1为主键,上图是一个MylSAM表的主索引(Primary key)示意。可以看出
MylSAM的索引文件仅仅保存数据记录的地址。在MylSAM中,主键索引和二级索引(Secondary key)在结构上没
有任何区别,只是主键索引要求key是唯一的,而二级索引的key可以重复。
如果我们在Col2上建立一个二级索引,则此索引的结构如下图所示:
同样也是一棵B+Tree,data域保存数据记录的地址。因此,MylSAM中索引检索的算法为:首先按照B+Tree搜索算
法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。
2、MyISAM 与 InnoDB对比
MyISAM的索引方式都是“非聚簇”的,与InnoDB包含1个聚簇索引是不同的。
小结两种引擎中索引的区别:
① 在InnoDB存储引擎中,我们只需要根据
主键
值对聚簇索引
进行一次查找就能找到对应的记录,而在MyISAM 中却需要进行一次回表
(找到地址后根据地址去找表这个操作叫回表) 操作,意味着MyISAM中建立的索引相当于全部都是二级索引
。
② InnoDB的数据文件本身就是索引文件是一起的
,而MyISAM索引文件和数据文件是分离的
,索引文件仅保存数据记录的地址。
③ InnoDB的非聚簇索引data域存储相应记录主键的值
,而MyISAM索引记录的是地址
。换句话说,InnoDB的所有非聚簇索引都引用主键作为data域。
④ MyISAM的回表操作是十分快速
的,因为是拿着地址偏移量直接到文件中取数据的,反观InnoDB是通过获取主键之后再去聚簇索引里找记录,虽然说也不慢,但还是比不上直接用地址去访问。
⑤ InnoDB要求表必须有主键 ( MyISAM可以没有 )
。如果没有显式指定,则MySQL系统会自动选择一个可以非空且唯一标识数据记录的列作为主键。如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整型。
小结:
了解不同存储引擎的索引实现方式对于正确使用和优化索引都非常有帮助。比如:
举例1:知道了InnoDB的索引实现后,就很容易明白为什么不建议使用过长的字段作为主键,因为所有二级索引都
引用主键索引,过长的主键索引会令二级索引变得过大。
举例2:用非单调的字段作为主键在InnoDB中不是个好主意,因为InnoDB数据文件本身是一棵B+Tree,非单调的
主键会造成在插入新记录时,数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作
为主键则是一个很好的选择。
五、索引的代价
索引是个好东西,可不能乱建,它在空间和时间上都会有消耗:
- 空间上的代价
每建立一个索引都要为它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页,一个页默认会占用16KB
的存储空间,一棵很大的B+树由许多数据页组成,那就是很大的一片存储空间。 - 时间上的代价
每次对表中的数据进行增、删、改
操作时,都需要去修改各个B+树索引。而且我们讲过,B+树每层节点都是按照索引列的值从小到大的顺序排序
而组成了双向链表
。不论是叶子节点中的记录,还是内节点中的记录(也就是不论是用户记录还是目录项记录)都是按照索引列的值从小到大的顺序而形成了一个单向链表。而增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些记录移位 , 页面分裂 、 页面回收
等操作来维护好节点和记录的排序。如果我们建了许多索引,每个索引对应的B+树都要进行相关的维护操作,会给性能拖后腿。
六、MySQL数据结构选择的合理性
从MySQL的角度讲,不得不考虑一个现实问题就是磁盘10。如果我们能让索引的数据结构尽量减少硬盘的I/O操
作,所消耗的时间也就越小。可以说,磁盘的I/0 操作次数对索引的使用效率至关重要。
查找都是索引操作,一般来说索引非常大,尤其是关系型数据库,当数据量比较大的时候,索引的大小有可能几
个G甚至更多,为了减少索引在内存的占用,数据库索引是存储在外部磁盘上的。当我们利用索引查询的时候,
不可能把整个索引全部加载到内存,只能逐一加载,那么MySQL衡量查询效率的标准就是磁盘IO次数。
1、全表遍历
顺序查找,最慢的,性能极差
2、Hash结构
Hash 本身是一个函数,又被称为散列函数,它可以帮助我们大幅提升检索数据的效率。
Hash 算法是通过某种确定性的算法(比如MD5、SHA1、SHA2、SHA3)将输入转变为输出。相同的输入永远可以得到相同的输出,假设输入内容有微小偏差,在输出中通常会有不同的结果。
举例:如果你想要验证两个文件是否相同,那么你不需要把两份文件直接拿来比对,只需要让对方把Hash函数
计算得到的结果告诉你即可,然后在本地同样对文件进行Hash函数的运算,最后通过比较这两个Hash函数的结
果是否相同,就可以知道这两个文件是否相同。
加速查找速度的数据结构,常见的有两类:
(1)树,例如平衡二叉搜索树,查询/插入/修改/删除的平均时间复杂度都是0(1og2N);
(2)哈希,例如HashMap,查询/插入/修改/删除的平均时间复杂度都是0(1);
采用Hash进行检索效率非常高,基本上一次检索就可以找到数据,而B+树需要自顶向下依次查找,多次访问节
点才能找到数据,中间需要多次I/O操作,从效率来说 Hash 比 B+ 树更快。
在哈希的方式下,一个元素k处于h(k)中,即利用哈希函数h,根据关键字k计算出槽的位置。函数h将关键字域映
射到哈希表T[0 … m-1]的槽位上。
上图中哈希函数h有可能将两个不同的关键字映射到相同的位置,这叫做 碰撞
,在数据库中一般采用链接法
来解决。在链接法中,将散列到同一槽位的元素放在一个链表中,如下图所示:
体会数组和hash表的查找方面的效率区别
1 | // 算法复杂度为 O(n) |
Hash结构效率高,那为什么索引结构要设计成树型呢?
原因1: Hash索引仅能满足(=)(<>)和IN查询。如果进行范围查询,哈希型的索引,时间复杂度会退化为
O(n);而树型的“有序”特性,依然能够保持O(log2N)的高效率。
原因2:Hash索引还有一个缺陷,数据的存储是没有顺序的,在ORDER BY的情况下,使用Hash索引还需要对数
据重新排序。
原因3:对于联合索引的情况,Hash值是将联合索引键合并后一起来计算的,无法对单独的一个键或者几个索引
键进行查询。
原因4:对于等值查询来说,通常Hash索引的效率更高,不过也存在一种情况,就是索引列的重复值如果很多,效
率就会降低。这是因为遇到Hash冲突时,需要遍历桶中的行指针来进行比较,找到查询的关键字,非常耗时。所
以,Hash索引通常不会用到重复值多的列上,比如列为性别、年龄的情况等。
Hash索引适用存储引擎如表所示:
Hash索引的适用性:
段的重复度低,而且经常需要进行等值查询的时候,采用Hash索引是个不错的选择。|
Hash 索引存在着很多限制,相比之下在数据库中B+树索引的使用面会更广,不过也有一些场景采用Hash索引
效率更高,比如在键值型(Key-Value)数据库中,Redis 存储的核心就是 Hash 表。
MySQL中的Memory 存储引擎支持Hash存储,如果我们需要用到查询的临时表时,就可以选择Memory存储引
擎,把某个字段设置为Hash索引,比如字符串类型的字段,进行Hash计算之后长度可以缩短到几个字节。当字
段的重复度低,而且经常需要进行等值查询的时候,采用Hash索引是个不错的选择。
另外,InnoDB本身不支持Hash索引,但是提供自适应 Hash 索引(Adaptive Hash Index)。什么情况下才会使
用自适应 Hash索引呢?如果某个数据经常被访问,当满足一定条件的时候,就会将这个数据页的地址存放到
Hash表中。这样下次查询的时候,就可以直接找到这个页面的所在位置。这样让B+树也具备了Hash索引的优
点。
采用自适应 Hash 索引目的是方便根据 SQL 的查询条件加速定位到叶子节点,特别是当 B+ 树比较深的时候,通过自适应 Hash 索引可以明显提高数据的检索效率。
我们可以通过innodb_adaptive_hash_index
变量来查看是否开启了自适应 Hash
,比如:
1 | show variables like '%adaptive_hash_index'; |
3、二叉搜索树
如果我们利用二叉树作为索引结构,那么磁盘的IO次数和索引树的高度是相关的。
二叉搜索树的特点
有序的
查找规则
二分查找
创造出来的二分搜索树如下图所示:
为了提高查询效率,就需要 减少磁盘IO数
。为了减少磁盘IO的次数,就需要尽量 降低树的高度
,需要把原来“瘦高”的树结构变的“矮胖”
,树的每层的分叉越多越好。
4、AVL树
特点:左右孩子节点高度差不超过1的二叉树
针对同样的数据,如果我们把二叉树改成 M 叉树
(M>2)呢?当 M=3 时,同样的 31 个节点可以由下面的三叉树
来进行存储:
分叉越多越矮胖则层数越低则IO次数越少
5、B-Tree
称为多路平衡查找树。注意:’ - ‘是连接符号不是减号
B树作为多路平衡查找树,它的每一个节点最多可以包括M个子节点,M 称为B 树的阶|
。每个磁盘块中包括
了关键字和子节点的指针。如果一个磁盘块中包括了x个关键字,那么指针数就是x+1。对于一个100阶的B树
来说,如果有3层的话最多可以存储约100万的索引数据。对于大量的索引数据来说,采用B树的结构是非常适
合的,因为树的高度要远小于二叉树的高度。
一个 M 阶的 B 树(M>2)有以下的特性:
- 根节点的儿子数的范围是 [2,M]。
- 每个中间节点包含 k-1 个关键字和 k 个孩子,孩子的数量 = 关键字的数量 +1,k 的取值范围为[ceil(M/2), M]。
- 叶子节点包括 k-1 个关键字(叶子节点没有孩子),k 的取值范围为 [ceil(M/2), M]。
- 假设中间节点节点的关键字为:Key[1], Key[2], …, Key[k-1],且关键字按照升序排序,即 Key[i]
<Key[i+1]。此时 k-1 个关键字相当于划分了 k 个范围,也就是对应着 k 个指针,即为:P[1], P[2], >…,P[k],其中 P[1] 指向关键字小于 Key[1] 的子树,P[i] 指向关键字属于 (Key[i-1], Key[i]) 的子树,P[k]>指向关键字大于 Key[k-1] 的子树。- 所有叶子节点位于同一层。
上面那张图所表示的 B 树就是一棵 3 阶的 B 树。我们可以看下磁盘块 2,里面的关键字为(8,12),它有 3 个孩子 (3,5),(9,10) 和 (13,15),你能看到 (3,5) 小于 8,(9,10) 在 8 和 12 之间,而 (13,15)大于 12,刚好符合刚才我们给出的特征。
然后我们来看下如何用 B 树进行查找。假设我们想要 查找的关键字是 9
,那么步骤可以分为以下几步:
- 我们与根节点的关键字 (17,35)进行比较,9 小于 17 那么得到指针 P1;
- 按照指针 P1 找到磁盘块 2,关键字为(8,12),因为 9 在 8 和 12 之间,所以我们得到指针 P2;
- 按照指针 P2 找到磁盘块 6,关键字为(9,10),然后我们找到了关键字 9。
你能看出来在 B 树的搜索过程中,我们比较的次数并不少,但如果把数据读取出来然后在内存中进行比较,这个时间就是可以忽略不计的。而读取磁盘块本身需要进行 I/O 操作,消耗的时间比在内存中进行比较所需要的时间要多,是数据查找用时的重要因素。 B 树相比于平衡二叉树来说磁盘 I/O 操作要少
,在数据查询中比平衡二叉树效率要高。所以 只要树的高度足够低,IO次数足够少
,就可以提高查询性能。
小结:
B树在插入和删除节点的时候如果导致树不平衡,就通过自动调整节点的位置来保持树的自平衡。
关键字集合分布在整棵树中,即叶子节点和非叶子节点都存放数据。搜索有可能在非叶子节点结束
其搜索性能等价于在关键字全集内做一次二分查找。
再举例1:
6、B+Tree
B+树也是一种多路搜索树,基于 B 树做出了改进,主流的DBMS都支持B+树的索引方式,比如MySQL。相比
于B-Tree,B+Tree适合文件索引系统。
MySQL官网说明:
B+ 树和 B 树的差异:
- 有 k 个孩子的节点就有 k 个关键字。也就是孩子数量 = 关键字数,而 B 树中,孩子数量 = 关键字数+1。
- 非叶子节点的关键字也会同时存在在子节点中,并且是在子节点中所有关键字的最大(或最小)。
- 非叶子节点仅用于索引,不保存数据记录,跟记录有关的信息都放在叶子节点中。而 B 树中,
非叶子节点既保存索引,也保存数据记录
。- 所有关键字都在叶子节点出现,叶子节点构成一个有序链表,而且叶子节点本身按照关键字的大小从小到大顺序链接。
整个过程一共进行了3次I/O操作,看起来B+树和B树的查询过程差不多,但是B+树和B树有个根本的差异在
于, B+树的中间节点并不直接存储数据 。这样的好处都有什么呢?
首先,B+树查询效率更稳定。因为B+树每次只有访问到叶子节点才能找到对应的数据,而在B树中,非叶子节
点也会存储数据,这样就会造成查询效率不稳定的情况,有时候访问到了非叶子节点就可以找到关键字,而有时
需要访问到叶子节点才能找到关键字。
其次,B+树的查询效率更高。这是因为通常B+树比B树更矮胖(阶数更大,深度更低),查询所需要的磁盘
I/O也会更少。同样的磁盘页大小,B+树可以存储更多的节点关键字。
不仅是对单个关键字的查询上,在查询范围上,B+树的效率也比B树高。这是因为所有关键字都出现在B+树的
叶子节点中,叶子节点之间会有指针,数据又是递增的,这使得我们范围查找可以通过指针连接查找。而在B树
中则需要通过中序遍历才能完成查询范围的查找,效率要低很多。
B 树和 B+ 树都可以作为索引的数据结构,在 MySQL 中采用的是 B+ 树。但B树和B+树各有自己的应用场景,不能说B+树完全比B树好,反之亦然。
思考题:为了减少IO,索引树会一次性加载吗?
不会.
1、数据库索引是存储在磁盘上的,如果数据量很大,必然导致索引的大小也会很大,超过几个G。
2、当我们利用索引查询时候,是不可能将全部几个G的索引都加载进内存的,我们能做的只能是:逐一加载每一个磁盘页,因为磁盘页对应着索引树的节点。思考题:B+树的存储能力如何?为何说一般查找行记录,最多只需1~3次磁盘IO
1
2
3
4
5
6 InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字
节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储
16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为10^3。也就是说一个深度为3的
B+Tree 索引可以维护10^3*10^3*10^3=10亿条记录。(这里假定一个数据页也存储10^3条行记录数据了)
实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree 的高度一般都在2~4层。MySQL的
InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1~3次磁盘l/0操作。思考题:为什么说B+树比B-树更适合实际应用中操作系统的文件索引和数据库索引?
1、B+树的磁盘读写代价更低
B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结
点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关
键字也就越多。相对来说IO读写次数也就降低了。2、B+树的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必
须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。思考题:Hash 索引与 B+ 树索引的区别
我们之前讲到过B+树索引的结构,Hash索引结构和B+树的不同,因此在索引使用上也会有差别。
1、Hash索引不能进行范围查询,而B+树可以。这是因为Hash索引指向的数据是无序的,而B+树的叶子节
点是个有序的链表。2、Hash索引不支持联合索引的最左侧原则(即联合索引的部分索引无法使用),而B+树可以。对于联合索
引来说,Hash索引在计算Hash值的时候是将索引键合并后再一起计算Hash值,所以不会针对每个索引单独
计算Hash值。因此如果用到联合索引的一个或者几个索引时,联合索引无法被利用。3、Hash索引 不支持 ORDER BY 排序,因为Hash索引指向的数据是无序的,因此无法起到排序优化的作用,而B+树索引数据是有序的,可以起到对该字段ORDER BY排序优化的作用。同理,我们也无法用Hash索引进行 模糊查询,而B+树使用LIKE进行模糊查询的时候,LIKE后面后模糊查询(比如%结尾)的话就可以起到优化作用
4、InnoDB不支持哈希索引
思考题:Hash 索引与 B+ 树索引是在建索引的时候手动指定的吗?
不是的,是一开始我们创建表的时候,每次插入数据,他背后都会去维护对应索引,如果又新加的二级索引才会再创建索引。你能看到,针对InnoDB和MylSAM存储引擎,都会默认采用B+树索引,无法使用Hash索引。InnoDB提供
的自适应Hash是不需要手动指定的。如果是Memory/Heap和NDB存储引擎,是可以进行选择Hash索引的。
7、R树
R-Tree在MySQL很少使用
,仅支持 geometry数据类型
,支持该类型的存储引擎只有myisam、bdb、innodb、ndb、archive几种。举个R树在现实领域中能够解决的例子:查找20英里以内所有的餐厅。如果没有R树你会怎么解决?一般情况下我们会把餐厅的坐标(x,y)分为两个字段存放在数据库中,一个字段记录经度,另一个字段记录纬度。这样的话我们就需要遍历所有的餐厅获取其位置信息,然后计算是否满足要求。如果一个地区有100家餐厅的话,我们就要进行100次位置计算操作了,如果应用到谷歌、百度地图这种超大数据库中,这种方法便必定不可行了。R树就很好的解决了这种高维空间搜索问题
。它把B树的思想很好的扩展到了多维空间,采用了B树分割空间的思想,并在添加、删除操作时采用合并、分解结点的方法,保证树的平衡性。因此,R树就是一棵用来存储高维数据的平衡树
。相对于B-Tree,R-Tree的优势在于范围查找。
##七、InnoDB数据存储结构
1. 数据库的存储结构:页
索引结构给我们提供了高效的索引方式,不过索引信息以及数据记录都保存在文件上的,确切说是存储在页结构中。另一方面,索引是在存储引擎中实现的,MySQL服务器上的存储引擎
负责对表中数据的读取和写入工作。不同存储引擎中存放的格式
一般不同的,甚至有的存储引擎比如Memory都不用磁盘来存储数据。
由于InnoDB
是MySQL的默认存储引擎
,所以本章剖析InooDB存储引擎的数据存储结构。
1.1 磁盘与内存交互基本单位:页
InnoDB将数据划分为若干个页,InnoDB中页的大小默认为16KB
。
以页
作为磁盘和内存之间交互的基本单位,也就是一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。也就是说,**在数据库中,不论读一行,还是读多行,都是将这些行所在的页进行加载。也就是说,数据库管理存储空间的基本单位是页(Page),数据库I/O操作的最小单位是页。**一个页中可以存储多个行记录。
记录是按照行来存储的,但是数据库的读取并不以行为单位,否则一次读取(也就是一次I/O操作)只能处理一行数据,效率会非常低。
1.2 页结构概述
页a、页b、页c…页n这些页可以不在物理结构上相连
,只要通过双向链表
相关联即可。每个数据页中的记录会按照主键值从小到大的顺序组成一个单向链表
,每个数据页都会为存储在它里边的记录生成一个页目录
,在通过主键查找某条记录的时候可以在页目录中使用二分法
快速定位到对应的槽,然后再遍历该槽对应的分组中的记录即可快速找到指定的记录。
#####1.3 页的大小
1.4 页的上层结构
区(Extent)是比页大一级的存储结构,在InnoDB存储引擎中,一个区会分配64个连续的页
。因为InnoDB中的页大小默认是16KB,所以一个区的大小是64*16KB=1MB
。
段(Segment)由一个或多个区组成,区在文件系统是一个连续分配的空间(在InnoDB中是连续的64个页),不过在段中不要求区与区之间是相邻的。段是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。
当我们创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时会创建一个表段,创建一个索引时会创建一个索引段。
表空间(Tablespace)是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但是一个段只能属于一个表空间。数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间
、用户表空间
、撤销表空间
、临时表空间
等。
2. 页的内部结构
2.1 第1部分:文件头部和文件尾部
2.1.1 File Header(文件头部)(38字节)
作用: 描述各种页的通用信息。(比如页的编号、其上一页、下一页是谁等)
大小:38字节
名称 | 占用空间大小 | 描述 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM |
4 字节 |
页的校验和(checksum值) |
FIL_PAGE_OFFSET |
4 字节 |
页号 |
FIL_PAGE_PREV |
4 字节 |
上一个页的页号 |
FIL_PAGE_NEXT |
4 字节 |
下一个页的页号 |
FIL_PAGE_LSN | 8 字节 |
页面被最后修改时对应的日志序列位置 |
FIL_PAGE_TYPE |
2 字节 |
该页的类型 |
FIL_PAGE_FILE_FLUSH_LSN | 8 字节 |
仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值 |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID |
4 字节 |
页属于哪个表空间 |
FIL_PAGE_OFFSET(4字节)
:每一个页都有一个单独的页号,就跟你的身份证号码一样,InnoDB通过页号可以唯一定位一个页。FIL_PAGE_TYPE(2字节)
:这个代表当前页的类型。
类型名称 | 十六进制 | 描述 |
---|---|---|
FIL_PAGE_TYPE_ALLOCATED | 0x0000 | 最新分配,还没有使用 |
FIL_PAGE_UNDO_LOG |
0x0002 | Undo日志页 |
FIL_PAGE_INODE | 0x0003 | 段信息节点 |
FIL_PAGE_IBUF_FREE_LIST | 0x0004 | Insert Buffer空闲列表 |
FIL_PAGE_IBUF_BITMAP | 0x0005 | Insert Buffer位图 |
FIL_PAGE_TYPE_SYS |
0x0006 | 系统页 |
FIL_PAGE_TYPE_TRX_SYS | 0x0007 | 事务系统数据 |
FIL_PAGE_TYPE_FSP_HDR | 0x0008 | 表空间头部信息 |
FIL_PAGE_TYPE_XDES | 0x0009 | 扩展描述页 |
FIL_PAGE_TYPE_BLOB | 0x000A | 溢出页 |
FIL_PAGE_INDEX |
0x45BF | 索引页,也就是我们所说的数据页 |
FIL_PAGE_PREV(4字节)和FIL_PAGE_NEXT(4字节)
:InnoDB都是以页为单位存放数据的,如果数据分散到多个不连续的页中存储的话需要把这些页关联起来,FIL_PAGE_PREV和FIL_PAGE_NEXT就分别代表本页的上一个和下一个页的页号。这样通过建立一个双向链表把许许多多的页就都串联起来了,保证这些页之间不需要是物理上的连续,而是逻辑上的连续。FIL_PAGE_SPACE_OR_CHKSUM(4字节)
:代表当前页面的校验和(checksum)。文件头部和文件尾部都有属性:FIL_PAGE_SPACE_OR_CHKSUM
作用:
InnoDB存储引擎以页为单位把数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间需要把数据同步到磁盘中。
但是在同步了一半的时候断电了,造成了该页传输的不完整。
为了检测一个页是否完整(也就是在同步的时候有没有发生只同步一半的尴尬情况),这时可以通过文件尾的校验和(checksum 值)与文件头的校验和做比对,如果两个值不相等则证明页的传输有问题,需要重新进行传输,否则认为页的传输已经完成。
FIL_PAGE_LSN(8字节)
:页面被最后修改时对应的日志序列位置(英文名是:Log Sequence Number)
2.1.2 File Trailer(文件尾部)(8字节)
- 前4个字节代表页的校验和:这个部分是和File Header中的校验和相对应的。
- 后4个字节代表页面被最后修改时对应的日志序列位置(LSN):这个部分也是为了校验页的完整性的,如果首部和尾部的LSN值校验不成功的话,就说明同步过程出现了问题。
2.2 第2部分:空闲空间、用户记录和最小最大记录
2.2.1 Free Space (空闲空间)
我们自己存储的记录会按照指定的行格式
存储到User Records
部分。但是在一开始生成页的时候,其实并没有User Records这个部分,每当我们插入一条记录,都会从Free Space部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records部分
,当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页
了。
2.2.2 User Records (用户记录)
User Records中的这些记录按照指定的行格式
一条一条摆在User Records部分,相互之间形成单链表
。
2.2.3 Infimum + Supremum(最小最大记录)
记录可以比较大小吗? 是的,记录可以比大小,对于一条完整的记录来说,比较记录的大小就是比较主键
的大小。比方说我们插入的4行记录的主键值分别是:1、2、3、4,这也就意味着这4条记录是从小到大依次递增。
InnoDB规定的最小记录与最大记录这两条记录的构造十分简单,都是由5字节大小的记录头信息和8字节大小的一个固定的部分组成的。
这两条记录不是我们自己定义的记录
,所以它们并不存放在页的User Records部分,他们被单独放在一个称为Infimum + Supremum的部分
2.3 第3部分:页目录和页面头部
2.3.1 Page Directory(页目录)
为什么需要页目录? 在页中,记录是以单向链表
的形式进行存储的。单向链表的特点就是插入、删除非常方便,但是检索效率不高
,最差的情况下需要遍历链表上的所有节点才能完成检索。因此在页结构中专门设计了页目录这个模块,专门给记录做一个目录
,通过二分查找法
的方式进行检索,提升效率。
页目录,二分法查找
- 将所有的记录
分成几个组
,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录。 - 第 1 组,也就是最小记录所在的分组只有 1 个记录; 最后一组,就是最大记录所在的分组,会有 1-8 条记录; 其余的组记录数量在 4-8 条之间。 这样做的好处是,除了第 1 组(最小记录所在组)以外,其余组的记录数会
尽量平分
。 - 在每个组中最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned 字段。
页目录用来存储每组最后一条记录的地址偏移量
,这些地址偏移量会按照先后顺序存储
起来,每组的地址偏移量也被称之为槽(slot)
,每个槽相当于指针指向了不同组的最后一个记录。
举例:
现在的page_demo表中正常的记录共有6条,InnoDB会把它们分成两组,第一组中只有一个最小记录,第二组中是剩余的5条记录。如下图:
从这个图中我们需要注意这么几点:
- 现在页目录部分中有两个槽,也就意味着我们的记录被分成了两个组,槽1中的值是112,代表最大记录的地址偏移量(就是从页面的0字节开始数,数112个字节);槽0中的值是99,代表最小记录的地址偏移量。
- 注意最小和最大记录的头信息中的n_owned属性
- 最小记录的n_owned值为1,这就代表着以最小记录结尾的这个分组中只有1条记录,也就是最小记录本身。
- 最大记录的n_owned值为5,这就代表着以最大记录结尾的这个分组中只有5条记录,包括最大记录本身还有我们自己插入的4条记录。
用箭头指向的方式替代数字,这样更易于我们理解,修改后如下
为什么最小记录的n_owned值为1,而最大记录的n_owned值为5呢?
InnoDB规定:对于最小记录所在的分组只能有1条记录,最大记录所在的分组拥有的记录条数只能在18条之间,剩下的分组中记录的条数范围只能在是 48 条之间。
分组是按照下边的步骤进行的:
- 初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。
- 之后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
- 在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。
2.3.2 Page Header(页面头部)
为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫Page Header的部分,这个部分占用固定的56个字节,专门存储各种状态信息。
名称 | 占用空间大小 | 描述 |
---|---|---|
PAGE_N_DIR_SLOTS | 2字节 | 在页目录中的槽数量 |
PAGE_HEAP_TOP | 2字节 | 还未使用的空间最小地址,也就是说从该地址之后就是Free Space |
PAGE_N_HEAP | 2字节 | 本页中的记录的数量(包括最小和最大记录以及标记为删除的记录) |
PAGE_FREE | 2字节 | 第一个已经标记为删除的记录的记录地址(各个已删除的记录通过next_record 也会组成一个单链表,这个单链表中的记录可以被重新利用) |
PAGE_GARBAGE | 2字节 | 已删除记录占用的字节数 |
PAGE_LAST_INSERT | 2字节 | 最后插入记录的位置 |
PAGE_DIRECTION | 2字节 | 记录插入的方向 |
PAGE_N_DIRECTION | 2字节 | 一个方向连续插入的记录数量 |
PAGE_N_RECS | 2字节 | 该页中记录的数量(不包括最小和最大记录以及被标记为删除的记录) |
PAGE_MAX_TRX_ID | 8字节 | 修改当前页的最大事务ID,该值仅在二级索引中定义 |
PAGE_LEVEL | 2字节 | 当前页在B+树中所处的层级 |
PAGE_INDEX_ID | 8字节 | 索引ID,表示当前页属于哪个索引 |
PAGE_BTR_SEG_LEAF | 10字节 | B+树叶子段的头部信息,仅在B+树的Root页定义 |
PAGE_BTR_SEG_TOP | 10字节 | B+树非叶子段的头部信息,仅在B+树的Root页定义 |
2.4 从数据页的角度看B+数如何查询
3. InnoDB行格式(或记录格式)
3.1 指定行格式的语法
1 | CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称 |
3.2 COMPACT行格式
在MySQL 5.1版本中,默认设置为Compact行格式。一条完整的记录其实可以被分为记录的额外信息和记录的真实数据两大部分。
3.2.1 变长字段长度列表
MySQL支持一些变长的数据类型,比如VARCHAR(M)、VARBINARY(M)、TEXT类型,BLOB类型,这些数据类型修饰列称为变长字段
,变长字段中存储多少字节的数据不是固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。在Compact行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表。
注意:这里面存储的变长长度和字段顺序是反过来的。比如两个varchar字段在表结构的顺序是a(10),b(15)。那么在变长字段长度列表中存储的长度顺序就是15,10,是反过来的。
3.2.2 NULL值列表
Compact行格式会把可以为NULL的列统一管理起来,存在一个标记为NULL值列表中。如果表中没有允许存储 NULL 的列,则 NULL值列表也不存在了。 为什么定义NULL值列表? 之所以要存储NULL是因为数据都是需要对齐的,如果没有标注出来NULL值的位置
,就有可能在查询数据的时候出现混乱
。如果使用一个特定的符号
放到相应的数据位表示空置的话,虽然能达到效果,但是这样很浪费空间,所以直接就在行数据得头部开辟出一块空间专门用来记录该行数据哪些是非空数据,哪些是空数据,格式如下:
- 二进制位的值为1时,代表该列的值为NULL。
- 二进制位的值为0时,代表该列的值不为NULL。
注意:同样顺序也是反过来存放的
3.2.3 记录头信息(5字节)
名称 | 大小(单位:bit) | 描述 |
---|---|---|
预留位1 |
1 | 没有使用 |
预留位2 |
1 | 没有使用 |
delete_mask |
1 | 标记该记录是否被删除 |
mini_rec_mask |
1 | B+树的每层非叶子节点中的最小记录都会添加该标记 |
n_owned |
4 | 表示当前记录拥有的记录数 |
heap_no |
13 | 表示当前记录在记录堆的位置信息 |
record_type |
3 | 表示当前记录的类型,0 表示普通记录,1 表示B+树非叶子节点记录,2 表示最小记录,3 表示最大记录 |
next_record |
16 | 表示下一条记录的相对位置 |
delete_mask
:这个属性表示当前记录的类型,一共有4种类型的记录: - 0:表示普通记录 - 1:表示B+树非叶节点记录 - 2:表示最小记录 - 3:表示最大记录1
2
3
4
5
6
7
8
9
10
11
12
:这个属性标记着当前记录是否被删除,占用1个二进制位。
- 值为0:代表记录并没有被删除
- 值为1:代表记录被删除掉了
**被删除的记录为什么还在页中存储呢?** 你以为它删除了,可它还在真实的磁盘上。这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后其他的记录在磁盘上需要`重新排列,导致性能消耗`。所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的`垃圾链表`,在这个链表中的记录占用的空间称之为`可重用空间`,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。
- `min_rec_mask`:B+树的每层非叶子节点中的最小记录都会添加该标记,min_rec_mask值为1。我们自己插入的四条记录的min_rec_mask值都是0,意味着它们都不是B+树的非叶子节点中的最小记录。
- ```
record_typeheap_no
:这个属性表示当前记录在本页中的位置。
怎么不见heap_no值为0和1的记录呢? MySQL会自动给每个页里加了两个记录,由于这两个记录并不是我们自己插入的,所以有时候也称为伪记录
或者虚拟记录
。这两个伪记录一个代表最小记录
,一个代表最大记录
。最小记录和最大记录的heap_no值分别是0和1,也就是说它们的位置最靠前
n_owned
:页目录中每个组中最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned 字段next_record
:记录头信息里该属性非常重要,它表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量
。
3.2.4 记录的真实数据
列名 | 是否必须 | 占用空间 | 描述 |
---|---|---|---|
row_id | 否 | 6字节 | 行ID,唯一标识一条记录 |
transaction_id | 是 | 6字节 | 事务ID |
roll_pointer | 是 | 7字节 | 回滚指针 |
一个表没有手动定义主键,则会选取一个Unique键作为主键,如果连Unique键都没有定义的话,则会为表默认添加一个名为row_id的隐藏列作为主键。所以row_id是在没有自定义主键以及Unique键的情况下才会存在的。
3.3 Dynamic和Compressed行格式
我们可以知道一个页的大小一般是16KB,也就是16384字节,而一个VARCHAR(M)类型的列就最多可以存储65533个字节,这样就可能出现一个页存放不了一条记录,这种现象称为行溢出
在Compact和Reduntant行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的一部分数据,把剩余的数据分散存储在几个其他的页中进行分页存储
,然后记录的真实数据处用20个字节存储指向这些页的地址(当然这20个字节中还包括这些分散在其他页面中的数据的占用的字节数),从而可以找到剩余数据所在的页。这称为页的扩展
。
在MySQL 8.0中,默认行格式就是Dynamic,Dynamic、Compressed行格式和Compact行格式挺像,只不过在处理行溢出数据时有分歧
- Compressed和Dynamic两种记录格式对于存放在BLOB中的数据采用了完全的行溢出的方式。如图,在数据页中只存放20个字节的指针(溢出页的地址),实际的数据都存放在Off Page(溢出页)中。
- Compact和Redundant两种格式会在记录的真实数据处存储一部分数据(存放768个前缀字节)。
4. 区、段和碎片区
4.1 为什么要有区?
B+
树的每一层中的页都会形成一个双向链表,如果是以页为单位
来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离得非常远
。我们介绍B+树索引的使用场景的时候特别提到范围查询只需要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描就可以了,而如果链表中相邻的两个页物理位置离得非常远,就是所谓的随机I/O
。再一次强调,磁盘的速度和内存的速度差了好几个数量级,随机I/O是非常慢
的,所以我们应该尽量让链表中相邻的页的物理位置也相邻,这样进行范围查询的时候才可以使用所谓的顺序I/O
。
引入区
的概念,一个区就是物理位置上连续的64个页
。因为InnoDB中的页的大小默认是16KB,所以一个区的大小是64*16KB=1MB
。在表中数据量大
的时候,为某个索引分配空间的时候就不再按照页的单位分配了,而是按照区为单位分配
,甚至在表中的数据特别多的时候,可以一次性分配多个连续的区。虽然可能造成一点点空间的浪费
(数据不足以填充满整个区),但是从性能角度看,可以消除很多的随机I/O,功大于过
!
4.2 为什么要有段?
对于范围查询,其实是对B+树叶子节点中的记录进行顺序扫描,而如果不区分叶子节点和非叶子节点,统统把节点代表的页面放到申请到的区中的话,进行范围扫描的效果就大打折扣了。所以InnoDB对B+树的叶子节点
和非叶子节点
进行了区别对待,也就是说叶子节点有自己独有的区,非叶子节点也有自己独有的区。存放叶子节点的区的集合就算是一个段(segment)
,存放非叶子节点的区的集合也算是一个段。也就是说一个索引会生成2个段,一个叶子节点段
,一个非叶子节点段
。
除了索引的叶子节点段和非叶子节点段之外,InnoDB中还有为存储一些特殊的数据而定义的段,比如回滚段。所以,常见的段有数据段
、索引段
、回滚段
。数据段即为B+树的叶子节点,索引段即为B+树的非叶子节点。
在InnoDB存储引擎中,对段的管理都是由引擎自身所完成,DBA不能也没有必要对其进行控制。这从一定程度上简化了DBA对于段的管理。
段其实不对应表空间中的某一个连续的物理区域,而是一个逻辑上的概念,由若干个零散的页面以及一些完整的区组成。
4.3 为什么要有碎片区?
默认情况下,一个使用InnoDB存储引擎的表只有一个聚簇索引,一个索引会生成2个段,而段是以区为单位申请存储空间的,一个区默认占用1M(64*16KB=1024KB)存储空间,所以**默认情况下一个只存在几条记录的小表也需要2M的存储空间么?**以后每次添加一个索引都要多申请2M的存储空间么?这对于存储记录比较少的表简直是天大的浪费。这个问题的症结在于到现在为止我们介绍的区都是非常纯粹
的,也就是一个区被整个分配给某一个段,或者说区中的所有页面都是为了存储同一个段的数据而存在的,即使段的数据填不满区中所有的页面,那余下的页面也不能挪作他用。
为了考虑以完整的区为单位分配给某个段对于数据量较小
的表太浪费存储空间的这种情况,InnoDB提出了一个碎片(fragment)区
的概念。在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,而是碎片区中的页可以用于不同的目的,比如有些页面用于段A,有些页面用于段B,有些页甚至哪个段都不属于。碎片区直属于表空间
,并不属于任何一个段。
所以此后为某个段分配存储空间的策略是这样的:
- 在刚开始向表中插入数据的时候,段是从某个碎片区以单个页面为单位来分配存储空间的。
- 当某个段已经占用了
32个碎片区
页面之后,就会申请以完整的区为单位来分配存储空间。
所以现在段不能仅定义为是某些区的集合,更精确的应该是某些零散的页面
已经一些完整的区
的集合。
4.4 区的分类
区大体上可以分为4种类型:
空闲的区(FREE)
:现在还没有用到这个区中的任何页面。有剩余空间的碎片区(FREE_FRAG)
:表示碎片区中还有可用的页面。没有剩余空间的碎片区(FULL_FRAG)
:表示碎片区中的所有页面都被使用,没有空闲页面。附属于某个段的区(FSEG)
:每一索引都可以分为叶子节点段和非叶子节点段
处于FREE
、FREE_FRAG
以及FULL_FRAG
这三种状态的区都是独立的,直属于表空间。而处于FSEG
状态的区是附属于某个段的。
八、索引的创建与设计原则
一、索引的声明与使用
1、索引的分类
MySQL的索引包括普通索引、唯一性索引、全文索引、单列索引、多列索引和空间索等。
- 从
功能逻辑
上说,索引主要有 4 种,分别是普通索引、唯一索引、主键索引、全文索引。 - 按照
物理实现方式
,索引可以分为 2 种:聚簇索引和非聚簇索引。 - 按照
作用字段个数
进行划分,分成单列索引和联合索引。
1.普通索引
在创建普通索引时,不附加任何限制条件,只是用于提高查询效率。这类索引可以创建在任何数据类型中,其值
是否唯一和非空,要由字段本身的完整性约束条件决定。建立索引以后,可以通过索引进行查询。例如,在表
student的字段name上建立一个普通索引,查询记录时就可以根据该索引进行查询。
2.唯一性索引
使用UNIQUE参数 可以设置索引为唯一性索引,在创建唯一性索引时,限制该索引的值必须是唯一的,但允许有
空值。在一张数据表里可以有多个 唯一索引。
例如,在表student的字段email中创建唯一性索引,那么字段email的值就必须是唯一的。通过唯一性索引,
可以更快速地确定某条记录。
- 主键索引
主键索引就是一种特殊的唯一性索引,在唯一索引的基础上增加了不为空的约束,也就是NOT NULL+UNIQUE,一
张表里 最多只有一个 主键索引。
**Why?**这是由主键索引的物理实现方式决定的,因为数据存储在文件中只能按照一种顺序进行存储。
4.单列索引
在表中的单个字段上创建索引。单列索引只根据该字段进行索引。单列索引可以是普通索引,也可以是唯一性索
引,还可以是全文索引。只要保证该索引只对应一个字段即可。一个表可以有多个单列索引
5.多列(组合、联合)索引
多列索引是在表的多个字段组合上创建一个索引。该索引指向创建时对应的多个字段,可以通过这几个字段进行
查询,但是只有查询条件中使用了这些字段中的第一个字段时才会被使用。例如,在表中的字段id、name和
gender上建立一个多列索引idx_id_name_gender,只有在查询条件中使用了字段id时该索引才会被使用。使用
组合索引时遵循 最左前缀集合。
6.全文索引
全文索引(也称全文检索)是目前搜索引擎使用的一种关键技术。它能够利用【分词技术】等多种算法智能分析
出文本文字中关键词的频率和重要性,然后按照一定的算法规则智能地筛选出我们想要的搜索结果。全文索引非
常适合大型数据集,对于小的数据集,它的用处比较小。
使用参数`FULLTEXT‘可以设置索引为全文索引。在定义索引的列上支持值的全文查找,允许在这些索引列中插
入重复值和空值。全文索引只能创建在CHAR、VARCHAR或TEXT类型及其系列类型的字段上,查询数据量较大
的字符串类型的字段时,使用全文索引可以提高查询速度。例如,表student的字段information是TEXT类
型,该字段包含了很多文字信息。在字段information上建立全文索引后,可以提高查询字段information的速度。
全文索引典型的有两种类型:自然语言的全文索引和布尔全文索引。
自然语言搜索引擎将计算每一个文档对象和查询的相关度。这里,相关度是基于匹配的关键词的个数,以及
关键词在文档中出现的次数。**在整个索引中出现次数越少的词语,匹配时的相关度就越高。**相反,非常常见
的单词将不会被搜索,如果一个词语的在超过50%的记录中都出现了,那么自然语言的搜索将不会搜索这类
词语。
MySQL数据库从3.23.23版开始支持全文索引,但MySQL5.6.4以前只有Myisam支持,5.6.4版本以后innodb才支
持,但是官方版本不支持中文分词,需要第三方分词插件。在5.7.6版本,MySQL内置了ngram全文解析器,用来
支持亚洲语种的分词。测试或使用全文索引时,要先看一下自己的MySQL版本、存储引擎和数据类型是否支持全
文索引。
随着大数据时代的到来,关系型数据库应对全文索引的需求已力不从心,逐渐被 solr、ElasticSearch等专门
的搜索引擎所替代。
7.补充:空间索引
使用 参数SPATIAL可以设置索引为 空间索引。空间索引只能建立在空间数据类型上,这样可以提高系统获取空间
数据的效率。MySQL中的空间数据类型包括GEOMETRY、POINT、LINESTRING和POLYGON等。目前只有MylSAM存储引擎支持空间检索,而且索引的字段不能为空值。对于初学者来说,这类索引很少会用到。
1 不同的存储引擎支持的索引类型也不一样
- InnoDB :支持 B-tree、Full-text 等索引,不支持 Hash索引;
- MyISAM : 支持 B-tree、Full-text 等索引,不支持 Hash 索引;
- Memory :支持 B-tree、Hash 等索引,不支持 Full-text 索引;
- NDB :支持 Hash 索引,不支持 B-tree、Full-text 等索引;
- Archive :不支持 B-tree、Hash、Full-text 等索引;
2、创建索引
①创建表的时候创建索引
1 | CREATE TABLE dept( |
如果显式
创建表时创建索引的话,基本语法格式如下:
1 | CREATE TABLE table_name [col_name data_type] |
UNIQUE
、FULLTEXT
和SPATIAL
为可选参数,分别表示唯一索引、全文索引和空间索引;INDEX
与KEY
为同义词,两者的作用相同,用来指定创建索引;index_name
指定索引的名称,为可选参数,如果不指定,那么MySQL默认col_name为索引名;col_name
为需要创建索引的字段列,该列必须从数据表中定义的多个列中选择;length
为可选参数,表示索引的长度,只有字符串类型的字段才能指定索引长度;ASC
或DESC
指定升序或者降序的索引值存储。
创建普通索引:
在book表中的book_name字段上建立普通索引,SQL语句如下:
1 | CREATE TABLE book( |
创建唯一索引:
1 | CREATE TABLE test1( |
该语句执行完毕之后,使用SHOW CREATE TABLE查看表结构
:
1 | SHOW INDEX FROM test1 \G |
主键索引:
设定为主键
后数据库会自动建立
索引,innodb为聚簇索引,语法:
- 随表一起建索引:
1 | CREATE TABLE student ( |
- 删除主键索引:
1 | ALTER TABLE student |
- 修改主键索引:必须先删除掉(drop)原索引,再新建(add)索引
创建单列索引:
1 | CREATE TABLE book3( |
该语句执行完毕之后,使用SHOW CREATE TABLE查看表结构
:
1 | SHOW INDEX FROM book3 \G |
创建组合索引:
创建表test3,在表中的id、name和age
字段上建立组合索引,SQL语句如下:
1 | CREATE TABLE book4( |
该语句执行完毕之后,使用SHOW INDEX 查看:
1 | SHOW INDEX FROM book4 \G |
创建全文索引:
举例1:创建表test4,在表中的info字段上建立全文索引,SQL语句如下:
1 | CREATE TABLE test4( |
在MySQL5.7及之后版本中可以不指定最后的ENGINE了,因为在此版本中InnoDB支持全文索引。
举例2:
1 | CREATE TABLE articles ( |
创建了一个给title和body字段添加全文索引的表。
举例3:
1 | CREATE TABLE `papers` ( |
不同于like方式的的查询:
1 | SELECT * FROM papers WHERE content LIKE ‘%查询字符串%’; |
全文索引用match+against方式查询:
1 | SELECT * FROM papers WHERE MATCH(title,content) AGAINST (‘查询字符串’); |
注意点:
- 使用全文索引前,
搞清楚版本支持
情况;- 全文索引比 like + % 快 N 倍,但是
可能存在精度问题
;- 如果需要全文索引的是大量数据,
建议先添加数据,再创建索引
。
创建空间索引:
空间索引创建中,要求空间类型的字段必须为 非空 。
举例:创建表test5,在空间类型为GEOMETRY的字段上创建空间索引,SQL语句如下:
1 | CREATE TABLE test5( |
②在已经存在的表上创建索引
在已经存在的表中创建索引可以使用ALTER TABLE语句或者CREATE INDEX语句。
使用ALTER TABLE语句创建索引 ALTER TABLE语句创建索引的基本语法如下:
1 | ALTER TABLE table_name ADD [UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY][index_name] (col_name[length],...) [ASC | DESC] |
1 | CREATE TABLE book5( |
使用CREATE INDEX创建索引 CREATE INDEX语句可以在已经存在的表上添加索引,在MySQL中,
CREATE INDEX被映射到一个ALTER TABLE语句上,基本语法结构为:
1 | CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name |
1 | CREATE TABLE book6( |
3、删除索引
3.1 使用ALTER TABLE删除索引 ALTER TABLE删除索引的基本语法格式如下:
1 | ALTER TABLE table_name DROP INDEX index_name; |
1 | ALTER TABLE book5 |
注:添加AUTO_INCREMENT主键自增长的约束字段的唯一索引不能被删除
3.2 使用DROP INDEX语句删除索引 DROP INDEX删除索引的基本语法格式如下:
1 | DROP INDEX index_name ON table_name; |
1 | DROP INDEX uk_idx_name ON book5; |
提示:
删除表中的列
时,如果要删除的列为索引的组成部分,则该列也会从索引中删除。如果组成索引的所有列都被删除,则整个索引将被删除
。
二、MySQL8.0索引新特性
1、支持降序索引
举例:分别在MySQL 5.7版本和MySQL 8.0版本中创建数据表ts1,结果如下:
1 | CREATE TABLE ts1(a int,b int,index idx_a_b(a,b desc)); |
在MySQL 5.7版本中查看数据表ts1的结构,结果如下:
从结果可以看出,索引
仍然是默认的升序
。
在MySQL 8.0版本中查看数据表ts1的结构,结果如下
从结果可以看出,索引已经是降序了。下面继续测试降序索引在执行计划中的表现。
分别在MySQL 5.7版本和MySQL 8.0版本的数据表ts1中插入800条随机数据,执行语句如下:
1 | DELIMITER // |
在MySQL 5.7版本中查看数据表ts1的执行计划,结果如下:
1 | EXPLAIN SELECT * FROM ts1 ORDER BY a,b DESC LIMIT 5; |
从结果可以看出,执行计划中扫描数为799,而且使用了Using filesort
。
提示 :
Using filesort
是MySQL中一种速度比较慢的外部排序,能避免是最好的。
多数情况下,管理员可以通过优化索引来尽量避免出现Using filesort,从而提高数据库执行速度。
在MySQL 8.0版本中查看数据表ts1的执行计划。从结果可以看出,执行计划中扫描数为5,而且没有使用Using filesort。
注意:
降序索引
只对查询中特定的排序顺序有效
,如果使用不当
,反而查询效率更低
。
例如,上述查询排序条件改为order by a desc, b desc,MySQL 5.7的执行计划要明显好于MySQL 8.0。
将排序条件修改为order by a desc, b desc后,下面来对比不同版本中执行计划的效果。 在MySQL 5.7版本中查看数据表ts1的执行计划,结果如下:
1 | EXPLAIN SELECT * FROM ts1 ORDER BY a DESC,b DESC LIMIT 5; |
在MySQL 8.0版本中查看数据表ts1的执行计划。
从结果可以看出,修改后MySQL 5.7的执行计划要明显好于MySQL 8.0。
2、隐藏索引
在MySQL 5.7版本及之前,只能通过显式的方式删除索引。
此时,如果发现删除索引后出现错误,又只能通过显式创建索引的方式将删除的索引创建回来。如果数据表中的数据量非常大,或者数据表本身比较大,这种操作就会消耗系统过多的资源,操作成本非常高。
从MySQL 8.x开始支持 隐藏索引
(invisible indexes) ,只需要将待删除的索引设置为隐藏索引,使查询优化器不再使用这个索引(即使使用force index(强制使用索引),优化器也不会使用该索引),确认将索引设置为隐藏索引后系统不受任何响应,就可以彻底删除索引。 这种通过先将索引设置为隐藏索引,再删除索引的方式就是软删除
。
同时,如果你想验证某个索引删除之后的查询性能影响,就可以暂时先隐藏该索引。
注意:
主键不能被设置为隐藏索引。当表中没有显式主键时,表中第一个唯一非空索引会成为隐式主键,也不能设
置为隐藏索引。
索引默认是可见的,在使用CREATE TABLE,CREATE INDEX或者ALTER TABLE等语句时可以通过VISIBLE或者
INVISIBLE关键词设置索引的可见性。
1.创建表时直接创建 在MySQL中创建隐藏索引通过SQL语句INVISIBLE来实现,其语法形式如下:
1 | CREATE TABLE tablename( |
上述语句比普通索引多了一个关键字INVISIBLE
,用来标记索引为不可见索引。
1 | CREATE TABLE book7( |
2.在已经存在的表上创建:
可以为已经存在
的表设置隐藏索引
,其语法形式如下:
1 | CREATE INDEX indexname |
1 | CREATE INDEX idx_cmt ON book7 (COMMENT) invisible; |
注:comment是表内的一个字段不是关键字
通过ALTER TABLE语句创建:
1 | ALTER TABLE tablename |
1 | ALTER TABLE book7 ADD INDEX id_cmt (COMMENT) invisible; |
切换索引可见状态 已存在的索引可通过如下语句切换可见状态:
1 | ALTER TABLE tablename ALTER INDEX index_name INVISIBLE; #切换成隐藏索引 |
如果将index_cname索引切换成可见状态,通过explain查看执行计划,发现优化器选择了index_cname索引。
注意:
当索引被隐藏时,它的内容仍然是和正常索引一样实时更新
的。如果一个索引需要长期被隐藏,那么可以将其删除,因为索引的存在会影响插入、更新和删除的性能
。
通过设置隐藏索引的可见性可以查看索引对调优的帮助。
使隐藏索引对查询优化器可见:
在MySQL 8.x版本中,为索引提供了一种新的测试方式,可以通过查询优化器的一个开关
(use_invisible_indexes)来打开某个设置,使隐藏索引对查询优化器可见。如果 use_invisible_indexes设置为off(默认),优化器会忽略隐藏索引。如果设置为on,即使隐藏索引不可见,优化器在生成执行计划时仍会考虑使用隐藏索引。
(1)在MySQL命令行执行如下命令查看查询优化器的开关设置。
1 | select @@optimizer_switch \G |
在输出的结果信息中找到如下属性配置。
1 | use_invisible_indexes=off |
此属性配置值为off,说明隐藏索引默认对查询优化器不可见。
(2)使隐藏索引对查询优化器可见,需要在MySQL命令行执行如下命令:
1 | set session optimizer_switch="use_invisible_indexes=on"; |
SQL语句执行成功,再次查看查询优化器的开关设置。
1 | select @@optimizer_switch \G |
此时,在输出结果中可以看到如下属性配置。
1 | use_invisible_indexes=on |
use_invisible_indexes属性的值为on,说明此时隐藏索引对查询优化器可见。
(3)使用EXPLAIN查看以字段invisible_column作为查询条件时的索引使用情况。
1 | explain select * from classes where cname = '高一2班'; |
查询优化器会使用隐藏索引来查询数据。
(4)如果需要使隐藏索引对查询优化器不可见,则只需要执行如下命令即可。
1 | mysql> set session optimizer_switch="use_invisible_indexes=off"; |
再次查看查询优化器的开关设置。
1 | mysql> select @@optimizer_switch \G |
此时,use_invisible_indexes属性的值已经被设置为“off”。
如果让隐藏索引对优化器可见,那就没有隐藏索引的意义了
三、索引的设计原则
为了使索引的使用效率更高,在创建索引时,必须考虑在哪些字段上创建索引和创建什么类型的索引。索引设计
**不合理或者缺少索引都会对数据库和应用程序的性能造成障碍。**高效的索引对于获得良好的性能非常重要。设计
索引时,应该考虑相应准则。
1、数据准备
创建数据库、创建表:
1 | CREATE DATABASE atguigudb1; |
创建模拟数据必需的存储函数:
1 | #函数1:创建随机产生字符串函数 |
创建函数,假如报错:
1 | This function has none of DETERMINISTIC...... |
由于开启过慢查询日志bin-log, 我们就必须为我们的function指定一个参数。
主从复制,主机会将写操作记录在bin-log日志中。从机读取bin-log日志,执行语句来同步数据。
如果使用函数来操作数据,会导致从机和主键操作时间不一致。所以,默认情况下,mysql不开启创建函数设置。
- 查看mysql是否允许创建函数:
1 | show variables like 'log_bin_trust_function_creators'; |
- 命令开启:允许创建函数设置:
1 | set global log_bin_trust_function_creators=1; # 不加global只是当前窗口有效。 |
- mysqld重启,上述参数又会消失。永久方法:
1 | #windows下:my.ini[mysqld]加上: |
创建插入模拟数据的存储过程:
1 | # 存储过程1:创建插入课程表存储过程 |
调用存储过程:
1 | CALL insert_course(100); |
2、哪些情况适合创建索引
①字段的数值有唯一性的限制
业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引
。(来源:Alibaba)
说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的。
②频繁作为 WHERE 查询条件的字段
某个字段在SELECT语句的 WHERE 条件中经常被使用到,那么就需要给这个字段创建索引了。
尤其是在数据量大的情况下,创建普通索引就可以大幅提升数据查询的效率。
比如student_info数据表(含100万条数据),假设我们想要查询 student_id=123110 的用户信息。
③经常 GROUP BY 和 ORDER BY 的列
索引就是让数据按照某种顺序进行存储或检索,因此当我们使用 GROUP BY 对数据进行分组查询,或者使用 ORDER BY 对数据进行排序的时候,就需要 对分组或者排序的字段进行索引
。
如果待排序的列有多个,那么可以在这些列上建立 组合索引
。
联合索引中要先放group by的字段后放order by的字段
因为执行的时候先执行group by的字段所以这样会更快
④UPDATE、DELETE 的 WHERE 条件列
对数据按照某个条件进行查询后再进行 UPDATE 或 DELETE 的操作,如果对 WHERE 字段创建了索引,就能大幅提升效率。原理是因为我们需要先根据 WHERE 条件列检索出来这条记录,然后再对它进行更新或删除。
如果进行更新的时候,更新的字段是非索引字段,提升的效率会更明显,这是因为非索引字段更新不需要对索引进行维护。
⑤DISTINCT 字段需要创建索引
有时候我们需要对某个字段进行去重,使用 DISTINCT,那么对这个字段创建索引,也会提升查询效率。
比如,我们想要查询课程表中不同的 student_id 都有哪些,如果我们没有对 student_id 创建索引,执行SQL 语句:
1 | SELECT DISTINCT(student_id) FROM `student_info`; |
运行结果(600637 条记录,运行时间 0.683s
):
如果我们对 student_id 创建索引,再执行 SQL 语句:
1 | SELECT DISTINCT(student_id) FROM `student_info`; |
运行结果(600637 条记录,运行时间 0.010s
):
你能看到 SQL 查询效率有了提升,同时显示出来的 student_id 还是按照 递增的顺序
进行展示的。这是因为索引会对数据按照某种顺序进行排序,所以在去重的时候也会快很多。
⑥多表 JOIN 连接操作时,创建索引注意事项
- 首先,
连接表的数量尽量不要超过 3 张
,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增长会非常快,严重影响查询的效率。 - 其次,
对 WHERE 条件创建索引
,因为 WHERE 才是对数据条件的过滤。如果在数据量非常大的情况下,没有 WHERE 条件过滤是非常可怕的。 - 最后,
对用于连接的字段创建索引
,并且该字段在多张表中的类型必须一致
。比如 course_id 在student_info 表和 course 表中都为 int(11) 类型,而不能一个为 int 另一个为 varchar 类型,不然发生隐式转换则索引失效。
举个例子,如果我们只对 student_id 创建索引,执行 SQL 语句:
1 | SELECT course_id, name, student_info.student_id, course_name |
运行结果(1 条数据,运行时间 0.189s
):
这里我们对 name
创建索引,再执行上面的 SQL 语句,运行时间为 0.002s
。
⑦使用列的类型小的创建索引
我们这里所说的类型大小指的就是该类型表示的数据范围的大小。
我们在定义表结构的时候要显式的指定列的类型,以整数类型为例,有TINYINT、MEDIUMINT、INT、
BIGINT等,它们占用的存储空间依次递增,能表示的整数范围当然也是依次递增。如果我们想要对某个整数列
建立索引的话,在表示的整数范围允许的情况下,尽量让索引列使用较小的类型,比如我们能使用INT就不要使
用BIGINT,能使用MEDIUMINT就不要使用INT。这是因为:
数据类型越小,在查询时进行的比较操作越快
数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下更多的记录,从而减少磁盘I/0带来的性能损耗,也就意味着可以把更多的数据页缓存在内存中,从而加快读写效率。
这个建议对于表的主键来说更加适用,因为不仅是聚簇索引中会存储主键值,其他所有的二级索引的节点处都会
存储一份记录的主键值,如果主键使用更小的数据类型,也就意味着节省更多的存储空间和更高效的I/O。
⑧使用字符串前缀创建索引
假设我们的字符串很长,那存储一个字符串就需要占用很大的存储空间。在我们需要为这个字符串列建立索引
时,那就意味着在对应的B+树中有这么两个问题:
1.B+树索引中的记录需要把该列的完整字符串存储起来,更费时。而且字符串越长,在索引中占用的存储空间越
大。
2.如果B+树索引中索引列存储的字符串很长,那在做字符串比较时会占用更多的时间。
我们可以通过截取字段的前面一部分内容建立索引,这个就叫 前缀索引。这样在查找记录时虽然不能精确的定位
到记录的位置,但是能定位到相应前缀所在的位置,然后根据前缀相同的记录的主键值回表查询完整的字符串
值。既节约空间,又减少了字符串的比较时间,还大体能解决排序的问题。
创建一张商户表,因为地址字段比较长,在地址字段上建立前缀索引
1 | create table shop(address varchar(120) not null); |
问题是,截取多少呢?截取得多了,达不到节省索引存储空间的目的;
截取得少了,重复内容太多,字段的散列度(选择性)会降低。
怎么计算不同的长度的选择性呢?
先看一下字段在全部数据中的选择度:
1 | select count(distinct address) / count(*) from shop; |
通过不同长度去计算,与全表的选择性对比:
公式:
1 | count(distinct left(列名, 索引长度))/count(*) |
例如:
1 | select count(distinct left(address,10)) / count(*) as sub10, -- 截取前10个字符的选择度 |
引申另一个问题:索引列前缀对排序的影响
拓展:Alibaba《Java开发手册》
【 强制
】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会 高达90% 以上 ,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。
⑨区分度高(散列性高)的列适合作为索引
数据相似性大的就不适合建立索引,如:男女性别
⑩使用最频繁的列放到联合索引的左侧
这样也可以较少的建立一些索引。同时,由于”最左前缀原则
“,可以增加联合索引的使用率。
①①在多个字段都要创建索引的情况下,联合索引优于单值索引
3、限制索引的数目
在实际工作中,我们也需要注意平衡,索引的数目不是越多越好。我们需要限制每张表上的索引数量,建议单张
表索引数量 不超过6个。原因:
1.每个索引都需要占用磁盘空间,索引越多,需要的磁盘空间就越大。
2.索引会影响 INSERT、DELETE、UPDATE等语句的性能,因为表中的数据更改的同时,索引也会进行调整和更
新,会造成负担。
3.优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的
执行计划,如果同时有很多个索引都可以用于查询,会增加MySQL优化器生成执行计划时间,降低查询性能。
4、哪些情况不适合创建索引
①在where中使用不到的字段,不要设置索引
group by, order by同样如此
②数据量小的表最好不要使用索引
在数据表中的数据行数比较少的情况下,比如不到 1000 行,是不需要创建索引的。
③有大量重复数据的列上不要建立索引
如,男女性别
当数据重复度大,比如 高于 10% 的时候,也不需要对这个字段使用索引。
④避免对经常更新的表创建过多的索引
⑤不建议用无序的值作为索引
例如身份证
、UUID
(在索引比较时需要转为ASCII,并且插入时可能造成页分裂)、MD5
、HASH
、无序长字符
串等。
⑥删除不再使用或者很少使用的索引
⑦不要定义冗余或重复的索引
(1)冗余索引
举例:建表语句如下
1 | CREATE TABLE person_info( |
通过idx_name_birthday_phone_number
索引就可以对 name
列进行快速搜索,再创建一个专门针对 name
列的索引就算是一个 冗余索引
,维护这个索引只会增加维护的成本,并不会对搜索有什么好处。
(2)重复索引
另一种情况,我们可能会对某个列 重复建立索引
,比方说这样:
1 | CREATE TABLE repeat_index_demo ( |
col1 既是主键、又给它定义为一个唯一索引
,还给它定义了一个普通索引
,可是主键本身就会生成聚簇索引,所以定义的唯一索引和普通索引是重复
的,这种情况要避免。
九、性能分析工具的使用
一、数据库服务器的优化步骤
当我们遇到数据库调优问题的时候,该如何思考呢?
这里把思考的流程整理成下面这张图。
整个流程划分成了 观察
(Show status) 和 行动
(Action) 两个部分。
字母 S 的部分代表观察(会使用相应的分析工具),字母 A 代表的部分是行动(对应分析可以采取的行动)。
我们可以通过观察了解数据库整体的运行状态,通过性能分析工具可以让我们了解执行慢的SQL都有哪些,查看
具体的SQL执行计划,甚至是SQL执行中的每一步的成本代价,这样才能定位问题所在,找到了问题,再采取相
应的行动。
详细解释一下这张图:
首先在S1部分,我们需要观察服务器的状态是否存在周期性的波动。如果存在周期性波动,有可能是周期性节点
的原因,比如双十一、促销活动等。这样的话,我们可以通过A1这一步骤解决,也就是加缓存,或者更改缓存
失效策略。
如果缓存策略没有解决,或者不是周期性波动的原因,我们就需要进一步分析查询延迟和卡顿的原因。接下来进入
S2这一步,我们需要开启慢查询。慢查询可以帮我们定位执行慢的SQL语句。我们可以通过设置
long_query_time参数定义“慢”的阈值,如果SQL执行时间超过了long_query_time,则会认为是慢查询。当收
集上来这些慢查询之后,我们就可以通过分析工具对慢查询日志进行分析。
在S3这一步骤中,我们就知道了执行慢的SQL,这样就可以针对性地用EXPLAIN 查看对应SQL语句的执行计
划,或者使用 show profile查看SQL中每一个步骤的时间成本。这样我们就可以了解SQL查询慢是因为执行
时间长,还是等待时间长。
如果是SQL等待时间长,我们进入A2步骤。在这一步骤中,我们可以调优服务器的参数,比如适当增加数据库
缓冲池等。如果是SQL执行时间长,就进入A3步骤,这一步中我们需要考虑是索引设计的问题?还是查询关联
的数据表过多?还是因为数据表的字段设计问题导致了这一现象。然后在这些维度上进行对应的调整。
如果A2和A3都不能解决问题,我们需要考虑数据库自身的SQL查询性能是否已经达到了瓶颈,如果确认没有达
到性能瓶颈,就需要重新检查,重复以上的步骤。如果已经达到了性能瓶颈,进入A4阶段,需要考虑增加服务
器,采用读写分离的架构,或者考虑对数据库进行分库分表,比如垂直分库、垂直分表和水平分表等。
以上就是数据库调优的流程思路。如果我们发现执行SQL时存在不规则延迟或卡顿的时候,就可以采用分析工具
帮我们定位有问题的SQL,这三种分析工具你可以理解是SQL调优的三个步骤:慢查询、EXPLAIN 和 SHOW
PROFILING.
小结:
二、查看系统性能参数
在MySQL中,可以使用 SHOW STATUS
语句查询一些MySQL数据库服务器的 性能参数 、 执行频率 。SHOW STATUS语句语法如下:
1 | SHOW [GLOBAL|SESSION] STATUS LIKE '参数'; |
一些常用的性能参数如下:
• Connections:连接MySQL服务器的次数。
• Uptime:MySQL服务器的上线时间。
• Slow_queries:慢查询的次数。
• Innodb_rows_read:Select查询返回的行数
• Innodb_rows_inserted:执行INSERT操作插入的行数
• Innodb_rows_updated:执行UPDATE操作更新的行数
• Innodb_rows_deleted:执行DELETE操作删除的行数
• Com_select:查询操作的次数。
• Com_insert:插入操作的次数。对于批量插入的 INSERT 操作,只累加一次。
• Com_update:更新操作的次数。
• Com_delete:删除操作的次数。
三、统计SQL的查询成本:last_query_cost
student_info 表:
1 | CREATE TABLE `student_info` ( |
如果我们想要查询 id=900001 的记录,然后看下查询成本,我们可以直接在聚簇索引上进行查找:
1 | SELECT student_id, class_id, NAME, create_time FROM student_info |
运行结果(1 条记录,运行时间为 0.042s
)
然后再看下查询上次执行sql的优化器的成本
,实际上我们只需要检索一个页即可:
1 | SHOW STATUS LIKE 'last_query_cost'; |
如果我们想要查询 id 在 900001 到 9000100 之间的学生记录呢?
1 | SELECT student_id, class_id, NAME, create_time FROM student_info |
运行结果(100 条记录,运行时间为 0.046s
):
然后再看下查询上次执行sql的优化器的成本
,这时我们大概需要进行 20 个页
的查询。
1 | mysql> SHOW STATUS LIKE 'last_query_cost'; |
你能看到页的数量是刚才的 20 倍,但是查询的效率并没有明显的变化,实际上这两个 SQL 查询的时间基本上一样,就是因为采用了顺序读取的方式
将页面一次性加载到缓冲池中,然后再进行查找。
虽然 页数量(last_query_cost)增加了不少
,但是通过缓冲池的机制,并 没有增加多少查询时间
。
使用场景:它对于比较开销是非常有用的,特别是我们有好几种查询方式可选的时候。
SQL查询是一个动态的过程,从页加载的角度来看,我们可以得到以下两点结论:
位置决定效率。如果页就在数据库缓冲池中,那么效率是最高的,否则还需要从内存或者 磁盘中进行
读取,当然针对单个页的读取来说,如果页存在于内存中,会比在磁盘中读取效率高很多。批量决定效率。如果我们从磁盘中对单一页进行随机读,那么效率是很低的(差不多10ms),而采用顺
序读取的方式,批量对页进行读取,平均一页的读取效率就会提升很多,甚至要快于单个页面在内存中
的随机读取。
所以说,遇到I/O并不用担心,方法找对了,效率还是很高的。我们首先要考虑数据存放的位置,如果是经
常使用的数据就要尽量放到缓冲池中,其次我们可以充分利用磁盘的吞吐能力,一次性批量读取数据,这样
单个页的读取效率也就得到了提升。
四、定位执行慢的 SQL:慢查询日志
MySQL的慢查询日志,用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值
的SQL,则会被记录到慢查询日志中。long_query_time的默认值为10,意思是运行10秒以上(不含10秒)的语
句,认为是超出了我们的最大忍耐时间值。
它的主要作用是,帮助我们发现那些执行时间特别长的SQL查询,并且有针对性地进行优化,从而提高系统的整
体效率。当我们的数据库服务器发生阻塞、运行变慢的时候,检查一下慢查询日志,找到那些慢查询,对解决问
题很有帮助。比如一条sql执行超过5秒钟,我们就算慢SQL,希望能收集超过5秒的sql,结合explain进行全面分
析。
默认情况下,MysQL数据库没有开启慢查询日志,需要我们手动来设置这个参数。如果不是调优需要的话,一般
**不建议启动该参数,**因为开启慢查询日志会或多或少带来一定的性能影响。
慢查询日志支持将日志记录写入文件。
1、开启慢查询日志参数
①开启slow_query_log
1 | set global slow_query_log='ON'; |
然后我们再来查看下慢查询日志是否开启
,以及慢查询日志文件的位置
:
你能看到这时慢查询分析已经开启,同时文件保存在 /var/lib/mysql/atguigu02-slow.log
文件中。
②修改long_query_time阈值
接下来我们来看下慢查询的时间阈值设置
(就是超过多少的执行时间算是慢查询),使用如下命令:
1 | show variables like '%long_query_time%'; |
这里如果我们想把时间缩短,比如设置为 1
秒,可以这样设置:
1 | #测试发现:设置global的方式对当前session的long_query_time失效。对新连接的客户端有效。所以可以一并执行下述语句 |
2、查看慢查询数目
查询当前系统中有多少条慢查询记录
1 | SHOW GLOBAL STATUS LIKE '%Slow_queries%'; |
3、案例演示
步骤1. 建表:
1 | CREATE TABLE `student` ( |
步骤2:设置参数 log_bin_trust_function_creators
创建函数,假如报错
1 | This function has none of DETERMINISTIC...... |
- 命令开启:允许创建函数设置:
1 | set global log_bin_trust_function_creators=1; # 不加global只是当前窗口有效。 |
步骤3:创建函数
随机产生字符串:
1 | DELIMITER // |
产生随机数值:
1 | DELIMITER // |
步骤4:创建存储过程
1 | DELIMITER // |
步骤5:调用存储过程
1 | #调用刚刚写好的函数, 4000000条记录,从100001号开始 |
4、测试及分析
①测试
1 | SELECT * FROM student WHERE stuno = 3455655; |
从上面的结果可以看出来,查询学生编号为“3455655”的学生信息花费时间为2.09秒。查询学生姓名为“oQmLUr”的学生信息花费时间为2.39秒。
已经达到了秒的数量级,说明目前查询效率是比较低的,下面的小节我们分析一下原因。
②分析
1 | show status like 'slow_queries'; |
5、慢查询日志分析工具:mysqldumpslow
在生产环境中,如果要手工分析日志,查找、分析SQL,显然是个体力活,MySQL提供了日志分析工具mysqldumpslow
。
通过mysqldumpslow 可以查看慢查询日志
查看mysqldumpslow的帮助信息
1 | mysqldumpslow --help |
mysqldumpslow 命令的具体参数如下:
- -a: 不将数字抽象成N,字符串抽象成S
-s: 是表示按照何种方式排序
:
c: 访问次数
l: 锁定时间
r: 返回记录t: 查询时间
al:平均锁定时间
ar:平均返回记录数
at:平均查询时间 (默认方式)
ac:平均查询次数-t: 即为返回前面多少条的数据
;-g: 后边搭配一个正则匹配模式,大小写不敏感的
;
举例:我们想要按照查询时间排序,查看前五条 SQL 语句,这样写即可:
1 | mysqldumpslow -s t -t 5 /var/lib/mysql/atguigu01-slow.log |
工作常用参考
:
1 | 得到返回记录集最多的10个SQL |
6、关闭慢查询日志
MySQL服务器停止慢查询日志功能有两种方法:
①方式1:永久性方式
1 | 配置文件 |
或者,把slow_query_log一项注释掉 或 删除
1 | [mysqld] |
重启MySQL服务,执行如下语句查询慢日志功能。
1 | SHOW VARIABLES LIKE '%slow%'; #查询慢查询日志所在目录 |
②方式2:临时性方式
使用SET语句来设置。
(1)停止MySQL慢查询日志功能,具体SQL语句如下。
1 | SET GLOBAL slow_query_log=off; |
(2)重启MySQL服务
,使用SHOW语句查询慢查询日志功能信息,具体SQL语句如下
1 | SHOW VARIABLES LIKE '%slow%'; |
7、删除慢查询日志
五、查看 SQL 执行成本:SHOW PROFILE
PROFILE可查看上条sql的执行成本
1 | show variables like 'profiling'; |
通过设置 profiling='ON’
来开启 show profile:
1 | mysql > set profiling = 'ON'; |
然后执行相关的查询语句。接着看下当前会话都有哪些 profiles,使用下面这条命令:
1 | show profiles; |
你能看到当前会话一共有 2 个查询。如果我们想要查看最近一次查询的开销
,可以使用:
1 | show profile; |
1 | show profile cpu,block io for query 2; |
show profile的常用查询参数:
① ALL:显示所有的开销信息。
② BLOCK IO:显示块IO开销。
③ CONTEXT SWITCHES:上下文切换开销。
④ CPU:显示CPU开销信息。
⑤ IPC:显示发送和接收开销信息。
⑥ MEMORY:显示内存开销信息。
⑦ PAGE FAULTS:显示页面错误开销信息。
⑧ SOURCE:显示和Source_function,Source_file,Source_line相关的开销信息。
⑨ SWAPS:显示交换次数开销信息。
日常开发需注意的结论:
converting HEAP to MyISAM:查询结果太大,内存不够,数据往磁盘上搬了。
Creating tmp table:创建临时表。先拷贝数据到临时表,用完后再删除临时表。
Copying to tmp table on disk:把内存中临时表复制到磁盘上,警惕!
locked。
如果在show profile诊断结果中出现了以上4条结果中的任何一条,则sql语句需要优化。
注意:
不过SHOW PROFILE 命令将被弃用,我们可以从information_schema中的 profiling数据表进行查看。
六、分析查询语句:EXPLAIN
1、概述
定位了查询慢的SQL之后,我们就可以使用EXPLAIN或DESCRIBE工具做针对性的分析查询语句。DESCRIBE语句
的使用方法与EXPLAIN语句是一样的,并且分析结果也是一样的。
MySQL中有专门负责优化SELECT语句的优化器模块,主要功能:通过计算分析系统中收集到的统计信息,为客户
端请求的Query提供它认为最优的执行计划(他认为最优的数据检索方式,但不见得是DBA认为是最优的,这部分
最耗费时间)。|
这个执行计划展示了接下来具体执行查询的方式,比如多表连接的顺序是什么,对于每个表采用什么访问方法来
具体执行查询等等。MySQL为我们提供了EXPLAIN语句来帮助我们查看某个查询语句的具体执行计划,大家看懂
EXPLAIN语句的各个输出项,可以有针对性的提升我们查询语句的性能。
能做什么?
- 表的读取顺序
- 数据读取操作的操作类型
- 哪些索引可以使用
- 哪些索引被实际使用
- 表之间的引用
- 每张表有多少行被优化器查询
版本情况:
- MySQL 5.6.3以前只能
EXPLAIN SELECT
;MYSQL 5.6.3以后就可以EXPLAIN SELECT,UPDATE,DELETE
- 在5.7以前的版本中,想要显示
partitions
需要使用explain partitions
命令;想要显示filtered
需要使用explain extended
命令。在5.7版本后,默认explain直接显示partitions和filtered中的信息。
2、基本语法
EXPLAIN 或 DESCRIBE语句的语法形式如下:
1 | EXPLAIN SELECT select_options |
如果我们想看看某个查询的执行计划的话,可以在具体的查询语句前边加一个 EXPLAIN
,就像这样:
1 | EXPLAIN SELECT 1; |
EXPLAIN
语句输出的各个列的作用如下:
3、数据准备
建表:
1 | CREATE TABLE s1 ( |
设置参数 log_bin_trust_function_creators:
创建函数,假如报错,需开启如下命令:允许创建函数设置:
1 | set global log_bin_trust_function_creators=1; # 不加global只是当前窗口有效。 |
创建函数:
1 | DELIMITER // |
创建存储过程:
创建往s1
表中插入数据的存储过程:
1 | DELIMITER // |
创建往s2
表中插入数据的存储过程:
1 | DELIMITER // |
调用存储过程:
s1表数据的添加:加入1万条记录:
1 | CALL insert_s1(10001,10000); |
s2表数据的添加:加入1万条记录:
1 | CALL insert_s2(10001,10000); |
4、EXPLAIN各列作用
①table
不论我们的查询语句有多复杂,里边儿 包含了多少个表
,到最后也是需要对每个表进行 单表访问
的,所以MySQL规定EXPLAIN语句输出的每条记录都对应着某个单表的访问方法
,该条记录的table列代表着该表的表名(有时不是真实的表名字,可能是简称)。
②id
在写的查询语句一般都以 SELECT
关键字开头,比较简单的查询语句里只有一个 SELECT 关键字,比如下边这个查询语句:
1 | SELECT * FROM s1 WHERE key1 = 'a'; |
稍微复杂一点的连接查询中也只有一个 SELECT 关键字,比如:
1 | SELECT * FROM s1 INNER JOIN s2 |
1 | mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2; |
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2) OR key3 = 'a'; |
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key2 FROM s2 WHERE common_field = 'a'); |
1 | mysql> EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2; |
1 | mysql> EXPLAIN SELECT * FROM s1 UNION ALL SELECT * FROM s2; |
小结:
- id如果相同,可以认为是一组,从上往下顺序执行
- 在所有组中,id值越大,优先级越高,越先执行
- 关注点:id号每个号码,表示一趟独立的查询, 一个sql的查询趟数越少越好
③select_type
具体分析如下:
1 | mysql> EXPLAIN SELECT * FROM s1; |
当然,连接查询也算是 SIMPLE
类型,比如:
1 | mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2; |
- PRIMARY
1 | mysql> EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2; |
- UNION
- UNION RESULT
- SUBQUERY
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2) OR key3 = 'a'; |
- DEPENDENT SUBQUERY
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2 WHERE s1.key2 = s2.key2) OR key3 = 'a'; |
- DEPENDENT UNION
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2 WHERE key1 = 'a' UNION SELECT key1 FROM s1 WHERE key1 = 'b'); |
- DERIVED
1 | mysql> EXPLAIN SELECT * FROM (SELECT key1, count(*) as c FROM s1 GROUP BY key1) AS derived_s1 where c > 1; |
- MATERIALIZED
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2); |
- UNCACHEABLE SUBQUERY
- UNCACHEABLE UNION
④partitions (可略)
如果想详细了解,可以如下方式测试。创建分区表:
1 | -- 创建分区表, |
1 | DESC SELECT * FROM user_partitions WHERE id>200; |
查询id大于200(200>100,p1分区)的记录,查看执行计划,partitions是p1,符合我们的分区规则
⑤type ☆
完整的访问方法如下: system , const , eq_ref , ref , fulltext , ref_or_null ,index_merge , unique_subquery , index_subquery , range , index , ALL
。
- system
1 | mysql> CREATE TABLE t(i int) Engine=MyISAM; |
查询这个表的执行计划:
1 | mysql> EXPLAIN SELECT * FROM t; |
- const
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE id = 10005; |
- eq_ref
1 | mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.id = s2.id; |
从执行计划的结果中可以看出,MySQL打算将s2作为驱动表,s1作为被驱动表,重点关注s1的访问方法是 eq_ref
,表明在访问s1表的时候可以 通过主键的等值匹配
来进行访问。
- ref
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = 'a'; |
- fulltext
全文索引 - ref_or_null
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' OR key1 IS NULL; |
- index_merge
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' OR key3 = 'a'; |
从执行计划的 type
列的值是 index_merge
就可以看出,MySQL 打算使用索引合并的方式来执行对 s1
表的查询。
- unique_subquery
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key2 IN (SELECT id FROM s2 where s1.key1 = s2.key1) OR key3 = 'a'; |
- index_subquery
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE common_field IN (SELECT key3 FROM s2 where s1.key1 = s2.key1) OR key3 = 'a'; |
- range
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 IN ('a', 'b', 'c'); |
或者:
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 > 'a' AND key1 < 'b'; |
- index
1 | mysql> EXPLAIN SELECT key_part2 FROM s1 WHERE key_part3 = 'a'; |
- ALL
1 | mysql> EXPLAIN SELECT * FROM s1; |
小结:
结果值从最好到最坏依次是:
system > const > eq_ref > ref
> fulltext > ref_or_null > index_merge > unique_subquery > index_subquery >range > index > ALL
其中比较重要的几个提取出来(见上图中的蓝色)。
SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,最好是 consts级别。(阿里巴巴开发手册要求)
⑥possible_keys和key
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 > 'z' AND key3 = 'a'; |
⑦key_len ☆
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE id = 10005; |
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key2 = 10126; |
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = 'a'; |
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key_part1 = 'a'; |
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key_part1 = 'a' AND key_part2 = 'b'; |
练习:
key_len的长度计算公式:
- varchar(10)变长字段且允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)+1(NULL)+2(变长字段)
- varchar(10)变长字段且不允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)+2(变长字段)
- char(10)固定字段且允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)+1(NULL)
- char(10)固定字段且不允许NULL = 10 * ( character set:utf8=3,gbk=2,latin1=1)
⑧ref
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = 'a'; |
1 | mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.id = s2.id; |
1 | mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s2.key1 = UPPER(s1.key1); |
⑨rows ☆
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 > 'z'; |
⑩filtered
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 > 'z' AND common_field = 'a'; |
1 | mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key1 WHERE |
①①Extra ☆
1 | mysql> EXPLAIN SELECT 1; |
- Impossible WHERE
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE 1 != 1; |
- Using where
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE common_field = 'a'; |
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' AND common_field = 'a'; |
- No matching min/max row
1 | mysql> EXPLAIN SELECT MIN(key1) FROM s1 WHERE key1 = 'abcdefg'; |
- Using index
1 | mysql> EXPLAIN SELECT key1 FROM s1 WHERE key1 = 'a'; |
- Using index condition
1 | SELECT * FROM s1 WHERE key1 > 'z' AND key1 LIKE '%a'; |
- Using join buffer (Block Nested Loop)
1 | mysql> EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.common_field = s2.common_field; |
- Not exists
1 | mysql> EXPLAIN SELECT * FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1 WHERE s2.id IS NULL; |
- Using intersect(…) 、 Using union(…) 和 Using sort_union(…)
1 | mysql> EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' OR key3 = 'a'; |
- Zero limit
1 | mysql> EXPLAIN SELECT * FROM s1 LIMIT 0; |
- Using filesort
1 | mysql> EXPLAIN SELECT * FROM s1 ORDER BY key1 LIMIT 10; |
1 | mysql> EXPLAIN SELECT * FROM s1 ORDER BY common_field LIMIT 10; |
- Using temporary
1 | mysql> EXPLAIN SELECT DISTINCT common_field FROM s1; |
再比如:
1 | mysql> EXPLAIN SELECT common_field, COUNT(*) AS amount FROM s1 GROUP BY common_field; |
1 | mysql> EXPLAIN SELECT key1, COUNT(*) AS amount FROM s1 GROUP BY key1; |
从 Extra
的 Using index
的提示里我们可以看出,上述查询只需要扫描 idx_key1
索引就可以搞定了,不再需要临时表了。
小结:
EXPLAIN不考虑各种Cache
EXPLAIN不能显示MySQL在执行查询时所作的优化工作
EXPLAIN不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况部分统计信息是估算的,并非精确值
七、EXPLAIN的进一步使用
1、EXPLAIN四种输出格式
这里谈谈EXPLAIN的输出格式。EXPLAIN可以输出四种格式: 传统格式 , JSON格式 , TREE格式
以及 可视化输出
。用户可以根据需要选择适用于自己的格式。
①传统格式
传统格式简单明了,输出是一个表格形式,概要说明查询计划。
1 | mysql> EXPLAIN SELECT s1.key1, s2.key1 FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1 WHERE s2.common_field IS NOT NULL; |
②JSON格式
- JSON格式:在EXPLAIN单词和真正的查询语句中间加上
FORMAT=JSON
。
1 | EXPLAIN FORMAT=JSON SELECT .... |
我们使用#
后边跟随注释的形式为大家解释了 EXPLAIN FORMAT=JSON
语句的输出内容,但是大家可能有疑问 “cost_info” 里边的成本看着怪怪的,它们是怎么计算出来的?
先看s1
表的 "cost_info"
部分:
1 | "cost_info": { |
read_cost
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
是由下边这两部分组成的:
- `IO`成本
- 检测 `rows × (1 - filter)` 条记录的 `CPU 成本`
> rows和filter都是我们前边介绍执行计划的输出列,在JSON格式的执行计划中,rows相当于rows_examined_per_scan,filtered名称不变
- `eval_cost` 是这样计算的:
检测 `rows × filter` 条记录的成本。
- `prefix_cost`就是单独查询 `s1` 表的成本,也就是:`read_cost + eval_cost`
- `data_read_per_join` 表示在此次查询中需要读取的数据量。
对于 `s2` 表的 `"cost_info"` 部分是这样的:
```json
"cost_info": {
"read_cost": "968.80",
"eval_cost": "193.76",
"prefix_cost": "3197.16",
"data_read_per_join": "1M"
}
123456
由于 s2
表是被驱动表,所以可能被读取多次,这里的 read_cost
和 eval_cost
是访问多次 s2 表后累加起来的值,大家主要关注里边儿的 prefix_cost
的值代表的是整个连接查询预计的成本,也就是单次查询 s1
表和多次查询 s2
表后的成本的和,也就是:
968.80 + 193.76 + 2034.60 = 3197.16
③TREE格式
TREE格式是8.0.16版本之后引入的新格式,主要根据查询的 各个部分之间的关系
和 各部分的执行顺序
来描述如何查询。
1 | EXPLAIN FORMAT=tree SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key2 WHERE |
④可视化输出
可视化输出,可以通过MySQL Workbench可视化查看MySQL的执行计划。通过点击Workbench的放大镜图标,即可生成可视化的查询计划。
上图按从左到右的连接顺序显示表。红色框表示 全表扫描
,而绿色框表示使用 索引查找
对于每个表,显示使用的索引。还要注意的是,每个表格的框上方是每个表访问所发现的行数的估计值以及访问该表的成本。
2、SHOW WARNINGS的使用
1 | mysql> EXPLAIN SELECT s1.key1, s2.key1 FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1 WHERE s2.common_field IS NOT NULL; |
1 | mysql> SHOW WARNINGS\G |
八、分析优化器执行计划:trace
1 | SET optimizer_trace="enabled=on",end_markers_in_json=on; |
开启后,可分析如下语句:
- SELECT
- INSERT
- REPLACE
- UPDATE
- DELETE
- EXPLAIN
- SET
- DECLARE
- CASE
- IF
- RETURN
- CALL
测试:执行如下SQL语句
1 | select * from student where id < 10; |
最后, 查询 information_schema.optimizer_trace
就可以知道MySQL是如何执行SQL的 :
1 | select * from information_schema.optimizer_trace\G |
九、MySQL监控分析视图-sys schema
1、Sys schema视图摘要
2、Sys schema视图使用场景
索引情况
1 | #1. 查询冗余索引 |
表相关
1 | # 1. 查询表的访问量 |
语句相关
1 | #1. 监控SQL执行的频率 |
IO相关
1 | #查看消耗磁盘IO的文件 |
Innodb 相关
1 | #行锁阻塞情况 |
十、索引优化与查询优化
一、数据准备
学员表
插 50万
条, 班级表
插 1万
条。
步骤1:建表
1 | #班级表 |
步骤2:设置参数
命令开启:允许创建函数设置:
1 | set global log_bin_trust_function_creators=1; |
步骤3:创建函数
保证每条数据都不同。
1 | #随机产生字符串 |
随机产生班级编号
1 | #用于随机产生多少到多少的编号 |
步骤4:创建存储过程
1 | #创建往stu表中插入数据的存储过程 |
创建往class表中插入数据的存储过程
1 | #执行存储过程,往class表添加随机数据 |
步骤5:调用存储过程
class
1 | #执行存储过程,往class表添加1万条数据 |
stu
1 | #执行存储过程,往stu表添加50万条数据 |
步骤6:删除某表上的索引
创建存储过程
1 | DELIMITER // |
执行存储过程
1 | CALL proc_drop_index("dbname","tablename"); |
二、索引失效案例
1、全值匹配我最爱
全值的等值匹配
2、最佳左前缀法则
拓展:Alibaba《Java开发手册》
索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
3、主键插入顺序
如果此时再插入一条主键值为 9
的记录,那它插入的位置就如下图:
可这个数据页已经满了,再插进来咋办呢?我们需要把当前 页面分裂
成两个页面,把本页中的一些记录移动到新创建的这个页中。页面分裂和记录移位意味着什么?意味着: 性能损耗
!所以如果我们想尽量避免这样无谓的性能损耗,最好让插入的记录的 主键值依次递增
,这样就不会发生这样的性能损耗了。
所以我们建议:让主键具有 AUTO_INCREMENT
,让存储引擎自己为表生成主键,而不是我们手动插入 ,比如: person_info
表:
1 | CREATE TABLE person_info( |
我们自定义的主键列 id
拥有 AUTO_INCREMENT
属性,在插入记录时存储引擎会自动为我们填入自增的主键值。这样的主键占用空间小,顺序写入,减少页分裂。
4、计算、函数、类型转换(自动或手动)导致索引失效
1 | EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%'; |
创建索引
1 | CREATE INDEX idx_name ON student(NAME); |
第一种:索引优化生效
1 | mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%'; |
第二种:索引优化失效
1 | mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc'; |
1 | mysql> SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc'; |
type为“ALL”,表示没有使用到索引,查询时间为 3.62
秒,查询效率较之前低很多。
再举例:
- student表的字段stuno上设置有索引
1 | CREATE INDEX idx_sno ON student(stuno); |
- 索引优化生效:
1 | EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno = 900000; |
再举例:
- student表的字段name上设置有索引
1 | CREATE INDEX idx_name ON student(NAME); |
1 | EXPLAIN SELECT id, stuno, NAME FROM student WHERE NAME LIKE 'abc%'; |
5、类型转换导致索引失效
下列哪个sql语句可以用到索引。(假设name字段上设置有索引)
1 | # 未使用到索引 |
1 | # 使用到索引 |
name=123发生类型转换,索引失效。(隐式的类型转换
)
6、范围条件右边的列索引失效
1 | ALTER TABLE student DROP INDEX idx_name; |
1 | create index idx_age_name_classid on student(age,name,classid); |
- 将范围查询条件放置语句最后:
1 | EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.name = 'abc' AND student.classId>20 ; |
7、 不等于(!= 或者<>)索引失效
当sql语句中有!=或者<>会出现索引失效的问题,尝试改写为等于
,或采用覆盖索引
主键索引仍不会失效
8、is null可以使用索引,is not null无法使用索引
1 | EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NULL; |
结论:最好在设计数据表的时候就将字段设置为 NOT NULL 约束,比如你可以将INT类型的字段,默认值设
置为0。将字符类型的默认值设置为空字符串(“)。
拓展:同理,在查询中使用 not like 也无法使用索引,导致全表扫描。
9、 like以通配符%开头索引失效
拓展:Alibaba《Java开发手册》
【强制】页面搜索严禁左模糊
或者全模糊
,如果需要请走搜索引擎来解决。
10、OR 前后存在非索引的列,索引失效
让OR的前后条件
都具备索引,如果缺少一个就会出现索引失效
1 | # 未使用到索引 |
1 | #使用到索引 |
11、数据库和表的字符集统一使用utf8mb4
统一使用utf8mb4( 5.5.3版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码。不同的 字符集
进行比较前需要进行 转换
会造成索引失效。
一般性建议:
- 对于单列索引,尽量选择针对当前query过滤性更好的索引
- 在选择组合索引的时候,当前query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。
- 在选择组合索引的时候,尽量选择能够包含当前query中的where子句中更多字段的索引。
- 在选择组合索引的时候,如果某个字段可能出现范围查询时,尽量把这个字段放在索引次序的最后面。
总之,书写SQL语句时,尽量避免造成索引失效的情况。
三、关联查询优化
1、数据准备
2、采用左外连接
1 | EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card; |
结论:type 有All
添加索引优化
1 | ALTER TABLE book ADD INDEX Y ( card); #【被驱动表】,可以避免全表扫描 |
可以看到第二行的 type 变为了 ref,rows 也变成了优化比较明显。这是由左连接特性决定的。左外连接LEFT JOIN条件
用于确定如何从右表搜索行,左边一定都有,所以 右边是我们的关键点,一定需要建立索引
。
1 | ALTER TABLE `type` ADD INDEX X (card); #【驱动表】,无法避免全表扫描 |
然后
1 | DROP INDEX Y ON book; |
3、采用内连接
两个表都没有索引,则小表驱动大表
若一个表有索引一个表没有,则有索引的为被驱动表(优化器会自己进行调整)
如果都有索引就按照正常的来
1 | drop index X on type; |
换成 inner join(MySQL自动选择驱动表
)
1 | EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card; |
添加索引优化
1 | ALTER TABLE book ADD INDEX Y ( card); |
1 | ALTER TABLE type ADD INDEX X (card); |
接着:
1 | DROP INDEX X ON `type`; |
接着:
1 | ALTER TABLE `type` ADD INDEX X (card); |
4、join语句原理
#####1.驱动表和被驱动表
驱动表就是主表,被驱动表就是从表、非驱动表。
· 对于内连接来说:
1 | SELECT * FROM A JOIN B ON ... |
A一定是驱动表吗?不一定,优化器会根据你查询语句做优化,决定先查哪张表。先查询的那张表就是驱动表,
反之就是被驱动表。通过explain关键字可以查看。
· 对于外连接来说:
1 | SELECT * FROM A LEFT JOIN B ON ... |
通常,大家会认为A就是驱动表,B就是被驱动表。但也未必。
优化器也会去调整表的位置。
测试如下:
1 | CREATE TABLE a(f1 INT, f2 INT, INDEX(f1))ENGINE=INNODB; |
#####2.简单嵌套循环连接
- Simple Nested-Loop Join
就是粗暴的相乘,最直接的,但性能最差
#####3.索引嵌套循环连接
- Index Nested-Loop Join
1 | EXPLAIN SELECT * FROM t1 STRAIGHT_JOIN t2 ON (t1.a=t2.a); |
Index Nested-Loop Join其优化的思路主要是为了减少内层表数据的匹配次数,所以要求被驱动表上必须有索引才
行。通过外层表匹配条件直接与内层表索引进行匹配,避免和内层表的每条记录去进行比较,这样极大的减少了
对内层表的匹配次数。
4.快嵌套循环连接
- Block Nested-Loop Join
如果存在索引,那么会使用index的方式进行join,如果join的列没有索引,被驱动表要扫描的次数太多了。每次访
问被驱动表,其表中的记录都会被加载到内存中,然后再从驱动表中取一条与其匹配,匹配结束后清除内存,然
后再从驱动表中加载一条记录,然后把被驱动表的记录在加载到内存匹配,这样周而复始,大大增加了I0的次
数。为了减少被驱动表的IO次数,就出现了Block Nested-Loop Join的方式。
不再是逐条获取驱动表的数据,而是一块一块的获取,引入了join buffer缓冲区,将驱动表join相关的部分数
据列(大小受join buffer的限制)缓存到join buffer中,然后全表扫描被驱动表,被驱动表的每一条记录一次性和join
buffer中的所有驱动表记录进行匹配(内存中操作),将简单嵌套循环中的多次比较合并成一次,降低了被驱动
表的访问频率。
注意:
这里缓存的不只是关联表的列,select后面的列也会缓存起来。
在一个有N个join关联的sql中会分配N-1个join buffer。所以查询的时候尽量减少不必要的字段,可以让join
buffer中可以存放更多的列。
参数设置:
- block_nested_loop
通过 show variables like’%optimizer_switch%’查看block_nested_loop状态。默认是开启的。
- join_buffer_size
驱动表能不能一次加载完,要看join buffer能不能存储所有的数据,默认情况下join_buffer_size=256k。
join_buffer_size的最大值在32位系统可以申请4G,而在64位操做系统下可以申请大于IG的Join Buffer空间(64位
Windows 除外,其大值会被截断为4GB并发出警告)。
如果直接使用join语句,MySQL优化器可能会选择表t1或t2作为驱动表,这样会影响我们分析SQL语句的执行过程。
所以,为了便于分析执行过程中的性能问题,我改用 straight_join
让MySQL使用固定的连接方式执行查询,这样优化器只会按照我们指定的方式去join。在这个语句里,t1 是驱动表,t2是被驱动表。
可以看到,在这条语句里,被驱动表t2的字段a上有索引,join过程用上了这个索引,因此这个语句的执行流程是这样的:
- 从表t1中读入一行数据 R;
- 从数据行R中,取出a字段到表t2里去查找;
- 取出表t2中满足条件的行,跟R组成一行,作为结果集的一部分;
- 重复执行步骤1到3,直到表t1的末尾循环结束。
这个过程是先遍历表t1,然后根据从表t1中取出的每行数据中的a值,去表t2中查找满足条件的记录。在形式上,这个过程就跟我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引,所以我们称之为**“Index Nested-Loop Join”,简称NLJ**。
它对应的流程图如下所示:
在这个流程里:
- 对驱动表t1做了全表扫描,这个过程需要扫描100行;
- 而对于每一行R,根据a字段去表t2查找,走的是树搜索过程。由于我们构造的数据都是一一对应的,因此每次的搜索过程都只扫描一行,也是总共扫描100行;
- 所以,整个执行流程,总扫描行数是200。
引申问题1:能不能使用join?
引申问题2:怎么选择驱动表?
比如:
N扩大1000倍的话,扫描行数就会扩大1000倍;而M扩大1000倍,扫描行数扩大不到10倍。两个结论:
- 使用join语句,性能比强行拆成多个单表执行SQL语句的性能要好;
- 如果使用join语句的话,需要让小表做驱动表。
- Simple Nested-Loop Join
- Block Nested-Loop Join
这个过程的流程图如下:
执行流程图也就变成这样:
总结1:能不能使用xxx join语句?
总结2:如果要使用join,应该选择大表做驱动表还是选择小表做驱动表?
总结3:什么叫作“小表”?
在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与join的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。
5、小结
保证被驱动表的JOIN字段已经创建了索引
需要JOIN 的字段,数据类型保持绝对一致。
LEFT JOIN 时,选择小表作为驱动表,
大表作为被驱动表
。减少外层循环的次数。INNER JOIN 时,MySQL会自动将
小结果集的表选为驱动表
。选择相信MySQL优化策略。能够直接多表关联的尽量直接关联,不用子查询。(减少查询的趟数)
不建议使用子查询,建议将子查询SQL拆开结合程序多次查询,或使用 JOIN 来代替子查询。
衍生表建不了索引
6.hash join
四、子查询优化
看看能不能将子查询优化成内外连接查询
MySQL从4.1版本开始支持子查询,使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结果作为另一个SELECT语句的条件。 子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作
。
子查询是 MySQL 的一项重要的功能,可以帮助我们通过一个 SQL 语句实现比较复杂的查询。但是,子查询的执行效率不高。
原因:
① 执行子查询时,MySQL需要为内层查询语句的查询结果建立一个临时表
,然后外层查询语句从临时表中查询记录。查询完毕后,再 撤销这些临时表
。这样会消耗过多的CPU和IO资源,产生大量的慢查询。
② 子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表都 不会存在索引
,所以查询性能会受到一定的影响。
③ 对于返回结果集比较大的子查询,其对查询性能的影响也就越大。
在MySQL中,可以使用连接(JOIN)查询来替代子查询。
连接查询 不需要建立临时表
,其 速度比子查询要快
,如果查询中使用索引的话,性能就会更好。
结论:
尽量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL替代
五、排序优化
1、排序优化
问题:
在 WHERE 条件字段上加索引,但是为什么在 ORDER BY 字段上还要加索引呢?
回答:
在MySQL中,支持两种排序方式,分别是FileSort 和Index 排序。
Index 排序中,索引可以保证数据的有序性,不需要再进行排序,效率更高。
FileSort排序则一般在内存中进行排序,占用CPU 较多。如果待排结果较大,会产生临时文件I/O到磁盘进
行排序的情况,效率较低。
优化建议:
- SQL 中,可以在 WHERE 子句和 ORDER BY 子句中使用索引,目的是在 WHERE 子句中
避免全表扫描
,在 ORDER BY 子句避免使用 FileSort 排序
。当然,某些情况下全表扫描,或者 FileSort 排序不一定比索引慢。但总的来说,我们还是要避免,以提高查询效率。 - 尽量使用 Index 完成 ORDER BY 排序。如果 WHERE 和 ORDER BY 后面是相同的列就使用单索引列;如果不同就使用联合索引。
- 无法使用 Index 时,需要对 FileSort 方式进行调优。
1 | INDEX a_b_c(a,b,c) |
2、案例实战
ORDER BY子句,尽量使用Index方式排序,避免使用FileSort方式排序。
执行案例前先清除student上的索引,只留主键:
1 | DROP INDEX idx_age ON student; |
场景:查询年龄为30岁的,且学生编号小于101000的学生,按用户名称排序
1 | EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ; |
查询结果如下:
1 | mysql> SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME; |
结论:
type 是 ALL,即最坏的情况。Extra 里还出现了 Using filesort,也是最坏的情况。优化是必须的。
优化思路:
方案一: 为了去掉filesort我们可以把索引建成
1 | #创建新索引 |
方案二: 尽量让where的过滤条件和排序使用上索引
建一个三个字段的组合索引:
1 | DROP INDEX idx_age_name ON student; |
结果竟然有 filesort的 sql 运行速度,超过了已经优化掉 filesort的 sql
,而且快了很多,几乎一瞬间就出现了结果。
结论:
- 两个索引同时存在,mysql自动选择最优的方案。(对于这个例子,mysql选择
idx_age_stuno_name)。但是,随着数据量的变化,选择的索引也会随之变化的
。- 当【范围条件】和【group by 或者 order by】的字段出现二选一时,优先观察条件字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。反之,亦然。
思考:这里我们使用如下索引,是否可行?
1 | DROP INDEX idx_age_stuno_name ON student; |
4、filesort算法:双路排序和单路排序
双路排序 (慢)
MySQL 4.1
之前是使用双路排序
,字面意思就是两次扫描磁盘,最终得到数据, 读取行指针和order by
列 ,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出- 从磁盘取排序字段,在buffer进行排序,再从
磁盘取其他字段
。
取一批数据,要对磁盘进行两次扫描,众所周知,IO是很耗时的,所以在mysql4.1之后,出现了第二种改进的算法,就是单路排序。
单路排序 (快)
从磁盘读取查询需要的 所有列
,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出, 它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空
间, 因为它把每一行都保存在内存中了。
结论及引申出的问题
由于单路是后出的,总体而言好过双路
但是用单路有问题
在sort_buffer中,单路比多路要多占用很多空间,因为单路是把所有字段都取出,所以有可能取出的数据的
总大小超出了 sort_buffer的容量,导致每次只能取sort_buffer容量大小的数据,进行排序(创建tmp文
件,多路合并),排完再取sort_buffer容量大小,再排 …… 从而多次I/O。
单路本来想省一次I/O操作,反而导致了大量的I/O操作,反而得不偿失。
优化策略:
尝试提高
sort_buffer_size
尝试提高
max_length_for_sort_data
提高这个参数,会增加用改进算法的概率。
1
SHOW VARIABLES LIKE '%max_length_for_sort_data%';
但是如果设的太高,数据总容量超出sort_buffer_size的概率就增大,明显症状是高的磁盘I/O活动和低的处理
器使用率。如果需要返回的列的总长度大于max_length_for_sort_data,使用双路算法,否则使用单路算法。
1024-8192字节之间调整。Order by 时
select *
是一个大忌
。最好只Query需要的字段
。
六、GROUP BY优化
- group by 使用索引的原则几乎跟order by一致 ,group by 即使没有过滤条件用到索引,也可以直接使用索引。
- group by 先排序再分组,遵照索引建的
最佳左前缀法则
- 当无法使用索引列,增大
max_length_for_sort_data
和sort_buffer_size
参数的设置 - where效率高于having,
能写在where限定的条件就不要写在having中
减少使用order by
,和业务沟通能不排序就不排序,或将排序放到程序端去做。Order by、group by、distinct这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。- 包含了order by、group by、distinct这些查询的语句,where条件过滤出来的结果集请保持在1000行以内,否则SQL会很慢。
七、优化分页查询
优化思路一:
在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容。
1 | EXPLAIN SELECT * FROM student t,(SELECT id FROM student ORDER BY id LIMIT 2000000,10) a |
优化思路二:
该方案适用于主键自增的表,可以把Limit 查询转换成某个位置的查询 。
1 | EXPLAIN SELECT * FROM student WHERE id > 2000000 LIMIT 10; |
八、优先考虑覆盖索引
1、什么是覆盖索引?
直接通过二级索引对应的数据找到了查询结果,无需回表
- 理解方式一:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数据,那就不需要读取行了。
一个索引包含了满足查询结果的数据就叫做覆盖索引
。 - 理解方式二:非聚簇复合索引的一种形式,它包括在查询里的SELECT、JOIN和WHERE子句用到的所有列(即建索引的字段正好是覆盖查询条件中所涉及的字段)。
简单说就是, 索引列+主键
包含 SELECT 到 FROM之间查询的列
。
2、覆盖索引的利弊
好处:
- 避免Innodb表进行索引的二次查询(回表)
- 可以把随机IO变成顺序IO加快查询效率
弊端:
索引字段的维护 总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。这是业务DBA,或者称为业务数据架构师的工作。
九、如何给字符串添加索引
有一张教师表,表定义如下:
1 | create table teacher( |
讲师要使用邮箱登录,所以业务代码中一定会出现类似于这样的语句:
1 | mysql> select col1, col2 from teacher where email='xxx'; |
如果email这个字段上没有索引,那么这个语句就只能做 全表扫描
。
1、前缀索引
MySQL是支持前缀索引的。默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串。
1 | mysql> alter table teacher add index index1(email); |
这两种不同的定义在数据结构和存储上有什么区别呢?下图就是这两个索引的示意图。
以及
如果使用的是index1
(即email整个字符串的索引结构),执行顺序是这样的:
- 从index1索引树找到满足索引值是’zhangssxyz@xxx.com’的这条记录,取得ID2的值;
- 到主键上查到主键值是ID2的行,判断email的值是正确的,将这行记录加入结果集;
- 取index1索引树上刚刚查到的位置的下一条记录,发现已经不满足email=‘zhangssxyz@xxx.com’的条件了,循环结束。
这个过程中,只需要回主键索引取一次数据,所以系统认为只扫描了一行。
如果使用的是index2
(即email(6)索引结构),执行顺序是这样的:
- 从index2索引树找到满足索引值是’zhangs’的记录,找到的第一个是ID1;
- 到主键上查到主键值是ID1的行,判断出email的值不是’zhangssxyz@xxx.com’,这行记录丢弃;
- 取index2上刚刚查到的位置的下一条记录,发现仍然是’zhangs’,取出ID2,再到ID索引上取整行然后判断,这次值对了,将这行记录加入结果集;
- 重复上一步,直到在idxe2上取到的值不是’zhangs’时,循环结束。
也就是说使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本
。前面已经讲过区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少。
2、前缀索引对覆盖索引的影响
结论:
使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时需要考虑的一个因素。
十、索引下推
Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式。
ICP可以减少存储引擎访问基表的次数以及MySQL服务器访问存储引擎的次数。
1、使用前后的扫描过程
在不使用ICP索引扫描的过程:
storage层:只将满足index key条件的索引记录对应的整行记录取出,返回给server层
server 层:对返回的数据,使用后面的where条件过滤,直至返回最后一行。
使用ICP扫描的过程:
- storage层:
首先将index key条件满足的索引记录区间确定,然后在索引上使用index filter进行过滤。将满足的indexfilter条件的索引记录才去回表取出整行记录返回server层。不满足index filter条件的索引记录丢弃,不回表、也不会返回server层。 - server 层:
对返回的数据,使用table filter条件做最后的过滤。
使用前后的成本差别:
使用前,存储层多返回了需要被index filter过滤掉的整行记录
使用ICP后,直接就去掉了不满足index filter条件的记录,省去了他们回表和传递server层的成本。
ICP的 加速效果
取决于在存储引擎内通过 ICP筛选
掉的数据的比例。
2、ICP的使用条件
ICP的使用条件:
① 只能用于二级索引(secondary index)。
②explain显示的执行计划中type值(join 类型)为 range 、 ref 、 eq_ref 或者ref_or_null
。
③ 并非全部where条件都可以用ICP筛选,如果where条件的字段不在索引列中,还是要读
取整表的记录到server端做where过滤。
④ ICP可以用于MyISAM和InnnoDB存储引擎。
⑤ MySQL 5.6版本的不支持分区表的ICP功能,5.7版本的开始支持。
⑥ 当SQL使用覆盖索引时,不支持ICP优化方法。因为这种情况下使用ICP不会减少IO。
另外,相关子查询的条件不能使用ICP
3、ICP使用案例
案例1:
1 | SELECT * FROM tuser |
案例2:
十一、普通索引 vs 唯一索引
从性能的角度考虑,你选择唯一索引还是普通索引呢?选择的依据是什么呢?
假设,我们有一个主键列为ID的表,表中有字段k,并且在k上有索引,假设字段 k 上的值都不重复。这个表的建表语句是:
这个表的建表语句是:
1 | mysql> create table test( |
表中R1~R5的(ID,k)值分别为(100,1)、(200,2)、(300,3)、(500,5)和(600,6)。
1、查询过程
假设,执行查询的语句是 select id from test where k=5。
- 对于普通索引来说,查找到满足条件的第一个记录(5,500)后,需要查找下一个记录,直到碰到第一个不满足k=5条件的记录。
- 对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。
那么,这个不同带来的性能差距会有多少呢?答案是,微乎其微
。
2、更新过程
为了说明普通索引和唯一索引对更新语句性能的影响这个问题,介绍一下changebuffer。
当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下, InooDB会将这些更新操作缓存在change buffer中
,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行changebuffer中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。
将change buffer中的操作应用到原数据页,得到最新结果的过程称为 merge
。除了 访问这个数据页
会触发merge外,系统有 后台线程会定期
merge。在数据库正常关闭(shutdown)
的过程中,也会执行merge操作。
如果能够将更新操作先记录在change buffer, 减少读磁盘
,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用 buffer pool 的,所以这种方式还能够 避免占用内存
,提高内存利用率。唯一索引的更新就不能使用change buffer
,实际上也只有普通索引可以使用。
如果要在这张表中插入一个新记录(4,400)的话,InnoDB的处理流程是怎样的?
3、change buffer的使用场景
- 普通索引和唯一索引应该怎么选择?其实,这两类索引在查询能力上是没差别的,主要考虑的是对
更新性能
的影响。所以,建议你尽量选择普通索引
。 - 在实际使用中会发现,
普通索引
和change buffer
的配合使用,对于数据量大
的表的更新优化还是很明显的。 - 如果所有的更新后面,都马上
伴随着对这个记录的查询
,那么你应该关闭change buffer
。而在其他情况下,change buffer都能提升更新性能。 - 由于唯一索引用不上change buffer的优化机制,因此如果
业务可以接受
,从性能角度出发建议优先考虑非唯一索引。但是如果”业务可能无法确保”的情况下,怎么处理呢?
- 首先,
业务正确性优先
。我们的前提是“业务代码已经保证不会写入重复数据”的情况下,讨论性能问题。如果业务不能保证,或者业务就是要求数据库来做约束,那么没得选,必须创建唯一索引。这种情况下,本节的意义在于,如果碰上了大量插入数据慢、内存命中率低的时候,给你多提供一个排查思路。 - 然后,在一些“
归档库
”的场景,你是可以考虑使用唯一索引的。比如,线上数据只需要保留半年,然后历史数据保存在归档库。这时候,归档数据已经是确保没有唯一键冲突了。要提高归档效率,可以考虑把表里面的唯一索引改成普通索引。
十二、其它查询优化策略
1、EXISTS 和 IN 的区分
问题:
不太理解哪种情况下应该使用 EXISTS,哪种情况应该用 IN。选择的标准是看能否使用表的索引吗?
2、COUNT(*)与COUNT(具体字段)效率
问:在 MySQL 中统计数据表的行数,可以使用三种方式: SELECT COUNT(*) 、 SELECT COUNT(1) 和 SELECT COUNT(具体字段)
,使用这三者之间的查询效率是怎样的?
答:
前提:如果你要统计的是某个字段的非空数据行数,则另当别论,毕竟比较执行效率的前提是结果一样才可以。
**环节1:*COUNT(*)和COUNT(1)都是对所有结果进行COUNT,COUNT()和COUNT(1)本质上并没有区别(二者)执行时间可能略有差别,不过你还是可以把它俩的执行效率看成是相等的)。如果有WHERE子句,则是对所有符合筛选条件的数据行进行统计;如果没有WHERE子句,则是对数据表的数据行数进行统计。
**环节2:**如果是MylSAM存储引擎,统计数据表的行数只需要0(1)的复杂度,这是因为每张MylSAM的数据表都有一个meta信息存储了 row_count值,而一致性则由表级锁来保证。
如果是InnoDB 存储引擎,因为 InnoDB支持事务,采用行级锁和MVCC机制,所以无法MylSAM一样,维护一个row_count变量,因此需要采用扫描全表,是0(n)的复杂度,进行循环+计数的方式来完成统计。
**环节3:*在InnoDB引擎中,如果采用COUNT(具体字段)来统计数据行数,要尽量采用二级索引。因为主键采用的索引是聚簇索引,聚簇索引包含的信息多,明显会大于二级索引(非聚簇索引)。对于COUNT()和COUNT(1)来说,它们不需要查找具体的行,只是统计行数,系统会自动采用占用空间更小的二级索引来进行统计。
如果有多个二级索引,会使用key_len小的二级索引进行扫描。当没有二级索引的时候,才会采用主键索引来进行统计。
3、关于SELECT(*)
在表查询中,建议明确字段,不要使用 * 作为查询的字段列表,推荐使用SELECT <字段列表> 查询。原因:
① MySQL 在解析的过程中,会通过 查询数据字典
将”*”按序转换成所有列名,这会大大的耗费资源和时间。
② 无法使用 覆盖索引
4、LIMIT 1 对优化的影响
针对的是会扫描全表的 SQL 语句,如果你可以确定结果集只有一条,那么加上 LIMIT 1
的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。
如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上 LIMIT 1
了。
5、多使用COMMIT
只要有可能,在程序中尽量多使用 COMMIT,这样程序的性能得到提高,需求也会因为 COMMIT 所释放
的资源而减少。
COMMIT 会有所释放的资源
:
- 回滚段上用于恢复数据的信息
- 被程序语句获得的锁
- redo / undo log buffer 中的空间
- 管理上述 3 种资源中的内部花费
十三、淘宝数据库,主键如何设计的?
聊一个实际问题:淘宝的数据库,主键是如何设计的?
某些错的离谱的答案还在网上年复一年的流传着,甚至还成为了所谓的MySQL军规。
其中,一个最明显的错误就是关于MySQL的主键设计。
大部分人的回答如此自信:用8字节的 BIGINT 做主键,而不要用INT。 错
!
这样的回答,只站在了数据库这一层,而没有 从业务的角度
思考主键。主键就是一个自增ID吗?站在 2022年的新年档口,用自增做主键,架构设计上可能 连及格都拿不到
。
1、自增ID的问题
自增ID做主键,简单易懂,几乎所有数据库都支持自增类型,只是实现上各自有所不同而已。自增ID除了简单,其他都是缺点,总体来看存在以下几方面的问题:
可靠性不高
存在自增ID回溯的问题,这个问题直到最新版本的MySQL 8.0才修复。安全性不高
对外暴露的接口可以非常容易猜测对应的信息。比如:/User/1/这样的接口,可以非常容易猜测用户ID的值为多少,总用户数量有多少,也可以非常容易地通过接口进行数据的爬取。性能差
自增ID的性能较差,需要在数据库服务器端生成。交互多
业务还需要额外执行一次类似last_insert_id()
的函数才能知道刚才插入的自增值,这需要多一次的网络交互。在海量并发的系统中,多1条SQL,就多一次性能上的开销。局部唯一性
最重要的一点,自增ID是局部唯一,只在当前数据库实例中唯一,而不是全局唯一,在任意服务器间都是唯一的。对于目前分布式系统来说,这简直就是噩梦。
2、业务字段做主键
为了能够唯一地标识一个会员的信息,需要为 会员信息表
设置一个主键。那么,怎么为这个表设置主键,才能达到我们理想的目标呢? 这里我们考虑业务字段做主键。
表数据如下:
在这个表里,哪个字段比较合适呢?
- 选择卡号(cardno)
会员卡号(cardno)看起来比较合适,因为会员卡号不能为空,而且有唯一性,可以用来 标识一条会员记录。
1 | mysql> CREATE TABLE demo.membermaster |
不同的会员卡号对应不同的会员,字段“cardno”唯一地标识某一个会员。如果都是这样,会员卡号与会员一一对应,系统是可以正常运行的。
但实际情况是, 会员卡号可能存在重复使用
的情况。比如,张三因为工作变动搬离了原来的地址,不再到商家的门店消费了 (退还了会员卡),于是张三就不再是这个商家门店的会员了。但是,商家不想让这个会 员卡空着,就把卡号是“10000001”的会员卡发给了王五。
从系统设计的角度看,这个变化只是修改了会员信息表中的卡号是“10000001”这个会员 信息,并不会影响到数据一致性。也就是说,修改会员卡号是“10000001”的会员信息, 系统的各个模块,都会获取到修改后的会员信息,不会出现“有的模块获取到修改之前的会员信息,有的模块获取到修改后的会员信息,而导致系统内部数据不一致”的情况。因此,从 信息系统层面
上看是没问题的。
但是从使用 系统的业务层面
来看,就有很大的问题 了,会对商家造成影响。
比如,我们有一个销售流水表(trans),记录了所有的销售流水明细。2020 年 12 月 01 日,张三在门店购买了一本书,消费了 89 元。那么,系统中就有了张三买书的流水记录,如下所示:
接着,我们查询一下 2020 年 12 月 01 日的会员销售记录:
1 | mysql> SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate |
如果会员卡“10000001”又发给了王五,我们会更改会员信息表。导致查询时:
1 | mysql> SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate |
这次得到的结果是:王五在 2020 年 12 月 01 日,买了一本书,消费 89 元。显然是错误的!
结论:千万不能把会员卡号当做主键。
- 选择会员电话 或 身份证号
会员电话可以做主键吗?不行的。在实际操作中,手机号也存在 被运营商收回
,重新发给别人用的情况。
那身份证号行不行呢?好像可以。因为身份证决不会重复,身份证号与一个人存在一一对 应的关系。可问题是,身份证号属于 个人隐私
,顾客不一定愿意给你。要是强制要求会员必须登记身份证号,会把很多客人赶跑的。其实,客户电话也有这个问题,这也是我们在设计会员信息表的时候,允许身份证号和电话都为空的原因。
所以,建议尽量不要用跟业务有关的字段做主键。毕竟,作为项目设计的技术人员,我们谁也无法预测在项目的整个生命周期中,哪个业务字段会因为项目的业务需求而有重复,或者重用之类的情况出现
。
经验:
刚开始使用 MySQL 时,很多人都很容易犯的错误是喜欢用业务字段做主键,想当然地认为了解业务需求,但实际情况往往出乎意料,而更改主键设置的成本非常高
。
3、淘宝的主键设计
在淘宝的电商业务中,订单服务是一个核心业务。请问, 订单表的主键
淘宝是如何设计的呢?是自增ID吗?
打开淘宝,看一下订单信息:
从上图可以发现,订单号不是自增ID!我们详细看下上述4个订单号
:
1 | 1550672064762308113 |
订单号是19位的长度,且订单的最后5位都是一样的,都是08113。且订单号的前面14位部分是单调递增的。
大胆猜测,淘宝的订单ID设计应该是:
1 | 订单ID = 时间 + 去重字段 + 用户ID后6位尾号 |
这样的设计能做到全局唯一
,且对分布式系统查询及其友好
。
4、推荐的主键设计
可通过改变UUID的时间排序,将时分秒放在前面,而不是默认的秒分时,就可做到有序
非核心业务 :对应表的主键自增ID,如告警、日志、监控等信息。
核心业务 :主键设计至少应该是全局唯一且是单调递增`。全局唯一保证在各系统之间都是唯一的,单调递增是希望插入时不影响数据库性能。
这里推荐最简单的一种主键设计:UUID。
UUID的特点:
全局唯一,占用36字节,数据无序,插入性能差。
认识UUID:
- 为什么UUID是全局唯一的?
- 为什么UUID占用36个字节?
- 为什么UUID是无序的?
MySQL数据库的UUID组成如下所示:
1 | UUID = 时间+UUID版本(16字节)- 时钟序列(4字节) - MAC地址(12字节) |
以UUID值e0ea12d4-6473-11eb-943c-00155dbaa39d
举例:为什么UUID是全局唯一的?
在UUID中时间部分占用60位,存储的类似TIMESTAMP的时间戳,但表示的是从1582-10-15 00:00:00.00到现在的100ns的计数。可以看到UUID存储的时间精度比TIMESTAMPE更高,时间维度发生重复的概率降低到1/100ns。
时钟序列是为了避免时钟被回拨导致产生时间重复的可能性。MAC地址用于全局唯一。
为什么UUID占用36个字节?
UUID根据字符串进行存储,设计时还带有无用”-“字符串,因此总共需要36个字节。
为什么UUID是随机无序的呢?
因为UUID的设计中,将时间低位放在最前面,而这部分的数据是一直在变化的,并且是无序。改造UUID
若将时间高低位互换,则时间就是单调递增的了,也就变得单调递增了。MySQL 8.0可以更换时间低位和时间高位的存储方式,这样UUID就是有序的UUID了。
MySQL 8.0还解决了UUID存在的空间占用的问题,除去了UUID字符串中无意义的”-“字符串,并且将字符串用二进制类型保存,这样存储空间降低为了16字节。
可以通过MySQL8.0提供的uuid_to_bin函数实现上述功能,同样的,MySQL也提供了bin_to_uuid函数进行
转化:
1 | SET @uuid = UUID(); |
通过函数uuid_to_bin(@uuid,true)将UUID转化为有序UUID了。全局唯一 + 单调递增,这不就是我们想要的主键!
有序UUID性能测试
16字节的有序UUID,相比之前8字节的自增ID,性能和存储空间对比究竟如何呢?
我们来做一个测试,插入1亿条数据,每条数据占用500字节,含有3个二级索引,最终的结果如下所示:从上图可以看到插入1亿条数据有序UUID是最快的,而且在实际业务使用中有序UUID在 业务端就可以生成 。
还可以进一步减少SQL的交互次数。另外,虽然有序UUID相比自增ID多了8个字节,但实际只增大了3G的存储空间,还可以接受
在当今的互联网环境中,非常不推荐自增ID作为主键的数据库设计。更推荐类似有序UUID的全局唯一的实现。
另外在真实的业务系统中,主键还可以加入业务和系统属性,如用户的尾号,机房的信息等。这样的主键设计就更为考验架构师的水平了。
1 | 如果不是MySQL8.0 肿么办? |
手动赋值字段做主键!
比如,设计各个分店的会员表的主键,因为如果每台机器各自产生的数据需要合并,就可能会出现主键重复的问题。
可以在总部 MySQL 数据库中,有一个管理信息表,在这个表中添加一个字段,专门用来记录当前会员编号的最大值。
门店在添加会员的时候,先到总部 MySQL 数据库中获取这个最大值,在这个基础上加 1,然后用这个值作为新会员的“id”,同时,更新总部 MySQL 数据库管理信息表中的当 前会员编号的最大值。
这样一来,各个门店添加会员的时候,都对同一个总部 MySQL 数据库中的数据表字段进 行操作,就解决了各门店添加会员时会员编号冲突的问题。
十一、数据库的设计规范
一、为什么需要数据库设计
二、范式
1、范式简介
在关系型数据库
中,关于数据表设计的基本原则、规则
就称为范式
。可以理解为,一张数据表的设计结构需要满足的某种设计标准的级别
。要想设计一个结构合理的关系型数据库,必须满足一定的范式。
2、范式都包括哪些
目前关系型数据库有六种常见范式,按照范式级别,从低到高分别是:
第一范式(1NF)
第二范式(2NF)
第三范式(3NF)
巴斯-科德范式(BCNF)
第四范式(4NF)
第五范式(5NF,又称完美范式)
满足第二范式就一定满足第一范式,属于包含关系
一般来说,在关系型数据库设计中,最高也就遵循到BCNF,普遍还是3NF。但也不绝对,有时候为了提高某些查询性能,我们还需要破坏范式规则,也就是反规范化。
3、键和相关属性的概念
举例:这里有两个表
球员表(player)
:球员编号 | 姓名 | 身份证号 | 年龄 | 球队编号球队表(team)
:球队编号 | 主教练 | 球队所在地
超键
:对于球员表来说,超键就是包括球员编号或者身份证号的任意组合,比如(球员编号)(球员编号,姓名)(身份证号,年龄)等。候选键
:就是最小的超键,对于球员表来说,候选键就是(球员编号)或者(身份证号)。主键
:我们自己选定,也就是从候选键中选择一个,比如(球员编号)。外键
:球员表中的球队编号。主属性 、 非主属性
:在球员表中,主属性是(球员编号)(身份证号),其他的属性(姓名)(年龄)(球队编号)都是非主属性。
4、第一范式(1st NF)
字段不可再拆分,具有原子特性(最小粒度)
举例1:
假设一家公司要存储员工的姓名和联系方式。它创建一个如下表:
该表不符合 1NF ,因为规则说“表的每个属性必须具有原子(单个)值”,lisi和zhaoliu员工的emp_mobile 值违反了该规则。为了使表符合 1NF ,我们应该有如下表数据:
举例2:
user 表的设计不符合第一范式
其中,user_info字段为用户信息,可以进一步拆分成更小粒度的字段,不符合数据库设计对第一范式的要求。
将user_info
拆分后如下:
举例3:
属性的原子性是 主观的
。
例如,Employees关系中雇员姓名应当使用1个(fullname)、2个(firstname和lastname)还是3个(firstname、middlename和lastname)属性表示呢?答案取决于应用程序。
如果应用程序需要分别处理雇员的姓名部分(如:用于搜索目的),则有必要把它们分开。否则,不需要。
表1:
表2:
地址被再细粒度拆分
5、第二范式(2nd NF)
第二范式要求,在满足第一范式的基础上,还要满足数据表里的每一条数据记录,都是可唯一标识的。而且所有非主键字段,都必须完全依赖主键,不能只依赖主键的一部分。如果知道主键的所有属性的值,就可以检索到任何元组(行)的任何属性的任何值。(要求中的主键,其实可以拓展替换为候选键)。
表中字段不存在除额外的依赖关系
举例1:
成绩表
(学号,课程号,成绩)关系中,(学号,课程号)可以决定成绩,但是学号不能决定成绩,课
程号也不能决定成绩,所以“(学号,课程号)→成绩”就是 完全依赖关系
。
举例2:
比赛表 player_game ,里面包含球员编号、姓名、年龄、比赛编号、比赛时间和比赛场地等属性,这里候选键和主键都为(球员编号,比赛编号),我们可以通过候选键(或主键)来决定如下的关系:
1 | (球员编号, 比赛编号) → (姓名, 年龄, 比赛时间, 比赛场地,得分) |
但是这个数据表不满足第二范式,因为数据表中的字段之间还存在着如下的对应关系:
1 | (球员编号) → (姓名,年龄) |
对于非主属性来说,并非完全依赖候选键。这样会产生怎样的问题呢?
数据冗余
:如果一个球员可以参加 m 场比赛,那么球员的姓名和年龄就重复了 m-1 次。一个比赛也可能会有 n 个球员参加,比赛的时间和地点就重复了 n-1 次。插入异常
:如果我们想要添加一场新的比赛,但是这时还没有确定参加的球员都有谁,那么就没法插入。删除异常
:如果我要删除某个球员编号,如果没有单独保存比赛表的话,就会同时把比赛信息删除掉。更新异常
:如果我们调整了某个比赛的时间,那么数据表中所有这个比赛的时间都需要进行调整,否则就会出现一场比赛时间不同的情况。
为了避免出现上述的情况,我们可以把球员比赛表设计为下面的三张表。
这样的话,每张数据表都符合第二范式,也就避免了异常情况的发生。
1NF
告诉我们字段属性需要是原子性
的,而2NF
告诉我们一张表就是一个独立的对象
,一张表只表达一个意思。
举例3:
定义了一个名为 Orders 的关系,表示订单和订单行的信息:
违反了第二范式,因为有非主键属性仅依赖于候选键(或主键)的一部分。例如,可以仅通过orderid找到订单的 orderdate,以及 customerid 和 companyname,而没有必要再去使用productid。
修改:
Orders表和OrderDetails表如下,此时符合第二范式。
6、第三范式(3rd NF)
第三范式是在第二范式的基础上,确保数据表中的每一个非主键字段都和主键字段直接相关,也就是说,要求数据表中的所有非主键字段不能依赖于其他非主键字段。(即,不能存在非主属性A依赖于非主属性B,非主属性B依赖于主键C的情况,即存在“A→B→C”的决定关系)通俗地讲,该规则的意思是所有非主键属性之间不能有依赖关系,必须 相互独立。
这里的主键可以拓展为候选键。
举例1:
部门信息表
:每个部门有部门编号(dept_id)、部门名称、部门简介等信息。员工信息表
:每个员工有员工编号、姓名、部门编号。列出部门编号后就不能再将部门名称、部门简介
等与部门有关的信息再加入员工信息表中。如果不存在部门信息表,则根据第三范式(3NF)也应该构建它,否则就会有大量的数据冗余。
举例2:
商品类别名称依赖于商品类别编号,不符合第三范式。
修改:
表1:符合第三范式的 商品类别表
的设计
表2:符合第三范式的 商品表
的设计
商品表goods通过商品类别id字段(category_id)与商品类别表goods_category进行关联。
举例3:球员player表
:球员编号、姓名、球队名称和球队主教练。现在,我们把属性之间的依赖关系画出来,如下图所示:
你能看到球员编号决定了球队名称,同时球队名称决定了球队主教练,非主属性球队主教练就会传递依赖于球员编号,因此不符合 3NF 的要求。
如果要达到 3NF 的要求,需要把数据表拆成下面这样:
举例4:
修改第二范式中的举例3。
此时的Orders关系包含 orderid、orderdate、customerid 和 companyname 属性,主键定义为 orderid。
customerid 和companyname均依赖于主键——orderid。例如,你需要通过orderid主键来查找代表订单中客户的customerid,同样,你需要通过 orderid 主键查找订单中客户的公司名称(companyname)。然而, customerid和companyname也是互相依靠的。为满足第三范式,可以改写如下:
符合3NF后的数据模型通俗地讲,2NF和3NF通常以这句话概括:“每个非键属性依赖于键,依赖于整个键,并且除了键别无他物”。
7.小结
关于数据表的设计,有三个范式要遵循。
(1)第一范式(1NF),确保每列保持原子性
数据库的每一列都是不可分割的原子数据项,不可再分的最小数据单元,而不能是集合、数组、记录等非原子数据项。
(2)第二范式(2NF),确保每列都和主键完全依赖
尤其在复合主键的情况下,非主键部分不应该依赖于部分主键。
(3)第三范式(3NF)确保每列都和主键列 直接相关,而不是间接相关
范式的优点:数据的标准化有助于消除数据库中的数据冗余,第三范式(3NF)通常被认为在性能、扩展性和数据完整性方面达到了最好的平衡
范式的缺点:范式的使用,可能降低查询的效率。因为范式等级越高,设计出来的数据表就越多、越精细,数据的冗余度就越低,进行数据查询的时候就可能需要关联多张表,这不但代价昂贵,也可能使一些索引策略无效。
范式只是提出了设计的标准,实际上设计数据表时,未必一定要符合这些标准。开发中,我们会出现为了性能和读取效率违反范式化的原则,通过增加少量的冗余或重复的数据来提高数据库的读性能,减少关联查询,join表的次数,实现空间换取时间的目的。因此在实际的设计过程中要理论结合实际,灵活运用。
范式本身没有优劣之分,只有适用场景不同。没有完美的设计,只有合适的设计,我们在数据表的设计中,还需要根据需求将范式和反范式混合使用。
三、反范式化
根据业务,适当的冗余存储字段,减少多表查询的压力
1、概述
规范化 vs 性能
- 为满足某种商业目标 ,
数据库性能比规范化数据库更重要
- 在数据规范化的同时 , 要综合考虑
数据库的性能
- 通过在给定的表中添加额外的字段,以大量
减少需要从中搜索信息所需的时间
- 通过在给定的表中插入计算列,以
方便查询
2、应用举例
举例1:
员工的信息存储在employees 表
中,部门信息存储在departments 表
中。通过 employees 表中的
department_id字段与 departments 表建立关联关系。如果要查询一个员工所在部门的名称:
1 | select employee_id,department_name |
如果经常需要进行这个操作,连接查询就会浪费很多时间。可以在 employees 表中增加一个冗余字段department_name,这样就不用每次都进行连接操作了。
举例2:
反范式化的 goods商品信息表 设计如下:
举例3:
有 2 个表,分别是 商品流水表(atguigu.trans )和 商品信息表(atguigu.goodsinfo)
。
商品流水表里有 400 万条流水记录,商品信息表里有 2000 条商品记录。
商品流水表:
商品信息表:
新的商品流水表如下所示:
举例4:
课程评论表 class_comment
,对应的字段名称及含义如下:
学生表 student
,对应的字段名称及含义如下:
在实际应用中,我们在显示课程评论的时候,通常会显示这个学生的昵称,而不是学生 ID,因此当我们想要查询某个课程的前 1000 条评论时,需要关联 class_comment 和 student这两张表来进行查询。
实验数据:模拟两张百万量级的数据表
为了更好地进行 SQL 优化实验,我们需要给学生表和课程评论表随机模拟出百万量级的数据。
我们可以通过存储过程来实现模拟数据。
反范式优化实验对比
如果我们想要查询课程 ID 为 10001 的前 1000 条评论,需要写成下面这样:
1 | SELECT p.comment_text, p.comment_time, stu.stu_name |
运行结果(1000 条数据行):
运行时长为0.395
秒,对于网站的响应来说,这已经很慢了,用户体验会非常差。
如果我们想要提升查询的效率,可以允许适当的数据冗余,也就是在商品评论表中增加用户昵称字段,在 class_comment 数据表的基础上增加 stu_name 字段,就得到了 class_comment2 数据表。
这样一来,只需单表查询就可以得到数据集结果:
1 | SELECT comment_text, comment_time, stu_name |
运行结果(1000 条数据):
优化之后只需要扫描一次聚集索引即可,运行时间为 0.039
秒,查询时间是之前的 1/10。
你能看到,在数据量大的情况下,查询效率会有显著的提升
。
3、反范式的新问题
- 存储
空间变大
了 - 一个表中字段做了修改,另一个表中冗余的字段也需要做同步修改,否则
数据不一致
- 若采用存储过程来支持数据的更新、删除等额外操作,如果更新频繁,会非常
消耗系统资源
- 在
数据量小
的情况下,反范式不能体现性能的优势,可能还会让数据库的设计更加 复杂
4、反范式的适用场景
当冗余信息有价值或者能 大幅度提高查询效率
的时候,我们才会采取反范式的优化。
- 增加冗余字段的建议
- 历史快照、历史数据的需要
在现实生活中,我们经常需要一些冗余信息,比如订单中的收货人信息,包括姓名、电话和地址等。每次发生的订单收货信息
都属于历史快照
,需要进行保存,但用户可以随时修改自己的信息,这时保存这些冗余信息是非常有必要的。
反范式优化也常用在数据仓库
的设计中,因为数据仓库通常存储历史数据
,对增删改的实时性要求不强,对历史数据的分析需求强。这时适当允许数据的冗余度,更方便进行数据分析。
四、BCNF(巴斯范式)
人们在3NF的基础上进行了改进,提出了巴斯范式(BCNF),也叫做巴斯-科德范式(Boyce-Codd Normal Form)。BCNF被认为没有新的设计规范加入,只是对第三范式中设计规范要求更强,使得数据库冗余度更小。
所以,称为是修正的第三范式,或扩充的第三范式,BCNF不被称为第四范式。
若一个关系达到了第三范式,并且它只有一个候选键,或者它的每个候选键都是单属性,则该关系自然达到BC范式。
一般来说,一个数据库设计符合3NF或BCNF就可以了。
1、案例
在这个表中,一个仓库只有一个管理员,同时一个管理员也只管理一个仓库。我们先来梳理下这些属性之间的依赖关系。
仓库名决定了管理员,管理员也决定了仓库名,同时(仓库名,物品名)的属性集合可以决定数量这个
属性。这样,我们就可以找到数据表的候选键。
候选键
:是(管理员,物品名)和(仓库名,物品名),然后我们从候选键中选择一个作为 主键 ,比如(仓库名,物品名)。主属性
:包含在任一候选键中的属性,也就是仓库名,管理员和物品名。非主属性
:数量这个属性。
2、是否符合三范式
如何判断一张表的范式呢?我们需要根据范式的等级,从低到高来进行判断。
- 首先,数据表每个属性都是
原子性
的,符合1NF
的要求; - 其次,数据表中
非主属性”数量“都与候选键全部依赖
,(仓库名,物品名)决定数量,(管理员,物品名)决定数量。因此,数据表符合`` 2NF` 的要求; - 最后,数据表中的非主属性,
不传递依赖于候选键
。因此符合3NF
的要求。
3、存在的问题
既然数据表已经符合了 3NF 的要求,是不是就不存在问题了呢?我们来看下面的情况:
- 增加一个仓库,但是还没有存放任何物品。根据数据表实体完整性的要求,主键不能有空值,因此会出现
插入异常
; - 如果仓库更换了管理员,我们就可能会
修改数据表中的多条记录
; - 如果仓库里的商品都卖空了,那么此时仓库名称和相应的管理员名称也会随之被删除。
你能看到,即便数据表符合 3NF 的要求,同样可能存在插入,更新和删除数据的异常情况。
4、问题解决
首先我们需要确认造成异常的原因:
主属性仓库名对于候选键(管理员,物品名)是部分依赖的关系,这样就有可能导致上面的异常情况。
因此引入BCNF,它在 3NF 的基础上消除了主属性对候选键的部分依赖或者传递依赖关系。
- 如果在关系R中,U为主键,A属性是主键的一个属性,若存在A->Y,Y为主属性,则该关系不属于BCNF。
根据 BCNF 的要求,我们需要把仓库管理关系 warehouse_keeper 表拆分成下面这样:仓库表
:(仓库名,管理员)库存表
:(仓库名,物品名,数量)
这样就不存在主属性对于候选键的部分依赖或传递依赖,上面数据表的设计就符合 BCNF。
再举例:
有一个学生导师表
,其中包含字段:学生ID,专业,导师,专业GPA,这其中学生ID和专业是联合主
键。
这个表的设计满足三范式,但是这里存在另一个依赖关系,“专业”依赖于“导师”,也就是说每个导师只
做一个专业方面的导师,只要知道了是哪个导师,我们自然就知道是哪个专业的了。
所以这个表的部分主键Major依赖于非主键属性Advisor,那么我们可以进行以下的调整,拆分成2个表:
学生导师表:
导师表:
五、第四范式
举例1:
职工表(职工编号,职工孩子姓名,职工选修课程)。
在这个表中,同一个职工可能会有多个职工孩子姓名。
同样,同一个职工也可能会有多个职工选修课程,即这里存在着多值事实,不符合第四范式。
如果要符合第四范式,只需要将上表分为两个表,使它们只有一个多值事实,例如: 职工表一
(职工编
号,职工孩子姓名), 职工表二
(职工编号,职工选修课程),两个表都只有一个多值事实,所以符合第四
范式。
举例2:
比如我们建立课程、教师、教材的模型。我们规定,每门课程有对应的一组教师,每门课程也有对应的一组教材,一门课程使用的教材和教师没有关系。
我们建立的关系表如下:
课程ID,教师ID,教材ID;这三列作为联合主键。
为了表述方便,我们用Name代替ID,这样更容易看懂:
这个表除了主键,就没有其他字段了,所以肯定满足BC范式,但是却存在 多值依赖
导致的异常。
假如我们下学期想采用一本新的英版高数教材,但是还没确定具体哪个老师来教,那么我们就无法在这
个表中维护Course高数和Book英版高数教材的的关系。
解决办法是我们把这个多值依赖的表拆解成2个表,分别建立关系。这是我们拆分后的表:
以及
六、第五范式、域键范式
除了第四范式外,我们还有更高级的第五范式(又称完美范式)和域键范式(DKNF)。
在满足第四范式(4NF)的基础上,消除不是由候选键所蕴含的连接依赖。如果关系模式R中的每一个连接依赖均由R的候选键所隐含
,则称此关系模式符合第五范式。
函数依赖是多值依赖的一种特殊的情况,而多值依赖实际上是连接依赖的一种特殊情况。但连接依赖不像函数依赖和多值依赖可以由 语义直接导出
,而是在 关系连接运算
时才反映出来。存在连接依赖的关系模式仍可能遇到数据冗余及插入、修改、删除异常等问题。
第五范式处理的是 无损连接问题
,这个范式基本 没有实际意义
,因为无损连接很少出现,而且难以察觉。
而域键范式试图定义一个 终极范式
,该范式考虑所有的依赖和约束类型,但是实用价值也是最小的,只存在理论研究中。
七、ER模型
数据库设计是牵一发而动全身的。那有没有什么办法提前看到数掘库的全貌呢?比如需要哪些数据表、数据表中应该有哪些字段,数据表与数据表之间有什么关系、通过什么字段进行连接,等等。这样我们才能进行整体的梳理和设计。
其实,ER模型就是一个这样的工具。ER模型也叫作实体关系模型,是用来描述现实生活中客观存在的事物、事物的属性,以及事物之间关系的一种数据模型。在开发基于数据库的信息系统的设计阶段,通常使用ER模型来描述信息需求和信息特性,帮助我们理清业务逻辑,从而设计出优秀的数据库。
ER 模型中有三个要素,分别是实体
、属性
和关系
。
实体
,可以看做是数据对象,往往对应于现实生活中的真实存在的个体。在 ER 模型中,用矩形
来表示。实体分为两类,分别是强实体
和弱实体
。强实体是指不依赖于其他实体的实体;弱实体是指对另一个实体有很强的依赖关系的实体。属性
,则是指实体的特性。比如超市的地址、联系电话、员工数等。在 ER 模型中用椭圆形
来表示。关系
,则是指实体之间的联系。比如超市把商品卖给顾客,就是一种超市与顾客之间的联系。在 ER 模型中用菱形
来表示。
注意:实体和属性不容易区分。这里提供一个原则:我们要从系统整体的角度出发去看,可以独立存在的是实体,不可再分的是属性
。也就是说,属性不能包含其他属性。
1、关系的类型
在 ER 模型的 3 个要素中,关系又可以分为 3 种类型,分别是一对一
、一对多
、多对多
。
一对一
:指实体之间的关系是一一对应的,比如个人与身份证信息之间的关系就是一对一的关系。一个人只能有一个身份证信息,一个身份证信息也只属于一个人。一对多
:指一边的实体通过关系,可以对应多个另外一边的实体。相反,另外一边的实体通过这个关
系,则只能对应唯一的一边的实体。比如说,我们新建一个班级表,而每个班级都有多个学生,每个学
生则对应一个班级,班级对学生就是一对多的关系。多对多
:指关系两边的实体都可以通过关系对应多个对方的实体。比如在进货模块中,供货商与超市之间的关系就是多对多的关系,一个供货商可以给多个超市供货,一个超市也可以从多个供货商那里采购商品。再比如一个选课表,有许多科目,每个科目有很多学生选,而每个学生又可以选择多个科目,这就是多对多的关系。
2、建模分析
ER 模型看起来比较麻烦,但是对我们把控项目整体非常重要。如果你只是开发一个小应用,或许简单设计几个表够用了,一旦要设计有一定规模的应用,在项目的初始阶段,建立完整的 ER 模型就非常关键了。
开发应用项目的实质,其实就是 建模
。
我们设计的案例是 电商业务
,由于电商业务太过庞大且复杂,所以我们做了业务简化,比如针对SKU(StockKeepingUnit,库存量单位)和SPU(Standard Product Unit,标准化产品单元)的含义上,我们直接使用了SKU,并没有提及SPU的概念。本次电商业务设计总共有8个实体,如下所示。
- 地址实体
- 用户实体
- 购物车实体
- 评论实体
- 商品实体
- 商品分类实体
- 订单实体
- 订单详情实体
其中, 用户
和 商品分类
是强实体,因为它们不需要依赖其他任何实体。而其他属于弱实体,因为它们虽然都可以独立存在,但是它们都依赖用户这个实体,因此都是弱实体。知道了这些要素,我们就可以
给电商业务创建 ER 模型了,如图:
在这个图中,地址和用户之间的添加关系,是一对多的关系,而商品和商品详情示一对1的关系,商品和
订单是多对多的关系。 这个 ER 模型,包括了 8个实体之间的 8种关系。
(1)用户可以在电商平台添加多个地址;
(2)用户只能拥有一个购物车;
(3)用户可以生成多个订单;
(4)用户可以发表多条评论;
(5)一件商品可以有多条评论;
(6)每一个商品分类包含多种商品;
(7)一个订单可以包含多个商品,一个商品可以在多个订单里。
(8)订单中又包含多个订单详情,因为一个订单中可能包含不同种类的商品
3、ER 模型的细化
有了这个 ER 模型,我们就可以从整体上 理解
电商的业务了。刚刚的 ER 模型展示了电商业务的框架,但是只包括了订单,地址,用户,购物车,评论,商品,商品分类和订单详情这八个实体,以及它们之间的关系,还不能对应到具体的表,以及表与表之间的关联。我们需要把 属性加上
,用 椭圆
来表示,这样我们得到的 ER 模型就更加完整了。
因此,我们需要进一步去设计一下这个 ER 模型的各个局部,也就是细化下电商的具体业务流程,然后把它们综合到一起,形成一个完整的 ER 模型。这样可以帮助我们理清数据库的设计思路。
接下来,我们再分析一下各个实体都有哪些属性,如下所示。
(1)
地址实体
包括用户编号、省、市、地区、收件人、联系电话、是否是默认地址。
(2)用户实体
包括用户编号、用户名称、昵称、用户密码、手机号、邮箱、头像、用户级别。
(3)购物车实体
包括购物车编号、用户编号、商品编号、商品数量、图片文件url。
(4)订单实体
包括订单编号、收货人、收件人电话、总金额、用户编号、付款方式、送货地址、下单时间。
(5)订单详情实体
包括订单详情编号、订单编号、商品名称、商品编号、商品数量。
(6)商品实体
包括商品编号、价格、商品名称、分类编号、是否销售,规格、颜色。
(7)评论实体
包括评论id、评论内容、评论时间、用户编号、商品编号
(8)商品分类实体
包括类别编号、类别名称、父类别编号
这样细分之后,我们就可以重新设计电商业务了,ER 模型如图:
4、ER 模型图转换成数据表
其实,任何一个基于数据库的应用项目,都可以通过这种 先建立 ER 模型
,再 转换成数据表
的方式,完成数据库的设计工作。创建 ER 模型不是目的,目的是把业务逻辑梳理清楚,设计出优秀的数据库。我建议你不是为了建模而建模,要利用创建 ER 模型的过程来整理思路,这样创建 ER 模型才有意义。
八、数据表的设计原则
综合以上内容,总结出数据表设计的一般原则:“三少一多”
数据表的个数越少越好
数据表中的字段个数越少越好
字段个数越多,数据余的可能性越大。设置字段个数少的前提是各个字段相互独立,而不是某个字段的取值可以由其他字段计算出来。当然字段个数少是相对的,我们通常会在数据冗余和检索效率中进行平衡。
数据表中联合主键的字段个数越少越好
设置主键是为了确定唯一性,当一个字段无法确定唯一性的时候,就需要采用联合主键的方式(也就是用多个字段来定义一个主键)。联合主键中的字段越多,占用的索引空间越大,不仅会加大理解难度,还会增加运行时间和索引空间,因此联合主键的字段个数越少越好。
使用主键和外键越多越好
这里的外键指的是一对一,一对多的关系,不是外键约束
数据库的设计实际上就是定义各种表,以及各种字段之间的关系。这些关系越多,证明这些实体之间的冗余度越低,利用度越高。这样做的好处在于不仅保证了数据表之间的独立性,还能提升相互之间的关联使用率。
“三少一多”原则的核心就是简单可复用。简单指的是用更少的表、更少的字段、更少的联合主键字段来完成数据表的设计。可复用则是通过主键、外键的使用来增强数据表之间的复用率。因为一个主键可以理解是一张表的代表。键设计得越多,证明它们之间的利用率越高。
注意:这个原则并不是绝对的,有时候我们需要牺牲数据的冗余度来换取数据处理的效率。
九、数据库对象编写建议
1、 关于库
- 【强制】库的名称必须控制在32个字符以内,只能使用英文字母、数字和下划线,建议以英文字母开头。
- 【强制】库名中英文
一律小写
,不同单词采用下划线
分割。须见名知意。 - 【强制】库的名称格式:业务系统名称_子系统名。
- 【强制】库名禁止使用关键字(如type,order等)。
- 【强制】创建数据库时必须
显式指定字符集
,并且字符集只能是utf8或者utf8mb4。创建数据库SQL举例:CREATE DATABASE crm_fundDEFAULT CHARACTER SET 'utf8'
; - 【建议】对于程序连接数据库账号,遵循
权限最小原则
使用数据库账号只能在一个DB下使用,不准跨库。程序使用的账号原则上不准有drop权限
。 - 【建议】临时库以
tmp_
为前缀,并以日期为后缀;备份库以bak_
为前缀,并以日期为后缀。
2、关于表、列
- 【强制】表和列的名称必须控制在32个字符以内,表名只能使用英文字母、数字和下划线,建议以
英文字母开头
。 - 【强制】
表名、列名一律小写
,不同单词采用下划线分割。须见名知意
。 - 【强制】表名要求有模块名强相关,同一模块的表名尽量使用
统一前缀
。比如:crm_fund_item - 【强制】创建表时必须
显式指定字符集
为utf8或utf8mb4。 - 【强制】表名、列名
禁止使用关键字
(如type,order等)。 - 【强制】创建表时必须
显式指定表存储引擎
类型。如无特殊需求,一律为InnoDB。 - 【强制】建表必须有comment。
- 【强制】字段命名应尽可能使用表达实际含义的英文单词或
缩写
。如:公司 ID,不要使用corporation_id, 而用corp_id 即可。 - 【强制】布尔值类型的字段命名为
is_描述
。如member表上表示是否为enabled的会员的字段命
名为 is_enabled。 - 【强制】
禁止在数据库中存储图片、文件等大的二进制数据通常文件很大
,短时间内造成数据量快速增长,数据库进行数据库读取时,通常会进行大量的随机IO操作,文件很大时,IO操作很耗时。通常存储于文件服务器,数据库只存储文件地址信息。 - 【建议】建表时关于主键:
表必须有主键
(1)强制要求主键为id,类型为int或bigint,且为auto_increment 建议使用unsigned无符号型。
(2)标识表里每一行主体的字段不要设为主键,建议设为其他字段如user_id,order_id等,并建立unique key索引。因为如果设为主键且主键值为随机插入,则会导致innodb内部页分裂和大量随机I/O,性能下降。 - 【建议】核心表(如用户表)必须有行数据的
创建时间字段
(create_time)和最后更新时间字段 (update_time)
,便于查问题。 - 【建议】表中所有字段尽量都是
NOT NULL
属性,业务可以根据需要定义DEFAULT值
。 因为使用
NULL值会存在每一行都会占用额外存储空间、数据迁移容易出错、聚合函数计算结果偏差等问
题。 - 【建议】所有存储相同数据的
列名和列类型必须一致
(一般作为关联列,如果查询时关联列类型
不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)。 - 【建议】中间表(或临时表)用于保留中间结果集,名称以
tmp_
开头。
备份表用于备份或抓取源表快照,名称以bak_
开头。中间表和备份表定期清理。
【示范】一个较为规范的建表语句:
1 | CREATE TABLE user_info ( |
3、关于索引
- 【强制】InnoDB表必须主键为id int/bigint auto_increment,且主键值
禁止被更新
。 - 【强制】InnoDB和MyISAM存储引擎表,索引类型必须为
BTREE
。 - 【建议】主键的名称以
pk_
开头,唯一键以uni_
或uk_
开头,普通索引以idx_
开头,一律
使用小写格式,以字段的名称或缩写作为后缀。 - 【建议】多单词组成的columnname,取前几个单词首字母,加末单词组成column_name。如:
sample 表 member_id 上的索引:idx_sample_mid。 - 【建议】单个表上的索引个数
不能超过6个
。 - 【建议】在建立索引时,多考虑建立
联合索引
,并把区分度最高的字段放在最前面。 - 【建议】在多表 JOIN 的SQL里,保证被驱动表的连接列上有索引,这样JOIN 执行效率最高。
- 【建议】建表或加索引时,保证表里互相不存在
冗余索引
。 比如:如果表里已经存在key(a,b),则key(a)为冗余索引,需要删除。
4、SQL编写
- 【强制】程序端SELECT语句必须指定具体字段名称,
禁止写成 *
。 - 【建议】程序端insert语句指定具体字段名称,不要写成INSERT INTO t1 VALUES(…)。
- 【建议】除静态表或小表(100行以内),DML语句必须有WHERE条件,且使用索引查找。
- 【建议】INSERT INTO…VALUES(XX),(XX),(XX)… 这里XX的值不要超过5000个。 值过多虽然上线很快,但会引起主从同步延迟。
- 【建议】SELECT语句不要使用UNION,推荐使用UNION ALL,并且UNION子句个数限制在5个以内。
- 【建议】线上环境,多表 JOIN 不要超过5个表。
- 【建议】减少使用ORDER BY,和业务沟通能不排序就不排序,或将排序放到程序端去做。ORDER
BY、GROUP BY、DISTINCT 这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。 - 【建议】包含了ORDER BY、GROUP BY、DISTINCT 这些查询的语句,WHERE 条件过滤出来的结果集请保持在1000行以内,否则SQL会很慢。
- 【建议】对单表的多次alter操作必须合并为一次
对于超过100W行的大表进行alter table,必须经过DBA审核,并在业务低峰期执行,多个alter需整合在一起。 因为alter table会产生表锁
,期间阻塞对于该表的所有写入,对于业务可能会产生极大影响。 - 【建议】批量操作数据时,需要控制事务处理间隔时间,进行必要的sleep。
- 【建议】事务里包含SQL不超过5个。
因为过长的事务会导致锁数据较久,MySQL内部缓存、连接消耗过多等问题。 - 【建议】事务里更新语句尽量基于主键或UNIQUE KEY,如UPDATE… WHERE id=XX;否则会产生间隙锁,内部扩大锁定范围,导致系统性能下降,产生死锁。
十二、数据库其它调优策略
一、数据库调优的措施
1、调优的目标
- 尽可能
节省系统资源
,以便系统可以提供更大负荷的服务。(吞吐量更大) - 合理的结构设计和参数调整,以提高用户操作
响应的速度
。(响应速度更快) - 减少系统的瓶颈,提高
MySQL数据库整体的性能
。
2、如何定位调优问题
有如下几种方式:
- 用户的反馈(主要)
- 日志分析(主要)
- 服务器资源使用监控
- 数据库内部状况监控
- 其它
除了活动会话监控以外,我们也可以对 事务
、 锁等待
等进行监控,这些都可以帮助我们对数据库的运行状态有更全面的认识。
3、调优的维度和步骤
需要调优的对象是整个数据库管理系统,它不仅包括 SQL 查询,还包括数据库的部署配置、架构等。从这个角度来说,我们思考的维度就不仅仅局限在 SQL 优化上了。通过如下的步骤我们进行梳理:
①选择适合的 DBMS
如果对事务性处理以及 安全性要求高的话,可以选择商业的数据库产品。这些数据库在事务处理和查询性能上都比较强,比如采用SQL Server、Oracle,那么单表存储上亿条数据是没有问题的。如果数据表设计得好,即使不采用分库分表的方式,查询效率也不差。
除此以外,你也可以采用开源的MySQL进行存储,它有很多存储引擎可以选择,如果进行事务处理的话可以选择InnoDB,非事务处理可以选择MylSAM。
NoSQL 阵营包括 键值型数据库、文档型数据库、搜索引擎、列式存储和图形数据库。这些数据库的优缺点和使用场景各有不同,比如列式存储数据库可以大幅度降低系统的I/O,适合于分布式文件系统,但如果数据需要频繁地增删改,那么列式存储就不太适用了。
DBMS的选择关系到了后面的整个设计过程,所以第一步就是要选择适合的DBMS。如果已经确定好了DBMS,那么这步可以跳过。
②优化表设计
根据实际需求你改变字段的数据类型,来减少占用的空间
,从而减少IO次数
,提升性能
③优化逻辑查询
改写sql语句
④优化物理查询
物理查询优化是在确定了逻辑查询优化之后,采用物理优化技术(比如索引等),通过计算代价模型对各种可能的访问路径进行估算,从而找到执行方式中代价最小的作为执行计划。在这个部分中,我们需要掌握的重点是对索引的创建和使用
。
⑤使用 Redis 或 Memcached 作为缓存
除了可以对 SQL 本身进行优化以外,我们还可以请外援
提升查询的效率。
因为数据都是存放到数据库中,我们需要从数据库层中取出数据放到内存中进行业务逻辑的操作,当用户量增大的时候,如果频繁地进行数据查询,会消耗数据库的很多资源。如果我们将常用的数据直接放到内存中,就会大幅提升查询的效率。
键值存储数据库可以帮我们解决这个问题。
常用的键值存储数据库有 Redis 和 Memcached,它们都可以将数据存放到内存中。
⑥库级优化
读写分离:
数据分片:
但需要注意的是,分拆在提升数据库性能的同时,也会增加
维护和使用成本
。
二、优化MySQL服务器
1、优化服务器硬件
服务器的硬件性能直接决定着MySQL数据库的性能。硬件的性能瓶颈直接决定MySQL数据库的运行速度和效率。针对性能瓶颈提高硬件配置,可以提高MySQL数据库查询、更新的速度。
(1)
配置较大的内存
(2)配置高速磁盘系统
(3)合理分布磁盘I/O
(4)配置多处理器
2、优化MySQL的参数
通过优化MySQL的参数可以提高资源利用率,从而达到提高MySQL服务器性能的目的。
MySQL服务的配置参数都在my.cnf或者my.ini文件的[mysqld]组中。配置完参数以后,需要重新启动MySQL服务才会生效。
下面对几个对性能影响比较大的参数进行详细介绍。
可以通过如下几个MySQL状态值来适当调整线程池的大小:
1 | show global status like 'Thread%'; |
当 Threads_cached 越来越少,但 Threads_connected 始终不降,且 Threads_created 持续升高,可适当增加 thread_cache_size 的大小。
这里给出一份my.cnf的参考
配置:
1 | [mysqld] |
很多情况还需要具体情况具体分析!
三、优化数据库结构
1、拆分表:冷热数据分离
2、增加中间表
举例1: 学生信息表
和 班级表
的SQL语句如下:
1 | CREATE TABLE `class` ( |
现在有一个模块需要经常查询带有学生名称(name)、学生所在班级名称(className)、学生班级班长(monitor)的学生信息。根据这种情况可以创建一个 temp_student 表。temp_student
表中存储学生名称(stu_name)、学生所在班级名称(className)和学生班级班长(monitor)信息。创建表的语句如下:
1 | CREATE TABLE `temp_student` ( |
接下来,从学生信息表和班级表中查询相关信息存储到临时表中:
1 | insert into temp_student(stu_name,className,monitor) |
以后,可以直接从temp_student表中查询学生名称、班级名称和班级班长,而不用每次都进行联合查询。
这样可以提高数据库的查询速度。
3、增加冗余字段
设计数据库表时应尽量遵循范式理论的规约,尽可能减少冗余字段,让数据库设计看起来精致、优雅。但是,合理地加入冗余字段可以提高查询速度
。
表的规范化程度越高,表与表之间的关系就越多,需要连接查询的情况也就越多。尤其在数据量大,而且需要频繁进行连接的时候,为了提升效率,我们也可以考虑增加冗余字段来减少连接。
4、优化数据类型
5、优化插入记录的速度
(1)MyISAM引擎的表
① 禁用索引
② 禁用唯一性检查
③ 使用批量插入
1 | insert into student values(1,'zhangsan',18,1); |
④ 使用LOAD DATA INFILE 批量导入
(2)InnoDB引擎的表
① 禁用唯一性检查
② 禁用外键检查
③ 禁止自动提交
6、使用非空约束
在设计字段的时候,如果业务允许,建议尽量使用非空约束
,能加非空就加非空
7、分析表、检查表与优化表
①分析表
MySQL中提供了ANALYZE TABLE语句分析表,ANALYZE TABLE语句的基本语法如下:
1 | ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name[,tbl_name]… |
默认的,MySQL服务会将 ANALYZE TABLE语句写到binlog中,以便在主从架构中,从服务能够同步数据。
可以添加参数LOCAL 或者 NO_WRITE_TO_BINLOG取消将语句写到binlog中。使用 ANALYZE TABLE
分析表的过程中,数据库系统会自动对表加一个 只读锁
。在分析期间,只能读取表中的记录,不能更新和插入记录。ANALYZE TABLE语句能够分析InnoDB和MyISAM类型的表,但是不能作用于视图
。
ANALYZE TABLE分析后的统计结果会反应到 cardinality 的值
,该值统计了表中某一键所在的列不重复的值的个数。
该值越接近表中的总行数,则在表连接查询或者索引查询时,就越优先被优化器选择使用
。也就是索引列的cardinality的值与表中数据的总条数差距越大,即使查询的时候使用了该索引作为查询条件,存储引擎实际查询的时候使用的概率就越小。下面通过例子来验证下。cardinality可以通过SHOW INDEX FROM 表名查看。
②检查表
MySQL中可以使用 CHECK TABLE
语句来检查表。CHECK TABLE语句能够检查InnoDB和MyISAM类型的表是否存在错误。
CHECK TABLE语句在执行过程中也会给表加上 只读锁
。
对于MyISAM类型的表,CHECK TABLE语句还会更新关键字统计数据。而且,CHECK TABLE也可以检查视图是否有错误,比如在视图定义中被引用的表已不存在。该语句的基本语法如下:
1 | CHECK TABLE tbl_name [, tbl_name] ... [option] ... |
该语句对于检查的表可能会产生多行信息。最后一行有一个状态的 Msg_type 值,Msg_text 通常为 OK。
如果得到的不是 OK,通常要对其进行修复;是 OK 说明表已经是最新的了。表已经是最新的,意味着存储引擎对这张表不必进行检查。
③优化表
方式1:OPTIMIZE TABLE
MySQL中使用 OPTIMIZE TABLE
语句来优化表。但是,OPTILMIZE TABLE语句只能优化表中的VARCHAR 、 BLOB 或 TEXT 类型的字段。一个表使用了这些字段的数据类型,若已经 删除
了表的一大部分数据,或者已经对含有可变长度行的表(含有VARCHAR、BLOB或TEXT列的表)进行了很多 更新
,则应使用OPTIMIZE TABLE来重新利用未使用的空间,并整理数据文件的 碎片` 。
OPTIMIZE TABLE 语句对InnoDB和MyISAM类型的表都有效。该语句在执行过程中也会给表加上 只读锁
。OPTILMIZE TABLE语句的基本语法如下:
1 | OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ... |
LOCAL | NO_WRITE_TO_BINLOG关键字的意义和分析表相同,都是指定不写入二进制日志。
执行完毕,Msg_text显示
‘numysql.SYS_APP_USER’, ‘optimize’, ‘note’, ‘Table does not support optimize, doing recreate +analyze instead’
原因是我服务器上的MySQL是InnoDB存储引擎。
到底优化了没有呢?看官网!
在MyISAM中,是先分析这张表,然后会整理相关的MySQL datafile,之后回收未使用的空间;在InnoDB中,回收空间是简单通过Alter table进行整理空间。
在优化期间,MySQL会创建一个临时表,优化完成之后会删除原始表,然后会将临时表rename成为原始表。
说明: 在多数的设置中,根本不需要运行OPTIMIZE TABLE。即使对可变长度的行进行了大量的更新,也不需要经常运行,
每周一次
或每月一次
即可,并且只需要对特定的表
运行。
8、小结
四、大表优化
1、限定查询的范围
禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内;
2、读/写分离
经典的数据库拆分方案,主库负责写,从库负责读。
- 一主一从模式:
- 双主双从模式:
3、垂直拆分
当数据量级达到 千万级
以上时,有时候我们需要把一个数据库切成多份,放到不同的数据库服务器上,减少对单一数据库服务器的访问压力。
垂直拆分的优点:
可以使得列数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。
垂直拆分的缺点:
主键会出现冗余,需要管理冗余列,并会引起 JOIN 操作。此外,垂直拆分会让事务变得更加复杂。
4、水平拆分
下面补充一下数据库分片的两种常见方案
:
- 客户端代理:
分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。 - 中间件代理:
在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。我们现在谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。
五、其它调优策略
1、服务器语句超时处理
在MySQL 8.0中可以设置 服务器语句超时的限制
,单位可以达到 毫秒级别
。当中断的执行语句超过设置的毫秒数后,服务器将终止查询影响不大的事务或连接,然后将错误报给客户端。
设置服务器语句超时的限制,可以通过设置系统变量 MAX_EXECUTION_TIME
来实现。默认情况下,MAX_EXECUTION_TIME的值为0,代表没有时间限制。 例如:
1 | SET GLOBAL MAX_EXECUTION_TIME=2000; |
2、创建全局通用表空间
最后删除表空间,sql语句如下:
1 | DROP TABLESPACE aiguigu1; |
####3. 8.0 新特性 隐藏索引对调优的帮助
十三、事务基本知识
一、数据库事务概述
事务是数据库区别于文件系统的重要特性之一,当我们有了事务就会让数据保持一致性,同时我们还能通过事务的机制 恢复到某个时间点,这样可以保证已提交到数据库的修改不会因为系统崩溃而丢失。
1、 存储引擎支持情况
SHOW ENGINES
命令来查看当前 MySQL 支持的存储引擎都有哪些,以及这些存储引擎是否支持事务。
能看出在 MySQL 中,只有InnoDB 是支持事务的
。
2、基本概念
事务:
一组逻辑操作单元,使数据从一种状态变换到另一种状态。
事务处理的原则:
保证所有事务都作为 一个工作单元
来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交( commit
),那么这些修改就 永久
地保存下来;要么数据库管理系统将 放弃
所作的所有 修改
,整个事务回滚( rollback
)到最初状态。
3、事务的ACID特性
① 原子性(atomicity)
原子性
是指事务是 一个不可分割的工作单位,要么全部提交,要么全部失败回滚。
②一致性(consistency)
根据定义,一致性是指事务执行前后,数据从一个 合法性状态
变换到另外一个 合法性状态
。这种状态是 语义上
的而不是语法上的,跟具体的业务有关。
那什么是合法的数据状态呢?满足 预定的约束
的状态就叫做合法的状态。通俗一点,这状态是由你自己来定义的(比如满足现实世界中的约束)。满足这个状态,数据就是一致的,不满足这个状态,数据就是不一致的!
如果事务中的某个操作失败了,系统就会自动撤销当前正在执行的事务,返回到事务操作之前的状态。
③隔离型(isolation)
事务的隔离性是指一个事务的执行 不能被其他事务干扰
,即一个事务内部的操作及使用的数据对 并发 的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
如果无法保证隔离性会怎么样?假设A账户有200元,B账户0元。A账户往B账户转账两次,每次金额为50元,分别在两个事务中执行。如果无法保证隔离性,会出现下面的情形:
1 | UPDATE accounts SET money = money - 50 WHERE NAME = 'AA'; |
④持久性(durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是 永久性的
,接下来的其他操作和数据库故障不应该对其有任何影响。
持久性是通过 事务日志
来保证的。日志包括了 重做日志
和 回滚日志
。当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到重做日志中,然后再对数据库中对应的行进行修改。
这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事务具有持久性。
4、事务的状态
我们现在知道 事务
是一个抽象的概念,它其实对应着一个或多个数据库操作,MySQL根据这些操作所执行的不同阶段把 事务
大致划分成几个状态:
- 活动的(active)
事务对应的数据库操作正在执行过程中时,我们就说该事务处在活动的
状态。 - 部分提交的(partially committed)
当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时
,我们就说该事务处在部分提交的
状态。 - 失败的(failed)
当事务处在活动的
或者部分提交的
状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在失败的
状态。 - 中止的(aborted)
如果事务执行了一部分而变为失败的
状态,那么就需要把已经修改的事务中的操作还原到事务执行前的状态。换句话说,就是要撤销失败事务对当前数据库造成的影响。我们把这个撤销的过程称之为回滚
。当回滚
操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处在了中止的
状态。 - 提交的(committed)
当一个处在部分提交的
状态的事务将修改过的数据都同步到磁盘
上之后,我们就可以说该事务处在了提交的
状态。
一个基本的状态转换图如下所示:
二、如何使用事务
使用事务有两种方式,分别为显式事务
和 隐式事务
。
1、显式事务
步骤1: START TRANSACTION
或者 BEGIN
,作用是显式开启一个事务。
1 | mysql> BEGIN; |
START TRANSACTION
语句相较于 BEGIN 特别之处在于,后边能跟随几个 修饰符 :
① READ ONLY
:标识当前事务是一个 只读事务
,也就是属于该事务的数据库操作只能读取数据,而不能修改数据。
补充:只读事务中只是不允许修改那些其他事务也能访问到的表中的数据,对于临时表来说(我们使用CREATE TMEPORARY TABLE创建的表),由于它们只能在当前会话中可见,所以只读事务其实也是可以对临时表进行增、删、改操作的。
② READ WRITE
:标识当前事务是一个 读写事务
,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据。
③ WITH CONSISTENT SNAPSHOT
:启动一致性读。
步骤2:一系列事务中的操作(主要是DML,不含DDL)
步骤3:提交事务 或 中止事务(即回滚事务)
1 | # 提交事务。当提交事务后,对数据库的修改是永久性的。 |
2、隐式事务
MySQL中有一个系统变量 autocommit
:
1 | mysql> SHOW VARIABLES LIKE 'autocommit'; |
当然,如果我们想关闭这种自动提交
的功能,可以使用下边两种方法之一:
显式的的使用
START TRANSACTION
或者BEGIN
语句开启一个事务。这样在本次事务提交或者回滚前会暂时关闭掉自动提交的功能。把系统变量
1
autocommit
的值设置为
1
OFF
,就像这样:
1
2
3SET autocommit = OFF;
#或
SET autocommit = 0;
3、隐式提交数据的情况
数据定义语言(Data definition language,缩写为:DDL)
隐式使用或修改mysql数据库中的表
事务控制或关于锁定的语句
- ① 当我们在一个事务还没提交或者回滚时就又使用
START TRANSACTION
或者BEGIN
语句开启了另一个事务时,会隐式的提交
上一个事务。即: - ② 当前的
autocommit
系统变量的值为OFF
,我们手动把它调为ON
时,也会隐式的提交
前边语句所属的事务。 - ③ 使用
LOCK TABLES 、 UNLOCK TABLES
等关于锁定的语句也会隐式的提交
前边语句所属的事务。
- ① 当我们在一个事务还没提交或者回滚时就又使用
三、 事务隔离级别
MySQL是一个 客户端/服务器
架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称为一个会话( Session
)。
每个客户端都可以在自己的会话中向服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说可能同时处理多个事务。
事务有 隔离性
的特性,理论上在某个事务 对某个数据进行访问
时,其他事务应该进行 排队
,当该事务提交之后,其他事务才可以继续访问这个数据。
但是这样对 性能影响太大
,我们既想保持事务的隔离性,又想让服务器在处理访问同一数据的多个事务时 性能尽量高些 ,那就看二者如何权衡取舍
了。
1、SQL中的四种隔离级别
按照严重性
来排一下序:
1 | 脏写 > 脏读 > 不可重复读 > 幻读 |
脏写:
脏读:
不可重复读:
幻读:
脏写
怎么没涉及到?因为脏写这个问题太严重了,不论是哪种隔离级别,都不允许脏写的情况发生。
不同的隔离级别有不同的现象,并有不同的锁和并发机制,隔离级别越高,数据库的并发性能就越差,4种事务隔离级别与并发性能的关系如下:
2、MySQL支持的四种隔离级别
MySQL
的默认
隔离级别为REPEATABLE READ
,我们可以手动修改一下事务的隔离级别。
1 | # 查看隔离级别,MySQL 5.7.20的版本之前: |
3、如何设置事务的隔离级别
通过下面的语句修改事务的隔离级别:
1 | SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL 隔离级别; |
或者:
1 | SET [GLOBAL|SESSION] TRANSACTION_ISOLATION = '隔离级别' |
关于设置时使用GLOBAL或SESSION的影响:
小结:
数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱
。
四、事务的常见分类
从事务理论的角度来看,可以把事务分为以下几种类型:
- 扁平事务(Flat Transactions)
- 带有保存点的扁平事务(Flat Transactions with Savepoints)
- 链事务(Chained Transactions)
- 嵌套事务(Nested Transactions)
- 分布式事务(Distributed Transactions)
十四、MySQL事务日志
事务有4种特性:原子性
、一致性
、隔离性
和持久性
。那么事务的四种特性到底是基于什么机制实现呢?
- 事务的隔离性由
锁机制
实现。 - 而事务的原子性、一致性和持久性由事务的 redo 日志和undo 日志来保证。
REDO LOG
称为重做日志
,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性。UNDO LOG
称为回滚日志
,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。
有的DBA或许会认为 UNDO 是 REDO 的逆过程,其实不然。
一、redo日志
1、为什么需要REDO日志
通过REDOLOG保证了mysql的持久性
一方面,缓冲池可以帮助我们消除CPU和磁盘之间的鸿沟,checkpoint机制可以保证数据的最终落盘,然而由于checkpoint 并不是每次变更的时候就触发
的,而是master线程隔一段时间去处理的。所以最坏的情况就是事务提交后,刚写完缓冲池,数据库宕机了,那么这段数据就是丢失的,无法恢复。
另一方面,事务包含 持久性
的特性,就是说对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个事务对数据库中所做的更改也不能丢失。
那么如何保证这个持久性呢? 一个简单的做法
:在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘,但是这个简单粗暴的做法有些问题
另一个解决的思路
:我们只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来。所以我们其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把 修改
了哪些东西 记录一下
就好。比如,某个事务将系统表空间中 第10号
页面中偏移量为 100
处的那个字节的值 1
改成 2
。
我们只需要记录一下:将第0号表空间的10号页面的偏移量为100处的值更新为 2 。
2、REDO日志的好处、特点
①好处
- redo日志降低了刷盘频率
- redo日志占用的空间非常小
②特点
- redo日志是顺序写入磁盘的
- 事务执行过程中,redo log不断记录
3、redo的组成
Redo log可以简单分为以下两个部分:redo log buffer、redo log file
重做日志的缓冲 (redo log buffer)
,保存在内存中,是易失的。
参数设置:innodb_log_buffer_size
:
redo log buffer 大小,默认 16M
,最大值是4096M,最小值为1M。
1 | show variables like '%innodb_log_buffer_size%'; |
重做日志文件 (redo log file)
,保存在硬盘中,是持久的。
4、redo的整体流程
以一个更新事务为例,redo log
流转过程,如下图所示:
1 | 第1步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝 |
体会:
Write-Ahead Log(预先日志持久化):在持久化一个数据页之前,先将内存中相应的日志页持久化。
5、redo log的刷盘策略
redo log的写入并不是直接写入磁盘的,InnoDB引擎会在写redo log的时候先写redo log buffer
,之后以 一定的频率
刷入到真正的redo log file
中。
(先写入对应内存的开辟的空间redo log buffer,然后根据某个策略写入磁盘redo log file)
这里的一定频率怎么看待呢?这就是我们要说的刷盘策略。
注意,redo log buffer刷盘到redo log file的过程并不是真正的刷到磁盘中去,只是刷入到 文件系统缓存(page cache)
中去(这是现代操作系统为了提高文件写入效率做的一个优化),真正的写入会交给系统自己来决定(比如page cache足够大了)。
那么对于InnoDB来说就存在一个问题,如果交给系统来同步,同样如果系统宕机,那么数据也丢失了(虽然整个系统宕机的概率还是比较小的)。
针对这种情况,InnoDB给出 innodb_flush_log_at_trx_commit
参数,该参数控制 commit提交事务时,如何将 redo log buffer 中的日志刷新到 redo log file 中。它支持三种策略
:
- 设置为0 :
表示每次事务提交时不进行刷盘操作
。(通过系统默认master thread每隔1s进行一次重做日志的同步) - 设置为1 :
表示每次事务提交时都将进行同步
,刷盘操作(默认值
) - 设置为2 :
表示每次事务提交
时都只把redo log buffer 内容写入 page cache(操作系统的缓存)
,不进行同步。由os自己决定什么时候同步到磁盘文件。
6、不同刷盘策略演示
①流程图
- 当参数为:
1
的情况
- 当参数为
2
的情况:
- 当参数为
0
的情况:
7、 写入redo log buffer 过程
①补充概念 : Mini-Transaction
一个事务可以包含若干条语句,每一条语句其实是由若干个 mtr
组成,每一个 mtr
又可以包含若干条redo日志,画个图表示它们的关系就是这样:
②redo 日志写入log buffer
每个mtr都会产生一组redo日志,用示意图来描述一下这些mtr产生的日志情况:
不同的事务可能是 并发 执行的
,所以 T1
、 T2
之间的 mtr
可能是 交替执行
的。
③redo log block的结构图
8、redo log file
①相关参数设置
innodb_log_group_home_dir
:指定 redo log 文件组所在的路径,默认值为./
,表示在数据库的数据目录下。MySQL的默认数据目录(var/lib/mysql
)下默认有两个名为ib_logfile0
和ib_logfile1
的文件,log buffer中的日志默认情况下就是刷新到这两个磁盘文件中。此redo日志文件位置还可以修改。innodb_log_files_in_group
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(log_files文件个数):指明redo log file的个数,命名方式如:ib_logfile0,iblogfile1…
iblogfilen。默认2个,最大100个。
```shell
mysql> show variables like 'innodb_log_files_in_group';
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| innodb_log_files_in_group | 2 |
+---------------------------+-------+
#ib_logfile0
#ib_logfile1
12345678innodb_flush_log_at_trx_commit
:控制 redo log 刷新到磁盘的策略,默认为1。innodb_log_file_size
48M1
2
3
4
5
(log_files文件个数中单个大小):单个 redo log 文件设置大小,默认值为
1
2
3
4
5
6
7
8
9
10
11
12
13
。最大值为512G,注意最大值指的是整个 redo log 系列文件之和,即(innodb_log_files_in_group * innodb_log_file_size )不能大于最大值512G。
```shell
mysql> show variables like 'innodb_log_file_size';
+----------------------+----------+
| Variable_name | Value |
+----------------------+----------+
| innodb_log_file_size | 50331648 |
+----------------------+----------+
123456
根据业务修改其大小,以便容纳较大的事务。编辑my.cnf
文件并重启数据库生效,如下所示
1 | [root@localhost ~]# vim /etc/my.cnf |
②日志文件组
上面redo log file的文件单个存储的方式
总共的redo日志文件大小其实就是: innodb_log_file_size × innodb_log_files_in_group
。
采用循环使用的方式向redo日志文件组里写数据的话,会导致后写入的redo日志覆盖掉前边写的redo日志?
当然!所以InnoDB的设计者提出了checkpoint的概念。
③checkpoint
如果 write pos 追上 checkpoint ,表示日志文件组满了,这时候不能再写入新的 redo log记录,MySQL 得停下来,清空一些记录,把 checkpoint 推进一下。
二、Undo日志
redo log是事务持久性的保证,undo log
是事务原子性
的保证。在事务中 更新数据
的 前置操
作 其实是要先写入一个 undo log
。
1、如何理解Undo日志
2、Undo日志的作用
- 作用1:回滚数据
- 作用2:MVCC
3、undo的存储结构
①回滚段与undo页
InnoDB对undo log的管理采用段的方式,也就是 回滚段(rollback segment)
。每个回滚段记录了1024 个 undo log segment
,而在每个undo log segment
段中进行 undo页
的申请。
在
InnoDB1.1版本之前
(不包括1.1版本),只有一个rollback segment
,因此支持同时在线的事务限制为1024
。虽然对绝大多数的应用来说都已经够用。从
1
1.1版本开始
InnoDB支持最大
1
128个rollback segment
,故其支持同时在线的事务限制提高到了
1
128*1024
。
1
2
3
4
5
6
7show variables like 'innodb_undo_logs';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| innodb_undo_logs | 128 |
+------------------+-------+
123456
②回滚段与事务
③回滚段中的数据分类
4、undo的类型
在InnoDB存储引擎
中,undo log分为:
- insert undo log
- update undo log
5、undo log的生命周期
①简要生成过程
只有Buffer Pool的流程:
有了Redo Log和Undo Log之后:
简述:先找是否有加载对应的BufferPool有就使用没有就加载读取 —-> 写入UndoLog —>操作数据 —> 写入RedoLogBuffer内存 —-> 写入RedoLog到磁盘文件
②详细生成过程
当我们执行INSERT时:
1 | begin; |
当我们执行UPDATE时:
1 | UPDATE user SET id=2 WHERE id=1; |
③undo log是如何回滚的
④undo log的删除
3、小结
先找是否有加载对应的BufferPool有就使用没有就加载读取 —-> 写入UndoLog —>操作执行数据 —> 写入RedoLogBuffer内存 —-> 写入RedoLog到磁盘文件
undo log
是逻辑日志,对事务回滚时,只是将数据库逻辑地恢复到原来的样子。
redo log
是物理日志,记录的是数据页的物理变化,undo log不是redo log的逆过程(逻辑上的逆过程,但是物理上不是直接的逆过程)。
十五、Mysql锁
事务的 隔离性
由 锁
来实现。
一、概述
在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。
为保证数据的一致性,需要对 并发操作进行控制
,因此产生了 锁
。
同时 锁机制
也为实现MySQL的各个隔离级别提供了保证。
锁冲突 也是影响数据库 并发访问性能
的一个重要因素。所以锁对数据库而言显得尤其重要,也更加复杂。
二、MySQL并发事务访问相同记录
并发事务访问相同记录的情况大致可以划分为3种:
1、读-读情况
读-读
情况,即并发事务相继 读取相同的记录
。读取操作本身不会对记录有任何影响,并不会引起什么问题,所以允许这种情况的发生。
2、写-写情况
写-写
情况,即并发事务相继对相同的记录做出改动。
在这种情况下会发生 脏写
的问题,任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务相继对一条记录做改动时,需要让它们 排队执行
,这个排队的过程其实是通过 锁
来实现的。
这个所谓的锁其实是一个 内存中的结构
,在事务执行前本来是没有锁的,也就是说一开始是没有 锁结构
和记录进行关联的,如图所示:
当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的 锁结构
,当没有的时候就会在内存中生成一个 锁结构
与之关联。
比如,事务 T1 要对这条记录做改动,就需要生成一个 锁结构与之关联
:
小结几种说法:
- 不加锁
意思就是不需要在内存中生成对应的锁结构
,可以直接执行操作。 - 获取锁成功,或者加锁成功
意思就是在内存中生成了对应的锁结构
,而且锁结构的is_waiting
属性为false
,也就是事务可以继续执行操作。 - 获取锁失败,或者加锁失败,或者没有获取到锁
意思就是在内存中生成了对应的锁结构
,不过锁结构的is_waiting
属性为true
,也就是事务需要等待,不可以继续执行操作。
3、读-写或写-读情况
读-写
或写-读
,即一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生 脏读 、 不可重复读 、 幻读
的问题。
各个数据库厂商对 SQL标准 的支持都可能不一样。比如MySQL
在 REPEATABLE READ
隔离级别上就已经解决了 幻读
问题。
4、并发问题的解决方案
怎么解决 脏读 、 不可重复读 、 幻读
这些问题呢?其实有两种可选的解决方案:
方案一:读操作利用多版本并发控制(
MVCC
),写操作进行加锁
。
普通的SELECT语句在READ COMMITTED和REPEATABLE READ隔离级别下会使用到MVCC读取记录。- 在
READ COMMITTED
隔离级别下,一个事务在执行过程中每次执行SELECT操作时都会生成一个ReadView,ReadView的存在本身就保证了事务不可以读取到未提交的事务所做的更改
,也就是避免了脏读现象; - 在
REPEATABLE READ
隔离级别下,一个事务在执行过程中只有第一次执行SELECT操作
才会生成一个ReadView,之后的SELECT操作都复用
这个ReadView,这样也就避免了不可重复读和幻读的问题。
- 在
方案二:读、写操作都采用
加锁
的方式。四个问题都可以解决。
幻读用间隙锁解决。
小结对比发现:
- 采用
MVCC
方式的话,读-写
操作彼此并不冲突,性能更高
。 - 采用
加锁
方式的话,读-写
操作彼此需要 排队执行 ,影响性能
。
一般情况下我们当然愿意采用 MVCC
来解决 读-写
操作并发执行的问题,但是业务在某些特殊情况下,要求必须采用 加锁
的方式执行。
三、锁的不同角度分类
锁的分类图,如下:
1、从数据操作的类型划分:读锁、写锁
需要注意的是对于 InnoDB 引擎来说,读锁和写锁可以加在表上,也可以加在行上。
也可以给读加排他锁
#####1.锁定读
2.写操作
平常用的无非就是DELETE、UPDATE、INSERT
2、从数据操作的粒度划分:表级锁、页级锁、行锁
①表锁(Table Lock)
(1)表级别的S锁、X锁
在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这个表添加表级别的 S锁
或者X锁
的。在对某个表执行一些诸如 ALTER TABLE
、 DROP TABLE
这类的 DDL 语句时,其他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞。同理,某个事务中对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行 DDL
语句也会发生阻塞。这个过程其实是通过在 server层
使用一种称之为 元数据锁
(英文名: Metadata Locks ,简称 MDL )结构来实现的。
一般情况下,不会使用InnoDB存储引擎提供的表级别的 S锁
和 X锁
。只会在一些特殊情况下,比方说 崩溃恢复 过程中用到。比如,在系统变量 autocommit=0
,innodb_table_locks = 1
时, 手动 获取InnoDB存储引擎提供的表t 的 S锁
或者 X锁
可以这么写:
LOCK TABLES t READ
:InnoDB存储引擎会对表 t 加表级别的S锁
。LOCK TABLES t WRITE
:InnoDB存储引擎会对表 t 加表级别的X锁
。
不过尽量避免在使用InnoDB存储引擎的表上使用LOCK TABLES
这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。InnoDB的厉害之处还是实现了更细粒度的行锁
,关于InnoDB表级别的 S锁
和 X锁
大家了解一下就可以了。
MySQL的表级锁有两种模式:(以MyISAM表进行操作的演示)
- 表共享读锁(Table Read Lock)
- 表独占写锁(Table Write Lock)
(2)意向锁 (intention lock)
1、意向锁的存在是为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存。
2、意向锁是一种不与行级锁冲突表级锁,这一点非常重要。
3、表明“某个事务正在某些行持有了锁或该事务准备去持有锁”
意向锁的并发性:
意向锁不会与行级的共享 / 排他锁互斥!正因为如此,意向锁并不会影响到多个事务对不同数据行加排他锁时的并发性。(不然我们直接用普通的表锁就行了)
我们扩展一下上面 teacher表的例子来概括一下意向锁的作用(一条数据从被锁定到被释放的过程中,可能存在多种不同锁,但是这里我们只着重表现意向锁)。
行锁与意向锁的兼容性、意向锁和意向锁的兼容性
从上面的案例可以得到如下结论:
- InnoDB 支持
多粒度锁
,特定场景下,行级锁可以与表级锁共存
。 - 意向锁之间互不排斥,但除了
IS
与S
兼容外, 意向锁会与共享锁 / 排他锁
互斥 。 - IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。
- 意向锁在保证并发性的前提下,实现了
行锁和表锁共存
且满足事务隔离性
的要求。
(3)自增锁(AUTO-INC锁)
在使用MySQL过程中,我们可以为表的某个列添加 AUTO_INCREMENT
属性。举例:
1 | CREATE TABLE `teacher` ( |
由于这个表的id字段声明了AUTO_INCREMENT,意味着在书写插入语句时不需要为其赋值,SQL语句修改如下所示。
1 | INSERT INTO `teacher` (name) VALUES ('zhangsan'), ('lisi'); |
上边的插入语句并没有为id列显式赋值,所以系统会自动为它赋上递增的值,结果如下所示。
1 | select * from teacher; |
现在我们看到的上面插入数据只是一种简单的插入模式,所有插入数据的方式总共分为三类,分别是:
“ Simple inserts
”,“ Bulk inserts
”和“ Mixed-mode inserts
”。
(4)元数据锁(MDL锁)
MySQL5.5引入了meta data lock,简称MDL锁,属于表锁范畴。
MDL 的作用是,保证读写的正确性。比如,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个 表结构做变更
,增加了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。
因此,当对一个表做增删改查操作的时候,加 MDL
读锁;当要对表做结构变更操作的时候,加 MDL
写锁。
读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性,解决了DML和DDL操作之间的一致性问题。不需要显式使用,在访问一个表的时候会被自动加上。
② InnoDB中的行锁
(1)记录锁(Record Locks)
举例如下:
记录锁是有S锁和X锁之分的,称之为 S型记录锁
和 X型记录锁
。
- 当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;
- 当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁。
(2)间隙锁(Gap Locks)
MySQL 在 REPEATABLE READ
隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用 MVCC
方案解决,也可以采用 加锁
方案解决。但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读取操作时,那些幻影记录尚不存在,我们无法给这些 幻影记录
加上 记录锁
。InnoDB提出了一种称之为Gap Locks 的锁,官方的类型名称为: LOCK_GAP
,我们可以简称为 gap锁
。
比如,把id值为8的那条记录加一个gap锁的示意图如下。
图中id值为8的记录加了gap锁,意味着 不允许别的事务在id值为8的记录前边的间隙插入新记录
,其实就是id列的值(3, 8)这个区间的新记录是不允许立即插入的。比如,有另外一个事务再想插入一条id值为4的新记录,它定位到该条新记录的下一条记录的id值为8,而这条记录上又有一个gap锁,所以就会阻塞插入操作,直到拥有这个gap锁的事务提交了之后,id列的值在区间(3, 8)中的新记录才可以被插入。
gap锁的提出仅仅是为了防止插入幻影记录而提出的。
(3)临键锁(Next-Key Locks)
可以说是间隙锁和记录锁的合体,同时实现两种功能
有时候我们既想 锁住某条记录
,又想 阻止 其他事务在该记录前边的 间隙插入新记录
,所以InnoDB就提出了一种称之为 Next-Key Locks
的锁,官方的类型名称为: LOCK_ORDINARY
,我们也可以简称为next-key锁 。Next-Key Locks是在存储引擎 innodb
、事务级别在 可重复读 的情况下使用的数据库锁,innodb默认的锁就是Next-Key locks。
1 | begin; |
(4) 插入意向锁(Insert Intention Locks)
我们说一个事务在 插入
一条记录时需要判断一下插入位置是不是被别的事务加了 gap锁
( next-key锁也包含 gap锁
),如果有的话,插入操作需要等待,直到拥有 gap锁
的那个事务提交。但是InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个 间隙
中 插入
新记录,但是现在在等待。InnoDB就把这种类型的锁命名为 Insert Intention Locks
,官方的类型名称为:LOCK_INSERT_INTENTION
,我们称为 插入意向锁 。插入意向锁是一种 Gap锁
,不是意向锁,在insert操作时产生。
插入意向锁是在插入一条记录行前,由 INSERT 操作产生的一种间隙锁
。
事实上插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁
。
③页锁
页锁就是在 页的粒度
上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。
当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般
。
每个层级的锁数量是有限制的,因为锁会占用内存空间, 锁空间的大小是有限的
。当某个层级的锁数量超过了这个层级的阈值时,就会进行锁升级
。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如InnoDB 中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。
3、从对待锁的态度划分:乐观锁、悲观锁
从对待锁的态度来看锁的话,可以将锁分成乐观锁和悲观锁,从名字中也可以看出这两种锁是两种看待数据并发的思维方式
。需要注意的是,乐观锁和悲观锁并不是锁,而是锁的 设计思想
。
①悲观锁(Pessimistic Locking)
悲观锁是一种思想,顾名思义,就是很悲观,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。
悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 阻塞
直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
)。
比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。Java中 synchronized
和 ReentrantLock
等独占锁就是悲观锁思想的实现。
②乐观锁(Optimistic Locking)
乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用 版本号机制
或者 CAS机制
实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。
在Java中 java.util.concurrent.atomic 包下的原子变量类就是使用了乐观锁的一种实现方式:CAS实现的
。
③两种锁的适用场景
4、按加锁的方式划分:显式锁、隐式锁
①隐式锁
session 1:
1 | begin; |
session 2:
1 | begin; |
执行下述语句,输出结果:
1 | SELECT * FROM performance_schema.data_lock_waits\G; |
隐式锁的逻辑过程如下:
A. InnoDB的每条记录中都一个隐含的trx_id字段,这个字段存在于聚簇索引的B+Tree中。
B. 在操作一条记录前,首先根据记录中的trx_id检查该事务是否是活动的事务(未提交或回滚)。如果是活动的事务,首先将 隐式锁
转换为显式锁
(就是为该事务添加一个锁)。
C. 检查是否有锁冲突,如果有冲突,创建锁,并设置为waiting状态。如果没有冲突不加锁,跳到E。
D. 等待加锁成功,被唤醒,或者超时。
E. 写数据,并将自己的trx_id写入trx_id字段。
②显式锁
通过特定的语句进行加锁,我们一般称之为显示加锁,例如:
显示加共享锁:
1 | select .... lock in share mode |
显示加排它锁:
1 | select .... for update |
5、其它锁之:全局锁
全局锁就是对 整个数据库实例 加锁
。当你需要让整个库处于 只读状态
的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。全局锁的典型使用 场景
是:做 全库逻辑备份
。
全局锁的命令:
1 | Flush tables with read lock |
6、其它锁之:死锁
6.1 概念
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。死锁示例:
这时候,事务1在等待事务2释放id=2的行锁,而事务2在等待事务1释放id=1的行锁。 事务1和事务2在互相等待对方的资源释放,就是进入了死锁状态。
#####6.2 产生条件
#####6.3 解决策略
当出现死锁以后,有 两种策略
:
一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数
innodb_lock_wait_timeout
来设置。另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务(将持有最少行级排他锁的事务进行回滚),让其他事务得以继续执行。将参数
innodb_deadlock_detect
设置为on
,表示开启这个逻辑。#####6.4 如何避免
四、锁的内存结构
InnoDB
存储引擎中的 锁结构
如下:
- 锁所在的事务信息 :
不论是表锁
还是行锁
,都是在事务执行过程中生成的,哪个事务生成了这个锁结构
,这里就记录这个事务的信息。
此锁所在的事务信息
在内存结构中只是一个指针,通过指针可以找到内存中关于该事务的更多信息,比方说事务id等。 - 索引信息 :
对于行锁
来说,需要记录一下加锁的记录是属于哪个索引的。这里也是一个指针。 - 表锁/行锁信息 :
表锁结构
和行锁结构
在这个位置的内容是不同的: - type_mode :
这是一个32位的数,被分成了lock_mode
、lock_type
和rec_lock_type
三个部分,如图所示:
- 其他信息 :
为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表。 - 一堆比特位 :
五、锁监控
关于MySQL锁的监控,我们一般可以通过检查 InnoDB_row_lock
等状态变量来分析系统上的行锁的争夺情况
1 | show status like 'innodb_row_lock%'; |
其他监控方法:
锁等待场景,我们依然使用记录锁中的案例,当事务2进行等待时,查询情况如下:
(1)查询正在被锁阻塞的sql语句。
1 | SELECT * FROM information_schema.INNODB_TRX\G; |
重要属性代表含义已在上述中标注。
(2)查询锁等待情况
1 | SELECT * FROM data_lock_waits\G; |
(3)查询锁的情况
1 | mysql > SELECT * from performance_schema.data_locks\G; |
从锁的情况可以看出来,两个事务分别获取了IX锁,我们从意向锁章节可以知道,IX锁互相时兼容的。
所以这里不会等待,但是事务1同样持有X锁,此时事务2也要去同一行记录获取X锁,他们之间不兼容,导致等待的情况发生。
十六、多版本并发控制
多版本:undolog
并发控制:readview+隐藏列
mvcc的实现依赖于:隐藏字段(rowId+trxId)+undolog+readview
一、什么是MVCC
MVCC
(Multiversion Concurrency Control),多版本并发控制。
顾名思义,MVCC 是通过数据行的多个版本管理来实现数据库的 并发控制
。这项技术使得在InnoDB的事务隔离级别下执行 一致性读
操作有了保证。
换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。
二、快照读与当前读
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理 读-写冲突
,做到即使有读写冲突时,也能做到 不加锁
, 非阻塞并发读
,而这个读指的就是 快照读
, 而非 当前读
。
当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。
1、快照读
快照读又叫一致性读,读取的是快照数据。不加锁
的简单的 SELECT 都属于快照读,即不加锁的非阻塞读;比如这样:
1 | SELECT * FROM player WHERE ... |
之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于MVCC,它在很多情况下,避免了加锁操作,降低了开销。
既然是基于多版本,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。
2、当前读
当前读读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的
SELECT,或者对数据进行增删改都会进行当前读。
比如:
1 | SELECT * FROM student LOCK IN SHARE MODE; # 共享锁 |
三、复习
1、再谈隔离级别
我们知道事务有 4
个隔离级别,可能存在三种并发问题:
另图:
2、隐藏字段、Undo Log版本链
undo日志的版本链,对于使用 InnoDB
存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列
。
- trx_id :每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的
事务id
赋值给trx_id
隐藏列。 - roll_pointer :每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到
undo日志
中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
insert undo只在事务回滚时起作用,当事务提交后,该类型的undo日志就没用了,它占用的UndoLog Segment也会被
系统回收
(也就是该undo日志占用的Undo页面链表要么被重用,要么被释放)。
假设之后两个事务id分别为 10
、 20
的事务对这条记录进行 UPDATE
操作,操作流程如下:
每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个 roll_pointer 属性
( INSERT 操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些 undo日志都连起来
,串成一个链表
:
对该记录每次更新后,都会将旧值放到一条 undo日志
中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer
属性连接成一个链表
,我们把这个链表称之为 版本链
,版本链的头节点就是当前记录最新的值。
每个版本
中还包含生成该版本时对应的 事务id
。
四、MVCC实现原理之ReadView
MVCC = 隐藏字段(rowId+trxId)+undolog+readview
MVCC
的实现依赖于:隐藏字段
、Undo Log
、Read View
。
1、什么是ReadView
- 隐藏字段(rowId+trxId)
- rowId:针对每一行
行格式
,都会对应有一个隐藏记录的rowid - trxId:一次
事务
的操作,就会生成一个trxid
- rowId:针对每一行
- Undo Log
根据隔离级别,来记录每次操作的数据记录,多版本
- Read View
根据对应的对应的undolog+trxid+隔离级别
,来判断决定
是否能够查询的到某记录数据
2、设计思路
使用 READ UNCOMMITTED
隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。
使用 SERIALIZABLE
隔离级别的事务,InnoDB规定使用加锁的方式来访问记录。
使用 READ COMMITTED
和 REPEATABLE READ
隔离级别的事务,都必须保证读到 已经提交了的 事务修改过的记录。假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的,这是ReadView要解决的主要问题。
MVCC解决的是READ COMMITTED
和 REPEATABLE READ
隔离级别的问题
这个ReadView中主要包含4个比较重要的内容,分别如下:
3、ReadView的规则
有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见。
4、MVCC整体操作流程
了解了这些概念之后,我们来看下当查询一条记录的时候,系统如何通过MVCC找到它:
- 首先获取事务自己的版本号,也就是事务 ID;
- 获取 ReadView;
- 查询得到的数据,然后与 ReadView 中的事务版本号进行比较;
- 如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
- 最后返回符合规则的数据。
在隔离级别为读已提交(Read Committed)时,一个事务中的每一次 SELECT 查询都会重新获取一次Read View。
如表所示:
注意,此时同样的查询语句都会重新获取一次 Read View,这时如果 Read View 不同,就可能产生不可重复读或者幻读的情况。
当隔离级别为可重复读的时候,就避免了不可重复读,这是因为一个事务只在第一次 SELECT 的时候会获取一次 Read View,而后面所有的 SELECT 都会复用这个 Read View,如下表所示:
五、举例说明
1、READ COMMITTED隔离级别下
READ COMMITTED :每次读取数据前都生成一个ReadView。
现在有两个 事务id 分别为 10 、 20 的事务在执行:
1 | # Transaction 10 |
此刻,表student 中 id
为 1
的记录得到的版本链表如下所示:
假设现在有一个使用 READ COMMITTED
隔离级别的事务开始执行:
1 | # 使用READ COMMITTED隔离级别的事务 |
之后,我们把 事务id
为 10
的事务提交一下:
1 | # Transaction 10 |
然后再到 事务id
为 20
的事务中更新一下表 student
中 id
为 1
的记录:
1 | # Transaction 20 |
此刻,表student中 id
为 1
的记录的版本链就长这样:
然后再到刚才使用 READ COMMITTED
隔离级别的事务中继续查找这个 id 为 1 的记录
如下:
1 | # 使用READ COMMITTED隔离级别的事务 |
2、REPEATABLE READ隔离级别下
使用 REPEATABLE READ
隔离级别的事务来说,只会在第一次执行查询语句
时生成一个 ReadView
,之后的查询就不会重复生成
了。
比如,系统里有两个 事务id
分别为 10
、 20
的事务在执行:
1 | # Transaction 10 |
此刻,表student 中 id
为 1
的记录得到的版本链表如下所示:
假设现在有一个使用 REPEATABLE READ
隔离级别的事务开始执行:
1 | # 使用REPEATABLE READ隔离级别的事务 |
之后,我们把 事务id
为 10
的事务提交一下,就像这样:
1 | # Transaction 10 |
然后再到 事务id
为 20
的事务中更新一下表 student 中 id
为 1
的记录:
1 | # Transaction 20 |
此刻,表student 中 id
为 1
的记录的版本链长这样:
然后再到刚才使用 REPEATABLE READ
隔离级别的事务中继续查找这个 id
为 1
的记录
如下:
1 | # 使用REPEATABLE READ隔离级别的事务 |
3、如何解决幻读
幻读只在 repeatable read下面才有
接下来说明InnoDB 是如何解决幻读的。
假设现在表 student 中只有一条数据,数据内容中,主键 id=1
,隐藏的 trx_id=10
,它的 undo log
如下图所示。
此时表student 中就有三条数据了,对应的 undo 如下图所示:
步骤3:接着事务 A 开启第二次查询,根据可重复读隔离级别的规则,此时事务 A 并不会再重新生成ReadView。此时表 student 中的 3 条数据都满足 where id>=1 的条件,因此会先查出来。然后根据ReadView 机制,判断每条数据是不是都可以被事务 A 看到。
1)首先 id=1 的这条数据,前面已经说过了,可以被事务 A 看到。
2)然后是 id=2 的数据,它的 trx_id=30,此时事务 A 发现,这个值处于 up_limit_id 和 low_limit_id 之间,因此还需要再判断 30 是否处于 trx_ids 数组内。由于事务 A 的 trx_ids=[20,30],因此在数组内,这表示 id=2 的这条数据是与事务 A 在同一时刻启动的其他事务提交的,所以这条数据不能让事务 A 看到。
3)同理,id=3 的这条数据,trx_id 也为 30,因此也不能被事务 A 看见。
结论:
最终事务 A 的第二次查询,只能查询出 id=1 的这条数据。
这和事务 A 的第一次查询的结果是一样的,因此没有出现幻读现象,所以说在 MySQL 的可重复读隔离级别下,不存在幻读问题。
六、总结
这里介绍了 MVCC
在 READ COMMITTD
、 REPEATABLE READ
这两种隔离级别的事务在执行快照读操作时访问记录的版本链的过程。这样使不同事务的 读-写
、 写-读
操作并发执行,从而提升系统性能
。
核心点在于 ReadView
的原理, READ COMMITTD
、 REPEATABLE READ
这两个隔离级别的一个很大不同就是生成ReadView的时机不同
:
- READ COMMITTD 在
每一次
进行普通SELECT操作前都会生成一个ReadView
- REPEATABLE READ 只在
第一次
进行普通SELECT操作前生成一个ReadView
,之后的查询操作都重复使用这个ReadView
就好了。
十七、其他数据库日志
一、MySQL支持的日志
1、日志类型
MySQL有不同类型的日志文件,用来存储不同类型的日志,分为 二进制日志
、 错误日志
、 通用查询日志
和 慢查询日志
,这也是常用的4种。
MySQL 8
又新增两种支持的日志: 中继日志
和 数据定义语句日志
。
使用这些日志文件,可以查看MySQL内部发生的事情。
这6类日志分别为:
除二进制日志
外,其他日志都是 文本文件
。 默认情况下,所有日志创建于 MySQL数据目录
中。
2、日志的弊端
- 日志功能会
降低MySQL数据库的性能
。 - 日志会
占用大量的磁盘空间
。
二、慢查询日志(slow query log)
第九章中的性能分析的工具中进行了讲解
三、通用查询日志(general query log)
通用查询日志用来 记录用户的所有操作
,包括启动和关闭MySQL服务、所有用户的连接开始时间和截止时间、发给 MySQL 数据库服务器的所有 SQL 指令等。
当我们的数据发生异常时,查看通用查询日志,还原操作时的具体场景,可以帮助我们准确定位问题。
1、查看当前状态
1 | SHOW VARIABLES LIKE '%general%'; |
2、启动日志
方式1:永久性方式:
修改my.cnf或者my.ini配置文件来设置。在[mysqld]组下加入log选项,并重启MySQL服务。格式如下:
1 | [mysqld] |
如果不指定目录和文件名,通用查询日志将默认存储在MySQL数据目录中的hostname.log文件中,hostname表示主机名。
方式2:临时性方式:
1 | SET GLOBAL general_log=on; # 开启通用查询日志 |
对应的,关闭操作SQL命令如下:
1 | SET GLOBAL general_log=off; # 关闭通用查询日志 |
查看设置后情况:
1 | SHOW VARIABLES LIKE 'general_log%'; |
3、查看日志
通用查询日志是以 文本文件
的形式存储在文件系统中的,可以使用 文本编辑器
直接打开日志文件。每台MySQL服务器的通用查询日志内容是不同的。
- 在Windows操作系统中,使用文本文件查看器;
- 在Linux系统中,可以使用vi工具或者gedit工具查看;
- 在Mac OSX系统中,可以使用文本文件查看器或者vi等工具查看。
从SHOW VARIABLES LIKE 'general_log%';
结果中可以看到通用查询日志的位置。
1 | /usr/sbin/mysqld, Version: 8.0.26 (MySQL Community Server - GPL). started with: |
在通用查询日志里面,我们可以清楚地看到,什么时候开启了新的客户端登陆数据库,登录之后做了什么 SQL 操作,针对的是哪个数据表等信息。
4、停止日志
方式1:永久性方式:
修改 my.cnf
或者 my.ini
文件,把[mysqld]组下的 general_log
值设置为 OFF
或者把general_log
一项注释掉。修改保存后,再 重启MySQL服务
,即可生效。
举例1:
1 | [mysqld] |
举例2:
1 | [mysqld] |
方式2:临时性方式
使用SET语句停止MySQL通用查询日志功能:
1 | SET GLOBAL general_log=off; |
查询通用日志功能:
1 | SHOW VARIABLES LIKE 'general_log%'; |
5、删除\刷新日志
如果数据的使用非常频繁,那么通用查询日志会占用服务器非常大的磁盘空间
。
数据管理员可以删除很长时间之前的查询日志,以保证MySQL服务器上的硬盘空间。
手动删除文件:
1 | SHOW VARIABLES LIKE 'general_log%'; |
可以看出,通用查询日志的目录默认为MySQL数据目录。在该目录下手动删除
通用查询日志atguigu01.log
。
使用如下命令重新生成查询日志文件,具体命令如下。
刷新MySQL数据目录,发现创建了新的日志文件。前提一定要开启通用日志
。
1 | mysqladmin -uroot -p flush-logs |
四、错误日志(error log)
1、启动日志
在MySQL数据库中,错误日志
功能是 默认开启
的。而且,错误日志
无法被禁止 。
默认情况下,错误日志存储在MySQL数据库的数据文件夹下,名称默认为 mysqld.log (Linux系统)
或hostname.err (mac系统)
。
如果需要制定文件名,则需要在my.cnf或者my.ini中做如下配置:
1 | [mysqld] |
修改配置项后,需要重启MySQL服务
以生效。
2、查看日志
MySQL错误日志是以文本文件形式
存储的,可以使用文本编辑器直接查看。
查询错误日志的存储路径:
1 | SHOW VARIABLES LIKE 'log_err%'; |
执行结果中可以看到错误日志文件是mysqld.log
,位于MySQL默认的数据目录
下。
3、删除\刷新日志
对于很久以前的错误日志,数据库管理员查看这些错误日志的可能性不大,可以将这些错误日志删除,以保证MySQL服务器上的 硬盘空间
。
MySQL的错误日志是以文本文件的形式存储在文件系统中的,可以直接删除
。
1 | [root@atguigu01 log]# mysqladmin -uroot -p flush-logs |
官网提示:
补充操作:
1 | install -omysql -gmysql -m0644 /dev/null /var/log/mysqld.log |
4. 8.0新特性
五、二进制日志(bin log)
binlog可以说是MySQL中比较 重要
的日志了,在日常开发及运维过程中,经常会遇到。
binlog即binary log,二进制日志文件,也叫作变更日志(update log)。
它记录了数据库所有执行的DDL
和 DML
等数据库更新事件的语句,但是不包含没有修改任何数据的语句(如数据查询语句select、show等)。
binlog主要应用场景:
- 用于
数据恢复
- 用于
数据复制
1、查看默认情况
查看记录二进制日志是否开启:在MySQL8中默认
情况下,二进制文件是开启的
。
1 | show variables like '%log_bin%'; |
2、日志参数设置
方式1:永久性方式:
修改MySQL的 my.cnf
或 my.ini
文件可以设置二进制日志的相关参数:
1 | [mysqld] |
重新启动MySQL服务,查询二进制日志的信息,执行结果:
1 | show variables like '%log_bin%'; |
设置带文件夹的bin-log日志存放目录
如果想改变日志文件的目录和名称,可以对my.cnf或my.ini中的log_bin参数修改如下:
1 | [mysqld] |
注意:新建的文件夹需要使用mysql用户,使用下面的命令即可。
1 | chown -R -v mysql:mysql binlog |
方式2:临时性方式:
如果不希望通过修改配置文件并重启的方式设置二进制日志的话,还可以使用如下指令,需要注意的是在mysql8中只有 会话级别
的设置,没有了global级别的设置。
1 | # global 级别 |
3、查看日志
当MySQL创建二进制日志文件时,先创建一个以“filename”为名称、以“.index”为后缀的文件,再创建一个以“filename”为名称、以“.000001”为后缀的文件。
MySQL服务 重新启动一次
,以“.000001”为后缀的文件就会增加一个,并且后缀名按1递增。即日志文件的个数与MySQL服务启动的次数相同;如果日志长度超过了 max_binlog_size
的上限(默认是1GB
),就会创建一个新的日志文件。
查看当前的二进制日志文件列表及大小。指令如下:
1 | SHOW BINARY LOGS; |
下面命令将行事件以 伪SQL的形式
表现出来
1 | mysqlbinlog -v "/var/lib/mysql/binlog/atguigu-bin.000002" |
前面的命令同时显示binlog格式的语句,使用如下命令不显示它
1 | mysqlbinlog -v --base64-output=DECODE-ROWS "/var/lib/mysql/binlog/atguigu-bin.000002" |
关于mysqlbinlog工具的使用技巧还有很多,例如只解析对某个库的操作或者某个时间段内的操作等。简单分享几个常用的语句,更多操作可以参考官方文档。
1 | # 可查看参数帮助 |
上面这种办法读取出binlog日志的全文内容比较多,不容易分辨查看到pos点信息,下面介绍一种更为方便的查询命令:
1 | mysql> show binlog events [IN 'log_name'] [FROM pos] [LIMIT [offset,] row_count]; |
1 | show binlog events in 'atguigu-bin.000002'; |
上面我们讲了这么多都是基于binlog的默认格式,binlog格式查看
1 | show variables like 'binlog_format'; |
除此之外,binlog还有2种格式,分别是Statement
和Mixed
4、使用日志恢复数据
mysqlbinlog
恢复数据的语法如下:
1 | mysqlbinlog [option] filename|mysql –uuser -ppass; |
这个命令可以这样理解:使用mysqlbinlog命令来读取filename中的内容,然后使用mysql命令将这些内容恢复到数据库中。
注意:
使用mysqlbinlog命令进行恢复操作时,必须是编号小的先恢复,例如atguigu-bin.000001必须在atguigu-bin.000002之前恢复。
5、删除二进制日志
MySQL的二进制文件可以配置自动删除,同时MySQL也提供了安全的手动删除二进制文件的方法。
PURGE MASTER LOGS 只删除指定部分的二进制日志文件, RESET MASTER 删除所有的二进制日志文件。
6、其它场景
六、再谈二进制日志(binlog)
1、写入机制
binlog的写入时机也非常简单,事务执行过程中,先把日志写到 binlog cache
,事务提交的时候,再把binlog cache写到binlog文件中。(类比redo log)
因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache。
write和fsync的时机,可以由参数 sync_binlog
控制,默认是 0
。为0的时候,表示每次提交事务都只write,由系统自行判断什么时候执行fsync。虽然性能得到提升,但是机器宕机,page cache里面的binglog 会丢失。
如下图:
为了安全起见,可以设置为 1
,表示每次提交事务都会执行fsync,就如同redo log 刷盘流程一样
。
最后还有一种折中方式,可以设置为N(N>1),表示每次提交事务都write,但累积N个事务后才fsync
。
在出现IO瓶颈的场景里,将sync_binlog设置成一个比较大的值,可以提升性能。同样的,如果机器宕机,会丢失最近N个事务的binlog日志。
2、binlog与redolog对比
3、两阶段提交
在执行更新语句过程,会记录redo log与binlog两块日志,以基本的事务为单位,redo log在事务执行过程中可以不断写入,而binlog只有在提交事务时才写入,所以redo log与binlog的 写入时机
不一样。
redo log与binlog两份日志之间的逻辑不一致,会出现什么问题?
由于binlog没写完就异常,这时候binlog里面没有对应的修改记录。
为了解决两份日志之间的逻辑一致问题,InnoDB存储引擎使用两阶段提交
方案。
使用两阶段提交
后,写入binlog时发生异常也不会有影响
最后就查看binlog日志,是否写入成功,如果成功就redolog就commit阶段就提交正常流程下去,如果失败,就回滚
另一个场景,redo log设置commit阶段发生异常,那会不会回滚事务呢?
并不会回滚事务,它会执行上图框住的逻辑,虽然redo log是处于prepare阶段,但是能通过事务id找到对应的binlog日志,所以MySQL认为是完整的,就会提交事务恢复数据。
七、中继日志(relay log)
1、介绍
2、查看中继日志
中继日志与二进制日志的格式相同,可以用 mysqlbinlog
工具进行查看。下面是中继日志的一个片段:
1 | SET TIMESTAMP=1618558728/*!*/; |
这一段的意思是,主服务器(“server id 1”)对表 atguigu.test 进行了 2 步操作:
1 | 定位到表 atguigu.test 编号是 91 的记录,日志位置是 832; |
3、恢复的典型错误
如果从服务器宕机,有的时候为了系统恢复,要重装操作系统,这样就可能会导致你的 服务器名称 与之前
不同 。而中继日志里是 包含从服务器名
的。
在这种情况下,就可能导致你恢复从服务器的时候,无法从宕机前的中继日志里读取数据,以为是日志文件损坏了,其实是名称不对了。
解决的方法也很简单,把从服务器的名称改回之前的名称
。
十八、主从复制
一、主从复制概述
1、如何提升数据库并发能力
一般应用对数据库而言都是“ 读多写少
”,也就说对数据库读取数据的压力比较大,有一个思路就是采用数据库集群的方案,做 主从架构
、进行 读写分离
,这样同样可以提升数据库的并发处理能力。但并不是所有的应用都需要对数据库进行主从架构的设置,毕竟设置架构本身是有成本的。
如果我们的目的在于提升数据库高并发访问的效率,那么首先考虑的是如何 优化SQL
和索引
,这种方式简单有效;其次才是采用 缓存的策略 ,比如使用 Redis将热点数据保存在内存数据库中
,提升读取的效率;最后才是对数据库采用 主从架构
,进行读写分离
。
2、主从复制的作用
主从同步设计不仅可以提高数据库的吞吐量,还有以下 3 个方面的作用。
第1个作用:读写分离
第2个作用就是数据备份
第3个作用是具有高可用性。
二、主从复制的原理
Slave
会从 Master
读取 binlog
来进行数据同步。
1、原理剖析
三个线程:
实际上主从同步的原理就是基于 binlog 进行数据同步的。在主从复制过程中,会基于 3 个线程
来操作,一个主库线程,两个从库线程。
二进制日志转储dump线程
(Binlog dump thread)是一个主库线程。当从库线程连接的时候, 主库可以将二进制日志发送给从库,当主库读取事件(Event)的时候,会在 Binlog 上加锁
,读取完成之后,再将锁释放掉。从库 I/O 线程
会连接到主库,向主库发送请求更新 Binlog。这时从库的 I/O 线程就可以读取到主库的二进制日志转储线程发送的 Binlog 更新部分,并且拷贝到本地的中继日志 (Relay log)。从库 SQL 线程
会读取从库中的中继日志,并且执行日志中的事件,将从库中的数据与主库保持同步。
复制三步骤:
复制的问题:延时
2、复制的基本原则
三、一主一从架构搭建
一台 主机
用于处理所有 写请求
,一台 从机
负责所有 读请求
,架构图如下:
1、准备工作
- 准备 `2台 CentOS 虚拟机
- 每台虚拟机上需要安装好MySQL (可以是MySQL8.0 )
说明:前面我们讲过如何克隆一台CentOS。大家可以在一台CentOS上安装好MySQL,进而通过克隆的方式复制出1台包含MySQL的虚拟机。
注意:克隆的方式需要修改新克隆出来主机的:
① MAC地址
② hostname
③ IP 地址
④ UUID
。
此外,克隆的方式生成的虚拟机(包含MySQL Server),则克隆的虚拟机MySQL Server的UUID相同,必须修改,否则在有些场景会报错。
比如: show slave status\G
,报如下的错误:
1 | Last_IO_Error: Fatal error: The slave I/O thread stops because master and slave have |
修改MySQL Server 的UUID方式: 保证从机和主机的uuid必须不同
1 | vim /var/lib/mysql/auto.cnf |
2、主机配置文件
建议mysql版本一致
且后台以服务运行
,主从所有配置项都配置在[mysqld]
节点下,且都是小写字母。具体参数配置如下:
必选
1
2
3
4
5[必须]主服务器唯一ID
server-id=1
[必须]启用二进制日志,指名路径。比如:自己本地的路径/log/mysqlbin
log-bin=atguigu-bin
1234可选
1
2
3
4
5
6
7
8
9
10
11
12
13[可选] 0(默认)表示读写(主机),1表示只读(从机)
read-only=0
设置日志文件保留的时长,单位是秒
binlog_expire_logs_seconds=6000
控制单个二进制日志大小。此参数的最大和默认值是1GB
max_binlog_size=200M
[可选]设置不要复制的数据库
binlog-ignore-db=test
[可选]设置需要复制的数据库,默认全部记录。比如:binlog-do-db=atguigu_master_slave
binlog-do-db=需要复制的主数据库名字
[可选]设置binlog格式
binlog_format=STATEMENT
123456789101112
binlog格式设置:
格式1: STATEMENT模式
(基于SQL语句的复制(statement-based replication, SBR))
1 | binlog_format=STATEMENT |
每一条会修改数据的sql语句
会记录到binlog中。这是默认的binlog格式。
格式2:ROW模式
(基于行的复制(row-based replication, RBR))
1 | binlog_format=ROW |
5.1.5版本的MySQL才开始支持,不记录每条sql语句的上下文信息,仅记录哪条数据被修改
了,修改成什么样
了。
格式3:MIXED模式
(混合模式复制(mixed-based replication, MBR))
1 | binlog_format=MIXED |
从5.1.8版本开始,MySQL提供了Mixed格式,实际上就是Statement与Row的结合
。
在Mixed模式下,一般的语句修改使用statment格式保存binlog。如一些函数,statement无法完成主从复制的操作,则采用row格式保存binlog。
MySQL会根据执行的每一条具体的sql语句来区分对待记录的日志形式,也就是在Statement和Row之间选择一种。
3、从机配置文件
要求主从所有配置项都配置在 my.cnf
的 [mysqld]
栏位下,且都是小写字母。
- 必选
1 | [必须]从服务器唯一ID |
- 可选
1 | [可选]启用中继日志 |
重启后台mysql服务,使配置生效。
注意:主从机都关闭防火墙
1 | service iptables stop #CentOS 6 |
4、主机:建立账户并授权
1 | #在主机MySQL里执行授权主从复制的命令 |
注意:如果使用的是MySQL8,需要如下的方式建立账户,并授权slave:
1 | CREATE USER 'slave1'@'%' IDENTIFIED BY '123456'; |
注意:在从机执行show slave status\G时报错:
Last_IO_Error: error connecting to master ‘slave1@192.168.1.150:3306’ - retry-time: 60 retries: 1 message: Authentication plugin ‘caching_sha2_password’ reported error: Authentication requires secure connection.
查询Master的状态,并记录下File和Position的值。
1 | show master status; |
记录下File和Position的值
执行完此步骤后不要再操作主服务器MySQL,防止主服务器状态值变化。
5、从机:配置需要复制的主机
步骤1:从机上复制主机的命令
1 | CHANGE MASTER TO |
举例:
1 | CHANGE MASTER TO MASTER_HOST='192.168.1.150',MASTER_USER='slave1',MASTER_PASSWORD='123456',MASTER_LOG_FILE='atguigu-bin.000007',MASTER_LOG_POS=154; |
步骤2:
1 | #启动slave同步 |
如果报错:
可以执行如下操作,删除之前的relay_log信息。然后重新执行 CHANGE MASTER TO …语句即可。
1 | reset slave; #删除SLAVE数据库的relaylog日志文件,并重新启用新的relaylog文件 |
接着,查看同步状态:
1 | SHOW SLAVE STATUS\G; |
上面两个参数都是Yes,则说明主从配置成功!
显式如下的情况,就是不正确的。可能错误的原因有:
1 | 1. 网络不通 |
6、停止主从同步
- 停止主从同步命令:
1 | stop slave; |
- 如何重新配置主从
如果停止从服务器复制功能,再使用需要重新配置主从。否则会报错如下:
重新配置主从,需要在从机上执行:
1 | stop slave; |
四、同步数据一致性问题
主从同步的要求:
1、理解主从延迟问题
进行主从同步的内容是二进制日志,它是一个文件,在进行 网络传输
的过程中就一定会 存在主从延迟(比如 500ms)
,这样就可能造成用户在从库上读取的数据不是最新的数据,也就是主从同步中的 数据不一致性
问题。
2、主从延迟问题原因
3、如何减少主从延迟
若想要减少主从延迟的时间,可以采取下面的办法:
- 降低多线程大事务并发的概率,优化业务逻辑
- 优化SQL,避免慢SQL,
减少批量操作
,建议写脚本以update-sleep这样的形式完成。 提高从库机器的配置
,减少主库写binlog和从库读binlog的效率差。- 尽量采用
短的链路
,也就是主库和从库服务器的距离尽量要短,提升端口带宽,减少binlog传输的网络延时。 - 实时性要求的业务读强制走主库,从库只做灾备,备份。
4、如何解决一致性问题
如果操作的数据存储在同一个数据库中,那么对数据进行更新的时候,可以对记录加写锁,这样在读取的时候就不会发生数据不一致的情况。但这时从库的作用就是 备份
,并没有起到 读写分离
,分担主库读压力
的作用
读写分离情况下,解决主从同步中数据不一致的问题, 就是解决主从之间 数据复制方式 的问题
,如果按照数据一致性 从弱到强
来进行划分,有以下 3 种复制方式。
方法 1:异步复制:
方法 2:半同步复制:
从库同步成功后会返回ack,主库收到一定数量的ack
,就算同步成功,就返回客户端成功
方法 3:组复制:
十九、数据库备份与恢复
一、物理备份与逻辑备份
- 物理备份:
备份数据文件,转储数据库物理文件到某一目录。物理备份恢复速度比较快,但占用空间比较大,MySQL中可以用xtrabackup
工具来进行物理备份。 - 逻辑备份:
对数据库对象利用工具进行导出工作,汇总入备份文件内。逻辑备份恢复速度慢,但占用空间小,更灵活。MySQL 中常用的逻辑备份工具为mysqldump
。逻辑备份就是备份sql语句
,在恢复的时候执行备份的sql语句实现数据库数据的重现。
二、mysqldump实现逻辑备份
1、备份一个数据库
基本语法:
1 | mysqldump –u 用户名称 –h 主机名称 –p密码 待备份的数据库名称[tbname, [tbname...]]> 备份文件名称.sql |
举例:使用root用户备份atguigu数据库:
1 | mysqldump -uroot -p atguigu>atguigu.sql #备份文件存储在当前目录下 |
备份文件剖析:
1 | -- MySQL dump 10.13 Distrib 8.0.26, for Linux (x86_64) |
2、备份全部数据库
若想用mysqldump备份整个实例,可以使用 --all-databases
或 -A
参数:
1 | mysqldump -uroot -pxxxxxx --all-databases > all_database.sql |
3、备份部分数据库
使用 --databases
或 -B
参数了,该参数后面跟数据库名称,多个数据库间用空格隔开。如果指定databases参数,备份文件中会存在创建数据库的语句,如果不指定参数,则不存在。
语法如下:
1 | mysqldump –u user –h host –p --databases [数据库的名称1 [数据库的名称2...]] > 备份文件名称.sql |
举例:
1 | mysqldump -uroot -p --databases atguigu atguigu12 >two_database.sql |
4、备份部分表
比如,在表变更前做个备份。语法如下:
1 | mysqldump –u user –h host –p 数据库的名称 [表名1 [表名2...]] > 备份文件名称.sql |
举例:备份atguigu数据库下的book表
1 | mysqldump -uroot -p atguigu book> book.sql |
book.sql文件内容如下
1 | mysqldump -uroot -p atguigu book> book.sql^C |
可以看到,book文件和备份的库文件类似。不同的是,book文件只包含book表的DROP、CREATE和INSERT语句。
备份多张表使用下面的命令,比如备份book和account表:
1 | 备份多张表 |
5、备份单表的部分数据
有些时候一张表的数据量很大,我们只需要部分数据。这时就可以使用 --where
选项了。where后面附带需要满足的条件。
举例:备份student表中id小于10的数据:
1 | mysqldump -uroot -p atguigu student --where="id < 10 " > student_part_id10_low_bak.sql |
内容如下所示,insert语句只有id小于10的部分
1 | LOCK TABLES `student` WRITE; |
6、排除某些表的备份
如果我们想备份某个库,但是某些表数据量很大或者与业务关联不大,这个时候可以考虑排除掉这些表,同样的,选项 --ignore-table
可以完成这个功能。
1 | mysqldump -uroot -p atguigu --ignore-table=atguigu.student > no_stu_bak.sql |
通过如下指定判定文件中没有student表结构:
1 | grep "student" no_stu_bak.sql |
7、只备份结构或只备份数据
只备份结构的话可以使用 --no-data
简写为 -d
选项;
只备份数据可以使用 --no-create-info
简写为 -t
选项。
只备份结构:
1 | mysqldump -uroot -p atguigu --no-data > atguigu_no_data_bak.sql |
只备份数据:
1 | mysqldump -uroot -p atguigu --no-create-info > atguigu_no_create_info_bak.sql |
8、备份中包含存储过程、函数、事件
mysqldump备份默认是不包含存储过程,自定义函数及事件的。
可以使用 --routines
或 -R
选项来备份存储过程及函数
使用 --events
或 -E
参数来备份事件。
举例:备份整个atguigu库,包含存储过程及事件:
- 使用下面的SQL可以查看当前库有哪些存储过程或者函数
1 | SELECT SPECIFIC_NAME,ROUTINE_TYPE ,ROUTINE_SCHEMA FROM |
下面备份atguigu库的数据,函数以及存储过程。
1 | mysqldump -uroot -p -R -E --databases atguigu > fun_atguigu_bak.sql |
查询备份文件中是否存在函数,如下所示,可以看到确实包含了函数
1 | grep -C 5 "rand_num" fun_atguigu_bak.sql |
9、mysqldump常用选项
mysqldump其他常用选项如下:
1 | --add-drop-database:在每个CREATE DATABASE语句前添加DROP DATABASE语句。 |
运行帮助命令 mysqldump --help
,可以获得特定版本的完整选项列表。
提示 :
如果运行mysqldump没有–quick或–opt选项,mysqldump在转储结果前将整个结果集装入内存。如果转储大数据库可能会出现问题,该选项默认启用,但可以用–skip-opt禁用。如果使用最新版本的mysqldump程序备份数据,并用于恢复到比较旧版本的MySQL服务器中,则不要使用–opt或-e选项。
三、mysql命令恢复数据
基本语法:
1 | mysql –u root –p [dbname] < backup.sql |
1、单库备份中恢复单库
使用root用户,将之前练习中备份的atguigu.sql文件中的备份导入数据库中,命令如下:
如果备份文件中包含了创建数据库的语句,则恢复的时候不需要指定数据库名称,如下所示
1 | mysql -uroot -p < atguigu.sql |
否则需要指定数据库名称,如下所示
1 | mysql -uroot -p atguigu4< atguigu.sql |
2、全量备份恢复
如果我们现在有昨天的全量备份,现在想整个恢复,则可以这样操作:
1 | mysql –u root –p < all.sql |
执行完后,MySQL数据库中就已经恢复了all.sql文件中的所有数据库。
3、从全量备份中恢复单库
可能有这样的需求,比如说我们只想恢复某一个库,但是我们有的是整个实例的备份,这个时候我们可以从全量备份中分离出单个库的备份。
举例:
1 | sed -n '/^-- Current Database: `atguigu`/,/^-- Current Database: `/p' all_database.sql > atguigu.sql |
4、从单库备份中恢复单表
这个需求还是比较常见的。比如说我们知道哪个表误操作了,那么就可以用单表恢复的方式来恢复。
举例:我们有atguigu整库的备份,但是由于class表误操作,需要单独恢复出这张表。
1 | cat atguigu.sql | sed -e '/./{H;$!d;}' -e 'x;/CREATE TABLE `class`/!d;q' > |
四、物理备份:直接复制整个数据库
说明:
在MySQL版本号中,第一个数字表示主版本号,主版本号相同的MySQL数据库文件格式相同。
此外,还可以考虑使用相关工具实现备份。比如, MySQLhotcopy
工具。
MySQLhotcopy是一个Perl脚本,它使用LOCK TABLES、FLUSH TABLES和cp或scp来快速备份数据库。它是备份数据库或单个表最快的途径,但它只能运行在数据库目录所在的机器上,并且只能备份MyISAM类型的表。多用于mysql5.5之前。
五、物理恢复:直接复制到数据库目录
步骤:
1)演示删除备份的数据库中指定表的数据
2)将备份的数据库数据拷贝到数据目录下,并重启MySQL服务器
3)查询相关表的数据是否恢复。需要使用下面的chown
操作
要求:
必须确保备份数据的数据库和待恢复的数据库服务器的主版本号相同。
因为只有MySQL数据库主版本号相同时,才能保证这两个MySQL数据库文件类型是相同的。这种方式对
MyISAM类型的表比较有效
,对于InnoDB类型的表则不可用。
因为InnoDB表的表空间不能直接复制。在Linux操作系统下,复制到数据库目录后,一定要将数据库的用户和组变成mysql,命令如下:
1
chown -R mysql.mysql /var/lib/mysql/dbname
其中,两个mysql分别表示组和用户;“-R”参数可以改变文件夹下的所有子文件的用户和组;“dbname”参数表示数据库目录。
提示 :
Linux操作系统下的权限设置非常严格。通常情况下,MySQL数据库只有root用户和mysql用户组下的mysql用户才可以访问,因此将数据库目录复制到指定文件夹后,一定要使用chown命令将文件夹的用户组变为mysql,将用户变为mysql。
六、表的导出与导入
1、表的导出
①使用SELECT…INTO OUTFILE导出文本文件
在MySQL中,可以使用SELECT…INTO OUTFILE语句将表的内容导出成一个文本文件。
举例:使用SELECT…INTO OUTFILE将atguigu数据库中account表中的记录导出到文本文件。
(1)选择数据库atguigu,并查询account表,执行结果如下所示。
1 | use atguigu; |
(2)mysql默认对导出的目录有权限限制,也就是说使用命令行进行导出的时候,需要指定目录进行操作。
查询secure_file_priv值:
1 | SHOW GLOBAL VARIABLES LIKE '%secure%'; |
(3)上面结果中显示,secure_file_priv变量的值为/var/lib/mysql-files/,导出目录设置为该目录,SQL语句如下。
1 | SELECT * FROM account INTO OUTFILE "/var/lib/mysql-files/account.txt"; |
(4)查看 /var/lib/mysql-files/account.txt`文件。
1 | 1 张三 90 |
②使用mysqldump命令导出文本文件
举例1:使用mysqldump命令将将atguigu数据库中account表中的记录导出到文本文件:
1 | mysqldump -uroot -p -T "/var/lib/mysql-files/" atguigu account |
mysqldump命令执行完毕后,在指定的目录/var/lib/mysql-files/下生成了account.sql和account.txt文件。
打开account.sql文件,其内容包含创建account表的CREATE语句。
1 | [root@node1 mysql-files]# cat account.sql |
打开account.txt文件,其内容只包含account表中的数据。
1 | [root@node1 mysql-files]# cat account.txt |
举例2:使用mysqldump将atguigu数据库中的account表导出到文本文件,使用FIELDS选项,要求字段之间使用逗号“,”间隔,所有字符类型字段值用双引号括起来:
1 | mysqldump -uroot -p -T "/var/lib/mysql-files/" atguigu account --fields-terminated-by=',' --fields-optionally-enclosed-by='\"' |
语句mysqldump语句执行成功之后,指定目录下会出现两个文件account.sql和account.txt。
打开account.sql文件,其内容包含创建account表的CREATE语句。
1 | [root@node1 mysql-files]# cat account.sql |
打开account.txt文件,其内容包含创建account表的数据。从文件中可以看出,字段之间用逗号隔开,字符类型的值被双引号括起来。
1 | [root@node1 mysql-files]# cat account.txt |
③使用mysql命令导出文本文件
举例1:使用mysql语句导出atguigu数据中account表中的记录到文本文件:
1 | mysql -uroot -p --execute="SELECT * FROM account;" atguigu> "/var/lib/mysql- |
打开account.txt文件,其内容包含创建account表的数据。
1 | [root@node1 mysql-files]# cat account.txt |
举例2:将atguigu数据库account表中的记录导出到文本文件,使用–veritcal参数将该条件记录分为多行显示:
1 | mysql -uroot -p --vertical --execute="SELECT * FROM account;" atguigu > "/var/lib/mysql-files/account_1.txt" |
打开account_1.txt文件,其内容包含创建account表的数据。
1 | [root@node1 mysql-files]# cat account_1.txt |
举例3:将atguigu数据库account表中的记录导出到xml文件,使用–xml参数,具体语句如下。
1 | mysql -uroot -p --xml --execute="SELECT * FROM account;" atguigu>"/var/lib/mysql- |
说明:如果要将表数据导出到html文件中,可以使用 --html
选项。
然后可以使用浏览器
打开。
2、表的导入
①使用LOAD DATA INFILE方式导入文本文件
举例1:
使用SELECT…INTO OUTFILE将atguigu数据库中account表的记录导出到文本文件
1 | SELECT * FROM atguigu.account INTO OUTFILE '/var/lib/mysql-files/account_0.txt'; |
删除account表中的数据:
1 | DELETE FROM atguigu.account; |
从文本文件account.txt中恢复数据:
1 | LOAD DATA INFILE '/var/lib/mysql-files/account_0.txt' INTO TABLE atguigu.account; |
查询account表中的数据:
1 | select * from account; |
举例2:
选择数据库atguigu,使用SELECT…INTO OUTFILE将atguigu数据库account表中的记录导出到文本文件,使用FIELDS选项和LINES选项,要求字段之间使用逗号”,”间隔,所有字段值用双引号括起来:
1 | SELECT * FROM atguigu.account INTO OUTFILE '/var/lib/mysql-files/account_1.txt' FIELDS TERMINATED BY ',' ENCLOSED BY '\"'; |
删除account表中的数据:
1 | DELETE FROM atguigu.account; |
从/var/lib/mysql-files/account.txt中导入数据到account表中:
1 | LOAD DATA INFILE '/var/lib/mysql-files/account_1.txt' INTO TABLE atguigu.account FIELDS TERMINATED BY ',' ENCLOSED BY '\"'; |
查询account表中的数据,具体SQL如下:
1 | select * from account; |
②使用mysqlimport方式导入文本文件
举例:
导出文件account.txt,字段之间使用逗号”,”间隔,字段值用双引号括起来:
1 | SELECT * FROM atguigu.account INTO OUTFILE '/var/lib/mysql-files/account.txt' FIELDS TERMINATED BY ',' ENCLOSED BY '\"'; |
删除account表中的数据:
1 | DELETE FROM atguigu.account; |
使用mysqlimport命令将account.txt文件内容导入到数据库atguigu的account表中:
1 | mysqlimport -uroot -p atguigu '/var/lib/mysql-files/account.txt' --fields-terminated-by=',' --fields-optionally-enclosed-by='\"' |
查询account表中的数据:
1 | select * from account; |
七、数据库迁移
1、概述
2、迁移方案
3、迁移注意点
相同版本的数据库之间迁移注意点:
指的是在主版本号相同的MySQL数据库之间进行数据库移动。
- 方式1: 因为迁移前后MySQL数据库的
主版本号相同
,所以可以通过复制数据库目录来实现数据库迁移,但是物理迁移方式只适用于MyISAM引擎的表。对于InnoDB表,不能用直接复制文件的方式备份数据库。 - 方式2: 最常见和最安全的方式是使用
mysqldump命令
导出数据,然后在目标数据库服务器中使用MySQL命令导入。
举例:
1 | host1的机器中备份所有数据库,并将数据库迁移到名为host2的机器上 |
在上述语句中,“|”
符号表示管道
,其作用是将mysqldump备份的文件给mysql命令;“–all-databases”表示要迁移所有的数据库。通过这种方式可以直接实现迁移。
不同版本的数据库之间迁移注意点:
例如,原来很多服务器使用5.7版本的MySQL数据库,在8.0版本推出来以后,改进了5.7版本的很多缺陷,因此需要把数据库升级到8.0版本旧版本与新版本的MySQL可能使用不同的默认字符集,例如有的旧版本中使用latin1作为默认字符集,而最新版本的MySQL默认字符集为utf8mb4。
如果数据库中有中文数据,那么迁移过程中需要对 默认字符集进行修改
,不然可能无法正常显示数据。
高版本
的MySQL数据库通常都会 兼容低版本
,因此可以从低版本的MySQL数据库迁移到高版本的MySQL数据库。
不同数据库之间迁移注意点:
4、迁移小结
八、删库了不敢跑,能干点啥?
1、delete:误删行
经验之谈:
2、truncate/drop :误删库/表
方案:
3、延迟复制备库
4、预防误删库/表的方法
5、rm:误删MySQL实例
对于一个有高可用机制的MySQL集群来说,不用担心 rm删除数据
了。
只是删掉了其中某一个节点的数据的话,HA系统就会开始工作,选出一个新的主库,从而保证整个集群的正常工作。
我们要做的就是在这个节点上把数据恢复回来,再接入整个集群。
九、MySQL常用命令
1、mysql
该mysql不是指mysql服务,而是指mysql的客户端工具。
语法 :
1 | mysql [options] [database] |
连接选项:
1 | 参数 : |
执行选项:
1 | -e, --execute=name 执行SQL语句并退出 |
此选项可以在Mysql客户端执行SQL语句,而不用连接到MySQL数据库再执行,对于一些批处理脚本,这种方式尤其方便。
1 | 示例: |
2、mysqladmin
mysqladmin 是一个执行管理操作的客户端程序。可以用它来检查服务器的配置和当前状态、创建并删除数据库等。
可以通过 : mysqladmin --help
指令查看帮助文档
1 | 示例 : |
3、mysqlbinlog
由于服务器生成的二进制日志文件以二进制格式保存,所以如果想要检查这些文本的文本格式,就会使用到mysqlbinlog 日志管理工具。
语法 :
1 | mysqlbinlog [options] log-files1 log-files2 ... |
4、mysqldump
mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移。备份内容包含创建表,及插入表的SQL语句。
语法 :
1 | mysqldump [options] db_name [tables] |
连接选项:
1 | #参数 : |
输出内容选项:
1 | 参数: |
5、mysqlimport/source
mysqlimport 是客户端数据导入工具,用来导入mysqldump 加 -T 参数后导出的文本文件。
语法:
1 | mysqlimport [options] db_name textfile1 [textfile2...] |
示例:
1 | mysqlimport -uroot -p test /tmp/city.txt |
如果需要导入sql文件,可以使用mysql中的source 指令 :
1 | source /root/tb_book.sql |
6、mysqlshow
mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引。
语法:
1 | mysqlshow [options] [db_name [table_name [col_name]]] |
参数:
1 | --count 显示数据库及表的统计信息(数据库,表 均可以不指定) |
示例:
1 | 查询每个数据库的表的数量及表中记录的数量 |