MySQL之InnoDB统计数据

背景

通过 show table statusshow index 可以看到表和索引相关的统计信息,例如表中有多少条记录,索引里有多少重复的,

就像我们平时统计表里的数据一共有多少条等等会通过一些聚合函数去做运算,这种运算好处是很准确,基本是实时的,但是带来的问题就是很慢,数据越大越慢,

因此一些业务场景我们选择将一些统计数据存在一个字段里,通过准实时的计算更新这个字段,每次查询这个字段就可以了,这个带来的问题则是实时准确性有偏差。

同样,mysql不可能查询表中记录一共有多少条要去每一页每一页中去实时算,那就太慢了,索引也是如此,需要有一个存放这个数据的地方,每次来这里查询即可。

以下基于Innodb引擎说明。

统计数据的存储方式

  1. 磁盘存储统计数据
  2. 内存存储统计数据

前者不会随着服务重启丢失数据,后者重启后数据将丢失。

MySQL提供了一个名为 innodb_stats_persistent 的系统变量,值为OFF时表示内存存储,ON时表示磁盘存储,从5.6.6开始,ON是默认值。

innodb_stats_persistent以表为单位进行控制,即有的表统计数据存磁盘,有的存内存,可通过创建表时或修改表指定统计数据存储方式:

// 创建时指定
create table 表名 (...) engine=innodb, stats_persistent = (1 | 0);

// 修改指定
alter table 表名 engine=innodb, stats_persistent = (1 | 0);
复制代码

基于磁盘统计

磁盘统计数据存放在两张表中:

  1. innodb_index_stats
  2. innodb_table_stats

image.png

innodb_table_stats

查询表统计数据:select * from mysql.innodb_table_stats

image.png

innodb_table_stats表中每个列的含义:

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

以上面截图中的 single_talbe 表举例,记录一共1995条,聚簇索引占8个页面,其他索引占14个页面。

n_rows收集方式

MySQL并不会去扫描全表计算记录有多少,而是根据一定的算法从聚簇索引中选取部分叶子节点页面,统计每个页面中的记录数量,随后计算这些页面数量的平均值,最后乘上全部叶子节点的数量就得出了记录数,

因此 n_rows 是否准确取决于统计时的采样页面数量,MySQL提供了一个 innodb_stats_persistent_sample_pages 变量,

在使用磁盘统计时,改变量表示采样页面数量,该值越大,统计结果越接近准确,不过值越大耗时也越长,需要平衡,默认值是20.

该值可以通过下面两种方式指定:

// 创建时指定
create table 表名 (...) engine=innodb, stats_sample_pages = 采样页面数量;

// 修改指定
alter table 表名 engine=innodb, stats_sample_pages = 采样页面数量;
复制代码

clustered_index_size与sum_of_other_index_size收集方式

  1. 一个索引占用两个段,叶子节点段和非叶子节点段
  2. 每个段由一些零散的页面和一些完整的区组成

因此统计出每个索引的两个段占用的页面数量就可以得出结果。

统计每个段占用的页面数量步骤如下:

  1. 从sys_indexes表找到每个索引对应的根页面位置
  2. 从根页面的page header中找到叶子节点段和非叶子节点段对应的segment header, 前后者分别对应根页面 page header 中的 page_btr_seg_leaf 和 page_btr_seg_top 两个字段。
  3. 从 segment header 找到对应的inode entry结构。
  4. 根据inode entry,找出该段对应的所有零散页面地址以及free、not_full、full链表的基节点。
  5. 直接统计出零散页面数量,然后从三个链表的list length字段中读出该段占用的区数量,每个区占用64个页,此时就可以统计出该段占用的页面了。

因此 clustered_index_size 的值通过上述步骤计算出叶子和非叶子段占用的页面数的和得出,sum_of_other_index_size 则是其他所有非聚簇索引的和。

当一个段的数据非常多(超过32个页面),会以区来申请空间,因此有的区里的页可能并没有使用,在统计的时候也被算进去了,因此这个数据也是不精准的,实际占用页面可能比这个小一些。

innodb_index_stats

