Mysql是怎么运行的(上)

文章目录

Mysql是怎么运行的

此文是读书笔记

原文链接(Mysql是在怎么运行的

Mysql处理一条语句的流程

服务器程序处理来自客户端的查询请求大致需要经过三个部分,分别是连接管理、解析与优化、存储引擎。

连接管理

客户端进程可以采用TCP/IP、命名管道或共享内存、Unix域套接字这几种方式之一来与服务器进程建立连接,每当有一个客户端进程连接到服务器进程时,服务器进程都会创建一个线程来专门处理与这个客户端的交互,当该客户端退出时会与服务器断开连接,服务器并不会立即把与该客户端交互的线程销毁掉,而是把它缓存起来,在另一个新的客户端再进行连接时,把这个缓存的线程分配给该新客户端。这样就起到了不频繁创建和销毁线程的效果,从而节省开销。

解析与优化

1、查询缓存

Mysql会将刚刚查询过的结果加到缓存,再次传过来相同的数据会直接通过缓存来返回

不过既然是缓存,那就有它缓存失效的时候。MySQL的缓存系统会监测涉及到的每张表,只要该表的结构或者数据被修改,如对该表使用了INSERT、 UPDATE、DELETE、TRUNCATE TABLE、ALTER TABLE、DROP TABLE或 DROP DATABASE语句,那使用该表的所有高速缓存查询都将变为无效并从高速缓存中删除!

当然,MySQL服务器并没有人聪明,如果两个查询请求在任何字符上的不同(例如:空格、注释、大小写),都会导致缓存不会命中。另外,如果查询请求中包含某些系统函数、用户自定义变量和函数、一些系统表,如 mysql 、information_schema、 performance_schema 数据库中的表,那这个请求就不会被缓存

虽然查询缓存有时可以提升系统性能,但也不得不因维护这块缓存而造成一些开销,比如每次都要去查询缓存中检索,查询请求处理完需要更新查询缓存,维护该查询缓存对应的内存区域。从MySQL 5.7.20开始,不推荐使用查询缓存,并在MySQL 8.0中删除
2、语法解析

如果查询缓存没有命中,接下来就需要进入正式的查询阶段了。因为客户端程序发送过来的请求只是一段文本而已,所以MySQL服务器程序首先要对这段文本做分析,判断请求的语法是否正确,然后从文本中将要查询的表、各种查询条件都提取出来放到MySQL服务器内部使用的一些数据结构上来。
3、查询优化

MySQL的优化程序会对我们的语句做一些优化,如外连接转换为内连接、表达式简化、子查询转为连接等等的一堆东西。优化的结果就是生成一个执行计划,这个执行计划表明了应该使用哪些索引进行查询,表之间的连接顺序是什么样的。我们可以使用EXPLAIN语句来查看某个语句的执行计划

存储引擎

MySQL服务器把数据的存储和提取操作都封装到了一个叫存储引擎的模块里。我们知道表是由一行一行的记录组成的,但这只是一个逻辑上的概念,物理上如何表示记录,怎么从表中读取数据,怎么把数据写入具体的物理存储器上,这都是存储引擎负责的事情。为了实现不同的功能,MySQL提供了各式各样的存储引擎,不同存储引擎管理的表具体的存储结构可能不同,采用的存取算法也可能不同。

我们可以用下面这个命令来查看当前服务器程序支持的存储引擎:

SHOW ENGINES;

设置表的存储引擎

我们可以为不同的表设置不同的存储引擎,也就是说不同的表可以有不同的物理存储结构,不同的提取和写入方式。

创建表时指定存储引擎

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

修改表的存储引擎

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

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

mysqld --default-storage-engine=MyISAM

基本配置

配置文件

一般会在$MYSQL_HOME/my.cnf中,但也不一定

与在命令行中指定启动选项不同的是,配置文件中的启动选项被划分为若干个组,每个组有一个组名,用中括号[]扩起来,像这样:

[server]
(具体的启动选项...)

[mysqld]
(具体的启动选项...)

[mysqld_safe]
(具体的启动选项...)

[client]
(具体的启动选项...)

[mysql]
(具体的启动选项...)

[mysqladmin]
(具体的启动选项...)
启动命令 类别 能读取的组
mysqld 启动服务器 [mysqld][server]
mysqld_safe 启动服务器 [mysqld][server][mysqld_safe]
mysql.server 启动服务器 [mysqld][server][mysql.server]
mysql 启动客户端 [mysql][client]
mysqladmin 启动客户端 [mysqladmin][client]
mysqldump 启动客户端 [mysqldump][client]

同一个配置文件中多个组的优先级

[server]
default-storage-engine=InnoDB

[mysqld]
default-storage-engine=MyISAM

那么,将以最后一个出现的组中的启动选项为准,比方说例子中default-storage-engine既出现在[mysqld]组也出现在[server]组,因为[mysqld]组在[server]组后边,就以[mysqld]组中的配置项为准。

系统变量

1、查看变量

MySQL服务器程序运行过程中会用到许多影响程序行为的变量,它们被称为MySQL系统变量,比如允许同时连入的客户端数量用系统变量max_connections表示,表的默认存储引擎用系统变量default_storage_engine表示,查询缓存的大小用系统变量query_cache_size表示,MySQL服务器程序的系统变量有好几百条

例如:

   SHOW VARIABLES LIKE 'default_storage_engine';
   SHOW VARIABLES LIKE 'max_connections';

2、系统变量的作用范围

GLOBAL:全局变量,影响服务器的整体操作。

SESSION:会话变量,影响某个客户端连接的操作。(注:SESSION有个别名叫LOCAL)

通过启动选项设置的系统变量的作用范围都是GLOBAL的,也就是对所有客户端都有效的,因为在系统启动的时候还没有客户端程序连接进来呢。

设置语法

SET [GLOBAL|SESSION] 系统变量名 = 值;

或者写成这样也行:

SET [@@(GLOBAL|SESSION).]var_name = XXX;

3、查看不同作用范围的系统变量

例如:

 SHOW SESSION VARIABLES LIKE 'default_storage_engine';
 SHOW GLOBAL VARIABLES LIKE 'default_storage_engine';

注意:并不是所有系统变量都具有GLOBAL和SESSION的作用范围。

有一些系统变量只具有GLOBAL作用范围,比方说max_connections,表示服务器程序支持同时最多有多少个客户端程序进行连接。

有一些系统变量只具有SESSION作用范围,比如insert_id,表示在对某个包含AUTO_INCREMENT列的表进行插入时,该列初始的值。

状态变量

为了让我们更好的了解服务器程序的运行情况,MySQL服务器程序中维护了很多关于程序运行状态的变量,它们被称为状态变量。比方说Threads_connected表示当前有多少客户端与服务器建立了连接,Handler_update表示已经更新了多少行记录等,像这样显示服务器程序状态信息的状态变量还有好几百个

由于状态变量是用来显示服务器程序运行状况的,所以它们的值只能由服务器程序自己来设置,我们程序员是不能设置的。与系统变量类似,状态变量也有GLOBAL和SESSION两个作用范围的,所以查看状态变量的语句可以这么写:

SHOW [GLOBAL|SESSION] STATUS [LIKE 匹配的模式];

字符集

四种重要的字符集

1、ASCII字符集

共收录128个字符,包括空格、标点符号、数字、大小写字母和一些不可见字符

2、ISO 8859-1字符集

收录256个字符,是在ASCII字符集的基础上又扩充了128个西欧常用字符(包括德法两国的字母),也可以使用1个字节来进行编码

个字符集也有一个别名latin1

3、GB2312字符集

收录了汉字以及拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母。其中收录汉字6763个,其他文字符号682个。同时这种字符集又兼容ASCII字符集,所以在编码方式上显得有些奇怪:

如果该字符在ASCII字符集中,则采用1字节编码。

否则采用2字节编码。

4、GBK字符集

GBK字符集只是在收录字符范围上对GB2312字符集作了扩充,编码方式上兼容GB2312

5、tf8字符集

收录地球上能想到的所有字符,而且还在不断扩充。这种字符集兼容ASCII字符集,采用变长编码方式,编码一个字符需要使用1~4个字节

MySQL中的utf8和utf8mb4

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

utf8mb4:正宗的utf8字符集,使用1~4个字节表示字符。

查看支持的字符集

SHOW CHARSET;

各级别的字符集和比较规则

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

服务器级别

数据库级别

表级别

列级别

1、服务器级别

系统变量 描述
character_set_server 服务器级别的字符集
collation_server 服务器级别的比较规则
show variables like 'character_set_server'
    -> ;
+----------------------+--------+
| Variable_name        | Value  |
+----------------------+--------+
| character_set_server | latin1 |
+----------------------+--------+
1 row in set (0.01 sec)

mysql> SHOW VARIABLES LIKE 'collation_server';
+------------------+-------------------+
| Variable_name    | Value             |
+------------------+-------------------+
| collation_server | latin1_swedish_ci |
+------------------+-------------------+
1 row in set (0.00 sec)

可以看到服务器编码方式是ISO 8859-1(别名latin1),比较规则是latin1_swedish_ci

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

[server]
character_set_server=gbk
collation_server=gbk_chinese_ci

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

2、数据库级别

如果想查看当前数据库使用的字符集和比较规则,可以查看下面两个系统变量的值

系统变量 描述
character_set_database 当前数据库的字符集
collation_database 当前数据库的比较规则
 mysql> SHOW VARIABLES LIKE 'collation_server';
+------------------+-------------------+
| Variable_name    | Value             |
+------------------+-------------------+
| collation_server | latin1_swedish_ci |
+------------------+-------------------+
1 row in set (0.00 sec)

mysql>  SHOW VARIABLES LIKE 'character_set_database';
+------------------------+--------+
| Variable_name          | Value  |
+------------------------+--------+
| character_set_database | latin1 |
+------------------------+--------+
1 row in set (0.00 sec)

mysql> SHOW VARIABLES LIKE 'collation_database';
+--------------------+-------------------+
| Variable_name      | Value             |
+--------------------+-------------------+
| collation_database | latin1_swedish_ci |
+--------------------+-------------------+
1 row in set (0.00 sec)

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

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 比较规则名称];

