其他分享
首页 > 其他分享> > 13. InnoDB 统计数据是如何收集的

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

作者:互联网

InnoDB 统计数据是如何收集的

标签: MySQL 是怎样运行的


我们前边唠叨查询成本的时候经常用到一些统计数据,比如通过SHOW TABLE STATUS可以看到关于表的统计数据,通过SHOW INDEX可以看到关于索引的统计数据,那么这些统计数据是怎么来的呢?它们是以什么方式收集的呢?本章将聚焦于InnoDB存储引擎的统计数据收集策略,看完本章大家就会明白为啥前边老说InnoDB的统计信息是不精确的估计值了(言下之意就是我们不打算介绍MyISAM存储引擎统计数据的收集和存储方式,有想了解的同学自己个儿看看文档哈)。

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

InnoDB提供了两种存储统计数据的方式:

设计MySQL的大叔们给我们提供了系统变量innodb_stats_persistent来控制到底采用哪种方式去存储统计数据。在MySQL 5.6.6之前,innodb_stats_persistent的值默认是OFF,也就是说InnoDB的统计数据默认是存储到内存的,之后的版本中innodb_stats_persistent的值默认是ON,也就是统计数据默认被存储到磁盘中。 不过InnoDB默认是以表为单位来收集和存储统计数据的,也就是说我们可以把某些表的统计数据(以及该表的索引统计数据)存储在磁盘上,把另一些表的统计数据存储在内存中。怎么做到的呢?我们可以在创建和修改表的时候通过指定STATS_PERSISTENT属性来指明该表的统计数据存储方式:

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

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

STATS_PERSISTENT=1时,表明我们想把该表的统计数据永久的存储到磁盘上,当STATS_PERSISTENT=0时,表明我们想把该表的统计数据临时的存储到内存中。如果我们在创建表时未指定STATS_PERSISTENT属性,那默认采用系统变量innodb_stats_persistent的值作为该属性的值。

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

当我们选择把某个表以及该表索引的统计数据存放到磁盘上时,实际上是把这些统计数据存储到了两个表里:

mysql> SHOW TABLES FROM mysql LIKE 'innodb%';
+---------------------------+
| Tables_in_mysql (innodb%) |
+---------------------------+
| innodb_index_stats       |
| innodb_table_stats       |
+---------------------------+
2 rows in set (0.01 sec)

可以看到,这两个表都位于mysql系统数据库下边,其中:

我们下边的任务就是看一下这两个表里边都有什么以及表里的数据是如何生成的。

innodb_table_stats

直接看一下这个innodb_table_stats表中的各个列都是干嘛的:

字段名描述
database_name 数据库名
table_name 表名
last_update 本条记录最后更新时间
n_rows 表中记录的条数
clustered_index_size 表的聚簇索引占用的页面数量
sum_of_other_index_sizes 表的其他索引占用的页面数量

注意这个表的主键是(database_name,table_name),也就是innodb_table_stats表的每条记录代表着一个表的统计信息。我们直接看一下这个表里的内容:

mysql> SELECT * FROM mysql.innodb_table_stats;
+---------------+---------------+---------------------+--------+----------------------+--------------------------+
| database_name | table_name   | last_update         | n_rows | clustered_index_size | sum_of_other_index_sizes |
+---------------+---------------+---------------------+--------+----------------------+--------------------------+
| mysql         | gtid_executed | 2018-07-10 23:51:36 |     0 |                   1 |                       0 |
| sys           | sys_config   | 2018-07-10 23:51:38 |     5 |                   1 |                       0 |
| xiaohaizi     | single_table | 2018-12-10 17:03:13 |   9693 |                   97 |                     175 |
+---------------+---------------+---------------------+--------+----------------------+--------------------------+
3 rows in set (0.01 sec)

可以看到我们熟悉的single_table表的统计信息就对应着mysql.innodb_table_stats的第三条记录。几个重要统计信息项的值如下:

n_rows统计项的收集

为啥老强调n_rows这个统计项的值是估计值呢?现在就来揭晓答案。InnoDB统计一个表中有多少行记录的套路是这样的:

clustered_index_size和sum_of_other_index_sizes统计项的收集

统计这两个数据需要大量用到我们之前唠叨的InnoDB表空间的知识,如果大家压根儿没有看那一章,那下边的计算过程大家还是不要看了(看也看不懂);如果看过了,那大家就会发现InnoDB表空间的知识真是有用啊啊啊!!!

