Mysql进阶

一、Linux下Mysql的安装和使用

1.1 安装前说明

1.1.1 查看是否安装过Mysql

检查mysql service:

systemctl status mysqld.service

如果是用rpm安装, 检查一下RPM PACKAGE:

rpm -qa | grep -i mysql 
# -i 忽略大小写

如果不存在旧版本包,显示如下:

1.1.2 Mysql的卸载

1. 关闭 mysql 服务

systemctl stop mysqld.service

2. 查看当前 mysql 安装状况

rpm -qa | grep -i mysql 
# 或
 yum list installed | grep mysql

3. 卸载上述命令查询出的已安装程序

yum remove mysql-xxx mysql-xxx mysql-xxx mysqk-xxxx

4. 删除 mysql 相关文件

find / -name mysql 
rm -rf xxx

5.删除 my.cnf

rm -rf /etc/my.cnf

1.2 Linux版安装

1.2.1 Mysql的四大版本

  • MySQL Community Server 社区版本,开源免费,自由下载,但不提供官方技术支持,适用于大多数普通用户。
  • MySQL Enterprise Edition 企业版本,需付费,不能在线下载,可以试用30天。提供了更多的功能和更完备的技术支持,更适合于对数据库的功能和可靠性要求较高的企业客户。
  • MySQL Cluster 集群版,开源免费。用于架设集群服务器,可将几个MySQL Server封装成一个Server。需要在社区版或企业版的基础上使用。
  • MySQL Cluster CGE 高级集群版,需付费。

1.2.2 下载指定版本

1、下载地址官网:MySQL

2、打开官网,点击DOWNLOAD

然后,点击 MySQL Community(GPL) Downloads

3、点击 MySQL Community Server

4、在General Availability(GA) Releases中选择适合的版本

如果安装Windows 系统下MySQL ,推荐下载 MSI安装程序,安装步骤详见上篇。

5、Linux系统下安装MySQL的几种方式

 

  • rpm命令使用rpm命令安装扩展名为".rpm"的软件包。.rpm包的一般格式:
  • yum命令需联网,从互联网获取 的yum源,直接使用yum命令安装。
  • 编译安装源码包针对 tar.gz 这样的压缩格式,要用tar命令来解压;如果是其它压缩格式,就使用其它命令。

这里不能直接选择CentOS 7系统的版本,所以选择与之对应的 Red Hat Enterprise Linux 直接点Download下载RPM Bundle全量包MySQL :: Download MySQL Community Server (Archived Versions)。包括了所有下面的组件。不需要一个一个下载了。

6、下载的tar包,用压缩工具打开解压后rpm安装包 (红框为抽取出来的安装包)

1.2.3 CentOS7下检查Mysql依赖

1. 检查/tmp临时目录权限(必不可少)

由于mysql安装过程中,会通过mysql用户在/tmp目录下新建tmp_db文件,所以请给/tmp较大的权限。执行 :

chmod -R 777 /tmp

2. 安装前,检查依赖

是否存在libaio包:

rpm -qa|grep libaio

是否存在net-tools包:

rpm -qa|grep net-tools

如果不存在需要到centos安装盘里进行rpm安装。

1.2.4 Mysql安装过程

1. 将安装程序拷贝到/opt目录下

在mysql的安装文件目录下执行:(必须按照顺序执行)

rpm -ivh mysql-community-common-8.0.25-1.el7.x86_64.rpm
rpm -ivh mysql-community-client-plugins-8.0.25-1.el7.x86_64.rpm
rpm -ivh mysql-community-libs-8.0.25-1.el7.x86_64.rpm
rpm -ivh mysql-community-client-8.0.25-1.el7.x86_64.rpm
rpm -ivh mysql-community-server-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 一起使用效果更好),展示进度条

2. 安装过程截图

3. 查看MySQL版本

执行如下命令,如果成功表示安装mysql成功。类似java -version如果打出版本等信息

mysql --version 
#或 
mysqladmin --version

执行如下命令,查看是否安装成功。需要增加 -i 不用去区分大小写,否则搜索不到。

rpm -qa|grep -i mysql

4. 服务的初始化

为了保证数据库目录与文件的所有者为 mysql 登录用户,如果你是以 root 身份运行 mysql 服务,需要执行下面的命令初始化:

mysqld --initialize --user=mysql

说明: --initialize 选项默认以“安全”模式来初始化,则会为 root 用户生成一个密码并将该密码标记为过期 ,登录后你需要设置一个新的密码。生成的 临时密码 会往日志中记录一份。查看密码:

cat /var/log/mysqld.log

5. 启动MySQL,查看状态

#加不加.service后缀都可以
启动:systemctl start mysqld.service
关闭:systemctl stop mysqld.service
重启:systemctl restart mysqld.service
查看状态:systemctl status mysqld.service

mysqld 这个可执行文件就代表着 MySQL 服务器程序,运行这个可执行文件就可以直接启动一个服务器进程。

ps -ef | grep -i mysql

6. 查看MySQL服务是否自启动

systemctl list-unit-files | grep mysqld.service
  • 如不是enabled可以运行如下命令设置自启动
systemctl enable mysqld.service
  • 如不希望进行自启动,运行如下命令设置
systemctl disable mysqld.service

1.3 Mysql登录

1.3.1 首次登录

通过 mysql -hlocalhost -P3306 -uroot -p 进行登录,在Enter password:录入初始化密码

1.3.2 修改密码

  • 因为初始化密码默认是过期的,所以查看数据库会报错
  • 修改密码:
ALTER USER 'root'@'localhost' IDENTIFIED BY 'new_password';
  • 5.7版本之后(不含5.7),mysql加入了全新的密码安全机制。设置新密码太简单会报错。

1.3.3 设置远程登录

1.3.3.1 开放端口

  • 查看开放的端口号
firewall-cmd --list-all
  • 设置开放的端口号
firewall-cmd --add-service=http --permanent 
firewall-cmd --add-port=3306/tcp --permanent
  • 重启防火墙
firewall-cmd --reload

1.3.3.2 修改配置

在Linux系统MySQL下测试:

use mysql;
select Host,User from user;

 可以看到root用户的当前主机配置信息为localhost。

修改Host为通配符%:

update user set host = '%' where user ='root';

Host修改完成后记得执行flush privileges使配置立即生效:

flush privileges;

1.4 字符集相关操作

1.4.1 修改Mysql5.7字符集

1.4.1.1. 修改步骤

在MySQL 8.0版本之前,默认字符集为 latin1 ,utf8字符集指向的是 utf8mb3 。网站开发人员在数据库设计的时候往往会将编码修改为utf8字符集。如果遗忘修改默认的编码,就会出现乱码的问题。从MySQL8.0开始,数据库的默认编码将改为 utf8mb4 ,从而避免上述乱码的问题。

1、查看默认使用的字符集

show variables like 'character%'; 
# 或者
 show variables like '%char%';

MySQL8.0中执行结果:

 MySQL5.7中执行结果:

 MySQL 5.7 默认的客户端和服务器都用了 latin1 ,不支持中文,保存中文会报错。

2、修改字符集

在MySQL5.7或之前的版本中,在文件最后加上中文字符集配置:

vim /etc/my.cnf character_set_server=utf8

3、重新启动MySQL服务

systemctl restart mysqld

但是原库、原表的设定不会发生变化,参数修改只对新建的数据库生效。

1.4.1.2. 已有库&表字符集的变更

MySQL5.7版本中,以前创建的库,创建的表字符集还是latin1。

修改已创建数据库的字符集

alter database dbtest1 character set 'utf8';

修改已创建数据表的字符集

alter table t_emp convert to character set 'utf8';

注意:但是原有的数据如果是用非'utf8'编码的话,数据本身编码不会发生改变。已有数据需要导出或删除,然后重新插入。

1.4.2 各级别的字符集

MySQL有4个级别的字符集和比较规则,分别是:

  • 服务器级别
  • 数据库级别
  • 表级别
  • 列级别

执行如下SQL语句:show variables like 'character%';

  • character_set_server:服务器级别的字符集c
  • haracter_set_database:当前数据库的字符集
  • character_set_client:服务器解码请求时使用的字符集
  • character_set_connection:服务器处理请求时会把请求字符串从character_set_client转为character_set_connection
  • character_set_results:服务器向客户端返回数据时使用的字符集

1. 服务器级别

character_set_server :服务器级别的字符集。

我们可以在启动服务器程序时通过启动选项或者在服务器程序运行过程中使用 SET 语句修改这两个变量的值。比如我们可以在配置文件中这样写:

[server]
character_set_server=gbk # 默认字符集
collation_server=gbk_chinese_ci #对应的默认的比较规则

当服务器启动的时候读取这个配置文件后这两个系统变量的值便修改了。

2. 数据库级别

character_set_database :当前数据库的字符集

我们在创建和修改数据库的时候可以指定该数据库的字符集和比较规则,具体语法如下:

CREATE DATABASE 数据库名
[[DEFAULT] CHARACTER SET 字符集名称]
[[DEFAULT] COLLATE 比较规则名称];

ALTER DATABASE 数据库名
[[DEFAULT] CHARACTER SET 字符集名称]
[[DEFAULT] COLLATE 比较规则名称];

3. 表级别

我们也可以在创建和修改表的时候指定表的字符集和比较规则,语法如下:

CREATE TABLE 表名 (列的信息)
[[DEFAULT] CHARACTER SET 字符集名称]
[COLLATE 比较规则名称]]

ALTER TABLE 表名
[[DEFAULT] CHARACTER SET 字符集名称]
[COLLATE 比较规则名称]

如果创建和修改表的语句中没有指明字符集和比较规则,将使用该表所在数据库的字符集和比较规则作为该表的字符集和比较规则。

4. 列级别

对于存储字符串的列,同一个表中的不同的列也可以有不同的字符集和比较规则。我们在创建和修改列定义的时候可以指定该列的字符集和比较规则,语法如下:

CREATE TABLE 表名(
列名 字符串类型 [CHARACTER SET 字符集名称] [COLLATE 比较规则名称],
其他列...
);

ALTER TABLE 表名 MODIFY 列名 字符串类型 [CHARACTER SET 字符集名称] [COLLATE 比较规则名称];

对于某个列来说,如果在创建和修改的语句中没有指明字符集和比较规则,将使用该列所在表的字符集和比较规则作为该列的字符集和比较规则。

提示:在转换列的字符集时需要注意,如果转换前列中存储的数据不能用转换后的字符集进行表示会发生错误。比方说原先列使用的字符集是utf8,列中存储了一些汉字,现在把列的字符集转换为ascii的话就会出错,因为ascii字符集并不能表示汉字字符。

提示:

# 这几种charset写法作用相同
create table my_test() character set utf8;
create table my_test() charset utf8;
create table my_test() charset=utf8;

5. 小结

我们介绍的这4个级别字符集和比较规则的联系如下:

  • 如果 创建或修改列 时没有显式的指定字符集和比较规则,则该列 默认用表的 字符集和比较规则
  • 如果 创建表时 没有显式的指定字符集和比较规则,则该表 默认用数据库的 字符集和比较规则如果
  • 创建数据库时 没有显式的指定字符集和比较规则,则该数据库 默认用服务器的 字符集和比较规则

知道了这些规则之后,对于给定的表,我们应该知道它的各个列的字符集和比较规则是什么,从而根据这个列的类型来确定存储数据时每个列的实际数据占用的存储空间大小了。比方说我们向表 t 中插入一条记录:

mysql> INSERT INTO t(col) VALUES('我们');
Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM t;
+--------+
| s |
+--------+
| 我们 |
+--------+
1 row in set (0.00 sec)

首先列 col 使用的字符集是 gbk ,一个字符 '我' 在 gbk 中的编码为 0xCED2 ,占用两个字节,两个字符的实际数据就占用4个字节。如果把该列的字符集修改为 utf8 的话,这两个字符就实际占用6个字节

1.4.3 字符集与比较规则

1. utf8 与 utf8mb4

utf8 字符集表示一个字符需要使用1~4个字节,但是我们常用的一些字符使用1~3个字节就可以表示了。而字符集表示一个字符所用的最大字节长度,在某些方面会影响系统的存储和性能,所以设计MySQL的设计者偷偷的定义了两个概念:

  • utf8mb3 :阉割过的 utf8 字符集,只使用1~3个字节表示字符。
  • utf8mb4 :正宗的 utf8 字符集,使用1~4个字节表示字符。

2. 比较规则

上表中,MySQL版本一共支持41种字符集,其中的 Default collation 列表示这种字符集中一种默认的比较规则,里面包含着该比较规则主要作用于哪种语言,比如 utf8_polish_ci 表示以波兰语的规则比较, utf8_spanish_ci 是以西班牙语的规则比较, utf8_general_ci 是一种通用的比较规则。

后缀表示该比较规则是否区分语言中的重音、大小写。具体如下:

最后一列 Maxlen ,它代表该种字符集表示一个字符最多需要几个字节。 

常用操作1:

#查看GBK字符集的比较规则
SHOW COLLATION LIKE 'gbk%';
#查看UTF-8字符集的比较规则
SHOW COLLATION LIKE 'utf8%';

常用操作2:

#查看服务器的字符集和比较规则
SHOW VARIABLES LIKE '%_server';
#查看数据库的字符集和比较规则
SHOW VARIABLES LIKE '%_database';
#查看具体数据库的字符集
SHOW CREATE DATABASE dbtest1;
#修改具体数据库的字符集
ALTER DATABASE dbtest1 DEFAULT CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

常用操作3:

#查看表的字符集
show create table employees;
#查看表的比较规则
show table status from atguigudb like 'employees';
#修改表的字符集和比较规则
ALTER TABLE emp1 DEFAULT CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

1.4.4请求到响应过程中字符集的变化

 为了体现出字符集在请求处理过程中的变化,我们这里特意修改一个系统变量的值:

mysql> set character_set_connection = gbk;
Query OK, 0 rows affected (0.00 sec)

现在假设我们客户端发送的请求是下边这个字符串:

SELECT * FROM t WHERE s = '我';

为了方便大家理解这个过程,我们只分析字符 '我' 在这个过程中字符集的转换。

现在看一下在请求从发送到结果返回过程中字符集的变化:

1、客户端发送请求所使用的字符集

一般情况下客户端所使用的字符集和当前操作系统一致,不同操作系统使用的字符集可能不一样,如下:

  • 类 Unix 系统使用的是 utf8
  • Windows 使用的是 gbk

当客户端使用的是 utf8 字符集,字符 '我' 在发送给服务器的请求中的字节形式就是:0xE68891

2、服务器接收到客户端发送来的请求其实是一串二进制的字节,它会认为这串字节采用的字符集是character_set_client ,然后把这串字节转换为 character_set_connection 字符集编码的字符。由于我的计算机上 character_set_client 的值是 utf8 ,首先会按照 utf8 字符集对字节串0xE68891 进行解码,得到的字符串就是 '我' ,然后按照 character_set_connection 代表的字符集,也就是 gbk 进行编码,得到的结果就是字节串 0xCED2 。

3、因为表 t 的列 col 采用的是 gbk 字符集,与 character_set_connection 一致,所以直接到列中找字节值为 0xCED2 的记录,最后找到了一条记录。(提示:如果某个列使用的字符集和character_set_connection代表的字符集不一致的话,还需要进行一次字符集转换。)

4、上一步骤找到的记录中的 col 列其实是一个字节串 0xCED2 , col 列是采用 gbk 进行编码的,所以首先会将这个字节串使用 gbk 进行解码,得到字符串 '我' ,然后再把这个字符串使用character_set_results 代表的字符集,也就是 utf8 进行编码,得到了新的字节串:0xE68891 ,然后发送给客户端。

5、由于客户端是用的字符集是 utf8 ,所以可以顺利的将 0xE68891 解释成字符 我 ,从而显示到我们的显示器上,所以我们人类也读懂了返回的结果。

总结图示如下:

 1.5 SQL大小写规范

1.5.1 Windows和Linux平台的区别

在 SQL 中,关键字和函数名是不用区分字母大小写的,比如 SELECT、WHERE、ORDER、GROUP BY 等关键字,以及 ABS、MOD、ROUND、MAX 等函数名。

不过在 SQL 中,还是要确定大小写的规范,因为在 Linux 和 Windows 环境下,可能会遇到不同的大小写问题。 windows系统默认大小写不敏感 ,但是 linux系统是大小写敏感的 。

通过如下命令查看:

SHOW VARIABLES LIKE '%lower_case_table_names%'

Windows系统下:

 Linux系统下:

 lower_case_table_names参数值的设置:默认为0,大小写敏感 。

  • 设置1,大小写不敏感。创建的表,数据库都是以小写形式存放在磁盘上,对于sql语句都是转换为小写对表和数据库进行查找。
  • 设置2,创建的表和数据库依据语句上格式存放,凡是查找都是转换为小写进行。

两个平台上SQL大小写的区别具体来说:

MySQL在Linux下数据库名、表名、列名、别名大小写规则是这样的:

  1. 数据库名、表名、表的别名、变量名是严格区分大小写的;
  2. 关键字、函数名称在 SQL 中不区分大小写;
  3. 列名(或字段名)与列的别名(或字段别名)在所有的情况下均是忽略大小写的
  4. MySQL在Windows的环境下全部不区分大小写
  5. 存储的内容不区分大小写,是因为比较规则默认是xxx_ci(大小写不敏感)

1.5.2 Linux下大小写规则设置

当想设置为大小写不敏感时,要在 my.cnf 这个配置文件 [mysqld] 中加入lower_case_table_names=1 ,然后重启服务器。

  • 但是要在重启数据库实例之前就需要将原来的数据库和表转换为小写,否则将找不到数据库名。
  • SHOW VARIABLES LIKE '%lower_case_table_names%'此参数适用于MySQL5.7。在MySQL 8下禁止在重新启动 MySQL 服务时将lower_case_table_names 设置成不同于初始化 MySQL 服务时设置的lower_case_table_names 值。如果非要将MySQL8设置为大小写不敏感,具体步骤为:
1、停止MySQL服务
2、删除数据目录,即删除 /var/lib/mysql 目录
3、在MySQL配置文件( /etc/my.cnf )中添加 lower_case_table_names=1
4、启动MySQL服务

1.5.3 SQL编写建议

如果你的变量名命名规范没有统一,就可能产生错误。这里有一个有关命名规范的建议:

  1. 关键字和函数名称全部大写;
  2. 数据库名、表名、表别名、字段名、字段别名等全部小写;
  3. SQL 语句必须以分号结尾。

数据库名、表名和字段名在 Linux MySQL 环境下是区分大小写的,因此建议你统一这些字段的命名规则,比如全部采用小写的方式。

虽然关键字和函数名称在 SQL 中不区分大小写,也就是如果小写的话同样可以执行。但是同时将关键词和函数名称全部大写,以便于区分数据库名、表名、字段名。

1.6 sql_mode的设置

1.6.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而报错。

1.6.2 宽松模式再举例

宽松模式举例1:

select * from employees group by department_id limit 10;

set sql_mode = ONLY_FULL_GROUP_BY;
select * from employees group by department_id limit 10; # 严格模式下此语句报错

宽松模式举例2:

insert into test1(id,age) values (1, 'aaa');  #宽松模式下此语句能插入成功,'aaa'转为0; 严格模式下此语句报错。

1.6.3 模式查看和设置

查看当前的sql_mode

select @@session.sql_mode
select @@global.sql_mode
#或者
show variables like 'sql_mode';

临时设置方式:设置当前窗口中设置sql_mode

SET GLOBAL sql_mode = 'modes...'; #全局
SET SESSION sql_mode = 'modes...'; #当前会话

举例:

#改为严格模式。此方法只在当前会话中生效,关闭当前会话就不生效了。
set SESSION sql_mode='STRICT_TRANS_TABLES';

#改为严格模式。此方法在当前服务中生效,重启MySQL服务后失效。
set GLOBAL sql_mode='STRICT_TRANS_TABLES';

永久设置方式:在/etc/my.cnf中配置sql_mode

在my.cnf文件(windows系统是my.ini文件),新增:

[mysqld]
sql_mode=ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR
_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

然后 重启MySQL 。当然生产环境上是禁止重启MySQL服务的,所以采用 临时设置方式 + 永久设置方式 来解决线上的问题,那么即便是有一天真的重启了MySQL服务,也会永久生效了。

二、Mysql的数据目录

2.1 Mysql8的主要目录结构

 2.1.1 数据库文件的存放路径

show variables like 'datadir';

 2.1.2 相关命令目录

 2.1.3 配置文件目录

2.2 数据库和文件系统的关系

2.2.1 查看默认数据库

查看一下在我的计算机上当前有哪些数据库:

mysql> SHOW DATABASES;

可以看到有4个数据库是属于MySQL自带的系统数据库。

  • mysql:它存储了MySQL的用户账户和权限信息,一些存储过程、事件的定义信息,一些运行过程中产生的日志信息,一些帮助信息以及时区信息等。
  • information_schema:保存着MySQL服务器 维护的所有其他数据库的信息 ,比如有哪些表、哪些视图、哪些触发器、哪些列、哪些索引。这些信息并不是真实的用户数据,而是一些描述性信息,有时候也称之为 元数据 。在系统数据库 information_schema 中提供了一些以innodb_sys 开头的表,用于表示内部系统表。
  • performance_schema:主要保存MySQL服务器运行过程中的一些状态信息,可以用来 监控 MySQL 服务的各类性能指标 。包括统计最近执行了哪些语句,在执行过程的每个阶段都花费了多长时间,内存的使用情况等信息。
  • sys:主要是通过 视图 的形式把 information_schema 和performance_schema 结合起来,帮助系统管理员和开发人员监控 MySQL 的技术性能。

2.2.2 数据库在文件系统中的表示

 这个数据目录下的文件和子目录比较多,除了 information_schema 这个系统数据库外,其他的数据库在 数据目录 下都有对应的子目录。

以我的 temp 数据库为例,在MySQL8.0 中打开:

2.2.3 表在文件系统种的表示

2.2.3.1 InnoDB存储引擎模式

1. 表结构

为了保存表结构, InnoDB 在 数据目录 下对应的数据库子目录下创建了一个专门用于 描述表结构的文件 ,文件名是这样:

表名.frm

比方说我们在 atguigu 数据库下创建一个名为 test 的表:那在数据库 atguigu 对应的子目录下就会创建一个名为 test.frm 的用于描述表结构的文件。.frm文件的格式在不同的平台上都是相同的。这个后缀名为.frm是以 二进制格式 存储的,我们直接打开是乱码的。

2. 表中数据和索引

系统表空间(system tablespace)

默认情况下,InnoDB会在数据目录下创建一个名为 ibdata1 、大小为 12M 的文件,这个文件就是对应的 系统表空间 在文件系统上的表示。

怎么才12M?注意这个文件是 自扩展文件 ,当不够用的时候它会自己增加文件大小。当然,如果想让系统表空间对应文件系统上多个实际文件,或者仅仅觉得原来的 ibdata1 这个文件名难听,那可以在MySQL启动时配置对应的文件路径以及它们的大小,比如这样修改一下my.cnf 配置文件:

[server]
innodb_data_file_path=data1:512M;data2:512M:autoextend

独立表空间

在MySQL5.6.6以及之后的版本中,InnoDB并不会默认的把各个表的数据存储到系统表空间中,而是为 每一个表建立一个独立表空间 ,也就是说我们创建了多少个表,就有多少个独立表空间。使用 独立表空间 来存储表数据的话,会在该表所属数据库对应的子目录下创建一个表示该独立表空间的文件,文件名和表名相同,只不过添加了一个 .ibd 的扩展名而已,所以完整的文件名称长这样:

表名.ibd

比如:我们使用了 独立表空间 去存储 atguigu 数据库下的 test 表的话,那么在该表所在数据库对应的 atguigu 目录下会为 test 表创建这两个文件:

test.frm 
test.ibd

其中 test.ibd 文件就用来存储 test 表中的数据和索引。