5、注意

由于字符集和比较规则是互相有联系的,如果我们只修改了字符集,比较规则也会跟着变化,如果只修改了比较规则,字符集也会跟着变化,具体规则如下:

只修改字符集,则比较规则将变为修改后的字符集默认的比较规则。

只修改比较规则,则字符集将变为修改后的比较规则对应的字符集。

不论哪个级别的字符集和比较规则,这两条规则都适用

MySQL中字符集的转换

系统变量 描述
character_set_client 服务器解码请求时使用的字符集
character_set_connection 服务器处理请求时会把请求字符串从character_set_client转为character_set_connection
character_set_results 服务器向客户端返回数据时使用的字符集

从这个分析中我们可以得出这么几点需要注意的地方:

服务器认为客户端发送过来的请求是用character_set_client编码的。

假设你的客户端采用的字符集和 character_set_client 不一样的话,这就会出现意想不到的情况。比如我的客户端使用的是utf8字符集,如果把系统变量character_set_client的值设置为ascii的话,服务器可能无法理解我们发送的请求,更别谈处理这个请求了。

服务器将把得到的结果集使用character_set_results编码后发送给客户端。

假设你的客户端采用的字符集和 character_set_results 不一样的话,这就可能会出现客户端无法解码结果集的情况,结果就是在你的屏幕上出现乱码。比如我的客户端使用的是utf8字符集,如果把系统变量character_set_results的值设置为ascii的话,可能会产生乱码。

character_set_connection只是服务器在将请求的字节串从character_set_client转换为character_set_connection时使用,它是什么其实没多重要,但是一定要注意,该字符集包含的字符范围一定涵盖请求中的字符,要不然会导致有的字符无法使用character_set_connection代表的字符集进行编码。比如你把character_set_client设置为utf8,把character_set_connection设置成ascii,那么此时你如果从客户端发送一个汉字到服务器,那么服务器无法使用ascii字符集来编码这个汉字,就会向用户发出一个警告。

知道了在MySQL中从发送请求到返回结果过程里发生的各种字符集转换,但是为什么要转来转去的呢?不晕么?

答:是的,很头晕,所以我们通常都把 character_set_client 、character_set_connection*、character_set_results*** 这三个系统变量设置成和客户端使用的字符集一致的情况,这样减少了很多无谓的字符集转换。为了方便我们设置,MySQL提供了一条非常简便的语句:

 SET NAMES 字符集名;
 SET NAMES utf8; //例如

这一条语句产生的效果和我们执行这3条的效果是一样的:

SET character_set_client = 字符集名;
SET character_set_connection = 字符集名;
SET character_set_results = 字符集名;

排序规则产生的不同的排序结果

比方说表t的列col使用的字符集是gbk,使用的比较规则是gbk_chinese_ci,进行如下实战,分别比较

mysql> create database test;
Query OK, 1 row affected (0.00 sec)

mysql> use test;
Database changed
mysql> CREATE TABLE t (
    ->     col VARCHAR(1)
    -> );
Query OK, 0 rows affected (0.16 sec)
mysql> INSERT INTO t (col) VALUES ('a'), ('b'), ('A'), ('B');
Query OK, 4 rows affected (0.01 sec)
Records: 4  Duplicates: 0  Warnings: 0