查询索引统计数据:select * from mysql.innodb_index_stats

image.png

表中字段含义:

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

重点关注 stat_name、stat_value、stat_description 三个值,一个索引会有多个统计项:

  1. size

表示该索引占用的页面(包含叶子或非叶子节点,并且未使用的页面也会统计进来)。

  1. n_leaf_page

表示该索引的叶子节点实际占用页面。

  1. n_diff_pfxNN

NN可以被替换为01、02、03....这样的数字,他表示截止该索引列,不重复的数据有多少。

例如idx_key1,n_diff_pfx01表示key1不重复的数据,n_diff_pfx02表示key1 + id不重复的数据。

因为普通二级索引列并不能保证自己是唯一的,得加上id才行,因此idx_key除了有 n_diff_pfx01 还有 n_diff_pfx02,但是主键(唯一二级索引也不会有)就没有。

  1. sample_size表示对计算上面不重复的列值时,采样的页面数量,该值为多少,就意味着采样了多少页面,对于多个列的联合索引来说,需要采样的页面数量是 innodb_stats_persistent_sample_pages * 联合索引列数,当需要被采样的页面数量大约该索引的叶子节点数量,意味着所有的叶子节点都会被采样,不同的索引对应的sample_size列值可能是不同的。

定期更新磁盘统计数据

随着对数据的增删改,统计数据也要跟着更新,否则使用统计数据计算成本将会受到影响。

自动更新

MySQL提供了 innodb_stats_auto_recalc 系统变量决定是否开启自动更新统计数据,默认值为ON,即打开。

每个表都维护了一个对应该表进行增删改查的记录条数,如果发生变动的记录超过表大小的10%, 将自动重新计算统计数据,并更新至表中,

自动更新是异步进行的。

可以手动指定某表是否需要自动统计:

1为自动,0为关闭。

create table 表名(..) Engine=InnoDB, STATS_AUTO_RECALC=(1|0)
复制代码

也可通过 analyze table 表名 的sql来让MySQL重新计算统计数据,该SQL是同步的,如果数据较多,该过程可能较慢。

手动更新

innodb_table_stats和innodb_index_stats与普通表并无区别,因此可以直接修改这两个表的统计数据:

  1. update
update innodb_table_stats set n_rows = 1 where table_name = '你的表名';

复制代码
  1. 重新加载更改后的数据

通知MySQL优化器重新加载统计数据。

flush table 你的表名

复制代码

之后通过 show table status 查看表统计数据,将看见手动更改的信息。

基于内存统计

innodb_table_stats 被设置为 OFF时,就是基于内存统计数据,或者直接将表的 stats_persistent 属性值设为 0.

基于内存统计的页面采样数量根据 innodb_stats_transient_sample_pages 控制,默认值是 8 。

内存统计会在服务重启或者执行某些操作后被清除,这就导致执行计划经常会变化,现在已经很少使用。

innodb_stats_method

在单点扫描区间特别多,或者连接查询涉及两表的等值匹配条件时,我们都无法确定要扫描的记录数量是多少,因此只能通过统计数据的平均值来计算一个单点扫描区间的记录数量。

不过统计不重复值的数量时,NULL非常的麻烦,在MySQL中,任何与NULL值进行比较表达式得到的值都是NULL,

有些人认为每个NULL都是唯一独立的,有些人认为NULL没有意义,可以看作是重复的,为了适配这两种情况,MySQL提供了 innodb_stats_method 系统变量,有三个候选值:

  1. nulls_equal:所有NULL值都是相等的,该值也是默认值,如果NULL值特别多,查询优化器将认为该列重复值个数特多,会倾向于不使用该索引。

  2. nulls_unequal:所有NULL值都不相等,如果NULL值特别多,会让优化器认为该列重复值个数特少,倾向使用该索引。

  3. nulls_ignored:直接忽略NULL值。

PS: 在5.7.22中,基于内存的统计 nulls_unequal 与 nulls_ignored 效果一样,基于磁盘统计,无论设置为什么,都会以 nulls_equal 去处理。

猜你喜欢

转载自juejin.im/post/7080098719265210381