系统表空间与独立表空间的设置

我们可以自己指定使用 系统表空间 还是 独立表空间 来存储数据,这个功能由启动参数innodb_file_per_table 控制,比如说我们想刻意将表数据都存储到 系统表空间 时,可以在启动MySQL服务器的时候这样配置:

[server] innodb_file_per_table=0 # 0:代表使用系统表空间; 1:代表使用独立表空间

默认情况:

mysql> show variables like 'innodb_file_per_table';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| innodb_file_per_table | ON |
+-----------------------+-------+
1 row in set (0.01 sec)

其他类型的表空间

随着MySQL的发展,除了上述两种老牌表空间之外,现在还新提出了一些不同类型的表空间,比如通用表空间(general tablespace)、临时表空间(temporary tablespace)等。

2.2.3.2 MyISAM存储引擎模式

1. 表结构

在存储表结构方面, MyISAM 和 InnoDB 一样,也是在 数据目录 下对应的数据库子目录下创建了一个专门用于描述表结构的文件:

表名.frm

2. 表中数据和索引

在MyISAM中的索引全部都是二级索引 ,该存储引擎的 数据和索引是分开存放的。

所以在文件系统中也是使用不同的文件来存储数据文件和索引文件,同时表数据都存放在对应的数据库子目录下。假如 test表使用MyISAM存储引擎的话,那么在它所在数据库对应的 atguigu 目录下会为 test 表创建这三个文件:

test.frm 存储表结构
test.MYD 存储数据 (MYData)
test.MYI 存储索引 (MYIndex)

举例:创建一个 MyISAM 表,使用 ENGINE 选项显式指定引擎。 InnoDB 是默认引擎。

CREATE TABLE `student_myisam` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(64) DEFAULT NULL,
`age` int DEFAULT NULL,
`sex` varchar(2) DEFAULT NULL,
PRIMARY KEY (`id`)
)ENGINE=MYISAM AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb3;

2.2.4 小结

举例: 数据库a , 表b 。

如果表b采用 InnoDB ,data\a中会产生1个或者2个文件:

  • b.frm :描述表结构文件,字段长度等
    • MySQL5.7 中会生成此文件
    • MySQL8.0中不再单独提供b.frm,而是合并在b.ibd文件中。
  • b.ibd 文件:存储数据信息和索引信息
    • 独立表空间 模式,data\a中会产生此ibd文件
    • 系统表空间 模式,数据信息和索引信息都存储在 ibdata1 中,而不是单独的b.ibd文件

如果表b采用 MyISAM ,data\a中会产生3个文件:

  • b.frm / b_xxx.sdi :
    • MySQL5.7 中: b.frm描述表结构文件,字段长度等。
    • MySQL8.0 中 b_xxx.sdi描述表结构文件,字段长度等
  • b.MYD (MYData):数据信息文件,存储数据信息(如果采用独立表存储模式)
  • b.MYI (MYIndex):存放索引信息文件

此外:

  • MySQL5.7 中会在data/a的目录下生成 db.opt 文件用于保存数据库的相关配置。比如:字符集、比较规则。而MySQL8.0不再提供db.opt文件。

三、用户与权限管理

3.1 用户管理

3.1.1 登录Mysql服务器

启动MySQL服务后,可以通过mysql命令来登录MySQL服务器,命令如下:

mysql –h host –P port –u username –p DatabaseName –e "SQL语句"

下面详细介绍命令中的参数:

  • -h参数 后面接主机名或者主机IP,hostname为主机,hostIP为主机IP。
  • -P参数 后面接MySQL服务的端口,通过该参数连接到指定的端口。MySQL服务的默认端口是3306,不使用该参数时自动连接到3306端口,port为连接的端口号。
  • -u参数 后面接用户名,username为用户名。
  • -p参数 会提示输入密码。
  • DatabaseName参数 指明登录到哪一个数据库中。如果没有该参数,就会直接登录到MySQL数据库中,然后可以使用USE命令来选择数据库。
  • -e参数 后面可以直接加SQL语句。登录MySQL服务器以后即可执行这个SQL语句,然后退出MySQL服务器。

举例:

mysql -uroot -p -hlocalhost -P3306 mysql -e "select host,user from user"

3.1.2 创建用户

CREATE USER语句的基本语法形式如下:

CREATE USER 用户名 [IDENTIFIED BY '密码'][,用户名 [IDENTIFIED BY '密码']];
  • 用户名参数表示新建用户的账户,由用户(User) 和 主机名(Host) 构成;
  • “[ ]”表示可选,也就是说,可以指定用户登录时需要密码验证,也可以不指定密码验证,这样用户可以直接登录。不过,不指定密码的方式不安全,不推荐使用。如果指定密码值,这里需要使用IDENTIFIED BY指定明文密码值。
  • CREATE USER语句可以同时创建多个用户。

举例:

CREATE USER zhang3 IDENTIFIED BY '123123'; # 默认host是 %
CREATE USER 'kangshifu'@'localhost' IDENTIFIED BY '123456';

3.1.3 修改用户

修改用户名:

UPDATE mysql.user SET USER='li4' WHERE USER='wang5'; 
FLUSH PRIVILEGES;

3.1.4 删除用户

方式1:使用DROP方式删除(推荐)

使用DROP USER语句来删除用户时,必须用于DROP USER权限。DROP USER语句的基本语法形式如下:

DROP USER user[,user]…;

举例:

DROP USER li4 ; # 默认删除host为%的用户
DROP USER 'kangshifu'@'localhost';

方式2:使用DELETE方式删除

DELETE FROM mysql.user WHERE Host=’hostname’ AND User=’username’;
FLUSH PRIVILEGES;

举例:

DELETE FROM mysql.user WHERE Host='localhost' AND User='Emily';
FLUSH PRIVILEGES;

注意:不推荐通过 DELETE FROM USER u WHERE USER='li4' 进行删除,系统会有残留信息保留。而drop user命令会删除用户以及对应的权限,执行命令后你会发现mysql.user表和mysql.db表的相应记录都消失了。

3.1.5 设置当前用户密码

旧的写法如下 :

# 修改当前用户的密码:(MySQL5.7测试有效)
SET PASSWORD = PASSWORD('123456');

这里介绍 推荐的写法 :

1. 使用ALTER USER命令来修改当前用户密码

用户可以使用ALTER命令来修改自身密码,如下语句代表修改当前登录用户的密码。基本语法如下:

ALTER USER USER() IDENTIFIED BY 'new_password';

2. 使用SET语句来修改当前用户密码

可以使用SET语句来修改密码,具体SQL语句如下:

SET PASSWORD='new_password';

该语句会自动将密码加密后再赋给当前用户

3.1.6 修改其他用户密码

1. 使用ALTER语句来修改普通用户的密码

可以使用ALTER USER语句来修改普通用户的密码。基本语法形式如下:

ALTER USER user [IDENTIFIED BY '新密码'][,user[IDENTIFIED BY '新密码']]…;

2. 使用SET命令来修改普通用户的密码

使用root用户登录到MySQL服务器后,可以使用SET语句来修改普通用户的密码。SET语句的代码如下:

SET PASSWORD FOR 'username'@'hostname'='new_password';

3. 使用UPDATE语句修改普通用户的密码(不推荐)

UPDATE MySQL.user SET authentication_string=PASSWORD("123456") WHERE User = "username" AND Host = "hostname";

3.1.7 Mysql8密码管理

1. 密码过期策略

  • 在MySQL中,数据库管理员可以手动设置账号密码过期,也可以建立一个自动密码过期策略。
  • 过期策略可以是全局的 ,也可以为每个账 设置单独的过期策略。

练习:

ALTER USER 'kangshifu'@'localhost' PASSWORD EXPIRE;
  • 方式①:使用SQL语句更改该变量的值并持久化
SET PERSIST default_password_lifetime = 180; # 建立全局策略,设置密码每隔180天过期
  • 方式②:配置文件my.cnf中进行维护
[mysqld] default_password_lifetime=180 #建立全局策略,设置密码每隔180天过期

手动设置指定时间过期方式:单独设置

每个账号既可延用全局密码过期策略,也可单独设置策略。在 CREATE USER 和 ALTER USER 语句上加入 PASSWORD EXPIRE 选项可实现单独设置策略。下面是一些语句示例。

#设置kangshifu账号密码每90天过期:
CREATE USER 'kangshifu'@'localhost' PASSWORD EXPIRE INTERVAL 90 DAY;
ALTER USER 'kangshifu'@'localhost' PASSWORD EXPIRE INTERVAL 90 DAY;
#设置密码永不过期:
CREATE USER 'kangshifu'@'localhost' PASSWORD EXPIRE NEVER;
ALTER USER 'kangshifu'@'localhost' PASSWORD EXPIRE NEVER;
#延用全局密码过期策略:
CREATE USER 'kangshifu'@'localhost' PASSWORD EXPIRE DEFAULT;
ALTER USER 'kangshifu'@'localhost' PASSWORD EXPIRE DEFAULT;

2. 密码重用策略

  • 手动设置密码重用方式1:全局
    • 方式①:使用SQL
SET PERSIST password_history = 6; #设置不能选择最近使用过的6个密码
SET PERSIST password_reuse_interval = 365; #设置不能选择最近一年内的密码
    • 方式②:my.cnf配置文件
[mysqld] password_history=6 password_reuse_interval=365
  • 手动设置密码重用方式2:单独设置
#不能使用最近5个密码:
CREATE USER 'kangshifu'@'localhost' PASSWORD HISTORY 5;
ALTER USER 'kangshifu'@'localhost' PASSWORD HISTORY 5;

#不能使用最近365天内的密码:
CREATE USER 'kangshifu'@'localhost' PASSWORD REUSE INTERVAL 365 DAY;
ALTER USER 'kangshifu'@'localhost' PASSWORD REUSE INTERVAL 365 DAY;

#既不能使用最近5个密码,也不能使用365天内的密码
CREATE USER 'kangshifu'@'localhost'
PASSWORD HISTORY 5
PASSWORD REUSE INTERVAL 365 DAY;

ALTER USER 'kangshifu'@'localhost'
PASSWORD HISTORY 5
PASSWORD REUSE INTERVAL 365 DAY;

3.2 权限管理

3.2.1 权限列表

MySQL到底都有哪些权限呢?

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服务器上的任何文件(说明用户可以读任何数据库目录下的文件,因为服务器可以访问这些文件)。

3.2.2 授权权限的原则

权限控制主要是出于安全因素,因此需要遵循以下几个经验原则 :

  1. 只授予能满足需要的最小权限 ,防止用户干坏事。比如用户只是需要查询,那就只给select权限就可以了,不要给用户赋予update、insert或者delete权限。
  2. 创建用户的时候限制用户的登录主机 ,一般是限制成指定IP或者内网IP段。
  3. 为每个用户设置满足密码复杂度的密码 。
  4. 定期清理不需要的用户 ,回收权限或者删除用户。

3.2.3 授予权限

给用户授权的方式有 2 种,分别是通过把 角色赋予用户给用户授权 和 直接给用户授权 。用户是数据库的使用者,我们可以通过给用户授予访问数据库中资源的权限,来控制使用者对数据库的访问,消除安全隐患。

授权命令:

GRANT 权限1,权限2,…权限n ON 数据库名称.表名称 TO 用户名@用户地址 [IDENTIFIED BY ‘密码口令’];

该权限如果发现没有该用户,则会直接新建一个用户。

比如:

#给li4用户用本地命令行方式,授予atguigudb这个库下的所有表的插删改查的权限。
GRANT SELECT,INSERT,DELETE,UPDATE ON atguigudb.* TO li4@localhost ;

#授予通过网络方式登录的joe用户 ,对所有库所有表的全部权限,密码设为123。注意这里唯独不包括grant的权限
GRANT ALL PRIVILEGES ON *.* TO joe@'%' IDENTIFIED BY '123';

3.2.4 查看权限

查看当前用户权限

SHOW GRANTS;
# 或
SHOW GRANTS FOR CURRENT_USER;
# 或
SHOW GRANTS FOR CURRENT_USER();

查看某用户的全局权限

SHOW GRANTS FOR 'user'@'主机地址' ;

3.2.5 收回权限

收回权限就是取消已经赋予用户的某些权限。收回用户不必要的权限可以在一定程度上保证系统的安全性。

MySQL中使用 REVOKE语句 取消用户的某些权限。

使用REVOKE收回权限之后,用户账户的记录将从db、host、tables_priv和columns_priv表中删除,但是用户账户记录仍然在user表中保存(删除user表中的账户记录使用DROP USER语句)。

注意:在将用户账户从user表删除之前,应该收回相应用户的所有权限。

收回权限命令:

REVOKE 权限1,权限2,…权限n ON 数据库名称.表名称 FROM 用户名@用户地址;

举例:

#收回全库全表的所有权限
REVOKE ALL PRIVILEGES ON *.* FROM joe@'%';
#收回mysql库下的所有表的插删改查权限
REVOKE SELECT,INSERT,UPDATE,DELETE ON mysql.* FROM joe@localhost;

注意: 须用户重新登录后才能生效

3.3 权限表

3.3.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.1
      • localhost 本地方式通过命令行方式的连接 ,比如mysql -u xxx -p xxx 方式的连接。
    • user : 表示用户名,同一用户通过不同方式链接的权限是不一样的。
    • password : 密码
      • 所有密码串通过 password(明文字符串) 生成的密文字符串。MySQL 8.0 在用户管理方面增加了角色管理,默认的密码加密方式也做了调整,由之前的 SHA1 改为了SHA2 ,不可逆。同时加上 MySQL 5.7 的禁用用户和用户过期的功能,MySQL 在用户管理方面的功能和安全性都较之前版本大大的增强了。
      • mysql 5.7 及之后版本的密码保存到 authentication_string 字段中不再使用password 字段。

权限列

    • Grant_priv字段: 表示是否拥有GRANT权限
    • Shutdown_priv字段:表示是否拥有停止MySQL服务的权限
    • Super_priv字段:表示是否拥有超级权限
    • Execute_priv字段:表示是否拥有EXECUTE权限。拥有EXECUTE权限,可以执行存储过程和函数。
    • Select_priv , Insert_priv等:为该用户所拥有的权限。

安全列

安全列只有6个字段,其中两个是ssl相关的(ssl_type、ssl_cipher),用于 加密 ;两个是x509相关的(x509_issuer、x509_subject),用于 标识用户 ;另外两个Plugin字段用于 验证用户身份 的插件,该字段不能为空。如果该字段为空,服务器就使用内建授权验证机制验证用户身份。

资源控制列

资源控制列的字段用来 限制用户使用的资源 ,包含4个字段,分别为:

    • max_questions,用户每小时允许执行的查询操作次数;
    • max_updates,用户每小时允许执行的更新操作次数;
    • max_connections,用户每小时允许执行的连接操作次数;
    • max_user_connections,用户允许同时建立的连接次数。

3.3.2 db表

使用DESCRIBE查看db表的基本结构:

用户列

db表用户列有3个字段,分别是Host、User、Db。这3个字段分别表示主机名、用户名和数据库名。表示从某个主机连接某个用户对某个数据库的操作权限,这3个字段的组合构成了db表的主键。

权限列

Create_routine_priv和Alter_routine_priv这两个字段决定用户是否具有创建和修改存储过程的权限。

3.3.3 tables_priv表和columns_priv表

tables_priv表用来对表设置操作权限 ,columns_priv表用来对表的某一列设置权限 。tables_priv表和columns_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。

3.3.4 procs_priv表

procs_priv表可以对 存储过程和存储函数设置操作权限 ,表结构如图

3.4 访问控制

3.4.1 连接核实阶段

当用户试图连接MySQL服务器时,服务器基于用户的身份以及用户是否能提供正确的密码验证身份来确定接受或者拒绝连接。即客户端用户会在连接请求中提供用户名、主机地址、用户密码,MySQL服务器接收到用户请求后,会使用user表中的host、user和authentication_string这3个字段匹配客户端提供信息。

服务器只有在user表记录的Host和User字段匹配客户端主机名和用户名,并且提供正确的密码时才接受连接。如果连接核实没有通过,服务器就完全拒绝访问;否则,服务器接受连接,然后进入阶段2等待用户请求。

3.4.2 请求核实阶段

一旦建立了连接,服务器就进入了访问控制的阶段2,也就是请求核实阶段。对此连接上进来的每个请求,服务器检查该请求要执行什么操作、是否有足够的权限来执行它,这正是需要授权表中的权限列发挥作用的地方。这些权限可以来自user、db、table_priv和column_priv表。

确认权限时,MySQL首先 检查user表 ,如果指定的权限没有在user表中被授予,那么MySQL就会继续 检查db表 ,db表是下一安全层级,其中的权限限定于数据库层级,在该层级的SELECT权限允许用户查看指定数据库的所有表中的数据;如果在该层级没有找到限定的权限,则MySQL继续 检查tables_priv表 以及 columns_priv表 ,如果所有权限表都检查完毕,但还是没有找到允许的权限操作,MySQL将 返回错误信息 ,用户请求的操作不能执行,操作失败。

3.5 角色管理

3.5.1 角色的理解

引入角色的目的是 方便管理拥有相同权限的用户 。恰当的权限设定,可以确保数据的安全性,这是至关重要的。 

3.5.2 创建角色

创建角色使用 CREATE ROLE 语句,语法如下:

CREATE ROLE 'role_name'[@'host_name'] [,'role_name'[@'host_name']]...

角色名称的命名规则和用户名类似。如果 host_name省略,默认为% , role_name不可省略 ,不可为空。

练习:我们现在需要创建一个经理的角色,就可以用下面的代码:

CREATE ROLE 'manager'@'localhost';

3.5.3 给角色赋予权限

创建角色之后,默认这个角色是没有任何权限的,我们需要给角色授权。给角色授权的语法结构是:

GRANT privileges ON table_name TO 'role_name'[@'host_name'];

练习1:我们现在想给经理角色授予商品信息表、盘点表和应付账款表的只读权限,就可以用下面的代码来实现:

GRANT SELECT ON demo.settlement TO 'manager';
GRANT SELECT ON demo.goodsmaster TO 'manager';
GRANT SELECT ON demo.invcount TO 'manager';

3.5.4 查看角色权限

赋予角色权限之后,我们可以通过 SHOW GRANTS 语句,来查看权限是否创建成功了:

mysql> SHOW GRANTS FOR 'manager';
+-------------------------------------------------------+
| Grants for manager@% |
+-------------------------------------------------------+
| GRANT USAGE ON *.* TO `manager`@`%` |
| GRANT SELECT ON `demo`.`goodsmaster` TO `manager`@`%` |
| GRANT SELECT ON `demo`.`invcount` TO `manager`@`%` |
| GRANT SELECT ON `demo`.`settlement` TO `manager`@`%` |
+-------------------------------------------------------+

只要你创建了一个角色,系统就会自动给你一个“ USAGE ”权限,意思是 连接登录数据库的权限 。代码的最后三行代表了我们给角色“manager”赋予的权限,也就是对商品信息表、盘点表和应付账款表的只读权限。

结果显示,库管角色拥有商品信息表的只读权限和盘点表的增删改查权限。

3.5.5 回收角色权限

角色授权后,可以对角色的权限进行维护,对权限进行添加或撤销。添加权限使用GRANT语句,与角色授权相同。撤销角色或角色权限使用REVOKE语句。

修改了角色的权限,会影响拥有该角色的账户的权限。

撤销角色权限的SQL语法如下:

REVOKE privileges ON tablename FROM 'rolename';

练习1: (1)使用如下语句撤销school_write角色的权限。

#撤销school_write角色的权限。 REVOKE INSERT, UPDATE, DELETE ON school.* FROM 'school_write'; #撤销school_write角色的权限。 SHOW GRANTS FOR 'school_write';

3.5.6 删除角色

当我们需要对业务重新整合的时候,可能就需要对之前创建的角色进行清理,删除一些不会再使用的角色。删除角色的操作很简单,你只要掌握语法结构就行了。

DROP ROLE role [,role2]...

注意, 如果你删除了角色,那么用户也就失去了通过这个角色所获得的所有权限 。

练习:执行如下SQL删除角色school_read。

DROP ROLE 'school_read';

3.5.7 给用户赋予角色

角色创建并授权后,要赋给用户并处于 激活状态 才能发挥作用。给用户添加角色可使用GRANT语句,语法形式如下:

GRANT role [,role2,...] TO user [,user2,...];

在上述语句中,role代表角色,user代表用户。可将多个角色同时赋予多个用户,用逗号隔开即可。

练习:给kangshifu用户添加角色school_read权限。

#(1)使用GRANT语句给kangshifu添加school_read权限
GRANT 'school_read' TO 'kangshifu'@'localhost';

#(2)添加完成后使用SHOW语句查看是否添加成功,
SHOW GRANTS FOR 'kangshifu'@'localhost';

#(3)使用kangshifu用户登录,然后查询当前角色,如果角色未激活,结果将显示NONE。
SELECT CURRENT_ROLE();
mysql> select current_role();
+----------------+
| current_role() |
+----------------+
| NONE           |
+----------------+

3.5.8 激活角色

方式1:使用set default role 命令激活角色

SET DEFAULT ROLE ALL TO 'kangshifu'@'localhost';

#使用 SET DEFAULT ROLE 为下面4个用户默认激活所有已拥有的角色如下
SET DEFAULT ROLE ALL TO
'dev1'@'localhost',
'read_user1'@'localhost',
'read_user2'@'localhost',
'rw_user1'@'localhost';

方式2:将activate_all_roles_on_login设置为ON

mysql> show variables like 'activate_all_roles_on_login';
+-----------------------------+-------+
| Variable_name | Value |
+-----------------------------+-------+
| activate_all_roles_on_login | OFF |
+-----------------------------+-------+
1 row in set (0.00 sec)

设置:

SET GLOBAL activate_all_roles_on_login=ON;

这条 SQL 语句的意思是,对 所有角色永久激活 。运行这条语句之后,用户才真正拥有了赋予角色的所有权限。

3.5.9 撤销用户角色

撤销用户角色的SQL语法如下:

REVOKE role FROM user;

练习:

#撤销kangshifu用户的school_read角色。
REVOKE 'school_read' FROM 'kangshifu'@'localhost';

# 撤销后,执行如下查询语句,查看kangshifu用户的角色信息
SHOW GRANTS FOR 'kangshifu'@'localhost';
#执行发现,用户kangshifu之前的school_read角色已被撤销。

3.5.10 设置强制角色

方式1:服务启动前设置

[mysqld] mandatory_roles='role1,role2@localhost,r3@%.atguigu.com'

方式2:运行时设置

SET PERSIST mandatory_roles = 'role1,role2@localhost,r3@%.example.com'; #系统重启后仍然有效
SET GLOBAL mandatory_roles = 'role1,role2@localhost,r3@%.example.com'; #系统重启后失效

四、逻辑架构

4.1 逻辑架构剖析

4.1.1 服务器处理客户端请求

以查询为例展示:

 

4.1.2 连接层

系统(客户端)访问 MySQL 服务器前,做的第一件事就是建立 TCP 连接。

经过三次握手建立连接成功后, MySQL 服务器对 TCP 传输过来的账号密码做身份认证、权限获取。

  • 用户名或密码不对,会收到一个Access denied for user错误,客户端程序结束执行
  • 用户名密码认证通过,会从权限表查出账号拥有的权限与连接关联,之后的权限判断逻辑,都将依赖于此时读到的权限

TCP 连接收到请求后,必须要分配给一个线程专门与这个客户端的交互。所以还会有个线程池,去走后面的流程。每一个连接从线程池中获取线程,省去了创建和销毁线程的开销。