mysql>  SELECT * FROM t ORDER BY col;
+------+
| col  |
+------+
| a    |
| A    |
| b    |
| B    |
+------+
4 rows in set (0.00 sec)

mysql> ALTER TABLE t MODIFY col VARCHAR(10) COLLATE gbk_bin;
Query OK, 4 rows affected (0.09 sec)
Records: 4  Duplicates: 0  Warnings: 0

mysql>  SELECT * FROM t ORDER BY col;
+------+
| col  |
+------+
| A    |
| B    |
| a    |
| b    |
+------+
4 rows in set (0.00 sec)

InnoDB存储引擎

介绍

InnoDB是一个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还是存在的。

1、页

InnoDB采取读取数据的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。

2、行

以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行格式或者记录格式。设计InnoDB存储引擎的大佬们到现在为止设计了4种不同类型的行格式,分别是Compact、Redundant、Dynamic和Compressed行格式,随着时间的推移,他们可能会设计出更多的行格式,但是不管怎么变,在原理上大体都是相同的。

指定行格式的语法:

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

实例:

mysql> USE xiaohaizi;
Database changed

mysql> CREATE TABLE record_format_demo (
    ->     c1 VARCHAR(10),
    ->     c2 VARCHAR(10) NOT NULL,
    ->     c3 CHAR(10),
    ->     c4 VARCHAR(10)
    -> ) CHARSET=ascii ROW_FORMAT=COMPACT;
Query OK, 0 rows affected (0.03 sec)

COMPACT行格式介绍

1、记录的额外信息

这部分信息是服务器为了描述这条记录而不得不额外添加的一些信息,这些额外信息分为3类,分别是变长字段长度列表、NULL值列表和记录头信息,我们分别看一下。

1)、变长字段长度列表

我们知道MySQL支持一些变长的数据类型,比如VARCHAR(M)、VARBINARY(M)、各种TEXT类型,各种BLOB类型,我们也可以把拥有这些数据类型的列称为变长字段,变长字段中存储多少字节的数据是不固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来,这样才不至于把MySQL服务器搞懵,所以这些变长字段占用的存储空间分为两部分:

真正的数据内容

占用的字节数

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

变长字段长度列表中只存储值为 非NULL 的列内容占用的长度,值为 NULL 的列的长度是不储存的

2)、NULL值列表

我们知道表中的某些列可能存储NULL值,如果把这些NULL值都放到记录的真实数据中存储会很占地方,所以Compact行格式把这些值为NULL的列统一管理起来,存储到NULL值列表中、

MySQL规定NULL值列表必须用整数个字节的位表示,如果使用的二进制位个数不是整数个字节,则在字节的高位补0。

表record_format_demo只有3个值允许为NULL的列,对应3个二进制位,不足一个字节,所以在字节的高位补0

如果一个表中有9个允许为NULL,那这个记录的NULL值列表部分就需要2个字节来表示了。

3)、记录头信息

用于描述记录的记录头信息,它是由固定的5个字节组成。5个字节也就是40个二进制位,

这些二进制位代表的详细信息如下表:

名称 大小(单位:bit) 描述
预留位1 1 没有使用
预留位2 1 没有使用
delete_mask 1 标记该记录是否被删除
min_rec_mask 1 B+树的每层非叶子节点中的最小记录都会添加该标记
n_owned 4 表示当前记录拥有的记录数
heap_no 13 表示当前记录在记录堆的位置信息
record_type 3 表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录
next_record 16 表示下一条记录的相对位置
2、记录的真实数据
记录的真实数据除了我们自己定义的列的数据以外,MySQL会为每个记录默认的添加一些列(也称为隐藏列):DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR
InnoDB表对主键的生成策略:优先使用用户自定义主键作为主键,如果用户没有定义主键,则选取一个Unique键作为主键,如果表中连Unique键都没有定义的话,则InnoDB会为表默认添加一个名为row_id的隐藏列作为主键。

其他行格式不再罗列:参考链接

InnoDB数据页结构(类比操作系统的段页,段时槽)

一个InnoDB数据页的存储空间大致被划分成了7个部分,有的部分占用的字节数是确定的,有的部分占用的字节数是不确定的。下面我们用表格的方式来大致描述一下这7个部分都存储一些什么内容

名称 中文名 占用空间大小 简单描述
File Header 文件头部 38字节 页的一些通用信息
Page Header 页面头部 56字节 数据页专有的一些信息
Infimum + Supremum 最小记录和最大记录 26字节 两个虚拟的行记录
User Records 用户记录 不确定 实际存储的行记录内容
Free Space 空闲空间 不确定 页中尚未使用的空间
Page Directory 页面目录 不确定 页中的某些记录的相对位置
File Trailer 文件尾部 8字节 校验页是否完整
记录在页中数据实际按照主键值由小到大顺序串联成一个单链表
1、Page Directory(页目录)
给页数据添加一个目录方便查找,具体如下:
  • 将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。
  • 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示该记录拥有多少条记录,也就是该组内共有几条记录。
  • 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的Page Directory,也就是页目录(此时应该返回头看看页面各个部分的图)。页面目录中的这些地址偏移量被称为槽(英文名:Slot),所以这个页面目录就是由槽组成的。
    2、Page Header(页面头部)
    InnoDB的大佬们为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫Page Header的部分,它是页结构的第二部分,这个部分占用固定的56个字节,专门存储各种状态信息
    3、File Header(文件头部)
    它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、下一个页是谁啦等等
    4、File Trailer
    用于检验页是否完整的部分,占用固定的8个字节。

B+树索引

介绍

在没有索引的情况下,不论是根据主键列或者其他列的值进行查找,由于我们并不能快速的定位到记录所在的页,所以只能从第一个页沿着双向链表一直往下找,在每一个页中根据我们刚刚介绍过的查找方式去查找指定的记录。因为要遍历所有的数据页,所以这种方式显然是超级耗时的。因此有了索引

索引结构:

record_type:记录头信息的一项属性,表示记录的类型,0表示普通记录、2表示最小记录、3表示最大记录

next_record:记录头信息的一项属性,表示下一条地址相对于本条记录的地址偏移量

各个列的值:这里只记录在index_demo表中的三个列,分别是c1、c2和c3。

其他信息:除了上述3种信息以外的所有信息,包括其他隐藏列的值以及记录的额外信息。

索引是通过复用了之前存储用户记录的数据页来存储目录项,为了和用户记录做一下区分,把这些用来表示目录项的记录称为目录项记录。那InnoDB怎么区分一条记录是普通的用户记录还是目录项记录呢?别忘了记录头信息里的record_type属性,它的各个取值代表的意思如下:

0:普通的用户记录

1:目录项记录

2:最小记录