这两个统计项的收集过程如下:

这里需要大家注意一个问题,我们说一个段的数据在非常多时(超过32个页面),会以为单位来申请空间,这里头的问题是以区为单位申请空间中有一些页可能并没有使用,但是在统计clustered_index_sizesum_of_other_index_sizes时都把它们算进去了,所以说聚簇索引和其他的索引占用的页面数可能比这两个值要小一些。

innodb_index_stats

直接看一下这个innodb_index_stats表中的各个列都是干嘛的:

字段名描述
database_name 数据库名
table_name 表名
index_name 索引名
last_update 本条记录最后更新时间
stat_name 统计项的名称
stat_value 对应的统计项的值
sample_size 为生成统计数据而采样的页面数量
stat_description 对应的统计项的描述

注意这个表的主键是(database_name,table_name,index_name,stat_name),其中的stat_name是指统计项的名称,也就是说innodb_index_stats表的每条记录代表着一个索引的一个统计项。可能这会大家有些懵逼这个统计项到底指什么,别着急,我们直接看一下关于single_table表的索引统计数据都有些什么:

mysql> SELECT * FROM mysql.innodb_index_stats WHERE table_name = 'single_table';
+---------------+--------------+--------------+---------------------+--------------+------------+-------------+-----------------------------------+
| database_name | table_name   | index_name   | last_update         | stat_name   | stat_value | sample_size | stat_description                 |
+---------------+--------------+--------------+---------------------+--------------+------------+-------------+-----------------------------------+
| xiaohaizi     | single_table | PRIMARY     | 2018-12-14 14:24:46 | n_diff_pfx01 |       9693 |         20 | id                               |
| xiaohaizi     | single_table | PRIMARY     | 2018-12-14 14:24:46 | n_leaf_pages |         91 |       NULL | Number of leaf pages in the index |
| xiaohaizi     | single_table | PRIMARY     | 2018-12-14 14:24:46 | size         |         97 |       NULL | Number of pages in the index     |
| xiaohaizi     | single_table | idx_key1     | 2018-12-14 14:24:46 | n_diff_pfx01 |       968 |         28 | key1                             |
| xiaohaizi     | single_table | idx_key1     | 2018-12-14 14:24:46 | n_diff_pfx02 |     10000 |         28 | key1,id                           |
| xiaohaizi     | single_table | idx_key1     | 2018-12-14 14:24:46 | n_leaf_pages |         28 |       NULL | Number of leaf pages in the index |
| xiaohaizi     | single_table | idx_key1     | 2018-12-14 14:24:46 | size         |         29 |       NULL | Number of pages in the index     |
| xiaohaizi     | single_table | idx_key2     | 2018-12-14 14:24:46 | n_diff_pfx01 |     10000 |         16 | key2                             |
| xiaohaizi     | single_table | idx_key2     | 2018-12-14 14:24:46 | n_leaf_pages |         16 |       NULL | Number of leaf pages in the index |
| xiaohaizi     | single_table | idx_key2     | 2018-12-14 14:24:46 | size         |         17 |       NULL | Number of pages in the index     |
| xiaohaizi     | single_table | idx_key3     | 2018-12-14 14:24:46 | n_diff_pfx01 |       799 |         31 | key3                             |
| xiaohaizi     | single_table | idx_key3     | 2018-12-14 14:24:46 | n_diff_pfx02 |     10000 |         31 | key3,id                           |
| xiaohaizi     | single_table | idx_key3     | 2018-12-14 14:24:46 | n_leaf_pages |         31 |       NULL | Number of leaf pages in the index |
| xiaohaizi     | single_table | idx_key3     | 2018-12-14 14:24:46 | size         |         32 |       NULL | Number of pages in the index     |
| xiaohaizi     | single_table | idx_key_part | 2018-12-14 14:24:46 | n_diff_pfx01 |       9673 |         64 | key_part1                         |
| xiaohaizi     | single_table | idx_key_part | 2018-12-14 14:24:46 | n_diff_pfx02 |       9999 |         64 | key_part1,key_part2               |
| xiaohaizi     | single_table | idx_key_part | 2018-12-14 14:24:46 | n_diff_pfx03 |     10000 |         64 | key_part1,key_part2,key_part3     |
| xiaohaizi     | single_table | idx_key_part | 2018-12-14 14:24:46 | n_diff_pfx04 |     10000 |         64 | key_part1,key_part2,key_part3,id |
| xiaohaizi     | single_table | idx_key_part | 2018-12-14 14:24:46 | n_leaf_pages |         64 |       NULL | Number of leaf pages in the index |
| xiaohaizi     | single_table | idx_key_part | 2018-12-14 14:24:46 | size         |         97 |       NULL | Number of pages in the index     |
+---------------+--------------+--------------+---------------------+--------------+------------+-------------+-----------------------------------+
20 rows in set (0.03 sec)