4.1.3 服务层

  • SQL Interface: SQL接口
    • 接收用户的SQL命令,并且返回用户需要查询的结果。比如SELECT ... FROM就是调用SQLInterface
    • MySQL支持DML(数据操作语言)、DDL(数据定义语言)、存储过程、视图、触发器、自定义函数等多种SQL语言接口
  • Parser: 解析器
    • 在解析器中对 SQL 语句进行语法分析、语义分析。将SQL语句分解成数据结构,并将这个结构传递到后续步骤,以后SQL语句的传递和处理就是基于这个结构的。如果在分解构成中遇到错误,那么就说明这个SQL语句是不合理的。
    • 在SQL命令传递到解析器的时候会被解析器验证和解析,并为其创建 语法树 ,并根据数据字典丰富查询语法树,会 验证该客户端是否具有执行该查询的权限 。创建好语法树后,MySQL还会对SQl查询进行语法上的优化,进行查询重写。
  • Optimizer: 查询优化器
    • SQL语句在语法解析之后、查询之前会使用查询优化器确定 SQL 语句的执行路径,生成一个执行计划 。
    • 这个执行计划表明应该 使用哪些索引 进行查询(全表检索还是使用索引检索),表之间的连接顺序如何,最后会按照执行计划中的步骤调用存储引擎提供的方法来真正的执行查询,并将查询结果返回给用户。
    • 它使用“ 选取-投影-连接 ”策略进行查询。例如:
SELECT id,name FROM student WHERE gender = '女';

这个SELECT查询先根据WHERE语句进行 选取 ,而不是将表全部查询出来以后再进行gender过滤。 这个SELECT查询先根据id和name进行属性 投影 ,而不是将属性全部取出以后再进行过滤,将这两个查询条件 连接 起来生成最终查询结果。

  • Caches & Buffers: 查询缓存组件
    • MySQL内部维持着一些Cache和Buffer,比如Query Cache用来缓存一条SELECT语句的执行结果,如果能够在其中找到对应的查询结果,那么就不必再进行查询解析、优化和执行的整个过程了,直接将结果反馈给客户端。
    • 这个缓存机制是由一系列小缓存组成的。比如表缓存,记录缓存,key缓存,权限缓存等 
    • 这个查询缓存可以在 不同客户端之间共享 。
    • 从MySQL 5.7.20开始,不推荐使用查询缓存,并在 MySQL 8.0中删除 。

4.1.4 引擎层

插件式存储引擎层( Storage Engines),真正的负责了MySQL中数据的存储和提取,对物理服务器级别维护的底层数据执行操作,服务器通过API与存储引擎进行通信。

不同的存储引擎具有的功能不同,这样我们可以根据自己的实际需要进行选取。MySQL 8.0.25默认支持的存储引擎如下:

4.1.5 存储层

所有的数据,数据库、表的定义,表的每一行的内容,索引,都是存在文件系统 上,以文件的方式存在的,并完成与存储引擎的交互。

当然有些存储引擎比如InnoDB,也支持不使用文件系统直接管理裸设备,但现代文件系统的实现使得这样做没有必要了。在文件系统之下,可以使用本地磁盘,可以使用DAS、NAS、SAN等各种存储系统。

4.1.6 小结

MySQL架构图本节开篇所示。下面为了熟悉SQL执行流程方便,我们可以简化如下:

 简化为三层结构:

1. 连接层:客户端和服务器端建立连接,客户端发送 SQL 至服务器端;

2. SQL 层(服务层):对 SQL 语句进行查询处理;与数据库文件的存储方式无关;

3. 存储引擎层:与数据库文件打交道,负责数据的存储和读取。

4.2 SQL执行流程

4.2.1 SQL执行流程

 MySQL的查询流程:

1. 查询缓存:Server 如果在查询缓存中发现了这条 SQL 语句,就会直接将结果返回给客户端;如果没有,就进入到解析器阶段。需要说明的是,因为查询缓存往往效率不高,所以在 MySQL8.0 之后就抛弃了这个功能。

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 、 ALTERTABLE 、 DROP TABLE 或 DROP DATABASE 语句,那使用该表的所有高速缓存查询都将变为无效并从高速缓存中删除!对于更新压力大的数据库 来说,查询缓存的命中率会非常低。

2. 解析器:在解析器中对 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语句正确,则会生成一个这样的语法树

 3. 优化器:在优化器中会确定 SQL 语句的执行路径,比如是根据全表检索 ,还是根据索引检索 等。

举例:如下语句是执行两个表的 join:

select * from test1 join test2 using(ID) where test1.name='zhangwei' and test2.name='mysql高级课程';
方案1:可以先从表 test1 里面取出 name='zhangwei'的记录的 ID 值,再根据 ID 值关联到表 test2,再判断 test2 里面 name的值是否等于 'mysql高级课程'。

方案2:可以先从表 test2 里面取出 name='mysql高级课程' 的记录的 ID 值,再根据 ID 值关联到 test1, 再判断 test1 里面 name的值是否等于 zhangwei。 这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。优化器阶段完成后,这个语句的执行方案就确定下来了,然后进入执行器阶段。

在查询优化器中,可以分为逻辑查询优化阶段和物理查询优化阶段。

4. 执行器:截止到现在,还没有真正去读写真实的表,仅仅只是产出了一个执行计划。于是就进入了 执行器阶段 。

在执行之前需要判断该用户是否具备权限 。如果没有,就会返回权限错误。如果具备权限,就执行SQL查询并返回结果。

在 MySQL8.0 以下的版本,如果设置了查询缓存,这时会将查询结果进行缓存。

select * from test where id=1;

比如:表 test 中,ID 字段没有索引,那么执行器的执行流程是这样的:

调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是1,如果不是则跳过,如果是则将这行存在结果集中; 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。 执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。

至此,这个语句就执行完成了。对于有索引的表,执行的逻辑也差不多。

SQL 语句在 MySQL 中的流程是: SQL语句→查询缓存→解析器→优化器→执行器 。

4.2.2 Mysql8的SQL执行原理

1. 确认profiling 是否开启

mysql> select @@profiling;
mysql> show variables like 'profiling';

profiling=0 代表关闭,我们需要把 profiling 打开,即设置为 1:

mysql> set profiling=1;

2. 多次执行相同SQL查询

然后我们执行一个 SQL 查询(你可以执行任何一个 SQL 查询):

mysql> select * from employees;

3. 查看profiles

查看当前会话所产生的所有 profiles:

mysql> show profiles; # 显示最近的几次查询

4. 查看profile

显示执行计划,查看程序的执行步骤:

mysql> show profile;

 当然你也可以查询指定的 Query ID,比如:

mysql> show profile for query 7;
mysql> show profile cpu,block io for query 6;

4.2.3 Mysql5的SQL执行原理

上述操作在MySQL5.7中测试,发现前后两次相同的sql语句,执行的查询过程仍然是相同的。不是会使用缓存吗?这里我们需要显式开启查询缓存模式 。在MySQL5.7中如下设置:

1. 配置文件中开启查询缓存

在 /etc/my.cnf 中新增一行:

query_cache_type=1

2. 重启mysql服务

systemctl restart mysqld

3. 开启查询执行计划

由于重启过服务,需要重新执行如下指令,开启profiling。

mysql> set profiling=1;

4. 执行语句两次:

mysql> select * from locations; 
mysql> select * from locations;

5. 查看profiles

6. 查看profile

显示执行计划,查看程序的执行步骤:

mysql> show profile for query 1;

mysql> show profile for query 2;

 结论不言而喻。执行编号2时,比执行编号1时少了很多信息,从截图中可以看出查询语句直接从缓存中获取数据。

4.2.4 SQL语法顺序

随着Mysql版本的更新换代,其优化器也在不断的升级,优化器会分析不同执行顺序产生的性能消耗不同而动态调整执行顺序。

需求:查询每个部门年龄高于20岁的人数且高于20岁人数不能少于2人,显示人数最多的第一名部门信息下面是经常出现的查询顺序:

 4.3 数据库缓冲池

InnoDB 存储引擎是以页为单位来管理存储空间的,我们进行的增删改查操作其实本质上都是在访问页面(包括读页面、写页面、创建新页面等操作)。

而磁盘 I/O 需要消耗的时间很多,而在内存中进行操作,效率则会高很多,为了能让数据表或者索引中的数据随时被我们所用,DBMS 会申请占用内存来作为数据缓冲池 ,在真正访问页面之前,需要把在磁盘上的页缓存到内存中的 Buffer Pool 之后才可以访问。

这样做的好处是可以让磁盘活动最小化,从而 减少与磁盘直接进行 I/O 的时间 。要知道,这种策略对提升 SQL 语句的查询性能来说至关重要。如果索引的数据在缓冲池里,那么访问的成本就会降低很多。

4.3.1 缓冲池VS查询缓存

缓冲池和查询缓存是一个东西吗?不是。

1. 缓冲池(Buffer Pool)

首先我们需要了解在 InnoDB 存储引擎中,缓冲池都包括了哪些。

在 InnoDB 存储引擎中有一部分数据会放到内存中,缓冲池则占了这部分内存的大部分,它用来存储各种数据的缓存,如下图所示:

 从图中,能看到 InnoDB 缓冲池包括了数据页、索引页、插入缓冲、锁信息、自适应 Hash 和数据字典信息等。

缓存原则:

“ 位置 * 频次 ”这个原则,可以帮我们对 I/O 访问效率进行优化。

首先,位置决定效率,提供缓冲池就是为了在内存中可以直接访问数据。

其次,频次决定优先级顺序。因为缓冲池的大小是有限的,比如磁盘有 200G,但是内存只有 16G,缓冲池大小只有 1G,就无法将所有数据都加载到缓冲池里,这时就涉及到优先级顺序,会 优先对使用频次高的热数据进行加载 。

2. 查询缓存

那么什么是查询缓存呢?

查询缓存是提前把查询结果缓存 起来,这样下次不需要执行就可以直接拿到结果。

需要说明的是,在MySQL 中的查询缓存,不是缓存查询计划,而是查询对应的结果。

因为命中条件苛刻,而且只要数据表发生变化,查询缓存就会失效,因此命中率低。

4.3.2 缓冲池如何读取数据

缓冲池管理器会尽量将经常使用的数据保存起来,在数据库进行页面读操作的时候,首先会判断该页面是否在缓冲池中,如果存在就直接读取,如果不存在,就会通过内存或磁盘将页面存放到缓冲池中再进行读取。

缓存在数据库中的结构和作用如下图所示:

 4.3.3 查看设置缓冲池的大小

如果你使用的是 InnoDB 存储引擎,可以通过查看 innodb_buffer_pool_size 变量来查看缓冲池的大小。命令如下:

show variables like 'innodb_buffer_pool_size';

 能看到此时 InnoDB 的缓冲池大小只有 134217728/1024/1024=128MB。我们可以修改缓冲池大小,比如改为256MB,方法如下:

set global innodb_buffer_pool_size = 268435456;

或者修改配置文件永久生效:

[server] innodb_buffer_pool_size = 268435456

然后再来看下修改后的缓冲池大小,此时已成功修改成了 256 MB:

4.3.4 多个buffer pool实例

[server] innodb_buffer_pool_instances = 2

这样就表明我们要创建2个 Buffer Pool 实例。我们看下如何查看缓冲池的个数,使用命令:

show variables like 'innodb_buffer_pool_instances';

 那每个 Buffer Pool 实例实际占多少内存空间呢?其实使用这个公式算出来的:

innodb_buffer_pool_size/innodb_buffer_pool_instances

也就是总共的大小除以实例的个数,结果就是每个 Buffer Pool 实例占用的大小。

4.3.5 引申问题

Buffer Pool是MySQL内存结构中十分核心的一个组成,你可以先把它想象成一个黑盒子。

黑盒下的更新数据流程

 更新到一半突然发生错误了,想要回滚到更新之前的版本,该怎么办?连数据持久化的保证、事务回滚都做不到还谈什么崩溃恢复?

答案:Redo Log & Undo Log

五、存储引擎

5.1 查看存储引擎

查看mysql提供什么存储引擎:

show engines;

 5.2 设置系统默认的存储引擎

查看默认的存储引擎:

show variables like '%storage_engine%';
#或 
SELECT @@default_storage_engine;

 修改默认的存储引擎

如果在创建表的语句中没有显式指定表的存储引擎的话,那就会默认使用 InnoDB 作为表的存储引擎。

如果我们想改变表的默认存储引擎的话,可以这样写启动服务器的命令行:

SET DEFAULT_STORAGE_ENGINE=MyISAM;

或者修改 my.cnf 文件:

default-storage-engine=MyISAM

# 重启服务
systemctl restart mysqld.service

5.3 设置表的存储引擎

存储引擎是负责对表中的数据进行提取和写入工作的,我们可以为不同的表设置不同的存储引擎 ,也就是说不同的表可以有不同的物理存储结构,不同的提取和写入方式。

5.3.1 创建表时指定存储引擎

我们之前创建表的语句都没有指定表的存储引擎,那就会使用默认的存储引擎 InnoDB 。如果我们想显式的指定一下表的存储引擎,那可以这么写:

CREATE TABLE 表名( 建表语句; ) ENGINE = 存储引擎名称;

5.3.2 修改表的存储引擎

如果表已经建好了,我们也可以使用下边这个语句来修改表的存储引擎:

ALTER TABLE 表名 ENGINE = 存储引擎名称;

5.4 引擎介绍

  • InnoDB 引擎:具备外键支持功能的事务存储引擎
  • MyISAM 引擎:主要的非事务处理存储引擎
  • Archive 引擎:用于数据存档
  • Blackhole 引擎:丢弃写操作,读操作会返回空内容
  • CSV 引擎:存储数据时,以逗号分隔各个数据项
  • Memory 引擎:置于内存的表
  • Federated 引擎:访问远程表
  • Merge引擎:管理多个MyISAM表构成的表集合
  • NDB引擎:MySQL集群专用存储引擎

5.5 MyISAM和InnoDB

MySQL5.5之前的默认存储引擎是MyISAM,5.5之后改为了InnoDB。

对比项

MyISAM

InnoDB

外键

不支持

支持

事务

不支持

支持

行表锁

表锁,即使操作一条记录也会锁住整个表,不适合高并发的操作

行锁,操作时只锁某一行,不对其它行有影响,适合高并发的操作

缓存

只缓存索引,不缓存真实数据

不仅缓存索引还要缓存真实数据,对内存要求较高,而且内存大小对性能有决定性的影响

自带系统表使用

Y

N

关注点

性能:节省资源、消耗少、简单业务

事务:并发写、事务、更大资源

默认安装

Y

Y

默认使用

N

Y

5.6 InnoDB架构

1. 缓冲池 缓冲池是主内存中的一部分空间,用来缓存已使用的表和索引数据。缓冲池使得经常被使用的数据能够直接在内存中获得,从而提高速度。

2. 更改缓存 更改缓存是一个特殊的数据结构,当受影响的索引页不在缓存中时,更改缓存会缓存辅助索引页的更改。索引页被其他读取操作时会加载到缓存池,缓存的更改内容就会被合并。不同于集群索引,辅助索引并非独一无二的。当系统大部分闲置时,清除操作会定期运行,将更新的索引页刷入磁盘。更新缓存合并期间,可能会大大降低查询的性能。在内存中,更新缓存占用一部分InnoDB缓冲池。在磁盘中,更新缓存是系统表空间的一部分。更新缓存的数据类型由innodb_change_buffering配置项管理。

3. 自适应哈希索引 自适应哈希索引将负载和足够的内存结合起来,使得InnoDB像内存数据库一样运行,不需要降低事务上的性能或可靠性。这个特性通过innodb_adaptive_hash_index选项配置,或者通过--skip-innodb_adaptive_hash_index命令行在服务启动时关闭。

4. 重做日志缓存 重做日志缓存存放要放入重做日志的数据。重做日志缓存大小通过innodb_log_buffer_size配置项配置。重做日志缓存会定期地将日志文件刷入磁盘。大型的重做日志缓存使得大型事务能够正常运行而不需要写入磁盘。

5. 系统表空间 系统表空间包括InnoDB数据字典、双写缓存、更新缓存和撤销日志,同时也包括表和索引数据。多表共享,系统表空间被视为共享表空间。

6. 双写缓存 双写缓存位于系统表空间中,用于写入从缓存池刷新的数据页。只有在刷新并写入双写缓存后,InnoDB才会将数据页写入合适的位置。

7. 撤销日志 撤销日志是一系列与事务相关的撤销记录的集合,包含如何撤销事务最近的更改。如果其他事务要查询原始数据,可以从撤销日志记录中追溯未更改的数据。撤销日志存在于撤销日志片段中,这些片段包含于回滚片段中。

8. 每个表一个文件的表空间 每个表一个文件的表空间是指每个单独的表空间创建在自身的数据文件中,而不是系统表空间中。这个功能通过innodb_file_per_table配置项开启。每个表空间由一个单独的.ibd数据文件代表,该文件默认被创建在数据库目录中。

9. 通用表空间 使用CREATE TABLESPACE语法创建共享的InnoDB表空间。通用表空间可以创建在MySQL数据目录之外能够管理多个表并支持所有行格式的表。

10. 撤销表空间 撤销表空间由一个或多个包含撤销日志的文件组成。撤销表空间的数量由innodb_undo_tablespaces配置项配置。

11. 临时表空间 用户创建的临时表空间和基于磁盘的内部临时表都创建于临时表空间。innodb_temp_data_file_path配置项定义了相关的路径、名称、大小和属性。如果该值为空,默认会在innodb_data_home_dir变量指定的目录下创建一个自动扩展的数据文件。

12. 重做日志 重做日志是基于磁盘的数据结构,在崩溃恢复期间使用,用来纠正数据。正常操作期间,重做日志会将请求数据进行编码,这些请求会改变InnoDB表数据。遇到意外崩溃后,未完成的更改会自动在初始化期间重新进行。

六、索引的数据结构

6.1 为什么使用索引

减少全量读取,以更少的查询次数找到目标数据。

6.2 索引及其优缺点

6.2.1 索引概述

MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。

索引的本质:索引是数据结构。可以简单理解为排好序的快速查找数据结构,满足特定查找算法。

这些数据结构以某种方式指向数据,这样就可以在这些数据结构的基础上实现高级查找算法。

6.2.2 优点

  1. 类似大学图书馆建书目索引,提高数据检索的效率,降低数据库的IO成本,这是创建索引最主要的原因
  2. 通过创建唯一索引,可以保证数据库中每一行数据的唯一性。
  3. 在实现数据参考完整性方面,可以加速表和表之间的连接。换句话说,对于有依赖关系的子表和父表联合查询时,可以提高查询速度。
  4. 在使用分组和排序字句进行数据查询时,可以显著减少查询中分组和排序的时间,降低CPU的消耗。

6.2.3 缺点

  1. 创建索引和维护索引都要耗费时间,并且随着数据量的增加,所耗费的时间也会增加。
  2. 索引需要占磁盘空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间。存储在磁盘上,如果有大量的索引,索引文件就可能比数据文件更快达到最大文件尺寸。

6.3 InnoDB中索引的推演

6.3.1 索引之前的查找

举个例子:

SELECT [列名列表] FROM 表名 WHERE 列名 = xxx;

数据库是以页为基本空间存储数据的,根据某些条件进行分页。如果确定数据在某个页中,则遍历页中每行数据进行查找。

如果没有办法快速定位到某一页时,就需要从第一个页沿着双向链表一直往下找,在每一个页中遍历查找,

6.3.2 设计索引

建一个表

mysql> CREATE TABLE index_demo(
-> c1 INT,
-> c2 INT,
-> c3 CHAR(1),
-> PRIMARY KEY(c1)
-> ) ROW_FORMAT = Compact;

表中有两个INT类型的列,一个CHAR类型的列,而且我们规定了c1列为主键,这个表使用Compact行格式来实际存储记录。

简化index_demo表的行格式示意图:

 在示意图里记录的几个部分:

  • record_type:记录头信息的一项属性,表示记录的类型,0表示普通记录,2表示最小记录,3表示最大记录,1表示目录项
  • next_record:记录头信息的一项属性,表示下一条地址相对于本条记录的地址偏移量。
  • 各个列的值:记录每个字段的值。
  • 其他信息:其他隐藏列的值以及记录的额外信息。

把信息竖起来,并将多条记录放到页里的示意图就是:

6.3.2.1 简单的索引设计方案

因为各个页中的记录没有规律,所以根据某个条件查询记录时要遍历所有的数据页。

如果我们想要快速的定位到需要查找的记录在哪些数据页该怎么办?苦役快速定位记录所在的数据页而建立一个目录,建这个目录必须完成下面这些事:

  • 下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值
  • 给所有的页建立一个目录项

应该就像这样:

 以页28为例,对应目录项2,这个目录项中包含着该页的页号28以及页中最小主键值5.

只需把几个目录项在物理存储器上连续存储,就可以实现根据主键值快速查找某条记录的功能了。

比如:查找主键为20的记录,具体查找过程分为两步:

  1. 先从目录项中根据二分法快速确定出主键值为20的记录在目录项3中。
  2. 再根据前边说的在页中查找记录的方式去页9中定位具体的记录。

6.3.2.2 InnoDB中的索引方案

1、迭代1次:目录项记录的页

把前面按使用到的目录项放到数据页中的样子是这样:

从图中可以看出来,我们分配了一个编号为30的页专门存储目录项记录。这里再次强调目录项记录和普通用户记录。

不同点:

  • 目录项记录的record_type值是1,而普通用户记录的record_type值是0
  • 目录项记录只有主键值和页的编号两个列,而普通的用户记录的列是自己定义的,可能包含很多列,以及InnoDB的隐藏列。

相同点:两者用的都是一样的数据页,都会为主键值页目录,从而在按照主键值进行查找时可以使用二分法来加快查询速度。

现在以查找主键为20的记录为例,根据某个主键值去查找记录的步骤就可以大致分为下面两步:

  1. 先到存储目录项记录页通过二分法快速定位到对应目录项,因为12< 20 < 209,所以定位到对应的记录所在的页就是页9.
  2. 再到存储用户记录的页9中根据二分法快速定位到主键值为20 的用户记录。

2、迭代2次:多个目录项记录的页

 从图中可以看出,我们插了一条主键值为320的用户记录之后需要两个新的数据页:

  • 为存储该用户记录而新生成了页31
  • 存储目录项记录的页30容量已满,需要一个新的页32存放新的目录项

现在因为存储目录项记录的页不止一个,所以如果我们想根据主键值查找一条用户记录大致需要3个步骤,以查找主键值为20的记录为例:

  1. 确定目录项记录页
  2. 通过目录项记录也确定用户记录真实所在的页
  3. 在真实存储用户记录的页中定位到具体的记录

3、迭代3次:目录项记录页的目录页

 如图,我们生成了一个存储更高级目录项的 页33 ,这个页中的两条记录分别代表页30和页32,如果用户记录的主键值在 [1, 320) 之间,则到页30中查找更详细的目录项记录,如果主键值 不小于320 的话,就到页32中查找更详细的目录项记录。

我们可以用下边这个图来描述它:

 4、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 (页目录),所以在页面内也可以通过 二分法 实现快速定位记录。

6.3.3 常见索引概念

索引按照物理实现方式,索引可以分为 2 种:聚簇(聚集)和非聚簇(非聚集)索引。

我们也把非聚集索引称为二级索引或者辅助索引。

6.3.3.1 聚簇索引

特点:

1、使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:

  • 页内的记录是按照主键的大小顺序排成一个 单向链表 。
  • 各个存放 用户记录的页 也是根据页中用户记录的主键大小顺序排成一个 双向链表 。
  • 存放 目录项记录的页 分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个 双向链表 。

2、B+树的 叶子节点 存储的是完整的用户记录。所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。

优点:

  • 数据访问更快 ,因为聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非聚簇索引更快
  • 聚簇索引对于主键的 排序查找 和 范围查找 速度非常快
  • 按照聚簇索引排列顺序,查询显示一定范围数据的时候,由于数据都是紧密相连,数据库不用从多个数据块中提取数据,所以 节省了大量的io操作 。