3:最大记录

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

  • 目录项记录的record_type值是1,而普通用户记录的record_type值是0。
  • 目录项记录只有主键值和页的编号两个列,而普通的用户记录的列是用户自己定义的,可能包含很多列,另外还有InnoDB自己添加的隐藏列。
  • 只有在存储目录项记录的页中的主键值最小的目录项记录的min_rec_mask值为1,其他别的记录的min_rec_mask值都是0。
    如果我们表中的数据非常多则会产生很多存储目录项记录的页,那我们怎么根据主键值快速定位一个存储目录项记录的页呢?其实也简单,为这些存储目录项记录的页再生成一个更高级的目录,就像是一个多级目录一样,大目录里嵌套小目录,小目录里才是实际的数据,所以现在各个页的示意图就是这样子:

这种存储的名称叫做B+树。我们的实际用户记录其实都存放在B+树的最底层的节点上,这些节点也被称为叶子节点或叶节点,其余用来存放目录项的节点称为非叶子节点或者内节点,其中B+树最上面的那个节点也称为根节点。

聚簇索引

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

  • 页内的记录是按照主键的大小顺序排成一个单向链表。
  • 各个存放用户记录的页也是根据页中用户记录的主键大小顺序排成一个双向链表。
  • 存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表。
    B+树的叶子节点存储的是完整的用户记录
    所有完整的用户记录都存放在这个聚簇索引的叶子节点处。这种聚簇索引并不需要我们在MySQL语句中显式的使用INDEX语句去创建(后边会介绍索引相关的语句),InnoDB存储引擎会自动的为我们创建聚簇索引。另外有趣的一点是,在InnoDB存储引擎中,聚簇索引就是数据的存储方式(所有的用户记录都存储在了叶子节点),也就是所谓的索引即数据,数据即索引。

联合索引

以多个列的大小作为排序规则,也就是同时为多个列建立索引

小结

1、B+树中每层节点都是按照索引列值从小到大的顺序排序而组成了双向链表,而且每个页内的记录(不论是用户记录还是目录项记录)都是按照索引列的值从小到大的顺序而形成了一个单链表。如果是联合索引的话,则页面和记录先按照联合索引前面的列排序,如果该列值相同,再按照联合索引后边的列排序。

2、每个索引都对应一棵B+树,B+树分为好多层,最下面一层是叶子节点,其余的是内节点。所有用户记录都存储在B+树的叶子节点,所有目录项记录都存储在内节点。

3、InnoDB存储引擎会自动为主键(如果没有它会自动帮我们添加)建立聚簇索引,聚簇索引的叶子节点包含完整的用户记录。

4、可以为自己感兴趣的列建立二级索引,二级索引的叶子节点包含的用户记录由索引列 + 主键组成,所以如果想通过二级索引来查找完整的用户记录的话,需要通过回表操作,也就是在通过二级索引找到主键值之后再到聚簇索引中查找完整的用户记录。

5、通过索引查找记录是从B+树的根节点开始,一层一层向下搜索。由于每个页面都按照索引列的值建立了Page Directory(页目录),所以在这些页面中的查找非常快。

基本操作

1、创建表的时候指定需要建立索引的单个列或者建立联合索引的多个列:

CREATE TALBE 表名 (
    各种列的信息 ··· , 
    [KEY|INDEX] 索引名 (需要被索引的单个列或多个列)
)

2、修改索引

ALTER TABLE 表名 ADD [INDEX|KEY] 索引名 (需要被索引的单个列或多个列);

3、删除索引:

ALTER TABLE 表名 DROP [INDEX|KEY] 索引名;

索引的代价

空间上的代价:每一棵B+树的每一个节点都是一个数据页,一个页默认会占用16KB的存储空间

时间上的代价:增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些记录移位

几种使用方式

0、新建一个表,插入10w条数据

CREATE TABLE person_info(
    id INT NOT NULL auto_increment,
    name VARCHAR(100) NOT NULL,
    birthday DATE NOT NULL,
    phone_number CHAR(11) NOT NULL,
    country varchar(100) NOT NULL,
    PRIMARY KEY (id),
    KEY idx_name_birthday_phone_number (name, birthday, phone_number)
);
DELIMITER //

CREATE PROCEDURE InsertDummyData()
BEGIN
    DECLARE i INT DEFAULT 1;

    WHILE i <= 100000 DO
        INSERT INTO person_info (name, birthday, phone_number, country)
        VALUES (
            CONCAT('Person', i),
            DATE_ADD('1990-01-01', INTERVAL (i-1) DAY),
            CONCAT('1234567', LPAD(i, 5, '0')),
            'Country' 
        );
        
        SET i = i + 1;
    END WHILE;
END //

DELIMITER ;

1、匹配左边的列

如果我们想使用联合索引中尽可能多的列,搜索条件中的各个列必须是联合索引中从最左边连续的列

2、匹配列前缀

也就是说这些字符串的前n个字符,也就是前缀都是排好序的,所以对于字符串类型的索引列来说,我们只匹配它的前缀也是可以快速定位记录的,比方说我们想查询名字以'As'开头的记录,那就可以这么写查询语句:

SELECT * FROM person_info WHERE name LIKE 'As%';

但是需要注意的是,如果只给出后缀或者中间的某个字符串,比如这样:

SELECT * FROM person_info WHERE name LIKE '%As';

小技巧:

如果想获取以As结尾的数据,可以把整个字符逆序再前搜索

SELECT * FROM person_info WHERE name LIKE 'sA%';

3、匹配范围值

所有记录都是按照索引列的值从小到大的顺序排好序的,所以这极大的方便我们查找索引列的值在某个范围内的记录

4、用于排序

ORDER BY子句里使用到了我们的索引列,就有可能省去在内存或文件中排序的步骤

ORDER BY的子句后边的列的顺序也必须按照索引列的顺序给出

也有无法使用索引的几种场景:

  • ASC、DESC混用

    explain SELECT * FROM person_info ORDER BY name DESC, birthday ASC LIMIT 10 ;
    +----+-------------+-------------+------+---------------+------+---------+------+-------+----------------+
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
    +----+-------------+-------------+------+---------------+------+---------+------+-------+----------------+
    | 1 | SIMPLE | person_info | ALL | NULL | NULL | NULL | NULL | 99859 | Using filesort |
    +----+-------------+-------------+------+---------------+------+---------+------+-------+----------------+

  • WHERE子句中出现非排序使用到的索引列
    就是排序条件没有加索引

  • 排序列包含非同一个索引的列
    排序规则上包含了一个加了索引的列,一个不包含索引的列

  • 排序列使用了复杂的表达式

    SELECT * FROM person_info ORDER BY UPPER(name) LIMIT 10;

5、用于分组

和使用B+树索引进行排序是一个道理,分组列的顺序也需要和索引列的顺序一致,也可以只使用索引列中左边的列进行分组,等等的~