这个结果有点儿多,正确查看这个结果的方式是这样的:

定期更新统计数据

随着我们不断的对表进行增删改操作,表中的数据也一直在变化,innodb_table_statsinnodb_index_stats表里的统计数据是不是也应该跟着变一变了?当然要变了,不变的话MySQL查询优化器计算的成本可就差老鼻子远了。设计MySQL的大叔提供了如下两种更新统计数据的方式:

手动更新innodb_table_statsinnodb_index_stats

其实innodb_table_statsinnodb_index_stats表就相当于一个普通的表一样,我们能对它们做增删改查操作。这也就意味着我们可以手动更新某个表或者索引的统计数据。比如说我们想把single_table表关于行数的统计书记更改一下可以这么做:

之后我们使用SHOW TABLE STATUS语句查看表的统计数据时就看到Rows行变为了1

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

当我们把系统变量innodb_stats_persistent的值设置为OFF时,之后创建的表的统计数据默认就都是非永久性的了,或者我们直接在创建表或修改表时设置STATS_PERSISTENT属性的值为0,那么该表的统计数据就是非永久性的了。

与永久性的统计数据不同,非永久性的统计数据采样的页面数量是由innodb_stats_transient_sample_pages控制的,这个系统变量的默认值是8

另外,由于非永久性的统计数据经常更新,所以导致MySQL查询优化器计算查询成本的时候依赖的是经常变化的统计数据,也就会生成经常变化的执行计划,这个可能让大家有些懵逼。不过最近的MySQL版本都不咋用这种基于内存的非永久性统计数据了,所以我们也就不深入唠叨它了。

innodb_stats_method的使用

我们知道索引列不重复的值的数量这个统计数据对于MySQL查询优化器十分重要,因为通过它可以计算出在索引列中平均一个值重复多少行,它的应用场景主要有两个:

在统计索引列不重复的值的数量时,有一个比较烦的问题就是索引列中出现NULL值怎么办,比方说某个索引列的内容是这样:

+------+
| col  |
+------+
|    1 |
|    2 |
| NULL |
| NULL |
+------+

此时计算这个col列中不重复的值的数量就有下边的分歧:

```
mysql> SELECT 1 = NULL;
+----------+
| 1 = NULL |
+----------+
|     NULL |
+----------+
1 row in set (0.00 sec)

mysql> SELECT 1 != NULL;
+-----------+
| 1 != NULL |
+-----------+
|      NULL |
+-----------+
1 row in set (0.00 sec)

mysql> SELECT NULL = NULL;
+-------------+
| NULL = NULL |
+-------------+
|        NULL |
+-------------+
1 row in set (0.00 sec)

mysql> SELECT NULL != NULL;
+--------------+
| NULL != NULL |
+--------------+
|         NULL |
+--------------+
1 row in set (0.00 sec)
```

所以每一个`NULL`值都是独一无二的,也就是说统计索引列不重复的值的数量时,应该把`NULL`值当作一个独立的值,所以`col`列的不重复的值的数量就是:`4`(分别是1、2、NULL、NULL这四个值)。

设计MySQL的大叔蛮贴心的,他们提供了一个名为innodb_stats_method的系统变量,相当于在计算某个索引列不重复值的数量时如何对待NULL值这个锅甩给了用户,这个系统变量有三个候选值:

反正这个锅是甩给用户了,当你选定了innodb_stats_method值之后,优化器即使选择了不是最优的执行计划,那也跟设计MySQL的大叔们没关系了哈~ 当然对于用户的我们来说,最好不在索引列中存放NULL值才是正解。

总结

 

标签:13,stats,统计数据,收集,索引,innodb,InnoDB,table,NULL
来源: https://www.cnblogs.com/zouzhibin/p/15898731.html