缺点:

  • 插入速度严重依赖于插入顺序 ,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列为主键
  • 更新主键的代价很高 ,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新
  • 二级索引访问需要两次索引查找 ,第一次找到主键值,第二次根据主键值找到行数据

6.3.3.2 二级索引(辅助索引、非聚簇索引)

 概念:回表 我们根据这个以c2列大小排序的B+树只能确定我们要查找记录的主键值,所以如果我们想根据c2列的值查找到完整的用户记录的话,仍然需要到 聚簇索引 中再查一遍,这个过程称为 回表 。也就是根据c2列的值查询一条完整的用户记录需要使用到 2 棵B+树!

问题:为什么我们还需要一次 回表 操作呢?直接把完整的用户记录放到叶子节点不OK吗?

如果每个索引都是聚簇索引,那么数据存在严重冗余,每次新增更新的时候,也许进行多次更新,效率更低。

6.3.3.3 联合索引

我们也可以同时以多个列的大小作为排序规则,也就是同时为多个列建立索引,比方说我们想让B+树按照 c2和c3列 的大小进行排序,这个包含两层含义:

  • 先把各个记录和页按照c2列进行排序。
  • 在记录的c2列相同的情况下,采用c3列进行排序

注意一点,以c2和c3列的大小为排序规则建立的B+树称为 联合索引 ,本质上也是一个二级索引。它的意思与分别为c2和c3列分别建立索引的表述是不同的,不同点如下:

  • 建立 联合索引 只会建立如上图一样的1棵B+树。
  • 为c2和c3列分别建立索引会分别以c2和c3列的大小为排序规则建立2棵B+树。

6.3.4 InnoDB的B+数索引的注意事项

  1. 根页面位置万年不动
  2. 内节点中目录项记录的唯一性
  3. 一个页面最少存储2条记录

6.4 MyISAM中的索引方案

B树索引适用存储引擎如表所示:

索引 / 存储引擎

MyISAM

InnoDB

Memory

B-Tree索引

支持

支持

支持

6.4.1 MyISAM索引的原理

 如果我们在Col2上建立一个二级索引,则此索引的结构如下图所示:

 可以看到,MyISAM所有的索引都是非聚簇的。

6.4.2 MyISAM与InnoDB对比

MyISAM的索引方式都是“非聚簇”的,与InnoDB包含1个聚簇索引是不同的。小结两种引擎中索引的区别:

  1. 在InnoDB存储引擎中,我们只需要根据主键值对 聚簇索引 进行一次查找就能找到对应的记录,而在MyISAM 中却需要进行一次 回表 操作,意味着MyISAM中建立的索引相当于全部都是 二级索引 。
  2. InnoDB的数据文件本身就是索引文件,而MyISAM索引文件和数据文件是 分离的 ,索引文件仅保存数据记录的地址。
  3. InnoDB的非聚簇索引data域存储相应记录 主键的值 ,而MyISAM索引记录的是 地址 。换句话说,InnoDB的所有非聚簇索引都引用主键作为data域
  4. MyISAM的回表操作是十分 快速 的,因为是拿着地址偏移量直接到文件中取数据的,反观InnoDB是通过获取主键之后再去聚簇索引里找记录,虽然说也不慢,但还是比不上直接用地址去访问。
  5. InnoDB要求表 必须有主键 ( MyISAM可以没有 )。如果没有显式指定,则MySQL系统会自动选择一个可以非空且唯一标识数据记录的列作为主键。如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整型。

6.5 索引的代价

索引是个好东西,可不能乱建,它在空间和时间上都会有消耗:

空间上的代价

每建立一个索引都要为它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页,一个页默认会占用 16KB 的存储空间,一棵很大的B+树由许多数据页组成,那就是很大的一片存储空间。

时间上的代价

每次对表中的数据进行 增、删、改 操作时,都需要去修改各个B+树索引。而且我们讲过,B+树每层节点都是按照索引列的值 从小到大的顺序排序 而组成了 双向链表 。不论是叶子节点中的记录,还是内节点中的记录(也就是不论是用户记录还是目录项记录)都是按照索引列的值从小到大的顺序而形成了一个单向链表。而增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些 记录移位 , 页面分裂 、 页面回收 等操作来维护好节点和记录的排序。如果我们建了许多索引,每个索引对应的B+树都要进行相关的维护操作,会给性能拖后腿。

6.6 MySQL数据结构选择的合理性

6.6.1 全表遍历

最慢最差

6.6.2 Hash结构

上图中哈希函数h有可能将两个不同的关键字映射到相同的位置,这叫做 碰撞 ,在数据库中一般采用 链接法 来解决。在链接法中,将散列到同一槽位的元素放在一个链表中.

问题:Hash结构效率高,那为什么索引结构要设计成树型呢?

因为随着数据量增大,碰撞的次数越来越多,扩容的成本也增大。

6.6.3 二叉搜索树

如果我们利用二叉树作为索引结构,那么磁盘的IO次数和索引树的高度是相关的。

1. 二叉搜索树的特点

2. 查找规则

理想情况:

 现实情况:

 为了提高查询效率,就需要 减少磁盘IO数 。为了减少磁盘IO的次数,就需要尽量 降低树的高度 ,需要把原来“瘦高”的树结构变的“矮胖”,树的每层的分叉越多越好。

6.6.4 AVL树

 针对同样的数据,如果我们把二叉树改成 M 叉树 (M>2)呢?当 M=3 时,同样的 31 个节点可以由下面的三叉树来进行存储:

 6.6.5 B-TreeB

B 树的结构如下图所示:

 一个 M 阶的 B 树(M>2)有以下的特性:

  1. 根节点的儿子数的范围是 [2,M]。
  2. 每个中间节点包含 k-1 个关键字和 k 个孩子,孩子的数量 = 关键字的数量 +1,k 的取值范围为[ceil(M/2), M]。
  3. 叶子节点包括 k-1 个关键字(叶子节点没有孩子),k 的取值范围为 [ceil(M/2), M]。
  4. 假设中间节点节点的关键字为:Key[1], Key[2], …, Key[k-1],且关键字按照升序排序,即 Key[i]
  5. 所有叶子节点位于同一层。

上面那张图所表示的 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 ,那么步骤可以分为以下几步:

  1. 我们与根节点的关键字 (17,35)进行比较,9 小于 17 那么得到指针 P1;
  2. 按照指针 P1 找到磁盘块 2,关键字为(8,12),因为 9 在 8 和 12 之间,所以我们得到指针 P2;
  3. 按照指针 P2 找到磁盘块 6,关键字为(9,10),然后我们找到了关键字 9。

能看出来在 B 树的搜索过程中,我们比较的次数并不少,但如果把数据读取出来然后在内存中进行比较,这个时间就是可以忽略不计的。而读取磁盘块本身需要进行 I/O 操作,消耗的时间比在内存中进行比较所需要的时间要多,是数据查找用时的重要因素。 B 树相比于平衡二叉树来说磁盘 I/O 操作要少 ,在数据查询中比平衡二叉树效率要高。所以 只要树的高度足够低,IO次数足够少,就可以提高查询性能 。

6.6.6 B+Tree

B+ 树和 B 树的差异:

  1. 有 k 个孩子的节点就有 k 个关键字。也就是孩子数量 = 关键字数,而 B 树中,孩子数量 = 关键字数+1。
  2. 非叶子节点的关键字也会同时存在在子节点中,并且是在子节点中所有关键字的最大(或最小)。
  3. 非叶子节点仅用于索引,不保存数据记录,跟记录有关的信息都放在叶子节点中。而 B 树中, 非叶子节点既保存索引,也保存数据记录 。
  4. 所有关键字都在叶子节点出现,叶子节点构成一个有序链表,而且叶子节点本身按照关键字的大小从小到大顺序链接

B 树和 B+ 树都可以作为索引的数据结构,在 MySQL 中采用的是 B+ 树。

但B树和B+树各有自己的应用场景,不能说B+树完全比B树好,反之亦然。

思考题:

为了减少IO,索引树会一次性加载吗?

B+树的存储能力如何?为何说一般查找行记录,最多只需1~3次磁盘IO

为什么说B+树比B-树更适合实际应用中操作系统的文件索引和数据库索引?

Hash 索引与 B+ 树索引的区别

Hash 索引与 B+ 树索引是在建索引的时候手动指定的吗?

6.6.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数据存储结构

7.1 数据库的存储结构:页

索引结构给我们提供了高效的索引方式,不过索引信息以及数据记录都是保存在文件上的,确切说是存储在页结构中。另一方面,索引是在存储引擎中实现的,MySQL服务器上的存储引擎负责对表中数据的读取和写入工作。不同存储引擎中存放的格式一般是不同的,甚至有的存储引擎比如Memory都不用磁盘来存储数据。

由于InnoDB是MySQL的默认存储引擎,所以本章剖析InnoDB存储引擎的数据存储结构。

7.1.1 磁盘和内存交互的基本单位:页

InnoDB将数据划分为若干个页,InnoDB中页的大小默认为16KB。

以页作为磁盘和内存之间交互的基本单位,也就是一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。在数据库中,不论读一行,还是读多行,都是将这些行所在的页进行加载。

也就是说,数据库管理存储空间的基本单位是页(Page),数据库I/O操作的最小单位是页。一个页中可以存储多个行记录。

7.1.2 页结构概述

页a、页b、页c…页n这些页可以不在物理结构上相连,只要通过双向链表相关联即可。

每个数据页中的记录会按照主键值从小到大的顺序组成一个单向链表,每个数据页都会为存储在它里边的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应的分组中的记录即可快速找到指定的记录。

7.1.3 页的大小

不同的数据库系统的页大小不同。比如在MySQL的InnoDB存储引擎中,默认页的大小是16KB,可以通过如下命令查看:

mysql> show variables like 'innodb_page_size';

+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.03 sec)

注:SQL Server中页的大小是8KB,而在Oracle中用术语“块”(Block)代表”页“,Oracle支持的大小快为2KB、8KB、16KB、32KB、64KB。

7.1.4 页的上层结构

 区(Extent)是比页大一级的存储结构,在InnoDB存储引擎中,一个区会分配64个连续的页。因为InnoDB中的页大小默认是16KB,所以一个区的大小是64*16KB=1MB。

段(Segment)由一个或多个区组成,区在文件系统是一个连续分配的空间(在InnoDB中是连续的64个页),不过在段中不要求区与区之间是相邻的。段是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。当我们创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时会创建一个表段,创建一个索引时会创建一个索引段。

表空间(Tablespace)是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但是一个段只能属于一个表空间。数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间、用户表空间、撤销表空间、临时表空间等。

7.2 页的内部结构

页如果按类型划分的话,常见的有数据页(保存B+树节点)、系统页、Undo页和事务数据页等。数据页是我们最常使用的页。

数据页的16KB大小的存储空间被划分为七个部分,分别是:

  • 文件头(File Header)
  • 页头(Page Header)
  • 最大最小记录(Inflmum+supremum)
  • 用户记录(User Records)
  • 空闲空间(Free Space)
  • 页目录(Page Directory)
  • 文件尾(File Tailer)

7.2.1 文件头部和文件尾部

7.2.1.1 文件头

作用: 描述各种页的通用信息。(比如页的编号、其上一页、下一页是谁等)

大小: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字节):这个代表当前页的类型。

7.2.1.2 文件尾

  • 前4个字节代表页的校验和:这个部分是和File Header中的校验和相对应的。
  • 后4个字节代表页面被最后修改时对应的日志序列位置(LSN):这个部分也是为了校验页的完整性的,如果首部和尾部的LSN值校验不成功的话,就说明同步过程出现了问题。
  • FIL_PAGE_PREV(4字节)和FIL_PAGE_NEXT(4字节):InnoDB都是以页为单位存放数据的,如果数据分散到多个不连续的页中存储的话需要把这些页关联起来,FIL_PAGE_PREV和FIL_PAGE_NEXT就分别代表本页的上一个和下一个页的页号。这样通过建立一个双向链表把许许多多的页就都串联起来了,保证这些页之间不需要是物理上的连续,而是逻辑上的连续。
  • FIL_PAGE_SPACE_OR_CHKSUM(4字节):代表当前页面的校验和(checksum)。

7.2.2 空闲空间、用户记录、最小最大记录

7.2.2.1 Free space(空闲空间)

我们自己存储的记录会按照指定的行格式存储到User Records部分。

但是在一开始生成页的时候,其实并没有User Records这个部分,每当我们插入一条记录,都会从Free Space部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records部分。

当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。

 7.2.2.2 User Records(用户记录)

User Records中的这些记录按照指定的行格式一条一条摆在User Records部分,相互之间形成单链表。(单链表是由行格式决定的)

7.2.2.3 Infimum + Suremum(最小最大记录)

记录可以比较大小吗? 是的,记录可以比大小,对于一条完整的记录来说,比较记录的大小就是比较主键的大小。比方说我们插入的4行记录的主键值分别是:1、2、3、4,这也就意味着这4条记录是从小到大依次递增。

InnoDB规定的最小记录与最大记录这两条记录的构造十分简单,都是由5字节大小的记录头信息和8字节大小的一个固定的部分组成的.

这两条记录不是我们自己定义的记录,所以它们并不存放在页的User Records部分,他们被单独放在一个称为Infimum + Supremum的部分。

7.2.3 页目录和页面头部

7.2.3.1 Page Directory(页目录)

为什么需要页目录? 在页中,记录是以单向链表的形式进行存储的。单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。因此在页结构中专门设计了页目录这个模块,专门给记录做一个目录,通过二分查找法的方式进行检索,提升效率。

页目录,二分法查找

将所有的记录分成几个组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录。

  1. 第 1 组,也就是最小记录所在的分组只有 1 个记录;
  2. 最后一组,就是最大记录所在的分组,会有 1-8 条记录;其余的组记录数量在 4-8 条之间。这样做的好处是,除了第 1 组(最小记录所在组)以外,其余组的记录数会尽量平分。
  3. 在每个组中最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned 字段。
  4. 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录。

从这个图中我们需要注意这么几点:

  • 现在页目录部分中有两个槽,也就意味着我们的记录被分成了两个组,槽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条记录,最大记录所在的分组拥有的记录条数只能在1~8 条之间,剩下的分组中记录的条目数范围只能是在4~8条之间。

分组是按照下边的步骤进行的:

  • 初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。
  • 之后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
  • 在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。

小结:

在一个数据页中查找指定主键值的记录的过程分为两步:

  1. 通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录。
  2. 通过记录的next_record属性遍历该槽所在的组中的各个记录。

7.2.3.2 Page Header(页面头部)

为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫Page Header的部分,这个部分占用固定的56个字节,专门存储各种状态信息。

 7.3 InnoDB行格式

数据以行为单位来向表中插入数据,这些记录在磁盘上的存放方式也被称为行格式或者记录格式。InnoDB存储引擎设计了4种不同类型的行格式,分别是Compact、Redundant、Dynamic和Compressed。

mysql> select @@innodb_default_row_format;
+-----------------------------+
| @@innodb_default_row_format |
+-----------------------------+
| dynamic                     |
+-----------------------------+
1 row in set (0.00 sec)

7.3.1 指定行格式的语法

CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
ALTER TABLE 表名 ROW_FORMAT=行格式名称

7.3.2 COMPACT行格式

在MySQL 5.1版本中,默认设置为Compact行格式。一条完整的记录其实可以被分为记录的额外信息和记录的真实数据两大部分。

 7.3.2.1 变长字段长度列表

MySQL支持一些变长的数据类型,比如VARCHAR(M)、VARBINARY(M)、TEXT类型,BLOB类型,这些数据类型修饰列称为变长字段.

变长字段中存储多少字节的数据不是固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。

在Compact行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表。

注意:这里面存储的变长长度和字段顺序是反过来的。比如两个varchar字段在表结构的顺序是a(10),b(15)。那么在变长字段长度列表中存储的长度顺序就是15,10,是反过来的。

以record_test_table表中的第一条记录举例:因为record_test_table表的col1、col2、col4列都是VARCHAR(8)类型的,所以这三个列的值的长度都需要保存在记录开头处,注意record_test_table表中的各个列都使用的是ascii字符集(每个字符只需要1个字节来进行编码)。

 又因为这些长度值需要按照列的逆序存放,所以最后变长字段长度列表的字节串用十六进制表示的效果就是(各个字节之间实际上没有空格,用空格隔开只是方便理解):06 04 08

把这个字节串组成的变长字段长度列表填入上边的示意图中的效果就是

 7.3.2.2 NULL值列表

Compact行格式会把可以为NULL的列统一管理起来,存在一个标记为NULL值列表中。如果表中没有允许存储 NULL 的列,则 NULL值列表也不存在了。

为什么定义NULL值列表?

之所以要存储NULL是因为数据都是需要对齐的,如果没有标注出来NULL值的位置,就有可能在查询数据的时候出现混乱。如果使用一个特定的符号放到相应的数据位表示空置的话,虽然能达到效果,但是这样很浪费空间,所以直接就在行数据得头部开辟出一块空间专门用来记录该行数据哪些是非空数据,哪些是空数据,格式如下:

  • 二进制位的值为1时,代表该列的值为NULL。
  • 二进制位的值为0时,代表该列的值不为NULL。

例如:字段 a、b、c,其中a是主键,在某一行中存储的数依次是 a=1、b=null、c=2。那么Compact行格式中的NULL值列表中存储:01。第一个0表示c不为null,第二个1表示b是null。这里之所以没有a是因为数据库会自动跳过主键,因为主键肯定是非NULL且唯一的,在NULL值列表的数据中就会自动跳过主键。

7.3.2.3 记录头信息

名称

大小(单位: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

这个属性标记着当前记录是否被删除,占用1个二进制位。

  • 值为0:代表记录并没有被删除
  • 值为1:代表记录被删除掉了

被删除的记录为什么还在页中存储呢? 你以为它删除了,可它还在真实的磁盘上。这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后其他的记录在磁盘上需要重新排列,导致性能消耗。所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的垃圾链表,在这个链表中的记录占用的空间称之为可重用空间,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。

min_rec_mask

B+树的每层非叶子节点中的最小记录都会添加该标记,min_rec_mask值为1。

我们自己插入的四条记录的min_rec_mask值都是0,意味着它们都不是B+树的非叶子节点中的最小记录。

record_type

这个属性表示当前记录的类型,一共有4种类型的记录:

  • 0:表示普通记录
  • 1:表示B+树非叶节点记录
  • 2:表示最小记录
  • 3:表示最大记录

heap_no

这个属性表示当前记录在本页中的位置。

怎么不见heap_no值为0和1的记录呢? MySQL会自动给每个页里加了两个记录,由于这两个记录并不是我们自己插入的,所以有时候也称为伪记录或者虚拟记录。这两个伪记录一个代表最小记录,一个代表最大记录。最小记录和最大记录的heap_no值分别是0和1,也就是说它们的位置最靠前。

n_owned

页目录中每个组中最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned 字段

next_record

记录头信息里该属性非常重要,它表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。

删除操作变化:

 从图中可以看出来,删除第2条记录前后主要发生了这些变化:

第2条记录并没有从存储空间中移除,而是把该条记录的delete_mask值设置为1

第2条记录的next_record值变为了0,意味着该记录没有下一条记录了。

第1条记录的next_record指向了第3条记录。

最大记录的n_owned值从5变成了4。

所以,不论我们怎么对页中的记录做增删改操作,InnoDB始终会维护一条记录的单链表链表中的各个节点是按照主键值由小到大的顺序连接起来的。

添加操作变化:

 直接复用了原来被删除记录的存储空间。

说明:当数据页中存在多条被删除掉的记录时,这些记录的next_record属性将会把这些被删除掉的记录组成一个垃圾链表,以备之后重用这部分存储空间。

7.3.2.4 记录的真实数据

录的真实数据除了我们自己定义的列的数据以外,还会有三个隐藏列:

列名

是否必须

占用空间

描述

row_id

6字节

行ID,唯一标识一条记录

transaction_id

6字节

事务ID

roll_pointer

7字节

回滚指针

实际上这几个列的真正名称其实是:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR。

一个表没有手动定义主键,则会选取一个Unique键作为主键,如果连Unique键都没有定义的话,则会为表默认添加一个名为row_id的隐藏列作为主键。所以row_id是在没有自定义主键以及Unique键的情况下才会存在的。

7.3.3 Dynamic和Compressed行格式

行溢出:

InnoDB存储引擎可以将一条记录中的某些数据存储在真正的数据页面之外。

很多DBA喜欢MySQL数据库提供的VARCHAR(M)类型,认为可以存放65535字节。这是真的吗?如果我们使用 ascii字符集的话,一个字符就代表一个字节,我们看看VARCHAR(65535)是否可用。

CREATE TABLE varchar_size_demo(
c VARCHAR(65535)
) CHARSET=ascii ROW_FORMAT=Compact;

结果如下:

ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs

报错信息表达的意思是:MySQL对一条记录占用的最大存储空间是有限制的,除BLOB或者TEXT类型的列之外, 其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过65535个字节。

这个65535个字节除了列本身的数据之外,还包括一些其他的数据,以Compact行格式为例,比如说我们为了存储一个VARCHAR(M)类型的列,除了真实数据占有空间以外,还需要记录的额外信息。

如果该VARCHAR类型的列没有NOT NULL属性,那最多只能存储65532个字节的数据,因为变长字段的长度占用 2个字节,NULL值标识需要占用1个字节。

CREATE TABLE varchar_size_demo(
c VARCHAR(65532)
) CHARSET=ascii ROW_FORMAT=Compact;

#如果有not null属性,那么就不需要NULL值标识,也就可以多存储一个字节,即65533个字节

CREATE TABLE varchar_size_demo(
c VARCHAR(65533) not null
) CHARSET=ascii ROW_FORMAT=Compact;

通过上面的案例,我们可以知道一个页的大小一般是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个前缀字节)。

Compressed行记录格式的另一个功能就是,存储在其中的行数据会以zlib的算法进行压缩,因此对于BLOB、TEXT、VARCHAR这类大长度类型的数据能够进行非常有效的存储。

7.4 区、段、碎片区

7.4.1 为什么要有区

B+树的每一层中的页都会形成一个双向链表,如果是以页为单位来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离得非常远。我们介绍B+树索引的使用场景的时候特别提到范围查询只需要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描就可以了,而如果链表中相邻的两个页物理位置离得非常远,就是所谓的随机I/O。再一次强调,磁盘的速度和内存的速度差了好几个数量级,随机I/O是非常慢的,所以我们应该尽量让链表中相邻的页的物理位置也相邻,这样进行范围查询的时候才可以使用所谓的顺序I/O。

引入区的概念,一个区就是物理位置上连续的64个页。因为InnoDB中的页的大小默认是16KB,所以一个区的大小是64*16KB=1MB。在表中数据量大的时候,为某个索引分配空间的时候就不再按照页的单位分配了,而是按照区为单位分配,甚至在表中的数据特别多的时候,可以一次性分配多个连续的区。虽然可能造成一点点空间的浪费(数据不足以填充满整个区),但是从性能角度看,可以消除很多的随机I/O,功大于过!

7.4.2 为什么要有段

对于范围查询,其实是对B+树叶子节点中的记录进行顺序扫描,而如果不区分叶子节点和非叶子节点,统统把节点代表的页面放到申请到的区中的话,进行范围扫描的效果就大打折扣了。所以InnoDB对B+树的叶子节点和非叶子节点进行了区别对待,也就是说叶子节点有自己独有的区,非叶子节点也有自己独有的区。存放叶子节点的区的集合就算是一个段(segment),存放非叶子节点的区的集合也算是一个段。也就是说一个索引会生成2个段,一个叶子节点段,一个非叶子节点段。

除了索引的叶子节点段和非叶子节点段之外,InnoDB中还有为存储一些特殊的数据而定义的段,比如回滚段。所以,常见的段有数据段、索引段、回滚段。数据段即为B+树的叶子节点,索引段即为B+树的非叶子节点。