回表

回表(Ref or Bookmark Lookup)是指在使用索引的查询过程中,当索引无法覆盖所有查询需要的列时,数据库引擎需要回到原始表(base table)中查找缺失的列数据。

使用聚集索引(主键或第一个唯一索引)就不会回表,普通索引就会回表。

具体来说,回表通常涉及两个步骤:

使用索引进行检索: 查询优化器选择了一个合适的索引,以快速地定位到符合查询条件的行。这一步是通过 B 树等索引结构进行的,非常高效。

回到原始表中获取数据: 由于索引无法包含所有查询所需的列,数据库引擎需要回到原始表中,通过找到的行的主键或聚簇索引,再次访问原始数据行,以获取未涵盖在索引中的列的数据。

查询优化器会事先对表中的记录计算一些统计数据,然后再利用这些统计数据根据查询的条件来计算一下需要回表的记录数,需要回表的记录数越多,就越倾向于使用全表扫描,反之倾向于使用二级索引 + 回表的方式。

对于有排序需求的查询,上面讨论的采用全表扫描还是二级索引 + 回表的方式进行查询的条件也是成立的,比方说下面这个查询:

SELECT * FROM person_info ORDER BY name, birthday, phone_number;

explain SELECT * FROM person_info ORDER BY name, birthday, phone_number;
+----+-------------+-------------+------+---------------+------+---------+------+-------+----------------+
| id | select_type | table       | type | possible_keys | key  | key_len | ref  | rows  | Extra          |
+----+-------------+-------------+------+---------------+------+---------+------+-------+----------------+
|  1 | SIMPLE      | person_info | ALL  | NULL          | NULL | NULL    | NULL | 99859 | Using filesort |
+----+-------------+-------------+------+---------------+------+---------+------+-------+----------------+
1 row in set (0.00 sec)

添加了LIMIT 10的查询更容易让优化器采用二级索引 + 回表的方式进行查询。

SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10;

 explain SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10;
+----+-------------+-------------+-------+---------------+--------------------------------+---------+------+------+-------+
| id | select_type | table       | type  | possible_keys | key                            | key_len | ref  | rows | Extra |
+----+-------------+-------------+-------+---------------+--------------------------------+---------+------+------+-------+
|  1 | SIMPLE      | person_info | index | NULL          | idx_name_birthday_phone_number | 116     | NULL |   10 | NULL  |
+----+-------------+-------------+-------+---------------+--------------------------------+---------+------+------+-------+

这个limit的值太大了的时候也会进行全表扫描

为了彻底告别回表操作带来的性能损耗,我们建议:最好在查询列表里只包含索引列,

SELECT name, birthday, phone_number FROM person_info  
    WHERE name > 'Asa' AND name < 'Barlow';

索引建立的技巧

1、考虑列的基数

列的基数指的是某一列中不重复数据的个数,比方说某个列包含值2, 5, 8, 2, 5, 8, 2, 5, 8,虽然有9条记录,但该列的基数却是3

在记录行数一定的情况下,列的基数越大,该列中的值越分散,列的基数越小,该列中的值越集中

最好为那些列的基数大的列建立索引,为基数太小列的建立索引效果可能不好。

2、索引列的类型尽量小

数据类型越小,在查询时进行的比较操作越快(这是CPU层次的东东)

数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下更多的记录,从而减少磁盘I/O带来的性能损耗,也就意味着可以把更多的数据页缓存在内存中,从而加快读写效率。

3、索引字符串值的前缀

只对字符串的前几个字符建立索引

CREATE TABLE person_info(
    name VARCHAR(100) NOT NULL,
    birthday DATE NOT NULL,
    phone_number CHAR(11) NOT NULL,
    country varchar(100) NOT NULL,
    KEY idx_name_birthday_phone_number (name(10), birthday, phone_number)
);    

name(10)就表示在建立的B+树索引中只保留记录的前10个字符的编码,这种只索引字符串值的前缀的策略是我们非常鼓励的,尤其是在字符串类型能存储的字符比较多的时候。

当然这种方式对于排序有重大影响,不太使用

3、表达式会导致索引失效

4、尽量带有主键,并且主键递增

插入数据时以主键为顺序来逐步插入的。

如果没有主键递增

如果我们插入的主键值忽大忽小的话,假设某个数据页存储的记录已经满了,需要把页面分裂成两个页面,把本页中的一些记录移动到新创建的这个页中。这就会导致性能损耗。

所以如果我们想尽量避免这样无谓的性能损耗,最好让插入的记录的主键值依次递增,这样就不会发生这样的性能损耗了。所以建议:让主键具有AUTO_INCREMENT,让存储引擎自己为表生成主键,而不是我们手动插入

Mysql的数据目录简介

确定MySQL中的数据目录

确定MySQL把数据都存到哪个路径下,如下所示

SHOW VARIABLES LIKE 'datadir';

每当我们使用CREATE DATABASE 数据库名语句创建一个数据库的时候,每个数据库都对应数据目录下的一个子目录,或者说对应一个文件夹,我们每当我们新建一个数据库时,MySQL会帮我们做这两件事儿:

1、在数据目录下创建一个和数据库名同名的子目录(或者说是文件夹)。

2、在该与数据库名同名的子目录下创建一个名为db.opt的文件,这个文件中包含了该数据库的各种属性,比方说该数据库的字符集和比较规则是什么。

表在文件系统中的表示

数据其实都是以记录的形式插入到表中的,每个表的信息其实可以分为两种:

1、表结构的定义:表名.frm

2、表中的数据

表数据要根据不同的存储引擎有不同的表示

InnoDB是如何存储表数据的

存储的文件

表名.ibd

1、InnoDB其实是使用页为基本单位来管理存储空间的,默认的页大小为16KB。

2、对于InnoDB存储引擎来说,每个索引都对应着一棵B+树,该B+树的每个节点都是一个数据页,数据页之间不必要是物理连续的,因为数据页之间有双向链表来维护着这些页的顺序。

3、InnoDB的聚簇索引的叶子节点存储了完整的用户记录,也就是所谓的索引即数据,数据即索引。

4、为了更好的管理这些页,设计InnoDB的设计者们提出了一个表空间或者文件空间(英文名:table space或者file space)的概念,这个表空间是一个抽象的概念,它可以对应文件系统上一个或多个真实文件(不同表空间对应的文件数量可能不同)。每一个表空间可以被划分为很多很多很多个页,我们的表数据就存放在某个表空间下的某些页里。

几种表空间:

系统表空间(system tablespace):ibdata1

独立表空间(file-per-table tablespace):用户自定义的表空间

还有通用表空间(general tablespace)、undo表空间(undo tablespace)、临时表空间(temporary tablespace)等等

