《MySQL是怎么运行的:从根儿上理解MySQL》(11-13)学习总结

说明

文章的图片来源《MySQL是怎么运行的:从根儿上理解MySQL》,本篇文章只是个人学习总结,欢迎大家买一本看看,对于mysql是由浅入深的讲解非常细致

11.连接的原理

SELECT * FROM t1, t2;

  • 连接的本质就是查询每条记录然后组合起来,这种是笛卡尔积组合
  • 直接在from后面加表就可以了

image-20211101150155902

连接过程简介

为了不让笛卡尔积太大,所以在连接的时候需要过滤一些条件

  • 单表条件

t1.m1 > 1

  • 涉及两表条件

1.m1 = t2.m2、t1.n1 > t2.n2

SELECT * FROM t1, t2 WHERE t1.m1 > 1 AND t1.m1 = t2.m2 AND t2.n2 < ‘d’;

  • 三个条件t1.m1 > 1
  • t1.m1 = t2.m2
  • t2.n2 < ‘d’;

连接的大致过程

  1. 连接需要一个驱动表现在这个驱动表就是t1,t1会根据查询条件代价最小的那个索引去进行查询。然后才会回表对比,这个时候没有建立索引所以是全表扫描。

image-20211101150624605

  1. 接着就是去被驱动表t2上面去查找t1.m1=t2.m1的记录,一共就是查询两次。并且需要对比t2.n2 < ‘d’;如果符合条件那么就加入到结果集
    • 这里两条记录分别是t1.m1=2的时候,那么就要去找t2.m1=2而且t2.n2<d的记录
    • t1.m1=3也是同样的操作
    • 对于t2来说就是两个查询条件,并且执行了单表查询
  2. 所以总共查询了一次t1表,2次t2表

image-20211101151102908

内连接和外连接

CREATE TABLE student (
number INT NOT NULL AUTO_INCREMENT COMMENT '学号',
name VARCHAR(5) COMMENT '姓名',
major VARCHAR(30) COMMENT '专业',
PRIMARY KEY (number)
) Engine=InnoDB CHARSET=utf8 COMMENT '学生信息表';


CREATE TABLE score (
number INT COMMENT '学号',
subject VARCHAR(30) COMMENT '科目',
score TINYINT COMMENT '成绩',
PRIMARY KEY (number, score)
) Engine=InnoDB CHARSET=utf8 COMMENT '学生成绩表';

SELECT s1.number, s1.name, s2.subject, s2.score FROM student AS s1, score AS s2 WHERE s1.number = s2.number;

  • 上面这个查询可以查询出所有同学的姓名和科目和成绩
  • 但是如果有一个b同学如果没有参加考试导致最后的是没有成绩记录的这种应该如何解决?答案就是通过内连接和外连接,外连接即使被驱动表没有这个记录依然能够查询出来。

内连接

  • 驱动表在被驱动表没有找到这个匹配记录那么就不会加入到结果集

外连接

  • 就算没有在被驱动表找到这个记录,也仍然需要加入到结果集
    • 左外连接:左边的表是驱动表
    • 右外连接:右边的表是驱动表
  • 除了连接条件还有where过滤条件
  • on子句的过滤条件:如果驱动表没有在被驱动表找到记录仍然需要把记录加入结果集
  • on是为了把驱动表找不到的

嵌套循环连接

  • 如果有第三个表,那么查询出来的结果集就是第三个表连接驱动表