在InnoDB存储引擎中,对段的管理都是由引擎自身所完成,DBA不能也没有必要对其进行控制。这从一定程度上简化了DBA对于段的管理。

段其实不对应表空间中的某一个连续的物理区域,而是一个逻辑上的概念,由若干个零散的页面以及一些完整的区组成。

7.4.3 为什么要有碎片区

默认情况下,一个使用InnoDB存储引擎的表只有一个聚簇索引,一个索引会生成2个段,而段是以区为单位申请存储空间的,一个区默认占用1M(64*16KB=1024KB)存储空间,所以**默认情况下一个只存在几条记录的小表也需要2M的存储空间么?**以后每次添加一个索引都要多申请2M的存储空间么?这对于存储记录比较少的表简直是天大的浪费。这个问题的症结在于到现在为止我们介绍的区都是非常纯粹的,也就是一个区被整个分配给某一个段,或者说区中的所有页面都是为了存储同一个段的数据而存在的,即使段的数据填不满区中所有的页面,那余下的页面也不能挪作他用。

为了考虑以完整的区为单位分配给某个段对于数据量较小的表太浪费存储空间的这种情况,InnoDB提出了一个碎片(fragment)区的概念。在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,而是碎片区中的页可以用于不同的目的,比如有些页面用于段A,有些页面用于段B,有些页甚至哪个段都不属于。碎片区直属于表空间,并不属于任何一个段。

所以此后为某个段分配存储空间的策略是这样的:

在刚开始向表中插入数据的时候,段是从某个碎片区以单个页面为单位来分配存储空间的。

当某个段已经占用了32个碎片区页面之后,就会申请以完整的区为单位来分配存储空间。

所以现在段不能仅定义为是某些区的集合,更精确的应该是某些零散的页面已经一些完整的区的集合。

7.4.4 区的分类

区大体上可以分为4种类型:

空闲的区(FREE):现在还没有用到这个区中的任何页面。

有剩余空间的碎片区(FREE_FRAG):表示碎片区中还有可用的页面。

没有剩余空间的碎片区(FULL_FRAG):表示碎片区中的所有页面都被使用,没有空闲页面。

附属于某个段的区(FSEG):每一索引都可以分为叶子节点段和非叶子节点段

处于FREE、FREE_FRAG以及FULL_FRAG这三种状态的区都是独立的,直属于表空间。而处于FSEG状态的区是附属于某个段的。

附录:数据页加载的三种方式

InnoDB从磁盘中读取数据的最小单位是数据页。而你想得到的id=xxx的数据,就是这个数据页众多行中的一行。

对于MySQL存放的数据,逻辑概念上我们称之为表,在磁盘等物理层面而言是按数据页形式进行存放的,当其加载到MySQL中我们称之为缓存页。

如果缓冲池中没有该页数据,那么缓冲池有以下三种读取数据的方式,每种方式的读取效率都是不同的:

1.内存读取

如果该数据存在于内存中,基本上执行时间在1ms左右,效率还是很高的。

 2.随机读取

如果数据没有在内存中,就需要在磁盘上对该页进行查找,整体时间预估在10ms左右,这10ms中有6ms是磁盘的实际繁忙时间(包括了寻道和半圈旋转时间),有3ms是对可能发生的排队时间的估计值,另外还有1ms的传输时间,将页从磁盘服务器缓冲区传输到数据库缓冲区中。这10ms看起来很快,但实际上对于数据库来说消耗的时间已经非常长了,因为这还只是一个页的读取时间。

 3.顺序读取

顺序读取其实是一种批量读取的方式,因为我们请求的数据在磁盘上往往都是相邻存储的,顺序读取可以帮我们批量读取页面,这样的话,一次性加载到缓冲池中就不需要再对其他页面单独进行磁盘1/0操作了。如果一个磁盘的吞吐量是40MB/S,那么对于一个16KB大小的页来说,一次可以顺序读取2560(40MB/16KB)个页,相当于一个页的读取时间为0.4ms。采用批量读取的方式,即使是从磁盘上进行读取,效率也比从内存中只单独读取一个页的效率要高。

八、索引的创建和设计原则

8.1 索引的声明和使用

8.1.1 索引的分类

MySQL的索引包括普通索引、唯一性索引、全文索引、单列索引、多列索引和空间索引等。

  • 从 功能逻辑 上说,索引主要有 4 种,分别是普通索引、唯一索引、主键索引、全文索引。
  • 按照 物理实现方式 ,索引可以分为 2 种:聚簇索引和非聚簇索引。
  • 按照 作用字段个数 进行划分,分成单列索引和联合索引。

8.1.2 创建索引

8.1.2.1 创建表的同时创建索引

可以在单列后面声明索引

CREATE TABLE dept(
dept_id INT PRIMARY KEY AUTO_INCREMENT,
dept_name VARCHAR(20)
);
CREATE TABLE emp(
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(20) UNIQUE,
dept_id INT,
CONSTRAINT emp_dept_id_fk FOREIGN KEY(dept_id) REFERENCES dept(dept_id)
);

也可以显示创建索引

CREATE TABLE table_name [col_name data_type]
[UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name [length]) [ASC |
DESC]
  • UNIQUE 、 FULLTEXT 和 SPATIAL 为可选参数,分别表示唯一索引、全文索引和空间索引;
  • INDEX 与 KEY 为同义词,两者的作用相同,用来指定创建索引;
  • index_name 指定索引的名称,为可选参数,如果不指定,那么MySQL默认col_name为索引名;
  • col_name 为需要创建索引的字段列,该列必须从数据表中定义的多个列中选择;
  • length 为可选参数,表示索引的长度,只有字符串类型的字段才能指定索引长度;
  • ASC 或 DESC 指定升序或者降序的索引值存储。

1、创建普通索引

CREATE TABLE book(
    book_id INT ,
    book_name VARCHAR(100),
    comment VARCHAR(100),
    year_publication YEAR,
    INDEX(year_publication)
);

2、创建唯一索引

CREATE TABLE test1(
    id INT NOT NULL,
    name varchar(30) NOT NULL,
    UNIQUE INDEX uk_idx_id(id)
);

3、创建主键索引

CREATE TABLE student (
    id INT(10) UNSIGNED AUTO_INCREMENT ,
    student_no VARCHAR(200),
    student_name VARCHAR(200),
    PRIMARY KEY(id)
);

#修改主键索引:必须先删除掉(drop)原索引,再新建(add)索引
#删除主键索引:
ALTER TABLE studentdrop PRIMARY KEY ;

4、创建单列索引

CREATE TABLE test2(
    id INT NOT NULL,
    name CHAR(50) NULL,
    INDEX single_idx_name(name(20))
);

5、创建组合索引

CREATE TABLE test3(
    id INT(11) NOT NULL,
    name CHAR(30) NOT NULL,
    age INT(11) NOT NULL,
    info VARCHAR(255),
    INDEX multi_idx(id,name,age)
);

6、创建全文索引

CREATE TABLE test4(
    id INT NOT NULL,
    name CHAR(30) NOT NULL,
    age INT NOT NULL,
    info VARCHAR(255),
    FULLTEXT INDEX futxt_idx_info(info)
) ENGINE=MyISAM;

#在MySQL5.7及之后版本中可以不指定最后的ENGINE了,因为在此版本中InnoDB支持全文索引。
CREATE TABLE articles (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR (200),
    body TEXT,
    FULLTEXT index (title, body)
) ENGINE = INNODB ;


# 不同于like方式的的查询:
# 全文索引用match+against方式查询:
SELECT * FROM papers WHERE content LIKE ‘%查询字符串%’;
SELECT * FROM papers WHERE MATCH(title,content) AGAINST (‘查询字符串’);

注意点

  1. 使用全文索引前,搞清楚版本支持情况;
  2. 全文索引比 like + % 快 N 倍,但是可能存在精度问题;
  3. 如果需要全文索引的是大量数据,建议先添加数据,再创建索引。

7、创建空间索引

空间索引创建中,要求空间类型的字段必须为 非空 。

举例:创建表test5,在空间类型为GEOMETRY的字段上创建空间索引,SQL语句如下:

CREATE TABLE test5(
    geo GEOMETRY NOT NULL,
    SPATIAL INDEX spa_idx_geo(geo)
) ENGINE=MyISAM;

8.1.2.2 在已有表上创建索引

在已经存在的表中创建索引可以使用ALTER TABLE语句或者CREATE INDEX语句。

#1. 使用ALTER TABLE语句创建索引 ALTER TABLE语句创建索引的基本语法如下:
ALTER TABLE table_name 
ADD [UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name[length],...) [ASC | DESC]


#2. 使用CREATE INDEX创建索引 CREATE INDEX语句可以在已经存在的表上添加索引,在MySQL中,
#CREATE INDEX被映射到一个ALTER TABLE语句上,基本语法结构为:
CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name
ON table_name (col_name[length],...) [ASC | DESC]

8.1.3 删除索引

#1. 使用ALTER TABLE删除索引 ALTER TABLE删除索引的基本语法格式如下:
ALTER TABLE table_name DROP INDEX index_name;

#2. 使用DROP INDEX语句删除索引 DROP INDEX删除索引的基本语法格式如下:
DROP INDEX index_name ON table_name;

8.2 Mysql8.0索引新特性

8.2.1 支持降序索引

举例:分别在MySQL 5.7版本和MySQL 8.0版本中创建数据表ts1,结果如下:

CREATE TABLE ts1(
    a int,
    b int,
    index idx_a_b(a,b desc)
);

在MySQL 5.7版本中查看数据表ts1的结构,结果如下:

 从结果可以看出,索引仍然是默认的升序。

在MySQL 8.0版本中查看数据表ts1的结构,结果如下:

 从结果可以看出,索引已经是降序了。下面继续测试降序索引在执行计划中的表现。

#造800条数据后

#在MySQL 5.7版本中查看数据表ts1的执行计划,结果如下:
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。
EXPLAIN SELECT * FROM ts1 ORDER BY a,b DESC LIMIT 5;

#注意 降序索引只对查询中特定的排序顺序有效,如果使用不当,反而查询效率更低。例如,上述
#查询排序条件改为order by a desc, b desc,MySQL 5.7的执行计划要明显好于MySQL 8.0。
#将排序条件修改为order by a desc, b desc后,下面来对比不同版本中执行计划的效果。 在MySQL 5.7版本中查看数据表ts1的执行计划,结果如下:

8.2.2 隐藏索引

在MySQL 5.7版本及之前,只能通过显式的方式删除索引。此时,如果发现删除索引后出现错误,又只能通过显式创建索引的方式将删除的索引创建回来。如果数据表中的数据量非常大,或者数据表本身比较大,这种操作就会消耗系统过多的资源,操作成本非常高。

从MySQL 8.x开始支持 隐藏索引(invisible indexes) ,只需要将待删除的索引设置为隐藏索引,使查询优化器不再使用这个索引(即使使用force index(强制使用索引),优化器也不会使用该索引),确认将索引设置为隐藏索引后系统不受任何响应,就可以彻底删除索引。 这种通过先将索引设置为隐藏索引,再删除索引的方式就是软删除 。

1. 创建表时直接创建

在MySQL中创建隐藏索引通过SQL语句INVISIBLE来实现,其语法形式如下:

CREATE TABLE tablename(
    propname1 type1[CONSTRAINT1],
    propname2 type2[CONSTRAINT2],
    ……
    propnamen typen,
    INDEX [indexname](propname1 [(length)]) INVISIBLE
);

2. 在已经存在的表上创建

CREATE INDEX indexname
ON tablename(propname[(length)]) INVISIBLE;

3. 通过ALTER TABLE语句创建

ALTER TABLE tablename
ADD INDEX indexname (propname [(length)]) INVISIBLE;

4. 切换索引可见状态

已存在的索引可通过如下语句切换可见状态:

ALTER TABLE tablename ALTER INDEX index_name INVISIBLE; #切换成隐藏索引
ALTER TABLE tablename ALTER INDEX index_name VISIBLE; #切换成非隐藏索引

5. 使隐藏索引对查询优化器可见

在MySQL 8.x版本中,为索引提供了一种新的测试方式,可以通过查询优化器的一个开关(use_invisible_indexes)来打开某个设置,使隐藏索引对查询优化器可见。如果 use_invisible_indexes设置为off(默认),优化器会忽略隐藏索引。如果设置为on,即使隐藏索引不可见,优化器在生成执行计划时仍会考虑使用隐藏索引。

#在MySQL命令行执行如下命令查看查询优化器的开关设置。
mysql> select @@optimizer_switch
use_invisible_indexes=off

#使隐藏索引对查询优化器可见,需要在MySQL命令行执行如下命令:
mysql> set session optimizer_switch="use_invisible_indexes=on"; 
Query OK, 0 rows affected (0.00 sec)

SQL语句执行成功,再次查看查询优化器的开关设置。

mysql> select @@optimizer_switch \G
#use_invisible_indexes属性的值为on,说明此时隐藏索引对查询优化器可见。
use_invisible_indexes=on

使用EXPLAIN查看以字段invisible_column作为查询条件时的索引使用情况。

explain select * from classes where cname = '高一2班';

查询优化器会使用隐藏索引来查询数据。

如果需要使隐藏索引对查询优化器不可见,则只需要执行如下命令即可。

mysql> set session optimizer_switch="use_invisible_indexes=off";
Query OK, 0 rows affected (0.00 sec)

mysql> select @@optimizer_switch \G
此时,use_invisible_indexes属性的值已经被设置为“off”。

8.3 索引的设计原则

8.3.1 数据准备

第1步:创建数据库、创建表

#1.创建学生表和课程表
CREATE TABLE `student_info` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `student_id` INT NOT NULL ,
    `name` VARCHAR(20) DEFAULT NULL,
    `course_id` INT NOT NULL ,
    `class_id` INT(11) DEFAULT NULL,
    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE `course` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `course_id` INT NOT NULL ,
    `course_name` VARCHAR(40) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

第2步:创建模拟数据必需的存储函数

#函数1:创建随机产生字符串函数
DELIMITER //
CREATE FUNCTION rand_string(n INT)
RETURNS VARCHAR(255) #该函数会返回一个字符串
BEGIN
    DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
    DECLARE return_str VARCHAR(255) DEFAULT '';
    DECLARE i INT DEFAULT 0;
    WHILE i < n DO
        SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
        SET i = i + 1;
    END WHILE;
    RETURN return_str;
END //
DELIMITER ;


#函数2:创建随机数函数
DELIMITER //
CREATE FUNCTION rand_num (from_num INT ,to_num INT) RETURNS INT(11)
BEGIN
    DECLARE i INT DEFAULT 0;
    SET i = FLOOR(from_num +RAND()*(to_num - from_num+1)) ;
    RETURN i;
END //
DELIMITER ;

创建函数,假如报错:This function has none of DETERMINISTIC......

由于开启过慢查询日志bin-log, 我们就必须为我们的function指定一个参数。主从复制,主机会将写操作记录在bin-log日志中。从机读取bin-log日志,执行语句来同步数据。如果使用函数来操作数据,会导致从机和主键操作时间不一致。所以,默认情况下,mysql不开启创建函数设置。

  • 查看mysql是否允许创建函数:
show variables like 'log_bin_trust_function_creators';
  • 命令开启:允许创建函数设置:
set global log_bin_trust_function_creators=1; # 不加global只是当前窗口有效。
  • mysqld重启,上述参数又会消失。永久方法:
log_bin_trust_function_creators

第3步:创建插入模拟数据的存储过程

# 存储过程1:创建插入课程表存储过程
DELIMITER //
CREATE PROCEDURE insert_course( max_num INT )
BEGIN
    DECLARE i INT DEFAULT 0;
    SET autocommit = 0; #设置手动提交事务
    REPEAT #循环
    SET i = i + 1; #赋值
    INSERT INTO course (course_id, course_name ) VALUES
    (rand_num(10000,10100),rand_string(6));
    UNTIL i = max_num
    END REPEAT;
    COMMIT; #提交事务
END //
DELIMITER ;

# 存储过程2:创建插入学生信息表存储过程
DELIMITER //
CREATE PROCEDURE insert_stu( max_num INT )
BEGIN
    DECLARE i INT DEFAULT 0;
    SET autocommit = 0; #设置手动提交事务
    REPEAT #循环
    SET i = i + 1; #赋值
    INSERT INTO student_info (course_id, class_id ,student_id ,NAME ) VALUES
    (rand_num(10000,10100),rand_num(10000,10200),rand_num(1,200000),rand_string(6));
    UNTIL i = max_num
    END REPEAT;
    COMMIT; #提交事务
END //
DELIMITER ;

4、调用存储过程

CALL insert_course(100);
CALL insert_stu(1000000);

8.3.2 哪些情况适合建立索引

1. 字段的数值有唯一性的限制

业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。(来源:Alibaba)说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的。

2. 频繁作为 WHERE 查询条件的字段

某个字段在SELECT语句的 WHERE 条件中经常被使用到,那么就需要给这个字段创建索引了。尤其是在数据量大的情况下,创建普通索引就可以大幅提升数据查询的效率。比如student_info数据表(含100万条数据),假设我们想要查询 student_id=123110 的用户信息。

3. 经常 GROUP BY 和 ORDER BY 的列

索引就是让数据按照某种顺序进行存储或检索,因此当我们使用 GROUP BY 对数据进行分组查询,或者使用 ORDER BY 对数据进行排序的时候,就需要 对分组或者排序的字段进行索引 。如果待排序的列有多个,那么可以在这些列上建立 组合索引 。

4. UPDATE、DELETE 的 WHERE 条件列

对数据按照某个条件进行查询后再进行 UPDATE 或 DELETE 的操作,如果对 WHERE 字段创建了索引,就能大幅提升效率。原理是因为我们需要先根据 WHERE 条件列检索出来这条记录,然后再对它进行更新或删除。如果进行更新的时候,更新的字段是非索引字段,提升的效率会更明显,这是因为非索引字段更新不需要对索引进行维护。

5.DISTINCT 字段需要创建索引

有时候我们需要对某个字段进行去重,使用 DISTINCT,那么对这个字段创建索引,也会提升查询效率。

6. 多表 JOIN 连接操作时,创建索引注意事项

首先, 连接表的数量尽量不要超过 3 张 ,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增长会非常快,严重影响查询的效率。

其次, 对 WHERE 条件创建索引 ,因为 WHERE 才是对数据条件的过滤。如果在数据量非常大的情况下,没有 WHERE 条件过滤是非常可怕的。

最后, 对用于连接的字段创建索引 ,并且该字段在多张表中的 类型必须一致 。

7. 使用列的类型小的创建索引

8. 使用字符串前缀创建索引

创建一张商户表,因为地址字段比较长,在地址字段上建立前缀索引问题是,截取多少呢?截取得多了,达不到节省索引存储空间的目的;截取得少了,重复内容太多,字段的散列度(选择性)会降低。怎么计算不同的长度的选择性呢?

先看一下字段在全部数据中的选择度:

select count(distinct address) / count(*) from shop;

通过不同长度去计算,与全表的选择性对比:

count(distinct left(列名, 索引长度))/count(*)

9. 区分度高(散列性高)的列适合作为索引

10. 使用最频繁的列放到联合索引的左侧

这样也可以较少的建立一些索引。同时,由于"最左前缀原则",可以增加联合索引的使用率。

11. 在多个字段都要创建索引的情况下,联合索引优于单值索引

8.3.3 哪些情况不适合创建索引

1. 在where中使用不到的字段,不要设置索引

2. 数据量小的表最好不要使用索引

3. 有大量重复数据的列上不要建立索引

4. 避免对经常更新的表创建过多的索引

5. 不建议用无序的值作为索引

6. 删除不再使用或者很少使用的索引

7. 不要定义冗余或重复的索引

九、性能分析工具的使用

9.1 数据库服务器的优化步骤

整个流程划分成了 观察(Show status) 和 行动(Action) 两个部分。

字母 S 的部分代表观察(会使用相应的分析工具),字母 A 代表的部分是行动(对应分析可以采取的行动)。

 小结:

 9.2 查看系统性能参数

在MySQL中,可以使用 SHOW STATUS 语句查询一些MySQL数据库服务器的 性能参数 、 执行频率 。SHOW STATUS语句语法如下:

SHOW [GLOBAL|SESSION] STATUS LIKE '参数';

一些常用的性能参数如下:

  • Connections:连接MySQL服务器的次数。 (是累加次数,不是当前连接数)
  • Threads_connected: 当前连接线程数
  • 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:删除操作的次数。

9.3 统计SQL的查询成本:last_query_cost

我们依然使用第8章的 student_info 表为例:

如果我们想要查询 id=900001 的记录,然后看下查询成本,我们可以直接在聚簇索引上进行查找:

SELECT student_id, class_id, NAME, create_time FROM student_info 
WHERE id = 900001;

运行结果(1 条记录,运行时间为 0.042s )然后再看下查询优化器的成本,实际上我们只需要检索一个页即可

mysql> SHOW STATUS LIKE 'last_query_cost';
+-----------------+----------+
| Variable_name | Value |
+-----------------+----------+
| Last_query_cost | 1.000000 |
+-----------------+----------+

如果我们想要查询 id 在 900001 到 9000100 之间的学生记录呢?

SELECT student_id, class_id, NAME, create_time FROM student_info
WHERE id BETWEEN 900001 AND 900100;

mysql> SHOW STATUS LIKE 'last_query_cost';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| Last_query_cost | 21.134453 |

你能看到页的数量是刚才的 20 倍,但是查询的效率并没有明显的变化,实际上这两个 SQL 查询的时间基本上一样,就是因为采用了顺序读取的方式将页面一次性加载到缓冲池中,然后再进行查找。

虽然页数量(last_query_cost)增加了不少,但是通过缓冲池的机制并没有增加多少查询时间 。

使用场景:它对于比较开销是非常有用的,特别是我们有好几种查询方式可选的时候。

9.4 定位执行慢的日志:慢查询日志

9.4.1 开启慢查询日志

1. 开启slow_query_log

set global slow_query_log = 1;

mysql> SHOW GLOBAL VARIABLES like '%slow_query_log%';
+---------------------+------------------------------------+
| Variable_name       | Value                              |
+---------------------+------------------------------------+
| slow_query_log      | ON                                 |
| slow_query_log_file | /usr/local/mysql/data/mbp-slow.log |
+---------------------+------------------------------------+

2、修改long_query_time阈值

set global long_query_time = 1;

mysql> SHOW GLOBAL VARIABLES like '%long_query_time%';
+-----------------+----------+
| Variable_name   | Value    |
+-----------------+----------+
| long_query_time | 1.000000 |
+-----------------+----------+

9.4.2查看慢查询数目

查询当前系统中有多少条慢查询记录

SHOW GLOBAL STATUS LIKE '%Slow_queries%';

查看具体慢SQL:/usr/local/mysql/data/mbp-slow.log

9.4.3案例

造一个大数据量的表(千万级别)

执行查询:

SELECT * FROM student_info WHERE name = 'oQmLUr';

查询慢进慢日志。

show status like 'slow_queries';

9.5 查看SQL的执行成本:show profile

# 查看profiling是否开启
mysql > show variables like 'profiling';
# 通过设置 profiling='ON’ 来开启 show profile:
mysql > set profiling = 'ON';

# 然后执行相关的查询语句。接着看下当前会话都有哪些 profiles,使用下面这条命令:
mysql > show profiles;

能看到当前会话一共有 2 个查询

# 查看最近一次查询的开销
mysql > show profile;
# 查看某次查询的开销
mysql> 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:显示交换次数开销信息。

9.6 分析查询语句:explain

9.6.1 概述

官网介绍

MySQL :: MySQL 5.7 Reference Manual :: 8.8.2 EXPLAIN Output Format

MySQL :: MySQL 8.0 Reference Manual :: 8.8.2 EXPLAIN Output Format

9.6.2 基本语法

# EXPLAIN 或 DESCRIBE语句的语法形式如下:

EXPLAIN SELECT select_options
# 或者
DESCRIBE SELECT select_options