存储划分(太多了,没搞懂,还得看InnoDB的表空间):

区:也太多了不好管理,导致随机IO问题,组合一下组成一个区

表空间被划分为许多连续的区,每个区默认由64个页组成,每256个区划分为一组,每个组的最开始的几个页类型是固定的

段:实际是碎片区,方便将碎片化的磁盘空间连续起来组成一个区

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

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

MyISAM是如何存储表数据的

MyISAM并没有什么所谓的表空间一说,表数据都存放到对应的数据库子目录下。假如test表使用MyISAM存储引擎的话,那么在它所在数据库对应的xiaohaizi目录下会为test表创建这三个文件

test.frm
test.MYD
test.MYI

其中test.MYD代表表的数据文件,也就是我们插入的用户记录;test.MYI代表表的索引文件,我们为该表创建的索引都会放到这个文件中。

视图

存储视图的时候是不需要存储真实的数据的,只需要把它的结构存储起来就行了。和表一样,描述视图结构的文件也会被存储到所属数据库对应的子目录下面,只会存储一个视图名.frm的文件。

文件系统对数据库的影响

1、数据库名称和表名称不得超过文件系统所允许的最大长度。

2、特殊字符的问题:为了避免因为数据库名和表名出现某些特殊字符而造成文件系统不支持的情况,MySQL会把数据库名和表名中所有除数字和拉丁字母以外的所有字符在文件名里都映射成 @+编码值的形式作为文件名。比方说我们创建的表的名称为'test?',由于?不属于数字或者拉丁字母,所以会被映射成编码值,所以这个表对应的.frm文件的名称就变成了test@003f.frm。

3、文件长度受文件系统最大长度限制

几个重要的文件

1、mysql

这个数据库贼核心,它存储了MySQL的用户账户和权限信息,一些存储过程、事件的定义信息,一些运行过程中产生的日志信息,一些帮助信息以及时区信息等。

2、information_schema

这个数据库保存着MySQL服务器维护的所有其他数据库的信息,比如有哪些表、哪些视图、哪些触发器、哪些列、哪些索引等等。这些信息并不是真实的用户数据,而是一些描述性信息,有时候也称之为元数据。

3、performance_schema

这个数据库里主要保存MySQL服务器运行过程中的一些状态信息,算是对MySQL服务器的一个性能监控。包括统计最近执行了哪些语句,在执行过程的每个阶段都花费了多长时间,内存的使用情况等等信息。

4、sys

这个数据库主要是通过视图的形式把information_schema 和performance_schema结合起来,让程序员可以更方便的了解MySQL服务器的一些性能信息。

查询

单表查询

单表查询分为两种:

1、使用全表扫描进行查询

2、使用索引进行查询

SELECT * FROM single_table WHERE key1 = 'abc' AND key2 > 1000;

查询流程:

1、优化器一般会根据表的统计数据来判断到底使用哪个条件到对应的二级索引中查询扫描的行数会更少,选择那个扫描行数较少的条件到对应的二级索引中查询

2、使用二级索引定位记录的阶段,也就是根据条件key1 = 'abc'从idx_key1索引代表的B+树中找到对应的二级索引记录。

3、回表阶段,也就是根据上一步骤中找到的记录的主键值进行回表操作,也就是到聚簇索引中找到对应的完整的用户记录,再根据条件key2 > 1000到完整的用户记录继续过滤。将最终符合过滤条件的记录返回给用户。

几种查询方式(explain执行计划的type字段可以看到)

type: 代表连接类型或访问类型,表示数据库引擎选择如何查找表中的行。常见的类型有:

ALL:全表扫描,检查表中的每一行。

index:仅扫描索引而非实际数据。

range:通过索引范围查找行。

ref:通过非唯一索引查找单个行。

eq_ref:通过唯一索引查找单个行。

const:通过常量条件查找单个行。

索引合并是指优化器(query optimizer):

在执行查询时选择使用多个索引来提高性能的一种技术。索引合并通常发生在包含多个WHERE条件、多个AND/OR关系的复杂查询中。MySQL的优化器尝试组合多个单列索引,以满足查询的条件,从而加速查询的执行。

AND条件合并:

如果一个查询包含多个AND条件,每个条件可以使用不同的索引,优化器可能会选择合并这些索引。这样可以减小数据集的大小,提高查询性能。

OR条件合并:

对于包含多个OR条件的查询,每个条件可以使用不同的索引。MySQL优化器可以尝试将多个OR条件对应的索引结果集进行合并,以减少对表的扫描。

使用覆盖索引:

如果查询需要的字段都在一个或多个索引中,而不需要访问主表数据,优化器可能会选择使用覆盖索引,避免了回表的开销。

使用索引合并算法:

MySQL的优化器在执行计划中会使用索引合并算法,从而同时使用多个索引。这种情况通常发生在某些版本的MySQL中,并且具体效果取决于查询的结构和条件。

连接表

连接的本质就是把各个连接表中的记录都取出来依次匹配的组合加入结果集并返回给用户。

内连接和外连接语法

全连接

像这样的结果集就可以称之为笛卡尔积

查询方法:

 SELECT * FROM t1, t2;

以下几种写法都是等价的

SELECT * FROM t1 JOIN t2;
SELECT * FROM t1 INNER JOIN t2;
SELECT * FROM t1 CROSS JOIN t2;

内连接

inner join
外连接

在MySQL中,根据选取驱动表的不同,外连接仍然可以细分为2种:

左外连接

选取左侧的表为驱动表。

右外连接

选取右侧的表为驱动表。

语法:

SELECT * FROM t1 LEFT [OUTER] JOIN t2 ON 连接条件 [WHERE 普通过滤条件];

对于左(外)连接和右(外)连接来说,必须使用ON子句来指出连接条件。

join buffer

当被驱动表中的数据非常多时,每次访问被驱动表,被驱动表的记录会被加载到内存中,在内存中的每一条记录只会和驱动表结果集的一条记录做匹配,之后就会被从内存中清除掉。再从驱动表结果集中拿出另一条记录,再一次把被驱动表的记录加载到内存中一遍,周而复始,驱动表结果集中有多少条记录,就得把被驱动表从磁盘上加载到内存中多少次。所以我们可不可以在把被驱动表的记录加载到内存的时候,一次性和多条驱动表中的记录做匹配,这样就可以大大减少重复从磁盘上加载被驱动表的代价了。所以设计MySQL的大佬提出了一个join buffer的概念

join buffer就是执行连接查询前申请的一块固定大小的内存,先把若干条驱动表结果集中的记录装在这个join buffer中,然后开始扫描被驱动表,每一条被驱动表的记录一次性和join buffer中的多条驱动表记录做匹配,因为匹配的过程都是在内存中完成的,所以这样可以显著减少被驱动表的I/O代价。

查询的成本