for each row in t1 { #此处表示遍历满足对t1单表查询结果集中的每一条记录

for each row in t2 { #此处表示对于某条t1表的记录来说,遍历满足对t2单表查询结果集中的每一条记录

for each row in t3 { #此处表示对于某条t1和t2表的记录组合来说,对t3表进行单表查询 if row satisfies join conditions, send to client }

}

}

  • 可以回忆一下连接的整个过程
    • 首先就是在驱动表找到对应条件的记录,然后再去被驱动表中查找符合条件的记录。这个查找方式其实就是单表查询,驱动表没有任何单表过滤条件,相当于就是驱动表的所有记录都需要去被驱动表去查询对应的双表条件的数据。暂时这两种查询都是all类型的查询
    • 必须清楚表连接查询并不是只查询了一次表。所以才会有这个嵌套循环问题

SELECT * FROM t2 WHERE t2.m2 = 2 AND t2.n2 < ‘d’;

  • 对于表1的查询之后的表2的查询其实就是上面这条语句,也就说对于表2来说也是可以使用索引来进行等值的查询。这条语句如果是二级索引查询,那么查询代价就是ref,并且需要回表去对比索引n2<d是否符合,如果是唯一索引查询那么就是eq_ref.
  • 或者是在n2上面建立对应的索引

image-20211101164328960

基于块的循环嵌套连接

  • 目标尽量减少被驱动表的访问次数。不然表查询数据的时候其实是把数据加载进内存,然后再进行读取,在这个期间不一定能够把所有数据全部读入内存,所以会进行多次IO。每次IO的代价是非常大的
  • 而且如果没有缓存的话,正常来说一个读取数据的流程是,先从驱动表获取一条记录,然后把被驱动表加载进内存,接着匹配一条记录。然后从内存释放,然后再从驱动表获取一条记录重复上述的操作。所以对于这种问题那么就需要join buffer,相当于就是先把驱动表的查询记录先存放到这里,然后再读取一次被驱动表进内存一次性把所有的记录进行匹配
  • 对于这种加入了join buffer的嵌套连接查询就是块的循环嵌套连接

image-20211101165617119

12.基于成本优化

什么是成本

  • IO成本,也就是从磁盘加载数据到内存的时候
  • CPU成本,检测查询代价,排序等

读取一个页面的默认成本是1,读取一条记录的成本是0.2

基于成本优化的步骤

在查询之前都需要进行优化器的优化

  • 首先找出查询语句可以使用的所有索引
  • 计算全表扫描的代价
  • 使用不同索引的代价
  • 对比并选出成本最低的索引

SELECT * FROM single_table WHERE

key1 IN (‘a’, ‘b’, ‘c’) AND key2 > 10 AND

key2 < 1000 AND key3 > key2 AND

key_part1 LIKE ‘%hello%’ AND common_field = ‘123’;

根据步骤

  1. 这里可以使用的索引只有key1和key2

  2. 计算全表扫描的代价:聚簇索引+对比加入结果集。由于需要把对应的页加载进内存,所以需要两个成本IO和CPU成本

  • 聚簇所占的页面(IO成本)

  • 表的记录数(CPU成本)

  • 那么这些信息从哪里来?答案就是统计信息上面来。SHOW TABLE STATUS或者是查询特定的表的话就是show table status like “single_table”

    • 重点的参数是rows:也就是表到底有多少条记录,对于innodb来说这个值是预估的
    • Data_length:表占用存储空间的字节数对于MyISAM来说就是数据文件大小,对于innodb来说就是聚簇索引的大小,Data_length = 聚簇索引的页面数量 x 每个页面的大小
  • 假设聚簇索引的页面数量是97

    • IO成本

    97 * 1.0(成本常数) *+1.1(微调)=98.1

    • CPU成本(假设现在的表的总记录预估是9693)

    9693 * 0.2(成本常数)+1.0=1939.6

    • 总成本

    98.1 + 1939.6 = 2037.7

  1. 计算不同索引所需要的代价
    • 优先分析唯一二级索引再分析普通二级索引
  • 如果先使用idx_key2

key2 > 10 AND key2 < 1000也就是一个range代价查询,也就是二级索引+回表

  • 计算成本依赖的数据

    • 范围区间数量

    二级索引到底占用了多少个页,这里innodb会认为查询索引所需要的范围IO成本是读取一个页的IO成本也就是1 * 1.0=1.0

    image-20211101173409321

    • 需要回表的记录数

      • 先查找key2>10的第一条记录这个是常数级别查询
      • 然后就是查询key2<1000的第一条记录
      • 这个时候就统计他们之间大概的二级索引的记录数。如果相隔不超过10个页那么可以直接统计,如果不行那么只能从从左往右读10个页计算平均记录,然后乘以最左和最右之间的页数得到记录数。那么怎么计算他们之间的页数?

      image-20211101174138524

      • 对于目录项来说每个记录就是一个数据页,只要统计目录项的个数就可以知道对应的页数了。如果页面是在是太多就会继续递归到下一个目录项统计记录数得到b和c之间的页数
      • 索引查询CPU成本:假设最后计算出来的总记录数是页数 * 记录=95,那么cpu成本就是95 x 0.2 + 0.01 = 19.01。也就是要遍历的所有记录所需要的CPU成本(这里索引key2的查询成本就计算结束了,但是还有回表的成本还没有进行记录)
    • 获取记录之后仍然需要回表,对于上面仅仅只是计算索引key2搜索到对应记录所需要的CPU(遍历记录)和IO成本(要读取的内存页,默认范围代价是1个页的IO成本)

    • 回表IO成本:每次回表的IO成本是一个页面,上面计算总记录数也就是索引key2查出来的记录,都是需要回表的。所需要的IO成本是95 * 1.0

    • 回表CPU成本:这个时候就是95 * 0.2=19.0每条只需要检测除key2 > 10 AND key2 < 1000之外的查询条件。

    最后统计就是

    • IO成本:1(索引IO成本)+95(回表IO成本)=96
    • CPU成本:95 x 0.2(索引CPU成本) + 0.01 + 95 x 0.2(回表CPU成本) = 38.01

    总共就134.01

  • 对于idx_key1的查询成本,同样也是上面的步骤

  • 范围间的数量

3个单点区间(key1=a、b、c),刚才id_key2不一样的地方就是只有一个范围,这里虽然看上去数据更少,但是相对应的数据页相隔更远,所以是一个随机IO。每个区间相当于就是一次查询页IO成本也就是1.总的就是3 * 1.0=3.0

  • 需要回表的记录数

那么就要查询三个范围之间的一个记录数。计算方式也是和上面的key2一样,取最左最右,相隔不远直接计算记录数,相隔太远就每10页的记录平均值乘最左最右之间的页数(页数计算依靠目录项的记录和递归计算,相对成本比较小)

假设a、b、c的计算出来的的回表记录数是35、44、39

那么对于二级索引的CPU成本就是(35+44+39) * 0.1+0.01=23.61

  • 回表成本计算

IO成本:118 * 1

CPU成本:118 * 0.2

所以总成本是168.21(包括索引和IO)

image-20211101175805956

  1. 最后就是对比所有索引的代价选出成本最小的那个
  • 全表扫描的成本:2037.7

  • 使用idx_key2的成本:134.01

  • 使用idx_key1的成本:168.21

很明显这里已经可以看出来就是idx_key2成本更小,原因就是回表的记录数更少。

基于索引的统计数据的成本计算

SELECT * FROM single_table WHERE key1 IN (‘aa1’, ‘aa2’, ‘aa3’, … , ‘zzz’);

  • 如果一直这个in里面塞东西,最后造成的问题就是index dive太多,因为对于普通索引每个点都需要进行记录计算(平均记录+页数)。
  • eq_range_index_dive_limit这个就是in里面的数据最大的数量,如果超过数量那么就会通过索引统计数据来进行对记录计算
  • show index from single_table这就是维护的索引统计数据

属性

image-20211101190336427

  • Cardinality:索引中不重复的值的数量。相当于就是基数
  • 首先就是查询表的记录数,然后再查询Cardinality的数量,一个值重复的次数Rows ÷ Cardinality

SELECT * FROM single_table WHERE key1 IN (‘aa1’, ‘aa2’, ‘aa3’, … , ‘zzz’);

  • 加入key1计算出来Rows ÷ Cardinality=10说明key1的重复个数大概在10左右,假设上面的语句参数是20000个,那么也就是说回表的次数就是20000 * 10,原因就是每个key1重复大概10次左右。

连接查询的成本

Condition filtering介绍

对于驱动表可能查询一次,但是对于被驱动表可能被查询多次

  • 单表查询成本
  • 多次查询被驱动表成本(取决驱动表查出记录数)

1.SELECT * FROM single_table AS s1 INNER JOIN single_table2 AS s2;

  • 这个主要看s1驱动表的所有记录,因为就是一个全表扫描

2.SELECT * FROM single_table AS s1 INNER JOIN single_table2 AS s2

WHERE s1.key2 >10 AND s1.key2 < 1000;

  • 对于这条,就是看看key2>10和key2<1000的时候扇出值是多少

3.SELECT * FROM single_table AS s1 INNER JOIN single_table2 AS s2

WHERE s1.common_field > ‘xyz’;

  • 也是全表扫描,因为没有这个索引可以使用

4.SELECT * FROM single_table AS s1 INNER JOIN single_table2 AS s2

WHERE s1.key2 > 10 AND s1.key2 < 1000

AND s1.common_field > ‘xyz’;

  • 和第二条差不多,只不过回表之后多了一个判断s1.common_field > ‘xyz’;

5.SELECT * FROM single_table AS s1 INNER JOIN single_table2 AS s2

WHERE s1.key2 > 10 AND s1.key2 < 1000

AND s1.key1 IN (‘a’, ‘b’, ‘c’) AND s1.common_field > ‘xyz’;

  • 和查询2类似还是使用key2的索引。但是回表之后需要增加判断AND s1.key1 IN (‘a’, ‘b’, ‘c’) AND s1.common_field > ‘xyz’;

扇出值就是驱动表查询出来的记录,扇出越少,被驱动表就查询越少

  1. 对于上面的查询语句,全表扫描的单表查询需要猜到底多少条数据符合,最后计算驱动扇出
  2. 如果是索引执行的单表扫描,也还是要猜有多少个索引记录是符合条件的。
  3. 这个猜的过程就是Condition filtering

两表连接的成本分析

成本=单次访问驱动表+驱动表扇出次数 * 单次访问被驱动表的成本

  • 左连接和右连接都是固定的,只能做选择查询成本最低的方案
  • 考虑表的最优连接顺序
  • 并且再选择表查询成本最低的方案

SELECT * FROM single_table AS s1 INNER JOIN single_table2 AS s2

ON s1.key1 = s2.common_field

WHERE

s1.key2 > 10 AND s1.key2 < 1000 AND

s2.key2 > 1000 AND s2.key2 < 2000;

  • 连接表的顺序可以是s1或者是s2作为驱动表

s1作为驱动表

  1. s1的条件是s1.key2 > 10 AND s1.key2 < 1000只能使用key2的索引
  2. 然后再分析对于被驱动表的成本s2.common_field = 常数(没有用到索引,所以需要全表扫描)和s2.key2 > 1000 AND s2.key2 < 2000(key2是索引列可以使用索引),这里的常数是s1计算出来的那些记录。这里选择了key2的索引成本更小,减少全表扫描。

那么这个时候成本就是idx_key2访问s1的成本+s1的扇出值(也就是s1查出多少个记录,这个要靠猜)* idx_key2访问s2的成本

s2作为驱动表

分析驱动表的最低成本

  • 直接使用idx_key2成本更低,比全表扫描好

分析被驱动表的成本

s1.key1 = 常数

s1.key2 > 1000 AND s1.key2 < 2000

  • 这里和s1为驱动表不同,s1作为被驱动表可以使用key1作为索引,级别是ref,也可以使用key2索引级别是range

  • 在这里会选择key1的方案,因为级别是ref,但是对于这个的key1的常数无法预算,这里的成本就需要通过靠猜了。直接使用索引统计数据的猜出的s2扇出值。

成本=idx_key2访问s2的成本+s2的扇出值 * s1的最低查询成本

  • 尽量减少驱动表的扇出
  • 被驱动表成本尽可能低
  • 对于这里的等值,其实是对被驱动表的作用是最大的。
  • 在被驱动表上面建立索引最好即使唯一二级索引,能够减少回表的成本

多表连接的成本分析

  • 多表就代表连接方式很多,不可能全部连接方式都去尝试
  • 提前结束的预估方式

意思就是计算了ABC的成本是10,如果BC已经大于10,那么就不会继续计算下去。

  • 系统变量optimizer_search_depth

如果连接表分析小于这个值,那么就需要递归分析,直到分析次数大于这个值就停止,并且记录最下成本的连接方案。其实就是限制分析次数,但是它越大分析就越精准

  • 启发式规则

optimizer_prune_level来代表是不是用启发式规则,如果不符合这些那么就干脆不计算分析成本了。

调节成本常数

  • SHOW TABLES FROM mysql LIKE ‘%cost%’;成本常数存在哪个表
  • 这里查出来的是server和engine的cost。也就是不同层的一个成本分析

mysql.server_cost;

SELECT * FROM mysql.server_cost;

  • cost_name:成本常数
  • cost_value:值,如果是null那么就是默认值
  • last_update:最后一次更新时间
  • comment:注释

image-20211101200754519

  • row_evaluate_cost:检测记录的成本,全表扫描。如果成本变大,那么就尽量使用索引来进行搜索

image-20211101201113141

  • UPDATE mysql.server_cost SET cost_value = 0.4 WHERE cost_name = ‘row_evaluate_cost’;调整这些值
  • FLUSH OPTIMIZER_COSTS;修改成功刷新

mysql.engine_cost表

  • io_block_read_cost:从磁盘读取一个块,MyISAM是4096字节一个块,对于innodb是一个页
  • memory_block_read_cost:这是从内存中读取一个块的成本

image-20211101201646157

总结

  • 单表查询的成本选择,包括IO成本(取多少个页)和CPU成本(检测条件对比)
  • 接着就是连接成本
  1. 关键就是分析谁是驱动表,这个要靠计算分析驱动表的最低查询成本和驱动表的扇出值,被驱动表的成本等。那么什么时候需要用到扇出值(猜)?就是在被驱动表的关联的那个列有索引的时候,就需要使用扇出值来模拟驱动表搜索出来的记录(索引统计法),
  2. 然后就是驱动表的一个扇出值 * 被驱动表的单表查询最小代价就是被驱动表所需要的一个代价+驱动表的最小查询代价就是两表查询的一个代价。需要使用扇出值的部分就是如果
  • 索引的成本计算,如果是普通索引那么就需要计算索引成本+回表成本。然后和全表扫描进行对比

  • 对于范围查询,比如in(xx,aa,bb)可以通过index dive(计算平均记录和总查询记录数)或者是索引统计分析数据,快速计算范围内的记录数。索引统计计算的是一个索引列的基数,最后通过参数和(rows/基数)的相乘得到所需要的回表记录(也可以说是范围记录的大概计算)。

  • 最后就是成本参数

13.InnoDB 统计数据是如何收集的

两种不同的统计数据存储方式

  • 永久性统计数据

这种存储在磁盘

  • 非永久性统计数据

存储在内存,关闭后就消失了

  • mysql提供了innodb_stats_persistent来控制哪种方式去存储统计数据,通常存储在内存之后的版本就是存储到磁盘

CREATE TABLE 表名 (…) Engine=InnoDB, STATS_PERSISTENT = (1|0);

ALTER TABLE 表名 Engine=InnoDB, STATS_PERSISTENT = (1|0);

  • 对于统计数据的存储就是按照表为单位来进行存储,0是内存,1是磁盘存储

基于磁盘的永久性统计数据

  • 基本上每个表的统计信息都是存储到这两个表上面
  • innodb_index_stats:存储索引相关的统计信息
  • innodb_table_stats:存储表相关的统计信息

image-20211101204647291

innodb_table_stats

  • n_rows:就是表的预测行数
  • clustered_index_size:就是聚簇索引的页面个数
  • sum_of_other_index_sizes:其它索引占用的页面

n_rows统计项的收集

  • 随机取叶子节点的记录数。然后乘以叶子节点的全部个数,取决与取样的叶子节点。
  • innodb_stats_persistent_sample_pages这个变量就是用于控制系统取的叶子节点个数,个数多那么就越精准。但是耗费时间也多

CREATE TABLE 表名 (…) Engine=InnoDB, STATS_SAMPLE_PAGES = 具体的采样页面数量;

ALTER TABLE 表名 Engine=InnoDB, STATS_SAMPLE_PAGES = 具体的采样页面数量;

  • 可以通过上面两个语句来指定采样的页面数量

clustered_index_size和sum_of_other_index_sizes统计项的收集

  • 先找到各个索引的根页面位置,SYS_INDEXES存储了所有索引的位置
  • 从根页面的Page Header,找到Segment Header,也就是叶子节点段的节点和非叶子节点段节点的位置。
  • 连接到关键就是SYS_INDEXS表(表可以关联index)->索引页位置(索引页关联段)->Segment Header的位置
  • 找到INODE Entry之后计算零碎页+所有段占用的区的大小。

image-20211101205713731

image-20211101205744241

  • 通过ListLength来计算链表的区的多少,然后统计零碎页最后计算出索引占用的页大小sum_of_other_index_sizes。这里的计算有的段的页占用页数多,那么以区为单位进行空间分配,会把那些区没有使用的页也计算进来,也就是索引占用的页面比起估算值可能偏小

image-20211101210125829

innodb_index_stats

  • 记录的是每个索引的统计项

image-20211101210435432

  • index_name索引名

  • stat_name:索引统计项名称

    • n_leaf_pages:索引叶子节点占用的页数
    • size:索引一共占用的页数
    • n_diff_pfxNN:索引列不重复的值,NN表示的是前几个索引列的一个基数情况

    n_diff_pfx01表示的是统计key_part1这单单一个列不重复的值有多少。

    n_diff_pfx02表示的是统计key_part1、key_part2这两个列组合起来不重复的值有多少。

image-20211101210616305

  • 计算索引列不重复值可以通过取样,取多个页进行求平均值,如果是多个索引列,那么就要取sample * 索引列个数的页计算,如果大于索引的叶子节点个数,就全表扫描统计。

定期更新统计数据

  • innodb_stats_auto_recalc可以自动来统计信息,如果记录信息发生了变化,并且超过表中数据的百分之10,这个计算是异步的,可能会延迟几秒进行计算

CREATE TABLE 表名 (…) Engine=InnoDB, STATS_AUTO_RECALC = (1|0);

ALTER TABLE 表名 Engine=InnoDB, STATS_AUTO_RECALC = (1|0);

  • ANALYZE TABLE手动来更新统计信息,这个计算是同步的,所以对于索引多和采样多的表来说那么计算就会很麻烦(索引的重复基数,表的行数,表中索引占用的页面)

基于内存的非永久性统计数据

  • 如果修改innodb_stats_persistent为off(0)那么就是使用基于内存的统计数据
  • innodb_stats_transient_sample_pages就是取样数据页面个数,默认是8

innodb_stats_method的使用

直到索引列不重复值很重要

SELECT * FROM tbl_name WHERE key IN (‘xx1’, ‘xx2’, …, ‘xxn’);

  • 单点区间太多如果每次都要去单独计算每个区间的记录数,消耗大量性能,还不如直接通过索引基数来进行计算。

SELECT * FROM t1 JOIN t2 ON t1.column = t2.key WHERE …;

  • 连接查询的等值匹配条件的驱动表的扇出值未知,而且被驱动表的列有索引的时候,不能直接通过B+树去统计单点区间的记录数量,所以只能猜了,看看平均一个值会重复多少行来进行计算。

  • 如果索引列出现null怎么办?null作为一个独立的值或者是null只存在一个,又或者是不存在

  • innodb_stats_method这个方法就是把如何看待null值交给用户

image-20211101212825955

总结

  • innodb通过表为单位收集数据,可以基于内存或者是磁盘
  • 收集方式基本上都是样本+平均值减少计算量
  • 统计性数据包括了表类型的(数据行、索引占用页)、索引类(索引的记录基数)
  • innodb_stats_persistent控制统计数据的存储方式,innodb_stats_persistent_sample_pages是磁盘的样本页抽取个数,innodb_stats_transient_sample_pages是内存的。innodb_stats_auto_recalc是否自动进行数据统计
  • 创建表可以通过定STATS_PERSISTENT、STATS_AUTO_RECALC、STATS_SAMPLE_PAGES的来指定属性
  • innodb_method决定如何对待null
  • 总体来说就是统计数据使用的方案和统计数据的存储格式一个介绍。而且通过这次的统计能够更深刻意识到系统如何关联到表,如果从表到索引,索引到段的一个查询过程。

おすすめ

転載: blog.csdn.net/m0_46388866/article/details/121090140