# 举例
explain select * from employees;
describe select * from employees;

EXPLAIN 语句输出的各个列的作用如下:

列名

描述

id

在一个大的查询语句中每个SELECT关键字都对应一个 唯一的id

select_type

SELECT关键字对应的那个查询的类型

table

表名

partitions

匹配的分区信息

type

针对单表的访问方法

possible_keys

可能用到的索引

key

实际上使用的索引

key_len

实际使用到的索引长度

ref

当使用索引列等值查询时,与索引列进行等值匹配的对象信息

rows

预估的需要读取的记录条数

filtered

filtered 某个表经过搜索条件过滤后剩余记录条数的百分比

Extra

一些额外的信息

9.6.3 各列作用

先建两张示范表:

create table employees
(
    employee_id    int default 0 not null primary key,
    first_name     varchar(20)   null,
    last_name      varchar(25)   not null,
    email          varchar(25)   not null,
    phone_number   varchar(20)   null,
    hire_date      date          not null,
    job_id         varchar(10)   not null,
    salary         double(8, 2)  null,
    commission_pct double(2, 2)  null,
    manager_id     int           null,
    department_id  int           null,
    constraint emp_email_uk unique (email),
    constraint emp_emp_id_pk unique (employee_id)
);

create table departments
(
    department_id   int default 0 not null primary key,
    department_name varchar(30)   not null,
    manager_id      int           null,
    location_id     int           null,
    constraint dept_id_pk unique (department_id)
);

9.6.3.1 table

不论我们的查询语句有多复杂,里边包含了多少个表 ,到最后也是需要对每个表进行 单表访问 的。

所以MySQL规定EXPLAIN语句输出的每条记录都对应着某个单表的访问方法,该条记录的table列代表着该表的表名

(有时不是真实的表名字,可能是简称)。

9.6.3.2 id

一个SQL中,会拆分成多个子语句,id表示先后执行顺序。

  • id如果相同,可以认为是一组,从上往下顺序执行
  • 在所有组中,id值越大,优先级越高,越先执行
  • 关注点:id号每个号码,表示一趟独立的查询, 一个sql的查询趟数越少越好

9.6.3.3 select_type

名称

描述

SIMPLE

Simple SELECT (not using UNION or subqueries)

PRIMARY

Outermost SELECT

UNION

Second or later SELECT statement in a UNION

UNION RESULT

Result of a UNION

SUBQUERY

First SELECT in subquery

DEPENDENT SUBQUERY

First SELECT in subquery, dependent on outer query

DEPENDENT UNION

Second or later SELECT statement in a UNION, dependent on outer query

DERIVED

Derived table

MATERIALIZED

Materialized subquery

UNCACHEABLE SUBQUERY

A subquery for which the result cannot be cached and must be re-evaluated foreach row of the outer query

UNCACHEABLE UNION

The second or later select in a UNION that belongs to an uncacheable subquery

(see UNCACHEABLE SUBQUERY)

  • SIMPLE 简单查询

  • PRIMARY
  • UNION
  • UNION RESULT

  • SUBQUERY

  • DEPENDENT SUBQUERY

  • DEPENDENT UNION

  • DERIVED

  • MATERIALIZED

  • UNCACHEABLE SUBQUERY

(不常用,略)

  • UNCACHEABLE UNION

(不常用,略)

9.6.3.4 partition

创建分区表

CREATE TABLE user_partitions
(
    id   INT PRIMARY KEY auto_increment,
    NAME VARCHAR(12)
)PARTITION BY RANGE (id)( PARTITION p0 VALUES less than (100), PARTITION p1 VALUES less than MAXVALUE);

 9.6.3.5 type ★

完整的访问类型如下: system , const , eq_ref , ref , fulltext , ref_or_null ,index_merge ,unique_subquery , index_subquery , range , index , ALL

结果值从最好到最坏依次是:

system > const > eq_ref > ref > fulltext > ref_or_null > index_merge >unique_subquery > index_subquery > range > index > ALL

SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,最好是 consts级别。

  • system 直接使用系统值
CREATE TABLE t(i int) Engine=MyISAM;
INSERT INTO t VALUES(1);
INSERT INTO t VALUES(2);

 因为MyISAM存储引擎会将记录数保存至磁盘中,可以直接获取到表记录数。

  • const 转换为常量

  • eq_ref 通过主键的等值匹配

从执行计划的结果中可以看出,MySQL打算将s2作为驱动表,s1作为被驱动表,重点关注s1的访问方法是 eq_ref ,表明在访问s1表的时候可以 通过主键的等值匹配 来进行访问。

  • ref 通过索引查找,匹配单个索引值的行

  • fulltext

全文索引

  • ref_or_null 匹配单个索引或空值

  • index_merge 索引合并

  • unique_subquery

(暂时没造出相关语句)

  • index_subquery

(暂时没造出相关语句)

  • range 索引范围查询

  • index 使用索引(索引就能返回结果,不需要回表)

  • ALL 全表扫

 9.6.3.6 possible_key、key

可能使用的索引、和最终使用的索引

 9.6.3.7 key_len

使用的索引长度。常见的列类型长度计算:

 9.6.3.8 ref

在key列记录的索引中查找值所用的列或常量。

 9.6.3.9 rows

为了找到所需的行而需要读取的行数,估算值不精确.

9.6.3.10 filtered返回结果的行数占需读取行数的百分比。Filtered列的值越大越好。

 9.6.3.11 extra

一些补充信息:

  • Impossible WHERE
  • Using where 使用了where条件

  • No matching min/max row
  • Using index 使用索引返回结果(不用回表)

  • Using index condition 使用索引条件

  • Using join buffer (Block Nested Loop)
  • Not exists
  • Using filesort 使用了文件排序(非主键排序都会有)

  • Using temporary 使用了临时表

9.6.3.12 小结

  • EXPLAIN不考虑各种Cache
  • EXPLAIN不能显示MySQL在执行查询时所作的优化工作
  • EXPLAIN不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况
  • 部分统计信息是估算的,并非精确值

9.7 explain的进一步使用

EXPLAIN可以输出四种格式: 传统格式 , JSON格式 , TREE格式 以及 可视化输出 。用户可以根据需要选择适用于自己的格式。

9.7.1 传统格式

表格形式,概要说明查询计划。

9.7.2 JSON格式

#JSON格式:在EXPLAIN单词和真正的查询语句中间加上 FORMAT=JSON 。
explain format=json select * from employees e inner join departments d on e.department_id = d.department_id;
{
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "47.27"
    },
    "nested_loop": [
      {
        "table": {
          "table_name": "d",
          "access_type": "ALL",
          "possible_keys": [
            "PRIMARY",
            "dept_id_pk"
          ],
          "rows_examined_per_scan": 27,
          "rows_produced_per_join": 27,
          "filtered": "100.00",
          "cost_info": {
            "read_cost": "0.25",
            "eval_cost": "2.70",
            "prefix_cost": "2.95",
            "data_read_per_join": "2K"
          },
          "used_columns": [
            "department_id",
            "department_name",
            "manager_id",
            "location_id"
          ]
        }
      },
      {
        "table": {
          "table_name": "e",
          "access_type": "ref",
          "possible_keys": [
            "emp_dept_fk"
          ],
          "key": "emp_dept_fk",
          "used_key_parts": [
            "department_id"
          ],
          "key_length": "5",
          "ref": [
            "atguigudb.d.department_id"
          ],
          "rows_examined_per_scan": 8,
          "rows_produced_per_join": 240,
          "filtered": "100.00",
          "cost_info": {
            "read_cost": "20.25",
            "eval_cost": "24.08",
            "prefix_cost": "47.28",
            "data_read_per_join": "80K"
          },
          "used_columns": [
            "employee_id",
            "first_name",
            "last_name",
            "email",
            "phone_number",
            "hire_date",
            "job_id",
            "salary",
            "commission_pct",
            "manager_id",
            "department_id"
          ]
        }
      }
    ]
  }
}

cost_info部分解释:

  • read_cost 是由下边这两部分组成的:
    • IO 成本检测
    • rows × (1 - filter) 条记录的 CPU 成本
  • eval_cost 是这样计算的:检测 rows × filter 条记录的成本。
  • prefix_cost 就是单独查询 d 表的成本,也就是:read_cost + eval_cost
  • data_read_per_join 表示在此次查询中需要读取的数据量。

9.7.3 TREE格式

9.7.4 可视化输出

可视化输出,可以通过MySQL Workbench可视化查看MySQL的执行计划。通过点击Workbench的放大镜图标,即可生成可视化的查询计划。

9.8 分析优化器执行计划:trace

SET optimizer_trace="enabled=on",end_markers_in_json=on;

set optimizer_trace_max_mem_size=1000000;

开启后,可分析如下语句:

  • SELECT
  • INSERT
  • REPLACE
  • UPDATE
  • DELETE
  • EXPLAIN
  • SET
  • DECLARE
  • CASE
  • IF
  • RETURN
  • CALL

测试:执行如下SQL语句:

select * from employees where employees.employee_id < 10;

最后, 查询 information_schema.optimizer_trace 就可以知道MySQL是如何执行SQL的

mysql> select * from information_schema.optimizer_trace\G
*************************** 1. row ***************************
                            QUERY: select * from employees where employees.employee_id < 10
                            TRACE: {
  "steps": [
    {
      "join_preparation": {
        "select#": 1,
        "steps": [
          {
            "expanded_query": "/* select#1 */ select `employees`.`employee_id` AS `employee_id`,`employees`.`first_name` AS `first_name`,`employees`.`last_name` AS `last_name`,`employees`.`email` AS `email`,`employees`.`phone_number` AS `phone_number`,`employees`.`hire_date` AS `hire_date`,`employees`.`job_id` AS `job_id`,`employees`.`salary` AS `salary`,`employees`.`commission_pct` AS `commission_pct`,`employees`.`manager_id` AS `manager_id`,`employees`.`department_id` AS `department_id` from `employees` where (`employees`.`employee_id` < 10)"
          }
        ] /* steps */
      } /* join_preparation */
    },
    {
      "join_optimization": {
        "select#": 1,
        "steps": [
          {
            "condition_processing": {
              "condition": "WHERE",
              "original_condition": "(`employees`.`employee_id` < 10)",
              "steps": [
                {
                  "transformation": "equality_propagation",
                  "resulting_condition": "(`employees`.`employee_id` < 10)"
                },
                {
                  "transformation": "constant_propagation",
                  "resulting_condition": "(`employees`.`employee_id` < 10)"
                },
                {
                  "transformation": "trivial_condition_removal",
                  "resulting_condition": "(`employees`.`employee_id` < 10)"
                }
              ] /* steps */
            } /* condition_processing */
          },
          {
            "substitute_generated_columns": {
            } /* substitute_generated_columns */
          },
          {
            "table_dependencies": [
              {
                "table": "`employees`",
                "row_may_be_null": false,
                "map_bit": 0,
                "depends_on_map_bits": [
                ] /* depends_on_map_bits */
              }
            ] /* table_dependencies */
          },
          {
            "ref_optimizer_key_uses": [
            ] /* ref_optimizer_key_uses */
          },
          {
            "rows_estimation": [
              {
                "table": "`employees`",
                "range_analysis": {
                  "table_scan": {
                    "rows": 107,
                    "cost": 13.05
                  } /* table_scan */,
                  "potential_range_indexes": [
                    {
                      "index": "PRIMARY",
                      "usable": true,
                      "key_parts": [
                        "employee_id"
                      ] /* key_parts */
                    },
                    {
                      "index": "emp_email_uk",
                      "usable": true,
                      "key_parts": [
                        "email"
                      ] /* key_parts */
                    },
                    {
                      "index": "emp_emp_id_pk",
                      "usable": true,
                      "key_parts": [
                        "employee_id"
                      ] /* key_parts */
                    },
                    {
                      "index": "emp_dept_fk",
                      "usable": true,
                      "key_parts": [
                        "department_id",
                        "employee_id"
                      ] /* key_parts */
                    },
                    {
                      "index": "emp_job_fk",
                      "usable": true,
                      "key_parts": [
                        "job_id",
                        "employee_id"
                      ] /* key_parts */
                    },
                    {
                      "index": "emp_manager_fk",
                      "usable": true,
                      "key_parts": [
                        "manager_id",
                        "employee_id"
                      ] /* key_parts */
                    }
                  ] /* potential_range_indexes */,
                  "setup_range_conditions": [
                  ] /* setup_range_conditions */,
                  "group_index_range": {
                    "chosen": false,
                    "cause": "not_group_by_or_distinct"
                  } /* group_index_range */,
                  "skip_scan_range": {
                    "potential_skip_scan_indexes": [
                      {
                        "index": "PRIMARY",
                        "usable": false,
                        "cause": "query_references_nonkey_column"
                      },
                      {
                        "index": "emp_email_uk",
                        "usable": false,
                        "cause": "query_references_nonkey_column"
                      },
                      {
                        "index": "emp_emp_id_pk",
                        "usable": false,
                        "cause": "query_references_nonkey_column"
                      },
                      {
                        "index": "emp_dept_fk",
                        "usable": false,
                        "cause": "query_references_nonkey_column"
                      },
                      {
                        "index": "emp_job_fk",
                        "usable": false,
                        "cause": "query_references_nonkey_column"
                      },
                      {
                        "index": "emp_manager_fk",
                        "usable": false,
                        "cause": "query_references_nonkey_column"
                      }
                    ] /* potential_skip_scan_indexes */
                  } /* skip_scan_range */,
                  "analyzing_range_alternatives": {
                    "range_scan_alternatives": [
                      {
                        "index": "PRIMARY",
                        "ranges": [
                          "employee_id < 10"
                        ] /* ranges */,
                        "index_dives_for_eq_ranges": true,
                        "rowid_ordered": true,
                        "using_mrr": false,
                        "index_only": false,
                        "in_memory": 1,
                        "rows": 1,
                        "cost": 0.36,
                        "chosen": true
                      },
                      {
                        "index": "emp_emp_id_pk",
                        "ranges": [
                          "employee_id < 10"
                        ] /* ranges */,
                        "index_dives_for_eq_ranges": true,
                        "rowid_ordered": false,
                        "using_mrr": false,
                        "index_only": false,
                        "in_memory": 1,
                        "rows": 1,
                        "cost": 0.61,
                        "chosen": false,
                        "cause": "cost"
                      },
                      {
                        "index": "emp_dept_fk",
                        "chosen": false,
                        "cause": "no_valid_range_for_this_index"
                      },
                      {
                        "index": "emp_job_fk",
                        "chosen": false,
                        "cause": "no_valid_range_for_this_index"
                      },
                      {
                        "index": "emp_manager_fk",
                        "chosen": false,
                        "cause": "no_valid_range_for_this_index"
                      }
                    ] /* range_scan_alternatives */,
                    "analyzing_roworder_intersect": {
                      "usable": false,
                      "cause": "too_few_roworder_scans"
                    } /* analyzing_roworder_intersect */
                  } /* analyzing_range_alternatives */,
                  "chosen_range_access_summary": {
                    "range_access_plan": {
                      "type": "range_scan",
                      "index": "PRIMARY",
                      "rows": 1,
                      "ranges": [
                        "employee_id < 10"
                      ] /* ranges */
                    } /* range_access_plan */,
                    "rows_for_plan": 1,
                    "cost_for_plan": 0.36,
                    "chosen": true
                  } /* chosen_range_access_summary */
                } /* range_analysis */
              }
            ] /* rows_estimation */
          },
          {
            "considered_execution_plans": [
              {
                "plan_prefix": [
                ] /* plan_prefix */,
                "table": "`employees`",
                "best_access_path": {
                  "considered_access_paths": [
                    {
                      "rows_to_scan": 1,
                      "access_type": "range",
                      "range_details": {
                        "used_index": "PRIMARY"
                      } /* range_details */,
                      "resulting_rows": 1,
                      "cost": 0.46,
                      "chosen": true
                    }
                  ] /* considered_access_paths */
                } /* best_access_path */,
                "condition_filtering_pct": 100,
                "rows_for_plan": 1,
                "cost_for_plan": 0.46,
                "chosen": true
              }
            ] /* considered_execution_plans */
          },
          {
            "attaching_conditions_to_tables": {
              "original_condition": "(`employees`.`employee_id` < 10)",
              "attached_conditions_computation": [
              ] /* attached_conditions_computation */,
              "attached_conditions_summary": [
                {
                  "table": "`employees`",
                  "attached": "(`employees`.`employee_id` < 10)"
                }
              ] /* attached_conditions_summary */
            } /* attaching_conditions_to_tables */
          },
          {
            "finalizing_table_conditions": [
              {
                "table": "`employees`",
                "original_table_condition": "(`employees`.`employee_id` < 10)",
                "final_table_condition   ": "(`employees`.`employee_id` < 10)"
              }
            ] /* finalizing_table_conditions */
          },
          {
            "refine_plan": [
              {
                "table": "`employees`"
              }
            ] /* refine_plan */
          }
        ] /* steps */
      } /* join_optimization */
    },
    {
      "join_execution": {
        "select#": 1,
        "steps": [
        ] /* steps */
      } /* join_execution */
    }
  ] /* steps */
}
MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0
          INSUFFICIENT_PRIVILEGES: 0

9.9 Mysql监控分析视图:sys-schema

9.9.1 sys schema视图摘要

1. 主机相关:以host_summary开头,主要汇总了IO延迟的信息。

2. Innodb相关:以innodb开头,汇总了innodb buffer信息和事务等待innodb锁的信息。

3. I/o相关:以io开头,汇总了等待I/O、I/O使用量情况。

4. 内存使用情况:以memory开头,从主机、线程、事件等角度展示内存的使用情况

5. 连接与会话信息:processlist和session相关视图,总结了会话相关信息。

6. 表相关:以schema_table开头的视图,展示了表的统计信息。

7. 索引信息:统计了索引的使用情况,包含冗余索引和未使用的索引情况。

8. 语句相关:以statement开头,包含执行全表扫描、使用临时表、排序等的语句信息。

9. 用户相关:以user开头的视图,统计了用户使用的文件I/O、执行语句统计信息。

10. 等待事件相关信息:以wait开头,展示等待事件的延迟情况。

9.9.1 sys schema视图使用场景

索引情况

#1. 查询冗余索引
select * from sys.schema_redundant_indexes;
#2. 查询未使用过的索引
select * from sys.schema_unused_indexes;
#3. 查询索引的使用情况
select index_name,rows_selected,rows_inserted,rows_updated,rows_deleted from sys.schema_index_statistics where table_schema='dbname' ;

表相关

# 1. 查询表的访问量
select table_schema,table_name,sum(io_read_requests+io_write_requests) as io from sys.schema_table_statistics group by table_schema,table_name order by io desc;
# 2. 查询占用bufferpool较多的表
select object_schema,object_name,allocated,data from sys.innodb_buffer_stats_by_table order by allocated limit 10;
# 3. 查看表的全表扫描情况
select * from sys.statements_with_full_table_scans where db='dbname';

语句相关

#1. 监控SQL执行的频率
select db,exec_count,query from sys.statement_analysis order by exec_count desc;
#2. 监控使用了排序的SQL
select db,exec_count,first_seen,last_seen,query from sys.statements_with_sorting limit 1;
#3. 监控使用了临时表或者磁盘临时表的SQL
select db,exec_count,tmp_tables,tmp_disk_tables,query from sys.statement_analysis where tmp_tables>0 or tmp_disk_tables >0 order by (tmp_tables+tmp_disk_tables) desc;

IO相关

#1. 查看消耗磁盘IO的文件
select file,avg_read,avg_write,avg_read+avg_write as avg_io from sys.io_global_by_file_by_bytes order by avg_read limit 10;

Innodb 相关

#1. 行锁阻塞情况
select * from sys.innodb_lock_waits;

十、索引优化与查询优化

10.1 数据准备

建表:

CREATE TABLE `class` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `className` VARCHAR(30) DEFAULT NULL,
    `address` VARCHAR(40) DEFAULT NULL,
    `monitor` INT NULL ,
    PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE `student` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `stuno` INT NOT NULL ,
    `name` VARCHAR(20) DEFAULT NULL,
    `age` INT(3) DEFAULT NULL,
    `classId` INT(11) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

class表造10w记录,student表造50w记录。

10.2 索引失效案例

10.2.1 全值匹配

10.2.2 最佳左前缀法则

如果组合索引(a,b,c)

where a = 1 and b = 2 符合左前缀匹配,命中索引。

where c = 3 不符合左前缀匹配,无法命中索引。

10.2.3 主键插入顺序

 如果此时再插入一条主键值为 9 的记录,那它插入的位置就如下图:

 如果数据页已经满了,再插进来需要把当前页面分裂成两个页面,把本页中的一些记录移动到新创建的这个页中。页面分裂和记录移位意味着:性能损耗!需要避免这样无谓的性能损耗,最好让插入的记录的主键值依次递增 。

建议:让主键具有 AUTO_INCREMENT,让存储引擎自己为表生成主键。

10.2.4 计算、函数、类型转换导致索引失效

计算导致索引失效:

#索引失效
explain select * from student where id + 2 = 100;
# 索引生效
explain select * from student where id = 100 - 2;

 函数导致索引失效:

#创建索引:
CREATE INDEX idx_name ON student(NAME);

#索引生效
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%';
#索引失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc';

 类型转换导致索引失效

# 索引生效
explain select * from student where name = '98';
#索引失效
explain select * from student where name = 98;

 10.2.5不等于(!=或<>)索引失效

 10.2.6 is null可以使用索引,is not null无法使用索引

10.2.7 like 以通配符%开头索引失效

 10.2.8 OR前后存在非索引的列,索引失效

10.2.9 数据库和表的字符集统一使用utf8mb4