I/O成本:从磁盘到内存这个加载的过程损耗的时间称之为I/O成本。

CPU成本:读取以及检测记录是否满足对应的搜索条件、对结果集进行排序等这些操作损耗的时间称之为CPU成本。

Mysql会计算成本如下:

1、根据搜索条件,找出所有可能使用的索引

2、计算全表扫描的代价

3、计算使用不同索引执行查询的代价

4、对比各种执行方案的代价,找出成本最低的那一个

计算查询成本

计算成本时需要知道表格的信息,其中表格信息可以用以下语句查询

SHOW TABLE STATUS

例如:

SHOW TABLE STATUS LIKE 'single_table'\G
*************************** 1. row ***************************
           Name: single_table
         Engine: InnoDB
        Version: 10
     Row_format: Compact
           Rows: 9693
 Avg_row_length: 162
    Data_length: 1589248
Max_data_length: 0
   Index_length: 2523136
      Data_free: 4194304
 Auto_increment: 10001
    Create_time: 2024-01-08 06:27:52
    Update_time: NULL
     Check_time: NULL
      Collation: utf8_general_ci
       Checksum: NULL
 Create_options:
        Comment:

我们只关心两个

Rows:表数据行数

Data_length:表占用的存储空间字节数

全表扫描成本计算

通过Data_length可以知道聚簇索引的页面数目:

聚簇索引的页面数量 = 1589248 ÷ 16 ÷ 1024 = 97

我们现在已经得到了聚簇索引占用的页面数量以及该表记录数的估计值,所以就可以计算全表扫描成本了

I/O成本:

97 x 1.0 + 1.1 = 98.1

97指的是聚簇索引占用的页面数,1.0指的是加载一个页面的成本常数,后边的1.1是一个微调值,我们不用在意。

CPU成本:

9693 x 0.2 + 1.0 = 1939.6

9693指的是统计数据中表的记录数,对于InnoDB存储引擎来说是一个估计值,0.2指的是访问一条记录所需的成本常数,后边的1.0是一个微调值,我们不用在意。

总成本:

98.1 + 1939.6 = 2037.7

索引扫描成本计算

例如key2是一个普通索引,有查询条件 key2 > 10 AND key2 < 1000,也就是说对应的范围区间就是:(10, 1000)

使用二级索引 + 回表方式的查询,MySQL计算这种查询的成本依赖两个方面的数据:

范围区间数量:不论某个范围区间的二级索引到底占用了多少页面,查询优化器粗暴的认为读取索引的一个范围区间的I/O成本和读取一个页面是相同的,即I/O成本为1

需要回表的记录数

这个需要详细讨论:

步骤1:先根据key2 > 10这个条件访问一下idx_key2对应的B+树索引,找到满足key2 > 10这个条件的第一条记录,我们把这条记录称之为区间最左记录。我们前头说过在B+数树中定位一条记录的过程是贼快的,是常数级别的,所以这个过程的性能消耗是可以忽略不计的。

步骤2:然后再根据key2 < 1000这个条件继续从idx_key2对应的B+树索引中找出第一条满足这个条件的记录,我们把这条记录称之为区间最右记录,这个过程的性能消耗也可以忽略不计的。

步骤3:如果区间最左记录和区间最右记录相隔不太远(在MySQL 5.7.21这个版本里,只要相隔不大于10个页面即可),那就可以精确统计出满足key2 > 10 AND key2 < 1000条件的二级索引记录条数。否则只沿着区间最左记录向右读10个页面,计算平均每个页面中包含多少记录,然后用这个平均值乘以区间最左记录和区间最右记录之间的页面数量就可以了。

idx_key2在区间(10, 1000)之间大约有95条记录。读取这95条二级索引记录需要付出的CPU成本就是:

95 x 0.2 + 0.01 = 19.01

其中95是需要读取的二级索引记录条数,0.2是读取一条记录成本常数,0.01是微调。

根据这些记录里的主键值到聚簇索引中做回表操作

范围区间有多少记录,就需要进行多少次回表操作,也就是需要进行多少次页面I/O。我们上面统计了使用idx_key2二级索引执行查询时,预计有95条二级索引记录需要进行回表操作,所以回表操作带来的I/O成本就是:

95 x 1.0 = 95.0

其中95是预计的二级索引记录数,1.0是一个页面的I/O成本常数。

回表操作后得到的完整用户记录,然后再检测其他搜索条件是否成立

95 x 0.2 = 19.0

其中95是待检测记录的条数,0.2是检测一条记录是否符合给定的搜索条件的成本常数。

总I/O成本:

1.0 + 95 x 1.0 = 96.0 (范围区间的数量 + 预估的二级索引记录条数)

总CPU成本:

95 x 0.2 + 0.01 + 95 x 0.2 = 38.01 (读取二级索引记录的成本 + 读取并检测回表后聚簇索引记录的成本)

综上所述,使用idx_key2执行查询的总成本就是:

96.0 + 38.01 = 134.01

连接查询的成本

MySQL中连接查询采用的是嵌套循环连接算法,驱动表会被访问一次,被驱动表可能会被访问多次,所以对于两表连接查询来说,它的查询成本由下面两个部分构成:

1、单次查询驱动表的成本

2、多次查询被驱动表的成本(具体查询多少次取决于对驱动表查询的结果集中有多少条记录)

我们把对驱动表进行查询后得到的记录条数称之为驱动表的扇出

计算方法为:

连接查询总成本 = 单次访问驱动表的成本 + 驱动表扇出数 x 单次访问被驱动表的成本

成本常数

server_cost表中在server层进行的一些操作对应的成本常数,具体内容如下:

 SELECT * FROM mysql.server_cost;
+------------------------------+------------+---------------------+---------+
| cost_name                    | cost_value | last_update         | comment |
+------------------------------+------------+---------------------+---------+
| disk_temptable_create_cost   |       NULL | 2024-01-08 03:16:44 | NULL    |
| disk_temptable_row_cost      |       NULL | 2024-01-08 03:16:44 | NULL    |
| key_compare_cost             |       NULL | 2024-01-08 03:16:44 | NULL    |
| memory_temptable_create_cost |       NULL | 2024-01-08 03:16:44 | NULL    |
| memory_temptable_row_cost    |       NULL | 2024-01-08 03:16:44 | NULL    |
| row_evaluate_cost            |       NULL | 2024-01-08 03:16:44 | NULL    |
+------------------------------+------------+---------------------+---------+
6 rows in set (0.00 sec)

我们先看一下server_cost各个列都分别是什么意思:

cost_name:表示成本常数的名称。

cost_value:表示成本常数对应的值。如果该列的值为NULL的话,意味着对应的成本常数会采用默认值。

last_update:表示最后更新记录的时间。

comment:注释。

