我们前边在计算查询成本的时候会用到一些统计数据,比如通过 SHOW TABLE STATUS 可以看到关于表的统计数据,通过 SHOW INDEX 可以看到关于某个索引的统计数据,那么这些统计数据是怎么来的呢?本章节将分享 InnoDB 存储引擎的统计数据收集策略,看完后大家就会明白InnoDB 的统计信息为什么只是一个估计值。
InnoDB 提供了两种存储统计数据的方式:
MySQL中提供了系统变量 innodb_stats_persistent 来控制存储统计数据的方式。在 MySQL 5.6.6 之前,innodb_stats_persistent 的值默认是 OFF ,也就是 InnoDB 的统计数据默认是存储到内存的,之后的版本中 innodb_stats_persistent 的值默认是 ON ,也就是统计数据默认被存储到磁盘中。
不过 InnoDB 默认是以表为单位来收集和存储统计数据的,也就是说我们可以把一个database中的某些表的统计数据(以及该表的索引统计数据)存储在磁盘上,把另一些表的统计数据存储在内存中。这可以通过在创建和修改表的时候通过指定 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.00 sec)
可以看到,这两个表都位于 mysql 系统数据库下边,其中:
我们下边的任务就是看一下这两个表里边都有什么以及表里的数据是如何生成的。
可以先查看innodb_table_stats这个表中的前3行数据:
mysql> select * from mysql.innodb_table_stats limit 3;
+---------------+---------------+---------------------+--------+----------------------+--------------------------+
| database_name | table_name | last_update | n_rows | clustered_index_size | sum_of_other_index_sizes |
+---------------+---------------+---------------------+--------+----------------------+--------------------------+
| charset_demo | test | 2023-10-16 22:23:17 | 5 | 1 | 0 |
| mysql | gtid_executed | 2023-10-11 09:43:40 | 0 | 1 | 0 |
| sys | sys_config | 2023-10-11 09:43:43 | 6 | 1 | 0 |
+---------------+---------------+---------------------+--------+----------------------+--------------------------+
3 rows in set (0.00 sec)
各列属性说明:
字段名 | 描述 |
---|---|
database_name | 数据库名 |
table_name | 表名 |
last_update | 本条记录最后更新时间 |
n_rows | 表中记录的条数 |
clustered_index_size | 表的聚簇索引占用的页面数量 |
sum_of_other_index_sizes | 表的其他索引占用的页面数量 |
下面查看一下我们之前创建的single_table表的相关信息:
mysql> SELECT * FROM mysql.innodb_table_stats where table_name='single_table';
+---------------+--------------+---------------------+--------+----------------------+--------------------------+
| database_name | table_name | last_update | n_rows | clustered_index_size | sum_of_other_index_sizes |
+---------------+--------------+---------------------+--------+----------------------+--------------------------+
| test | single_table | 2023-11-15 16:00:21 | 10143 | 97 | 142 |
+---------------+--------------+---------------------+--------+----------------------+--------------------------+
1 row in set (0.01 sec)
几个重要统计信息项的值如下:
我们说过 n_rows 这个统计项的值是估计值,那是为什么呢。 InnoDB 统计一个表中有多少行记录的方法是这样的:
我们知道 InnoDB 默认是以表为单位来收集和存储统计数据的,所以我们也可以单独设置某个表的采样页面的数量,设置方式就是在创建或修改表的时候通过指定STATS_SAMPLE_PAGES 属性来指明该表的统计数据存储方式如下:
CREATE TABLE 表名 (…) Engine=InnoDB, STATS_SAMPLE_PAGES = 具体的采样页面数量;
ALTER TABLE 表名 Engine=InnoDB, STATS_SAMPLE_PAGES = 具体的采样页面数量;
如果我们在创建表的语句中并没有指定 STATS_SAMPLE_PAGES 属性的话,将默认使用系统变量innodb_stats_persistent_sample_pages 的值作为该属性的值。
这两个数据的统计需要使用到我们之前学习的InnoDB 表空间的知识。
这两个统计项的收集过程如下:
表innodb_index_stats位于mysql这个database下:
mysql> show create table innodb_index_stats;
+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| innodb_index_stats | CREATE TABLE `innodb_index_stats` (
`database_name` varchar(64) COLLATE utf8_bin NOT NULL,
`table_name` varchar(199) COLLATE utf8_bin NOT NULL,
`index_name` varchar(64) COLLATE utf8_bin NOT NULL,
`last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`stat_name` varchar(64) COLLATE utf8_bin NOT NULL,
`stat_value` bigint(20) unsigned NOT NULL,
`sample_size` bigint(20) unsigned DEFAULT NULL,
`stat_description` varchar(1024) COLLATE utf8_bin NOT NULL,
PRIMARY KEY (`database_name`,`table_name`,`index_name`,`stat_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin STATS_PERSISTENT=0 |
+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
下面看一下这个 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 |
+---------------+--------------+--------------+---------------------+--------------+------------+-------------+-----------------------------------+
| test | single_table | PRIMARY | 2023-10-25 09:56:21 | n_diff_pfx01 | 9982 | 20 | id |
| test | single_table | PRIMARY | 2023-10-25 09:56:21 | n_leaf_pages | 62 | NULL | Number of leaf pages in the index |
| test | single_table | PRIMARY | 2023-10-25 09:56:21 | size | 97 | NULL | Number of pages in the index |
| test | single_table | idx_key1 | 2023-10-25 09:56:21 | n_diff_pfx01 | 9904 | 16 | key1 |
| test | single_table | idx_key1 | 2023-10-25 09:56:21 | n_diff_pfx02 | 9904 | 16 | key1,id |
| test | single_table | idx_key1 | 2023-10-25 09:56:21 | n_leaf_pages | 16 | NULL | Number of leaf pages in the index |
| test | single_table | idx_key1 | 2023-10-25 09:56:21 | size | 17 | NULL | Number of pages in the index |
| test | single_table | idx_key2 | 2023-10-25 09:56:21 | n_diff_pfx01 | 9904 | 10 | key2 |
| test | single_table | idx_key2 | 2023-10-25 09:56:21 | n_leaf_pages | 10 | NULL | Number of leaf pages in the index |
| test | single_table | idx_key2 | 2023-10-25 09:56:21 | size | 11 | NULL | Number of pages in the index |
| test | single_table | idx_key3 | 2023-10-25 09:56:21 | n_diff_pfx01 | 9904 | 16 | key3 |
| test | single_table | idx_key3 | 2023-10-25 09:56:21 | n_diff_pfx02 | 9904 | 16 | key3,id |
| test | single_table | idx_key3 | 2023-10-25 09:56:21 | n_leaf_pages | 16 | NULL | Number of leaf pages in the index |
| test | single_table | idx_key3 | 2023-10-25 09:56:21 | size | 17 | NULL | Number of pages in the index |
| test | single_table | idx_key_part | 2023-10-25 09:56:21 | n_diff_pfx01 | 9905 | 33 | key_part1 |
| test | single_table | idx_key_part | 2023-10-25 09:56:21 | n_diff_pfx02 | 9905 | 33 | key_part1,key_part2 |
| test | single_table | idx_key_part | 2023-10-25 09:56:21 | n_diff_pfx03 | 9905 | 33 | key_part1,key_part2,key_part3 |
| test | single_table | idx_key_part | 2023-10-25 09:56:21 | n_diff_pfx04 | 9905 | 33 | key_part1,key_part2,key_part3,id |
| test | single_table | idx_key_part | 2023-10-25 09:56:21 | n_leaf_pages | 33 | NULL | Number of leaf pages in the index |
| test | single_table | idx_key_part | 2023-10-25 09:56:21 | size | 97 | NULL | Number of pages in the index |
+---------------+--------------+--------------+---------------------+--------------+------------+-------------+-----------------------------------+
20 rows in set (0.00 sec)
说明:
注意:对于普通的二级索引,并不能保证它的索引列值是唯一的,比如对于idx_key1来说,key1列就可能有很多值重复的记录。此时只有在索引列上加上主键值才可以区分两条索引列值都一样的二级索引记录。对于主键和唯一二级索引则没有这个问题,
它们本身就可以保证索引列值的不重复,所以也不需要再统计一遍在索引列后加上主键值的不重复值有多少。比如上边的idx_key1有n_diff_pfx01、n_diff_pfx02两个统计项,而idx_key2却只有n_diff_pfx01一个统计项。
注意:对于有多个列的联合索引来说,采样的页面数量是:innodb_stats_persistent_sample_pages× 索引列的个数。当需要采样的页面数量大于该索引的叶子节点数量的话,就直接采用全表扫描来统计索引列的不重复值数量了。所以大家可以在查询结果中看到不同索引对应的size列的值可能是不同的。
随着我们不断的对表进行增删改操作,表中的数据也一直在变化, innodb_table_stats 和 innodb_index_stats表里的统计数据也会跟着一起更新变化的。在MySQL中提供了如下两种更新统计数据的方式:
说明, InnoDB 默认是以表为单位来收集和存储统计数据的,我们也可以单独为某个表设置是否自动重新计算统计数的属性,设置方式就是在创建或修改表的时候通过指定 STATS_AUTO_RECALC 属性来指明该表的统计数据存储方式:
CREATE TABLE 表名 (…) Engine=InnoDB, STATS_AUTO_RECALC = (1|0);
ALTER TABLE 表名 Engine=InnoDB, STATS_AUTO_RECALC = (1|0);
当 STATS_AUTO_RECALC=1 时,表明我们想让该表自动重新计算统计数据,当 STATS_PERSISTENT=0 时,表明不想让该表自动重新计算统计数据。如果我们在创建表时未指定 STATS_AUTO_RECALC 属性,那默认采用系统变量 innodb_stats_auto_recalc 的值作为该属性的值。
mysql> use test;
Database changed
mysql> ANALYZE TABLE single_table;
+-------------------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+-------------------+---------+----------+----------+
| test.single_table | analyze | status | OK |
+-------------------+---------+----------+----------+
1 row in set (0.04 sec)
mysql>
注意:执行NAL YZE T ABLE语句会立即重新计算统计数据,也就是这个过程是同步的,在表中索引多或者采样页面特别多时这个过程可能会特别慢,所以最好不要经常运行 ANALYZE TABLE 语句,而且在业务量小的时候再执行。
其实 innodb_table_stats 和 innodb_index_stats 表就相当于一个普通的表一样,我们也能对它们做增删改查操作。这也就意味着我们可以手动更新某个表或者索引的统计数据。比如说我们想把 single_table 表关于行数的统计数据更改一下可以这么做:
之后我们使用 SHOW TABLE STATUS 语句查看表的统计数据时就看到 Rows 行变为了 1 。
当我们把系统变量 innodb_stats_persistent 的值设置为 OFF 时,之后创建的表的统计数据默认就都是非永久性的了,或者我们直接在创建表或修改表时设置 STATS_PERSISTENT 属性的值为 0 ,那么该表的统计数据就是非永久性的了。
与永久性的统计数据不同,非永久性的统计数据采样的页面数量是由 innodb_stats_transient_sample_pages 控制的,这个系统变量的默认值是 8 。这个是否使用跟使用的版本有关。
我们知道索引列不重复的值的数量这个统计数据对于 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.03 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 值之后,优化器即使选择了不是最优的执行计划,那就是自己设置的问题。所以对于用户来说,最好不在索引列中存放NULL值才是明智之举。
更多关于mysql的知识分享,请前往博客主页。编写过程中,难免出现差错,敬请指出