统一使用utf8mb4( 5.5.3版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码。不同的 字符集 进行比较前需要进行 转换 会造成索引失效。

10.3 关联查询优化

10.3.1 数据准备

10.3.2 采用左外连接

EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.car

 添加索引优化

ALTER TABLE book ADD INDEX Y ( card); #【被驱动表】,可以避免全表扫描
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;

 可以看到第二行的 type 变为了 ref,rows 也变成了优化比较明显。这是由左连接特性决定的。LEFT JOIN条件用于确定如何从右表搜索行,左边一定都有,所以 右边是我们的关键点,一定需要建立索引 。

ALTER TABLE `type` ADD INDEX X (card); #【驱动表】,无法避免全表扫描
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;

接着:

DROP INDEX Y ON book;
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;

10.3.3 采用内连接

drop index X on type;
drop index Y on book;(如果已经删除了可以不用再执行该操作)

换成 inner join(MySQL自动选择驱动表)

EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;

 添加索引优化

ALTER TABLE book ADD INDEX Y ( card);
EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;

ALTER TABLE type ADD INDEX X (card); 
EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;

 DROP INDEX X ON `type`; 
EXPLAIN SELECT SQL_NO_CACHE * FROM TYPE INNER JOIN book ON type.card=book.card;

ALTER TABLE `type` ADD INDEX X (card);
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` INNER JOIN book ON type.card=book.card;

10.3.4 join语句原理

EXPLAIN SELECT * FROM t1 STRAIGHT_JOIN t2 ON (t1.a=t2.a);

如果直接使用join语句,MySQL优化器可能会选择表t1或t2作为驱动表,这样会影响我们分析SQL语句的执行过程。所以,为了便于分析执行过程中的性能问题,我改用 straight_join 让MySQL使用固定的连接方式执行查询,这样优化器只会按照我们指定的方式去join。在这个语句里,t1 是驱动表,t2是被驱动表。

可以看到,在这条语句里,被驱动表t2的字段a上有索引,join过程用上了这个索引,因此这个语句的执行流程是这样的:

  1. 从表t1中读入一行数据 R;
  2. 从数据行R中,取出a字段到表t2里去查找;
  3. 取出表t2中满足条件的行,跟R组成一行,作为结果集的一部分;
  4. 重复执行步骤1到3,直到表t1的末尾循环结束。

这个过程是先遍历表t1,然后根据从表t1中取出的每行数据中的a值,去表t2中查找满足条件的记录。在形式上,这个过程就跟我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引,所以我们称之为“Index Nested-Loop Join”,简称NLJ。

在这个流程里:

  1. 对驱动表t1做了全表扫描,这个过程需要扫描100行;
  2. 而对于每一行R,根据a字段去表t2查找,走的是树搜索过程。由于我们构造的数据都是一一对应的,因此每次的搜索过程都只扫描一行,也是总共扫描100行;
  3. 所以,整个执行流程,总扫描行数是200。

两个结论:

  1. 使用join语句,性能比强行拆成多个单表执行SQL语句的性能要好;
  2. 如果使用join语句的话,需要让小表做驱动表

在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与join的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。

10.3.5 小结

  • 保证被驱动表的JOIN字段已经创建了索引
  • 需要JOIN 的字段,数据类型保持绝对一致。
  • LEFT JOIN 时,选择小表作为驱动表, 大表作为被驱动表 。减少外层循环的次数。
  • INNER JOIN 时,MySQL会自动将 小结果集的表选为驱动表 。选择相信MySQL优化策略。
  • 能够直接多表关联的尽量直接关联,不用子查询。(减少查询的趟数)
  • 不建议使用子查询,建议将子查询SQL拆开结合程序多次查询,或使用 JOIN 来代替子查询。
  • 衍生表建不了索引

10.4 子查询优化

MySQL从4.1版本开始支持子查询,使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结果作为另一个SELECT语句的条件。 子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作 。

子查询是 MySQL 的一项重要的功能,可以帮助我们通过一个 SQL 语句实现比较复杂的查询。但是,子查询的执行效率不高。原因:

  1. 执行子查询时,MySQL需要为内层查询语句的查询结果建立一个临时表 ,然后外层查询语句从临时表中查询记录。查询完毕后,再撤销这些临时表 。这样会消耗过多的CPU和IO资源,产生大量的慢查询。
  2. 子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表都不会存在索引 ,所以查询性能会受到一定的影响。
  3. 对于返回结果集比较大的子查询,其对查询性能的影响也就越大。

在MySQL中,可以使用连接(JOIN)查询来替代子查询。连接查询不需要建立临时表 ,其速度比子查询要快 ,如果查询中使用索引的话,性能就会更好。

结论:尽量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL替代

10.5 排序优化

10.5.1 排序优化

问题:在 WHERE 条件字段上加索引,但是为什么在 ORDER BY 字段上还要加索引呢?

优化建议:

  1. SQL 中,可以在 WHERE 子句和 ORDER BY 子句中使用索引,目的是在 WHERE 子句中 避免全表扫描 ,在 ORDER BY 子句 避免使用 FileSort 排序 。当然,某些情况下全表扫描,或者 FileSort 排序不一定比索引慢。但总的来说,我们还是要避免,以提高查询效率。
  2. 尽量使用 Index 完成 ORDER BY 排序。如果 WHERE 和 ORDER BY 后面是相同的列就使用单索引列;如果不同就使用联合索引。
  3. 无法使用 Index 时,需要对 FileSort 方式进行调优。
INDEX a_b_c(a,b,c)
order by 能使用索引最左前缀
- ORDER BY a
- ORDER BY a,b
- ORDER BY a,b,c
- ORDER BY a DESC,b DESC,c DESC
如果WHERE使用索引的最左前缀定义为常量,则order by 能使用索引
- WHERE a = const ORDER BY b,c
- WHERE a = const AND b = const ORDER BY c
- WHERE a = const ORDER BY b,c
- WHERE a = const AND b > const ORDER BY b,c
不能使用索引进行排序
- ORDER BY a ASC,b DESC,c DESC /* 排序不一致 */
- WHERE g = const ORDER BY b,c /*丢失a索引*/
- WHERE a = const ORDER BY c /*丢失b索引*/
- WHERE a = const ORDER BY a,d /*d不是索引的一部分*/
- WHERE a in (...) ORDER BY b,c /*对于排序来说,多个相等条件也是范围查询*/

10.5.2 filesort算法:双路排序和单路排序

双路排序 (慢)

  • MySQL 4.1之前是使用双路排序 ,字面意思就是两次扫描磁盘,最终得到数据, 读取行指针和order by列 ,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出
  • 从磁盘取排序字段,在buffer进行排序,再从 磁盘取其他字段 。

取一批数据,要对磁盘进行两次扫描,众所周知,IO是很耗时的,所以在mysql4.1之后,出现了第二种改进的算法,就是单路排序。

单路排序 (快)

从磁盘读取查询需要的 所有列 ,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出, 它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空间, 因为它把每一行都保存在内存中了。

优化策略

  1. 尝试提高 sort_buffer_size
  2. 尝试提高 max_length_for_sort_data
  3. Order by 时select * 是一个大忌。最好只Query需要的字段。

10.6 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、groupby、distinct这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。
  • 包含了order by、group by、distinct这些查询的语句,where条件过滤出来的结果集请保持在1000行以内,否则SQL会很慢。

10.7 优化分页查询

优化思路一

在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容

EXPLAIN SELECT * FROM student t,(SELECT id FROM student ORDER BY id LIMIT 2000000,10)a WHERE t.id = a.id;

转存失败重新上传取消

优化思路二

该方案适用于主键自增的表,可以把Limit 查询转换成某个位置的查询 。

EXPLAIN SELECT * FROM student WHERE id > 2000000 LIMIT 10;

转存失败重新上传取消

10.8 优先考虑覆盖索引

10.8.1 什么是覆盖索引?

理解方式一:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数据,那就不需要读取行了。一个索引包含了满足查询结果的数据就叫做覆盖索引。

理解方式二:非聚簇复合索引的一种形式,它包括在查询里的SELECT、JOIN和WHERE子句用到的所有列(即建索引的字段正好是覆盖查询条件中所涉及的字段)。

简单说就是, 索引列+主键 包含 SELECT 到 FROM之间查询的列 。

10.8.2 覆盖索引的利弊

好处:

1. 避免Innodb表进行索引的二次查询(回表)

2. 可以把随机IO变成顺序IO加快查询效率

弊端:

索引字段的维护 总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。这是业务DBA,或者称为业务数据架构师的工作。

10.9 给字符串添加索引

有一张教师表,表定义如下:

讲师要使用邮箱登录,所以业务代码中一定会出现类似于这样的语句:

create table teacher( ID bigint unsigned primary key, email varchar(64), ... )engine=innodb; mysql> select col1, col2 from teacher where email='xxx';

如果email这个字段上没有索引,那么这个语句就只能做 全表扫描 。

10.9.1 前缀索引

MySQL是支持前缀索引的。默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串。

mysql> alter table teacher add index index1(email); #或 mysql> alter table teacher add index index2(email(6));

这两种不同的定义在数据结构和存储上有什么区别呢?下图就是这两个索引的示意图。

 以及

 使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。前面已经讲过区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少。

10.9.2 前缀索引对覆盖索引的影响

使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是在选择是否使用前缀索引时需要考虑的一个因素。

10.10 索引下推

Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式。

ICP可以减少存储引擎访问基表的次数以及MySQL服务器访问存储引擎的次数。

10.10.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筛选 掉的数据的比例。

10.10.2 ICP的使用条件

ICP的使用条件:

  1. 只能用于二级索引(secondary index)
  2. explain显示的执行计划中type值(join 类型)为 range 、 ref 、 eq_ref 或者 ref_or_null 。
  3. 并非全部where条件都可以用ICP筛选,如果where条件的字段不在索引列中,还是要读取整表的记录到server端做where过滤。
  4. ICP可以用于MyISAM和InnnoDB存储引擎
  5. MySQL 5.6版本的不支持分区表的ICP功能,5.7版本的开始支持。
  6. 当SQL使用覆盖索引时,不支持ICP优化方法。

10.10.3 ICP使用案例

案例1

SELECT * FROM tuser
WHERE NAME LIKE '张%'
AND age = 10
AND ismale = 1;

 案例2

10.11 普通索引vs唯一索引

10.11.1 查询过程

假设,执行查询的语句是 select id from test where k=5。

  • 对于普通索引来说,查找到满足条件的第一个记录(5,500)后,需要查找下一个记录,直到碰到第一个不满足k=5条件的记录。
  • 对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。

那么,这个不同带来的性能差距会有多少呢?答案是, 微乎其微 。

10.11.2 更新过程

为了说明普通索引和唯一索引对更新语句性能的影响这个问题,介绍一下change buffer。

当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下, InooDB会将这些更新操作缓存在change buffer中 ,这样就不需要从磁盘中读入这个数据页了。

在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行changebuffer中与这个页有关的操作。

通过这种方式就能保证这个数据逻辑的正确性。将change buffer中的操作应用到原数据页,得到最新结果的过程称为 merge 。

除了 访问这个数据页 会触发merge外,系统有 后台线程会定期 merge。在 数据库正常关闭(shutdown) 的过程中,也会执行merge操作。

如果能够将更新操作先记录在change buffer, 减少读磁盘 ,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用 buffer pool 的,所以这种方式还能够 、避免占用内存 ,提高内存利用率。

唯一索引的更新就不能使用change buffer ,实际上也只有普通索引可以使用。如果要在这张表中插入一个新记录(4,400)的话,InnoDB的处理流程是怎样的?

10.11.3 change buffer的使用场景

  1. 普通索引和唯一索引应该怎么选择?其实,这两类索引在查询能力上是没差别的,主要考虑的是对 更新性能 的影响。所以,建议尽量选择普通索引 。
  2. 在实际使用中会发现, 普通索引 和 change buffer 的配合使用,对于 数据量大 的表的更新优化还是很明显的。
  3. 如果所有的更新后面,都马上 伴随着对这个记录的查询 ,那么应该关闭change buffer 。而在其他情况下,change buffer都能提升更新性能。
  4. 由于唯一索引用不上change buffer的优化机制,因此如果业务可以接受 ,从性能角度出发建议优先考虑非唯一索引。但是如果"业务可能无法确保"的情况下,怎么处理呢?
    1. 首先, 业务正确性优先 。我们的前提是“业务代码已经保证不会写入重复数据”的情况下,讨论性能问题。如果业务不能保证,或者业务就是要求数据库来做约束,那么没得选,必须创建唯一索引。这种情况下,本节的意义在于,如果碰上了大量插入数据慢、内存命中率低的时候,给你多提供一个排查思路。
    2. 然后,在一些“ 归档库 ”的场景,你是可以考虑使用唯一索引的。比如,线上数据只需要保留半年,然后历史数据保存在归档库。这时候,归档数据已经是确保没有唯一键冲突了。要提高归档效率,可以考虑把表里面的唯一索引改成普通索引。

10.12 其他查询优化策略

EXISTS和IN的区别

不太理解哪种情况下应该使用 EXISTS,哪种情况应该用 IN。选择的标准是看能否使用表的索引吗?

count(*)与 count(具体字段)效率

count(1)>=count(*)>count(具体字段)

关于select(*)

在表查询中,建议明确字段,不要使用 * 作为查询的字段列表,推荐使用SELECT 查询。原因:

① MySQL 在解析的过程中,会通过 查询数据字典 将"*"按序转换成所有列名,这会大大的耗费资源和时间。

② 无法使用 覆盖索引

limit 1对优化的影响

针对的是会扫描全表的 SQL 语句,如果你可以确定结果集只有一条,那么加上 LIMIT 1 的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。

如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上 LIMIT 1 了。

10.13 淘宝数据库主键如何设计

10.13.1 自增ID的问题

自增ID做主键,简单易懂,几乎所有数据库都支持自增类型,只是实现上各自有所不同而已。自增ID除了简单,其他都是缺点,总体来看存在以下几方面的问题:

  1. 可靠性不高:存在自增ID回溯的问题,这个问题直到最新版本的MySQL 8.0才修复。
  2. 安全性不高:对外暴露的接口可以非常容易猜测对应的信息。比如:/User/1/这样的接口,可以非常容易猜测用户ID的值为多少,总用户数量有多少,也可以非常容易地通过接口进行数据的爬取。
  3. 性能差:自增ID的性能较差,需要在数据库服务器端生成。
  4. 交互多:业务还需要额外执行一次类似 last_insert_id() 的函数才能知道刚才插入的自增值,这需要多一次的网络交互。在海量并发的系统中,多1条SQL,就多一次性能上的开销。
  5. 局部唯一性:最重要的一点,自增ID是局部唯一,只在当前数据库实例中唯一,而不是全局唯一,在任意服务器间都是唯一的。对于目前分布式系统来说,这简直就是噩梦。

10.13.2 业务字段作主键

建议尽量不要用跟业务有关的字段做主键。毕竟,作为项目设计的技术人员,我们谁也无法预测在项目的整个生命周期中,哪个业务字段会因为项目的业务需求而有重复,或者重用之类的情况出现。

10.13.3淘宝的主键设计

订单号不是自增ID!我们详细看下上述4个订单号:

1550672064762308113 1481195847180308113 1431156171142308113 1431146631521308113

订单号是19位的长度,且订单的最后5位都是一样的,都是08113。且订单号的前面14位部分是单调递增。大胆猜测,淘宝的订单ID设计应该是:

订单ID = 时间 + 去重字段 + 用户ID后6位尾号

10.13.4 推荐的主键设计

非核心业务 :对应表的主键自增ID,如告警、日志、监控等信息。

核心业务 :主键设计至少应该是全局唯一且是单调递增。全局唯一保证在各系统之间都是唯一的,单调递增是希望插入时不影响数据库性能。

这里推荐最简单的一种主键设计:UUID。

UUID的特点:全局唯一,占用36字节,数据无序,插入性能差

认识UUID:

  • 为什么UUID是全局唯一的?
  • 为什么UUID占用36个字节?
  • 为什么UUID是无序的?

MySQL数据库的UUID组成如下所示:

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函数进行转化:

SET @uuid = UUID();
SELECT @uuid,uuid_to_bin(@uuid),uuid_to_bin(@uuid,TRUE);

通过函数uuid_to_bin(@uuid,true)将UUID转化为有序UUID了。全局唯一 + 单调递增,这不就是我们想要的主键!

十一、数据库的设计规范

11.1 范式

11.1.1 范式简介

在关系型数据库中,关于数据表设计的基本原则、规则就称为范式。

可以理解为,一张数据表的设计结构需要满足的某种设计标准的级别 。要想设计一个结构合理的关系型数据库,必须满足一定的范式。

11.1.2 有哪些范式

目前关系型数据库有六种常见范式,按照范式级别,从低到高分别是:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、巴斯-科德范式(BCNF)、第四范式(4NF)和第五范式(5NF,又称完美范式)。

 11.1.3 键和相关属性的概念

举例,有两个表:

球员表(player) :球员编号 | 姓名 | 身份证号 | 年龄 | 球队编号

球队表(team) :球队编号 | 主教练 | 球队所在地

  • 超键 :对于球员表来说,超键就是包括球员编号或者身份证号的任意组合,比如(球员编号)(球员编号,姓名)(身份证号,年龄)等。
  • 候选键 :就是最小的超键,对于球员表来说,候选键就是(球员编号)或者(身份证号)。
  • 主键 :我们自己选定,也就是从候选键中选择一个,比如(球员编号)。
  • 外键 :球员表中的球队编号。
  • 主属性 、 非主属性 :在球员表中,主属性是(球员编号)(身份证号),其他的属性(姓名)(年龄)(球队编号)都是非主属性。

11.1.4 第一范式

表的每个属性必须具有原子(单个)值。

举例:user 表的设计不符合第一范式

 其中,user_info字段为用户信息,可以进一步拆分成更小粒度的字段,不符合数据库设计对第一范式的要求。将user_info拆分后如下:

 11.1.5 第二范式

每个表必须有主关键字(Primary key),其他数据元素与主关键字一一对应。

通常称这种关系为函数依赖(Functional dependence)关系,即表中其他数据元素都依赖于主关键字,或称该数据元素惟一地被主关键字所标识。

举例:

比赛表 player_game ,里面包含球员编号、姓名、年龄、比赛编号、比赛时间和比赛场地等属性,这里候选键和主键都为(球员编号,比赛编号),我们可以通过候选键(或主键)来决定如下的关系:

(球员编号, 比赛编号) → (姓名, 年龄, 比赛时间, 比赛场地,得分)

但是这个数据表不满足第二范式,因为数据表中的字段之间还存在着如下的对应关系:

(球员编号) → (姓名,年龄)
(比赛编号) → (比赛时间, 比赛场地)

对于非主属性来说,并非完全依赖候选键。这样会产生怎样的问题呢?

  1. 数据冗余 :如果一个球员可以参加 m 场比赛,那么球员的姓名和年龄就重复了 m-1 次。一个比赛也可能会有 n 个球员参加,比赛的时间和地点就重复了 n-1 次。
  2. 插入异常 :如果我们想要添加一场新的比赛,但是这时还没有确定参加的球员都有谁,那么就没法插入。
  3. 删除异常 :如果我要删除某个球员编号,如果没有单独保存比赛表的话,就会同时把比赛信息删除掉。
  4. 更新异常 :如果我们调整了某个比赛的时间,那么数据表中所有这个比赛的时间都需要进行调整,否则就会出现一场比赛时间不同的情况。

为了避免出现上述的情况,我们可以把球员比赛表设计为下面的三张表。

 11.1.6 第三范式

第三范式(Third Normal Form,3rd NF)就是指表中的所有数据元素不但要能唯一地被主关键字所标识,而且它们之间还必须相互独立,不存在其他的函数关系。

也就是说,对于一个满足2nd NF 的数据结构来说,表中有可能存在某些数据元素依赖于其他非关键字数据元素的现象,必须消除。

举例1:

部门信息表 :每个部门有部门编号(dept_id)、部门名称、部门简介等信息。

员工信息表 :每个员工有员工编号、姓名、部门编号。

列出部门编号后就不能再将部门名称、部门简介等与部门有关的信息再加入员工信息表中。如果不存在部门信息表,则根据第三范式(3NF)也应该构建它,否则就会有大量的数据冗余.

举例2:

11.2 反范式化

11.2.1 概述

  1. 为满足某种商业目标 , 数据库性能比规范化数据库更重要
  2. 在数据规范化的同时 , 要综合考虑数据库的性能
  3. 通过在给定的表中添加额外的字段,以大量减少需要从中搜索信息所需的时间
  4. 通过在给定的表中插入计算列,以方便查询

11.2.2 应用举例

员工的信息存储在 employees 表 中,部门信息存储在 departments 表 中。通过 employees 表中的department_id字段与 departments 表建立关联关系。如果要查询一个员工所在部门的名称:

select employee_id,department_name
from employees e join departments d
on e.department_id = d.department_id;

如果经常需要进行这个操作,连接查询就会浪费很多时间。可以在 employees 表中增加一个冗余字段department_name,这样就不用每次都进行连接操作了。

11.2.3 反范式的新问题

  • 存储空间变大了
  • 一个表中字段做了修改,另一个表中冗余的字段也需要做同步修改,否则数据不一致
  • 若采用存储过程来支持数据的更新、删除等额外操作,如果更新频繁,会非常消耗系统资源在数据量小的情况下,反范式不能体现性能的优势,可能还会让数据库的设计更加复杂

11.2.4 反范式的适用场景

1. 增加冗余字段的建议

当冗余信息有价值或者能 大幅度提高查询效率的时候,我们才会采取反范式的优化。

2. 历史快照、历史数据的需要

在现实生活中,我们经常需要一些冗余信息,比如订单中的收货人信息,包括姓名、电话和地址等。每次发生的 订单收货信息 都属于 历史快照 ,需要进行保存,但用户可以随时修改自己的信息,这时保存这些冗余信息是非常有必要的。

反范式优化也常用在 数据仓库 的设计中,因为数据仓库通常 存储历史数据 ,对增删改的实时性要求不强,对历史数据的分析需求强。这时适当允许数据的冗余度,更方便进行数据分析。

11.3 BCNF 巴斯范式

bcnf,全称为Boyce Codd Normal Form,中文叫巴斯范式/鲍依斯-科得范式,是由Boyce和Codd提出的,比3NF又进了一步,通常认为是修正的第三范式。

设关系模式R∈1NF,如果对于R的每个函数依赖X→Y,若Y不属于X,则X必含有超码,那么R∈BCNF。满足BCNF条件有:所有非主属性对每一个候选键都是完全函数依赖; 所有的主属性对每一个不包含它的候选键,也是完全函数依赖;没有任何属性完全函数依赖于非候选键的任何一组属性。

1. 案例

 在这个表中,一个仓库只有一个管理员,同时一个管理员也只管理一个仓库。

我们先来梳理下这些属性之间的依赖关系。

仓库名决定了管理员,管理员也决定了仓库名,同时(仓库名,物品名)的属性集合可以决定数量这个属性。

这样,我们就可以找到数据表的候选键。

候选键 :是(管理员,物品名)和(仓库名,物品名),然后我们从候选键中选择一个作为 主键 ,比如(仓库名,物品名)。

主属性 :包含在任一候选键中的属性,也就是仓库名,管理员和物品名。

非主属性 :数量这个属性。

2. 是否符合三范式

如何判断一张表的范式呢?我们需要根据范式的等级,从低到高来进行判断。

首先,数据表每个属性都是原子性的,符合 1NF 的要求;

其次,数据表中非主属性”数量“都与候选键全部依赖,(仓库名,物品名)决定数量,(管理员,物品名)决定数量。因此,数据表符合 2NF 的要求;

最后,数据表中的非主属性,不传递依赖于候选键。因此符合 3NF 的要求。

3. 存在的问题

既然数据表已经符合了 3NF 的要求,是不是就不存在问题了呢?我们来看下面的情况:

  1. 增加一个仓库,但是还没有存放任何物品。根据数据表实体完整性的要求,主键不能有空值,因此会出现 插入异常 ;
  2. 如果仓库更换了管理员,我们就可能会 修改数据表中的多条记录 ;
  3. 如果仓库里的商品都卖空了,那么此时仓库名称和相应的管理员名称也会随之被删除。

即便数据表符合 3NF 的要求,同样可能存在插入,更新和删除数据的异常情况。

4. 问题解决

首先需要确认造成异常的原因:主属性仓库名对于候选键(管理员,物品名)是部分依赖的关系,这样就有可能导致上面的异常情况。因此引入BCNF,它在 3NF 的基础上消除了主属性对候选键的部分依赖或者传递依赖关系。

根据 BCNF 的要求,我们需要把仓库管理关系 warehouse_keeper 表拆分成下面这样:

仓库表 :(仓库名,管理员)

库存表 :(仓库名,物品名,数量)

11.4 ER模型

11.4.1 基础概念

ER 模型中有三个要素,分别是实体、属性和关系。

实体 ,可以看做是数据对象,往往对应于现实生活中的真实存在的个体。在 ER 模型中,用 矩形来表示。实体分为两类,分别是 强实体 和 弱实体 。强实体是指不依赖于其他实体的实体;弱实体是指对另一个实体有很强的依赖关系的实体。

属性 ,则是指实体的特性。比如超市的地址、联系电话、员工数等。在 ER 模型中用 椭圆形 来表示。

关系 ,则是指实体之间的联系。比如超市把商品卖给顾客,就是一种超市与顾客之间的联系。在 ER 模型中用 菱形 来表示。注意:实体和属性不容易区分。这里提供一个原则:我们要从系统整体的角度出发去看,可以独立存在的是实体,不可再分的是属性。也就是说,属性不能包含其他属性。

11.4.2 关系的类型

在 ER 模型的 3 个要素中,关系又可以分为 3 种类型,分别是 一对一、一对多、多对多

一对一 :指实体之间的关系是一一对应的,比如个人与身份证信息之间的关系就是一对一的关系。一个人只能有一个身份证信息,一个身份证信息也只属于一个人。

一对多 :指一边的实体通过关系,可以对应多个另外一边的实体。相反,另外一边的实体通过这个关系,则只能对应唯一的一边的实体。比如说,我们新建一个班级表,而每个班级都有多个学生,每个学生则对应一个班级,班级对学生就是一对多的关系。

多对多 :指关系两边的实体都可以通过关系对应多个对方的实体。比如在进货模块中,供货商与超市之间的关系就是多对多的关系,一个供货商可以给多个超市供货,一个超市也可以从多个供货商那里采购商品。

11.4.3 建模分析

ER 模型看起来比较麻烦,但是对我们把控项目整体非常重要。如果你只是开发一个小应用,或许简单设计几个表够用了,一旦要设计有一定规模的应用,在项目的初始阶段,建立完整的 ER 模型就非常关键了。开发应用项目的实质,其实就是 建模 。

我们设计的案例是 电商业务 ,由于电商业务太过庞大且复杂,所以我们做了业务简化,比如针对SKU(StockKeepingUnit,库存量单位)和SPU(Standard Product Unit,标准化产品单元)的含义上,我们直接使用了SKU,并没有提及SPU的概念。本次电商业务设计总共有8个实体,如下所示。

  • 地址实体
  • 用户实体
  • 购物车实体
  • 评论实体
  • 商品实体
  • 商品分类实体
  • 订单实体
  • 订单详情实体

其中, 用户 和 商品分类 是强实体,因为它们不需要依赖其他任何实体。而其他属于弱实体,因为它们虽然都可以独立存在,但是它们都依赖用户这个实体,因此都是弱实体。知道了这些要素,我们就可以给电商业务创建 ER 模型了,如图:

11.4.4 ER模型的细化

有了这个 ER 模型,我们就可以从整体上 理解 电商的业务了。刚刚的 ER 模型展示了电商业务的框架,但是只包括了订单,地址,用户,购物车,评论,商品,商品分类和订单详情这八个实体,以及它们之间的关系,还不能对应到具体的表,以及表与表之间的关联。我们需要把 属性加上 ,用 椭圆 来表示,这样我们得到的 ER 模型就更加完整了。

11.4.5 ER模型图转换成数据表

通过绘制 ER 模型,我们已经理清了业务逻辑,现在,我们就要进行非常重要的一步了:

把绘制好的 ER模型,转换成具体的数据表,下面介绍下转换的原则:

  1. 一个 实体 通常转换成一个 数据表 ;
  2. 一个 多对多的关系 ,通常也转换成一个 数据表 ;
  3. 一个 1 对 1 ,或者 1 对多 的关系,往往通过表的 外键 来表达,而不是设计一个新的数据表;
  4. 属性 转换成表的 字段 。

11.5 数据表的设计原则

综合以上内容,总结出数据表设计的一般原则:"三少一多"

  1. 数据表的个数越少越好
  2. 数据表中的字段个数越少越好
  3. 数据表中联合主键的字段个数越少越好
  4. 使用主键和外键越多越好

11.6 数据库对象编写建议

11.6.1 关于库

  1. 【强制】库的名称必须控制在32个字符以内,只能使用英文字母、数字和下划线,建议以英文字母开头。
  2. 【强制】库名中英文 一律小写 ,不同单词采用 下划线 分割。须见名知意。
  3. 【强制】库的名称格式:业务系统名称_子系统名。
  4. 【强制】库名禁止使用关键字(如type,order等)。
  5. 【强制】创建数据库时必须 显式指定字符集 ,并且字符集只能是utf8或者utf8mb4。创建数据库SQL举例:CREATE DATABASE crm_fund DEFAULT CHARACTER SET 'utf8' ;
  6. 【建议】对于程序连接数据库账号,遵循 权限最小原则使用数据库账号只能在一个DB下使用,不准跨库。程序使用的账号原则上不准有drop权限 。
  7. 【建议】临时库以 tmp_ 为前缀,并以日期为后缀;备份库以 bak_ 为前缀,并以日期为后缀。

11.6.2 关于表、列

  1. 【强制】表和列的名称必须控制在32个字符以内,表名只能使用英文字母、数字和下划线,建议以 英文字母开头 。
  2. 【强制】 表名、列名一律小写 ,不同单词采用下划线分割。须见名知意。
  3. 【强制】表名要求有模块名强相关,同一模块的表名尽量使用 统一前缀 。比如:crm_fund_item
  4. 【强制】创建表时必须 显式指定字符集 为utf8或utf8mb4。
  5. 【强制】表名、列名禁止使用关键字(如type,order等)
  6. 【强制】创建表时必须 显式指定表存储引擎 类型。如无特殊需求,一律为InnoDB。
  7. 【强制】建表必须有comment。
  8. 【强制】字段命名应尽可能使用表达实际含义的英文单词或 缩写 。如:公司 ID,不要使用corporation_id, 而用corp_id 即可。
  9. 【强制】布尔值类型的字段命名为 is_描述 。如member表上表示是否为enabled的会员的字段命名为 is_enabled。
  10. 【强制】禁止在数据库中存储图片、文件等大的二进制数据通常文件很大,短时间内造成数据量快速增长,数据库进行数据库读取时,通常会进行大量的随机IO操作,文件很大时,IO操作很耗时。通常存储于文件服务器,数据库只存储文件地址信息。
  11. 【建议】建表时关于主键: 表必须有主键
    1. 强制要求主键为id,类型为int或bigint,且为auto_increment 建议使用unsigned无符号型。
    2. 标识表里每一行主体的字段不要设为主键,建议设为其他字段如user_id,order_id等,并建立unique key索引。因为如果设为主键且主键值为随机插入,则会导致innodb内部页分裂和大量随机I/O,性能下降
  12. 【建议】核心表(如用户表)必须有行数据的 创建时间字段 (create_time)和 最后更新时间字段(update_time),便于查问题。
  13. 【建议】表中所有字段尽量都是 NOT NULL 属性,业务可以根据需要定义 DEFAULT值 。 因为使用NULL值会存在每一行都会占用额外存储空间、数据迁移容易出错、聚合函数计算结果偏差等问题。1
  14. 【建议】所有存储相同数据的 列名和列类型必须一致 (一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)。
  15. 【建议】中间表(或临时表)用于保留中间结果集,名称以 tmp_ 开头。备份表用于备份或抓取源表快照,名称以 bak_ 开头。中间表和备份表定期清理。

11.6.3 关于索引

  1. 【强制】InnoDB表必须主键为id int/bigint auto_increment,且主键值 禁止被更新 。
  2. 【强制】InnoDB和MyISAM存储引擎表,索引类型必须为 BTREE
  3. 【建议】主键的名称以 pk_ 开头,唯一键以 uni_ 或 uk_ 开头,普通索引以 idx_ 开头,一律使用小写格式,以字段的名称或缩写作为后缀。
  4. 【建议】多单词组成的columnname,取前几个单词首字母,加末单词组成column_name。如:sample 表 member_id 上的索引:idx_sample_mid。
  5. 【建议】单个表上的索引个数 不能超过6个 。
  6. 【建议】在建立索引时,多考虑建立 联合索引 ,并把区分度最高的字段放在最前面。
  7. 【建议】在多表 JOIN 的SQL里,保证被驱动表的连接列上有索引,这样JOIN 执行效率最高。
  8. 【建议】建表或加索引时,保证表里互相不存在 冗余索引 。 比如:如果表里已经存在key(a,b),则key(a)为冗余索引,需要删除。

11.6.4 SQL编写

  1. 【强制】程序端SELECT语句必须指定具体字段名称,禁止写成 *。
  2. 【建议】程序端insert语句指定具体字段名称,不要写成INSERT INTO t1 VALUES(…)。
  3. 【建议】除静态表或小表(100行以内),DML语句必须有WHERE条件,且使用索引查找。
  4. 【建议】INSERT INTO…VALUES(XX),(XX),(XX).. 这里XX的值不要超过5000个。 值过多虽然上线很快,但会引起主从同步延迟。
  5. 【建议】SELECT语句不要使用UNION,推荐使用UNION ALL,并且UNION子句个数限制在5个以内。
  6. 【建议】线上环境,多表 JOIN 不要超过5个表。
  7. 【建议】减少使用ORDER BY,和业务沟通能不排序就不排序,或将排序放到程序端去做。ORDERBY、GROUP BY、DISTINCT 这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。
  8. 【建议】包含了ORDER BY、GROUP BY、DISTINCT 这些查询的语句,WHERE 条件过滤出来的结果集请保持在1000行以内,否则SQL会很慢。
  9. 【建议】对单表的多次alter操作必须合并为一次对于超过100W行的大表进行alter table,必须经过DBA审核,并在业务低峰期执行,多个alter需整合在一起。 因为alter table会产生 表锁 ,期间阻塞对于该表的所有写入,对于业务可能会产生极大影响。
  10. 【建议】批量操作数据时,需要控制事务处理间隔时间,进行必要的sleep。
  11. 【建议】事务里包含SQL不超过5个。因为过长的事务会导致锁数据较久,MySQL内部缓存、连接消耗过多等问题。
  12. 【建议】事务里更新语句尽量基于主键或UNIQUE KEY,如UPDATE… WHERE id=XX;

十二、数据库其他调优策略

12.1 数据库调优的策略

12.1.1 调优的目标

  • 尽可能节省系统资源 ,以便系统可以提供更大负荷的服务。(吞吐量更大)
  • 合理的结构设计和参数调整,以提高用户操作 响应的速度 。(响应速度更快)
  • 减少系统的瓶颈,提高MySQL数据库整体的性能。

12.1.2 如何定位调优问题

如何确定呢?一般情况下,有如下几种方式:

  • 用户的反馈(主要)
  • 日志分析(主要)
  • 服务器资源使用监控
  • 数据库内部状况监控
  • 其它

除了活动会话监控以外,我们也可以对 事务 、 锁等待 等进行监控,这些都可以帮助我们对数据库的运行状态有更全面的认识。

12.1.3 调优的维度和步骤

我们需要调优的对象是整个数据库管理系统,它不仅包括 SQL 查询,还包括数据库的部署配置、架构等。从这个角度来说,我们思考的维度就不仅仅局限在 SQL 优化上了。通过如下的步骤我们进行梳理:

  1. 选择适合的 DBMS
  2. 优化表设计
  3. 优化逻辑查询
  4. 优化物理查询
    1. 物理查询优化是在确定了逻辑查询优化之后,采用物理优化技术(比如索引等),通过计算代价模型对各种可能的访问路径进行估算,从而找到执行方式中代价最小的作为执行计划。在这个部分中,我们需要掌握的重点是对索引的创建和使用。
  5. 使用 Redis 或 Memcached 作为缓存
  6. 库级优化

读写分离

 数据分片

12.2 优化Mysql服务器

12.2.1 优化服务器硬件

服务器的硬件性能直接决定着MySQL数据库的性能。硬件的性能瓶颈直接决定MySQL数据库的运行速度和效率。针对性能瓶颈提高硬件配置,可以提高MySQL数据库查询、更新的速度。

  1. 配置较大的内存
  2. 配置高速磁盘系统
  3. 合理分布磁盘I/O
  4. 配置多处理器

12.2.2 优化Mysql的参数

  • innodb_buffer_pool_size :这个参数是Mysql数据库最重要的参数之一,表示InnoDB类型的 表和索引的最大缓存 。它不仅仅缓存索引数据 ,还会缓存表的数据 。这个值越大,查询的速度就会越快。但是这个值太大会影响操作系统的性能。
  • key_buffer_size :表示索引缓冲区的大小 。索引缓冲区是所有的线程共享 。增加索引缓冲区可以得到更好处理的索引(对所有读和多重写)。当然,这个值不是越大越好,它的大小取决于内存的大小。如果这个值太大,就会导致操作系统频繁换页,也会降低系统性能。对于内存在 4GB 左右的服务器该参数可设置为 256M 或 384M 。
  • table_cache :表示 同时打开的表的个数 。这个值越大,能够同时打开的表的个数越多。物理内存越大,设置就越大。默认为2402,调到512-1024最佳。这个值不是越大越好,因为同时打开的表太多会影响操作系统的性能。
  • query_cache_size :表示 查询缓冲区的大小 。可以通过在MySQL控制台观察,如果Qcache_lowmem_prunes的值非常大,则表明经常出现缓冲不够的情况,就要增加Query_cache_size的值;如果Qcache_hits的值非常大,则表明查询缓冲使用非常频繁,如果该值较小反而会影响效率,那么可以考虑不用查询缓存;Qcache_free_blocks,如果该值非常大,则表明缓冲区中碎片很多。MySQL8.0之后失效。该参数需要和query_cache_type配合使用。
  • query_cache_type 的值是0时,所有的查询都不使用查询缓存区。但是query_cache_type=0并不会导致MySQL释放query_cache_size所配置的缓存区内存。
    • 当query_cache_type=1时,所有的查询都将使用查询缓存区,除非在查询语句中指定SQL_NO_CACHE ,如SELECT SQL_NO_CACHE * FROM tbl_name。
    • 当query_cache_type=2时,只有在查询语句中使用 SQL_CACHE 关键字,查询才会使用查询缓存区。使用查询缓存区可以提高查询的速度,这种方式只适用于修改操作少且经常执行相同的查询操作的情况。
  • sort_buffer_size :表示每个需要进行排序的线程分配的缓冲区的大小 。增加这个参数的值可以提高 ORDER BY 或 GROUP BY 操作的速度。默认数值是2 097 144字节(约2MB)。对于内存在4GB左右的服务器推荐设置为6-8M,如果有100个连接,那么实际分配的总共排序缓冲区大小为100 × 6= 600MB。
  • join_buffer_size = 8M :表示 联合查询操作所能使用的缓冲区大小 ,和sort_buffer_size一样,该参数对应的分配内存也是每个连接独享。
  • read_buffer_size :表示 每个线程连续扫描时为扫描的每个表分配的缓冲区的大小(字节) 。当线程从表中连续读取记录时需要用到这个缓冲区。SET SESSION read_buffer_size=n可以临时设置该参数的值。默认为64K,可以设置为4M。
  • innodb_flush_log_at_trx_commit :表示 何时将缓冲区的数据写入日志文件 ,并且将日志文件写入磁盘中。该参数对于innoDB引擎非常重要。该参数有3个值,分别为0、1和2。该参数的默认值为1。
    • 值为 0 时,表示 每秒1次 的频率将数据写入日志文件并将日志文件写入磁盘。每个事务的commit并不会触发前面的任何操作。该模式速度最快,但不太安全,mysqld进程的崩溃会导致上一秒钟所有事务数据的丢失。
    • 值为 1 时,表示 每次提交事务时 将数据写入日志文件并将日志文件写入磁盘进行同步。该模式是最安全的,但也是最慢的一种方式。因为每次事务提交或事务外的指令都需要把日志写入(flush)硬盘。
    • 值为 2 时,表示 每次提交事务时 将数据写入日志文件, 每隔1秒 将日志文件写入磁盘。该模式速度较快,也比0安全,只有在操作系统崩溃或者系统断电的情况下,上一秒钟所有事务数据才可能丢失。
  • innodb_log_buffer_size :这是 InnoDB 存储引擎的 事务日志所使用的缓冲区 。为了提高性能,也是先将信息写入 Innodb Log Buffer 中,当满足 innodb_flush_log_trx_commit 参数所设置的相应条件(或者日志缓冲区写满)之后,才会将日志写到文件(或者同步到磁盘)中。
  • max_connections :表示 允许连接到MySQL数据库的最大数量 ,默认值是 151 。如果状态变量connection_errors_max_connections 不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这是可以考虑增大max_connections 的值。在Linux 平台下,性能好的服务器,支持 500-1000 个连接不是难事,需要根据服务器性能进行评估设定。这个连接数 不是越大越好 ,因为这些连接会浪费内存的资源。过多的连接可能会导致MySQL服务器僵死。
  • back_log :用于 控制MySQL监听TCP端口时设置的积压请求栈大小 。如果MySql的连接数达到max_connections时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即back_log,如果等待连接的数量超过back_log,将不被授予连接资源,将会报错。5.6.6 版本之前默认值为 50 , 之后的版本默认为 50 + (max_connections / 5), 对于Linux系统推荐设置为小于512的整数,但最大不超过900。如果需要数据库在较短的时间内处理大量连接请求, 可以考虑适当增大back_log 的值。
  • thread_cache_size : 线程池缓存线程数量的大小 ,当客户端断开连接后将当前线程缓存起来,当在接到新的连接请求时快速响应无需创建新的线程 。这尤其对那些使用短连接的应用程序来说可以极大的提高创建连接的效率。那么为了提高性能可以增大该参数的值。默认为60,可以设置为120。可以通过如下几个MySQL状态值来适当调整线程池的大小:
mysql> show global status like 'Thread%';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_cached | 2 |
| Threads_connected | 1 |
| Threads_created | 3 |
| Threads_running | 2 |
+-------------------+-------+
4 rows in set (0.01 sec)
  • wait_timeout :指定 一个请求的最大连接时间 ,对于4GB左右内存的服务器可以设置为5-10。
  • interactive_timeout :表示服务器在关闭连接前等待行动的秒数。

12.3 优化数据库结构

12.3.1 拆分数据:冷热数据分离

举例1: 会员members表 存储会员登录认证信息,该表中有很多字段,如id、姓名、密码、地址、电话、个人描述字段。其中地址、电话、个人描述等字段并不常用,可以将这些不常用的字段分解出另一个表。将这个表取名叫members_detail,表中有member_id、address、telephone、description等字段。这样就把会员表分成了两个表,分别为 members表 和 members_detail表 。

如果需要查询会员的基本信息或详细信息,那么可以用会员的id来查询。如果需要将会员的基本信息和详细信息同时显示,那么可以将members表和members_detail表进行联合查询.

12.3.2 增加中间表

如果有一个模块需要经常查询两张表的联合部分,可以建立一张表存储到临时表中,然后查询临时表。(不推荐)

12.3.3 增减冗余字段

设计数据库表时应尽量遵循范式理论的规约,尽可能减少冗余字段,让数据库设计看起来精致、优雅。

但是,合理地加入冗余字段可以提高查询速度。表的规范化程度越高,表与表之间的关系就越多,需要连接查询的情况也就越多。尤其在数据量大,而且需要频繁进行连接的时候,为了提升效率,我们也可以考虑增加冗余字段来减少连接。

12.3.4 优化数据类型

  • 对整数类型数据进行优化。

遇到整数类型的字段可以用 INT 型 。这样做的理由是,INT 型数据有足够大的取值范围,不用担心数据超出取值范围的问题。刚开始做项目的时候,首先要保证系统的稳定性,这样设计字段类型是可以的。但在数据量很大的时候,数据类型的定义,在很大程度上会影响到系统整体的执行效率。

对于 非负型 的数据(如自增ID、整型IP)来说,要优先使用无符号整型 UNSIGNED 来存储。因为无符号相对于有符号,同样的字节数,存储的数值范围更大。如tinyint有符号为-128-127,无符号为0-255,多出一倍的存储空间。

  • 既可以使用文本类型也可以使用整数类型的字段,要选择使用整数类型。

跟文本类型数据相比,大整数往往占用 更少的存储空间 ,因此,在存取和比对的时候,可以占用更少的内存空间。所以,在二者皆可用的情况下,尽量使用整数类型,这样可以提高查询的效率。如:将IP地址转换成整型数据。

  • 避免使用TEXT、BLOB数据类型
  • 避免使用ENUM类型情况
  • 使用TIMESTAMP存储时间情况
  • 用DECIMAL代替FLOAT和DOUBLE存储精确浮点数

总之,遇到数据量大的项目时,一定要在充分了解业务需求的前提下,合理优化数据类型,这样才能充分发挥资源的效率,使系统达到最优。

12.3.5 优化插入记录的速度

  1. MyISAM引擎的表:
    1. 禁用索引
    2. 禁用唯一性检查
    3. 使用批量插入
    4. 使用LOAD DATA INFILE 批量导入
  2. InnoDB引擎的表:
    1. 禁用唯一性检查
    2. 禁用外键检查
    3. 禁止自动提交

12.3.6 使用非空约束

在设计字段的时候,如果业务允许,建议尽量使用非空约束

12.3.7 分析表、检查表、优化表

12.3.7.1 分析表

MySQL中提供了ANALYZE TABLE语句分析表,ANALYZE TABLE语句的基本语法如下:

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 表名

12.3.7.2 检查表

MySQL中可以使用 CHECK TABLE 语句来检查表。CHECK TABLE语句能够检查InnoDB和MyISAM类型的表是否存在错误。CHECK TABLE语句在执行过程中也会给表加上只读锁 。

对于MyISAM类型的表,CHECK TABLE语句还会更新关键字统计数据。而且,CHECK TABLE也可以检查视图是否有错误,比如在视图定义中被引用的表已不存在。该语句的基本语法如下:

CHECK TABLE tbl_name [, tbl_name] ... [option] ... 
option = {QUICK | FAST | MEDIUM | EXTENDED | CHANGED}

其中,tbl_name是表名;option参数有5个取值,分别是QUICK、FAST、MEDIUM、EXTENDED和CHANGED。各个选项的意义分别是:

  • QUICK :不扫描行,不检查错误的连接。
  • FAST :只检查没有被正确关闭的表。
  • CHANGED :只检查上次检查后被更改的表和没有被正确关闭的表。
  • MEDIUM :扫描行,以验证被删除的连接是有效的。也可以计算各行的关键字校验和,并使用计算出的校验和验证这一点。
  • EXTENDED :对每行的所有关键字进行一个全面的关键字查找。这可以确保表是100%一致的,但是花的时间较长。

option只对MyISAM类型的表有效,对InnoDB类型的表无效。比如:

转存失败重新上传取消

该语句对于检查的表可能会产生多行信息。最后一行有一个状态的 Msg_type 值,Msg_text 通常为 OK。如果得到的不是 OK,通常要对其进行修复;是 OK 说明表已经是最新的了。表已经是最新的,意味着存储引擎对这张表不必进行检查。

12.3.7.3 优化表

方式:OPTIMIZE TABLE

MySQL中使用 OPTIMIZE TABLE 语句来优化表。但是,OPTILMIZE TABLE语句只能优化表中的VARCHAR 、 BLOB 或 TEXT 类型的字段。一个表使用了这些字段的数据类型,若已经 删除 了表的一大部分数据,或者已经对含有可变长度行的表(含有VARCHAR、BLOB或TEXT列的表)进行了很多 更新 ,则应使用OPTIMIZE TABLE来重新利用未使用的空间,并整理数据文件的 碎片 。

OPTIMIZE TABLE 语句对InnoDB和MyISAM类型的表都有效。该语句在执行过程中也会给表加上 只读锁 。OPTILMIZE TABLE语句的基本语法如下:

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。即使对可变长度的行进行了大量的更新,也不需要经常运行, 每周一次 或 每月一次 即可,并且只需要对 特定的表 运行。

12.3.8 小结

上述这些方法都是有利有弊的。

比如:

  • 修改数据类型,节省存储空间的同时,要考虑到数据不能超过取值范围;
  • 增加冗余字段的时候,不要忘了确保数据一致性;
  • 把大表拆分,也意味着查询会增加新的连接,从而增加额外的开销和运维的成本。

因此,一定要结合实际的业务需求进行权衡。

12.4 大表优化

12.4.1 限定查询的范围

禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内;

12.4.2 读写分离

经典的数据库拆分方案,主库负责写,从库负责读。

  • 一主一从模式:

  • 双主双从模式:

12.4.3 垂直拆分

当数据量级达到 千万级 以上时,有时候我们需要把一个数据库切成多份,放到不同的数据库服务器上,减少对单一数据库服务器的访问压力。

 垂直拆分的优点: 可以使得列数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。

垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起 JOIN 操作。此外,垂直拆分会让事务变得更加复杂。

12.4.4 水平拆分

 下面补充一下数据库分片的两种常见方案:

  • 客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。
  • 中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。我们现在谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值