成本常数名称 默认值 描述
disk_temptable_create_cost 40.0 创建基于磁盘的临时表的成本,如果增大这个值的话会让优化器尽量少的创建基于磁盘的临时表。
disk_temptable_row_cost 1.0 向基于磁盘的临时表写入或读取一条记录的成本,如果增大这个值的话会让优化器尽量少的创建基于磁盘的临时表。
key_compare_cost 0.1 两条记录做比较操作的成本,多用在排序操作上,如果增大这个值的话会提升filesort的成本,让优化器可能更倾向于使用索引完成排序而不是filesort。
memory_temptable_create_cost 2.0 创建基于内存的临时表的成本,如果增大这个值的话会让优化器尽量少的创建基于内存的临时表。
memory_temptable_row_cost 0.2 向基于内存的临时表写入或读取一条记录的成本,如果增大这个值的话会让优化器尽量少的创建基于内存的临时表。
row_evaluate_cost 0.2 这个就是我们之前一直使用的检测一条记录是否符合搜索条件的成本,增大这个值可能让优化器更倾向于使用索引而不是直接全表扫描。

修改方法:

UPDATE mysql.server_cost 
    SET cost_value = 0.4
    WHERE cost_name = 'row_evaluate_cost;
FLUSH OPTIMIZER_COSTS;

查询优化器--还需补充

设计MySQL的大佬还是依据一些规则,竭尽全力的把这个很糟糕的语句转换成某种可以比较高效执行的形式,这个过程也可以被称作查询重写

1、移除不必要的括号

((a = 5 AND b = c) OR ((a > c) AND (c < 5)))
重写为
(a = 5 and b = c) OR (a > c AND c < 5)

2、常量传递(constant_propagation)

a = 5 AND b > a
重写为
a = 5 AND b > 5

3、等值传递

a = b and b = c and c = 5
重写为
a = 5 and b = 5 and c = 5

4、移除没用的条件(trivial_condition_removal)

(a < 1 and b = b) OR (a = 6 OR 5 != 5)
简化为:
a < 1 OR a = 6

5、表达式计算

a = 5 + 1
重写为
a = 6

6、HAVING子句和WHERE子句的合并

如果查询语句中没有出现诸如SUM、MAX等等的聚集函数以及GROUP BY子句,优化器就把HAVING子句和WHERE子句合并起来。

7、常量表检测

以下两种情况被称为常量表:

查询的表中一条记录没有,或者只有一条记录。

使用主键等值匹配或者唯一二级索引列等值匹配作为搜索条件来查询某个表。

SELECT * FROM table1 INNER JOIN table2
    ON table1.column1 = table2.column2 
    WHERE table1.primary_key = 1;
重写为:
SELECT table1表记录的各个字段的常量值, table2.* FROM table1 INNER JOIN table2 
    ON table1表column1列的常量值 = table2.column2;

8、外连接消除

在外连接查询中,指定的WHERE子句中包含被驱动表中的列不为NULL值的条件称之为空值拒绝,在被驱动表的WHERE子句符合空值拒绝的条件后,外连接和内连接可以相互转换。这种转换带来的好处就是查询优化器可以通过评估表的不同连接顺序的成本,选出成本最低的那种连接顺序来执行查询。

9、In子查询
标量子查询

对于包含不相关的标量子查询或者行子查询的查询语句来说,MySQL会分别独立的执行外层查询和子查询,就当作两个单表查询就好了。

例如:

SELECT * FROM s1 WHERE 
    key1 = (SELECT common_field FROM s2 WHERE s1.key3 = s2.key3 LIMIT 1);

它的执行方式就是这样的:

  • 先从外层查询中获取一条记录,本例中也就是先从s1表中获取一条记录。

  • 然后从上一步骤中获取的那条记录中找出子查询中涉及到的值,本例中就是从s1表中获取的那条记录中找出s1.key3列的值,然后执行子查询。

  • 最后根据子查询的查询结果来检测外层查询WHERE子句的条件是否成立,如果成立,就把外层查询的那条记录加入到结果集,否则就丢弃。

  • 再次执行第一步,获取第二条外层查询中的记录,依次类推
    但是对于不相关的IN子查询,比如这样:

    SELECT * FROM s1
    WHERE key1 IN (SELECT common_field FROM s2 WHERE key3 = 'a');

如果还是按照上述查询可能会遇到一些问题:

  • 无法有效的使用索引,只能对外层查询进行全表扫描。
  • 在对外层查询执行全表扫描时,由于IN子句中的参数太多,这会导致检测一条记录是否符合和IN子句中的参数匹配花费的时间太长。
  • 该临时表的列就是子查询结果集中的列。
  • 写入临时表的记录会被去重。
  • 一般情况下子查询结果集不会大的离谱,所以会为它建立基于内存的使用Memory存储引擎的临时表,而且会为该表建立希索引。
    为了解决这个问题,设计MySQL的大佬把这个将子查询结果集中的记录保存到临时表的过程称之为物化(英文名:Materialize)。为了方便起见,我们就把那个存储子查询结果集的临时表称之为物化表。正因为物化表中的记录都建立了索引(基于内存的物化表有哈希索引,基于磁盘的有B+树索引),通过索引执行IN语句判断某个操作数在不在子查询结果集中变得非常快,从而提升了子查询语句的性能。

将子查询转换为semi-join

虽然将子查询进行物化之后再执行查询都会有建立临时表的成本,但是不管怎么说,我们见识到了将子查询转换为连接的强大作用,设计MySQL的大佬继续开脑洞:能不能不进行物化操作直接把子查询转换为连接呢?

这个还需要看

相关推荐
云和数据.ChenGuang4 小时前
Django 应用安装脚本 – 如何将应用添加到 INSTALLED_APPS 设置中 原创
数据库·django·sqlite
woshilys4 小时前
sql server 查询对象的修改时间
运维·数据库·sqlserver
Hacker_LaoYi4 小时前
SQL注入的那些面试题总结
数据库·sql
建投数据5 小时前
建投数据与腾讯云数据库TDSQL完成产品兼容性互认证
数据库·腾讯云
Hacker_LaoYi6 小时前
【渗透技术总结】SQL手工注入总结
数据库·sql
岁月变迁呀6 小时前
Redis梳理
数据库·redis·缓存
独行soc6 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍06-基于子查询的SQL注入(Subquery-Based SQL Injection)
数据库·sql·安全·web安全·漏洞挖掘·hw
你的微笑,乱了夏天7 小时前
linux centos 7 安装 mongodb7
数据库·mongodb
工业甲酰苯胺7 小时前
分布式系统架构:服务容错
数据库·架构
独行soc8 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍08-基于时间延迟的SQL注入(Time-Based SQL Injection)
数据库·sql·安全·渗透测试·漏洞挖掘