innodb索引统计信息

时间:2023-03-09 02:33:30
innodb索引统计信息
以下分析基于mysql5.6.10

统计信息相关字典表

information_schema.statistics

mysql.innodb_table_stats

mysql.innodb_index_stats

先初始化数据,我们看看这些表里存了些什么

drop table t1;
create table t1(c1 int,c2 int,c3 int,c4 int,
primary key(c1),
unique key idx1(c2),
key idx2(c3,c4)); insert into t1 values(1,1,1,1);
insert into t1 values(2,2,1,2);
insert into t1 values(3,3,1,3);
insert into t1 values(4,4,1,4);
insert into t1 values(5,5,2,1);
insert into t1 values(6,6,2,1);

  

mysql> analyze table t1;
+---------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+---------+---------+----------+----------+
| test.t1 | analyze | status | OK |
+---------+---------+----------+----------+
1 row in set (0.46 sec) mysql> show index from t1;
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| t1 | 0 | PRIMARY | 1 | c1 | A | 6 | NULL | NULL | | BTREE | | |
| t1 | 0 | idx1 | 1 | c2 | A | 6 | NULL | NULL | YES | BTREE | | |
| t1 | 1 | idx2 | 1 | c3 | A | 6 | NULL | NULL | YES | BTREE | | |
| t1 | 1 | idx2 | 2 | c4 | A | 6 | NULL | NULL | YES | BTREE | | |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
4 rows in set (0.00 sec) mysql> select * from mysql.innodb_table_stats where table_name='t1';
+---------------+------------+---------------------+--------+----------------------+--------------------------+
| database_name | table_name | last_update | n_rows | clustered_index_size | sum_of_other_index_sizes |
+---------------+------------+---------------------+--------+----------------------+--------------------------+
| test | t1 | 2013-08-22 21:23:07 | 6 | 1 | 2 |
+---------------+------------+---------------------+--------+----------------------+--------------------------+
1 row in set (0.00 sec) mysql> select * from mysql.innodb_index_stats where table_name='t1';
+---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+
| database_name | table_name | index_name | last_update | stat_name | stat_value | sample_size | stat_description |
+---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+
| test | t1 | PRIMARY | 2013-08-22 21:23:07 | n_diff_pfx01 | 6 | 1 | c1 |
| test | t1 | PRIMARY | 2013-08-22 21:23:07 | n_leaf_pages | 1 | NULL | Number of leaf pages in the index |
| test | t1 | PRIMARY | 2013-08-22 21:23:07 | size | 1 | NULL | Number of pages in the index |
| test | t1 | idx1 | 2013-08-22 21:23:07 | n_diff_pfx01 | 6 | 1 | c2 |
| test | t1 | idx1 | 2013-08-22 21:23:07 | n_leaf_pages | 1 | NULL | Number of leaf pages in the index |
| test | t1 | idx1 | 2013-08-22 21:23:07 | size | 1 | NULL | Number of pages in the index |
| test | t1 | idx2 | 2013-08-22 21:23:07 | n_diff_pfx01 | 2 | 1 | c3 |
| test | t1 | idx2 | 2013-08-22 21:23:07 | n_diff_pfx02 | 5 | 1 | c3,c4 |
| test | t1 | idx2 | 2013-08-22 21:23:07 | n_diff_pfx03 | 6 | 1 | c3,c4,c1 |
| test | t1 | idx2 | 2013-08-22 21:23:07 | n_leaf_pages | 1 | NULL | Number of leaf pages in the index |
| test | t1 | idx2 | 2013-08-22 21:23:07 | size | 1 | NULL | Number of pages in the index |
+---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+
11 rows in set (1.51 sec)

其中 show index from t1; 实际*问的是information_schema.statistics表。

等价于select * from information_schema.statistics where table_name='t1';

统计信息项

我们来试图将统计字典表中的字段和源码中的统计项联系起来,以下是源码中的统计信息项

unsigned n_uniq:10;/*!< number of fields from the beginning
which are enough to determine an index
entry uniquely */ ib_uint64_t* stat_n_diff_key_vals;
/*!< approximate number of different
key values for this index, for each
n-column prefix where 1 <= n <=
dict_get_n_unique(index) (the array is
indexed from 0 to n_uniq-1); we
periodically calculate new
estimates */ ib_uint64_t* stat_n_sample_sizes;
/*!< number of pages that were sampled
to calculate each of stat_n_diff_key_vals[],
e.g. stat_n_sample_sizes[3] pages were sampled
to get the number stat_n_diff_key_vals[3]. */ ib_uint64_t* stat_n_non_null_key_vals;
/* approximate number of non-null key values
for this index, for each column where
1 <= n <= dict_get_n_unique(index) (the array
is indexed from 0 to n_uniq-1); This
is used when innodb_stats_method is
"nulls_ignored". */ ulint stat_index_size;
/*!< approximate index size in
database pages */
ulint stat_n_leaf_pages;
/*!< approximate number of leaf pages in the
index tree */

根据注释,很容易看出联系,其中

对于 idx1(c1)

n_uniq=1 即c1

stat_n_diff_key_vals[0]=6

对于 idx2(c2,c3)

n_uniq=3 即(c2,c3,c1)

stat_n_diff_key_vals[0]=2 //idx2前缀c2不同的个数

stat_n_diff_key_vals[1]=5 //idx2前缀c2,c3不同的个数

stat_n_diff_key_vals[2]=6 //idx2前缀c2,c3,c1不同的个数

inndb统计信息相关参数

系统参数

innodb_stats_auto_recalc
innodb_stats_method
innodb_stats_on_metadata
innodb_stats_persistent
innodb_stats_persistent_sample_pages
innodb_stats_sample_pages
innodb_stats_transient_sample_pages

参考:http://dev.mysql.com/doc/refman/5.6/en/innodb-parameters.html

表参数,建表是指定

| STATS_AUTO_RECALC [=] {DEFAULT|0|1}
| STATS_PERSISTENT [=] {DEFAULT|0|1}

参考:http://docs.oracle.com/cd/E17952_01/refman-5.6-en/create-table.html

这里看到统计信息有两种类别persistent和transient,这里先大致介绍下区别,具体实现下见下章节

1 persistent 会将统计信息持久化到mysql.innodb_table_stats ,mysql.innodb_index_stats表中

2 persistent和transient统计算法不同,persistent比transient统计相对精确,当然也更耗时

一些说明:

Innodb有一个后台线程dict_stats_thread,专门用于更新persistent类型的统计信息

innodb_stats_auto_recalc 开启与否只会影响persistent类型的统计。

更新统计信息源码实现

1  transient

相关函数dict_stats_update_transient

获取stat_n_leaf_pages,stat_index_size比较简单,只需从B树的leaf segment和 no-leaf segment的描述项中获取,只需一次io。时B数某一时时刻快照的信息,这两个值是准确的。

参见

btr_get_size

fseg_n_reserved_pages

stat_n_diff_key_vals的获取是通过采样统计出来的,是一个统计值。

参见 btr_estimate_number_of_different_key_vals

/* We sample some pages in the index to get an estimate */
for (i = 0; i < n_sample_pages; i++) {
mtr_start(&mtr);
btr_cur_open_at_rnd_pos(index, BTR_SEARCH_LEAF, &cursor, &mtr); page = btr_cur_get_page(&cursor);
rec = page_rec_get_next(page_get_infimum_rec(page)); if (!page_rec_is_supremum(rec)) {
not_empty_flag = 1;
offsets_rec = rec_get_offsets(rec, index, offsets_rec,ULINT_UNDEFINED, &heap); if (n_not_null != NULL) {
btr_record_not_null_field_in_rec(
n_cols, offsets_rec, n_not_null);
}
}
while (!page_rec_is_supremum(rec)) {
rec_t* next_rec = page_rec_get_next(rec);
if (page_rec_is_supremum(next_rec)) {
total_external_size +=
btr_rec_get_externally_stored_len(
rec, offsets_rec);
break;
}
matched_fields = 0;
matched_bytes = 0;
offsets_next_rec = rec_get_offsets(next_rec, index, offsets_next_rec,ULINT_UNDEFINED,&heap); cmp_rec_rec_with_match(rec, next_rec,
offsets_rec, offsets_next_rec,
index, stats_null_not_equal,
&matched_fields,
&matched_bytes);
for (j = matched_fields; j < n_cols; j++) {
/* We add one if this index record has
a different prefix from the previous */
n_diff[j]++;
}
if (n_not_null != NULL) {
btr_record_not_null_field_in_rec(
n_cols, offsets_next_rec, n_not_null);
}
total_external_size
+= btr_rec_get_externally_stored_len(
rec, offsets_rec); rec = next_rec;
{
ulint* offsets_tmp = offsets_rec;
offsets_rec = offsets_next_rec;
offsets_next_rec = offsets_tmp;
}
}
if (n_cols == dict_index_get_n_unique_in_tree(index)) { if (btr_page_get_prev(page, &mtr) != FIL_NULL
|| btr_page_get_next(page, &mtr) != FIL_NULL) {
n_diff[n_cols - 1]++;
}
}
mtr_commit(&mtr);
}

btr_cur_open_at_rnd_pos

从根节点页开始,随机取一个记录,再从此记录找到其指向的下层页,从下层也随机取一个记录,这样依次向下层取记录,直到叶子节点页。

每次采样一页读取的页数为B树的深度。

n_diff

通过cmp_rec_rec_with_match比较页内前后记录后,后面不匹配的列都认为diff,记入n_diff

total_external_size

见后面章节

2  persistent类型

相关函数dict_stats_update_persistent

Persistent和transient统计不同的地方在于stat_n_diff_key_vals的计算

dict_stats_analyze_index关键代码如下

   for (n_prefix = n_uniq; n_prefix >= 1; n_prefix--) {

        /* Commit the mtr to release the tree S lock to allow
other threads to do some work too. */ mtr_commit(&mtr);
mtr_start(&mtr);
mtr_s_lock(dict_index_get_lock(index), &mtr); if (root_level != btr_height_get(index, &mtr)) {
break;
} if (level_is_analyzed
&& (n_diff_on_level[n_prefix - 1] >= N_DIFF_REQUIRED(index)
|| level == 1)) {
goto found_level;
}
/* search for a level that contains enough distinct records */ if (level_is_analyzed && level > 1) {
/* if this does not hold we should be on
"found_level" instead of here */
ut_ad(n_diff_on_level[n_prefix - 1]
< N_DIFF_REQUIRED(index)); level--;
level_is_analyzed = false;
}
/* descend into the tree, searching for "good enough" level */
for (;;) {
/* make sure we do not scan the leaf level
accidentally, it may contain too many pages */
ut_ad(level > 0);
/* scanning the same level twice is an optimization bug */ ut_ad(!level_is_analyzed); /* Do not scan if this would read too many pages.
Here we use the following fact:
the number of pages on level L equals the number
of records on level L+1, thus we deduce that the
following call would scan total_recs pages, because
total_recs is left from the previous iteration when
we scanned one level upper or we have not scanned any
levels yet in which case total_recs is 1. */ if (total_recs > N_SAMPLE_PAGES(index)) {
/* if the above cond is true then we are
not at the root level since on the root
level total_recs == 1 (set before we
enter the n-prefix loop) and cannot
be > N_SAMPLE_PAGES(index) */ ut_a(level != root_level); /* step one level back and be satisfied with
whatever it contains */
level++;
level_is_analyzed = true; break;
} dict_stats_analyze_index_level(index,level,n_diff_on_level,&total_recs,&total_pages,n_diff_boundaries,&mtr); level_is_analyzed = true; if (n_diff_on_level[n_prefix - 1]
>= N_DIFF_REQUIRED(index)
|| level == 1) {
/* we found a good level with many distinct
records or we have reached the last level we
could scan */
break;
}
level--;
level_is_analyzed = false;
}
found_level:
dict_stats_analyze_index_for_n_prefix(
index, level, total_recs, n_prefix,
n_diff_on_level[n_prefix - 1],
&n_diff_boundaries[n_prefix - 1], &mtr);
}

上面的逻辑分两个阶段

1 从根节点开始向下查找到合适的B树的某层L,条件为

if (total_recs > N_SAMPLE_PAGES(index))

L不可以是叶子层

2 从层L开始,将n_diff_boundaries分为N段,N为采样数。分别从N个段中,随机取N个记录,这N个记录依次向下层查找到合适的采样页。这个采样页不一定时叶子页。

分解一下:

1 dict_stats_analyze_index_level函数获取以下值

n_diff:本层不同值个数

total_recs:本层总记录数

total_pages:本层总页数

n_diff_boundaries:出现不同值的边界位置数组,数组长度为n_diff,0<= n_diff_boundaries<total_recs-1

2 为何是递减循环

for (n_prefix = n_uniq; n_prefix >= 1; n_prefix--)

为了dict_stats_analyze_index_level不从头根层开始向下分析,而是从当前层开始

3 分段采样

dict_stats_analyze_index_for_n_prefix

分段数

n_recs_to_dive_below = ut_min(N_SAMPLE_PAGES(index),

                      n_diff_for_this_prefix);

取分段中随机记录

left = n_diff_for_this_prefix * i / n_recs_to_dive_below;
right = n_diff_for_this_prefix * (i + 1)
/ n_recs_to_dive_below - 1;
rnd = ut_rnd_interval(0, (ulint) (right - left));

从随机记录开始向下取样,统计样页的n_diff,样页不一定是叶子页

dict_stats_analyze_index_below_cur

4 样页不一定是叶子页

当当前层的页的记录的n_pre都一致时,下层页也应一致。因此不需再向下层取样。

何时更新统计信息

1 create table/truncate table 会初始化统计信息

2 open table

如果设置innodb_stats_persistent,先从统计字典表中读取,如果读取不到则通过persistent方式更新统计信息

否则通过transient方式更新统计信息

3 analyze table

如果设置innodb_stats_persistent,则通过persistent方式更新统计信息,否则通过transient方式更新统计信息

4  数据发生变化

表距离上一次更新统计信息,发生变化的行数超过当前行数1/16时,通过transient方式更新统计信息

表距离上一次更新统计信息,发生变化的行数超过当前行数%10, 且设置了innodb_stats_auto_recalc和innodb_stats_persistent,通过persistent方式更新统计信息

何时使用统计信息

MRR http://dev.mysql.com/doc/refman/5.6/en/mrr-optimization.html

ROR(RowidOrderedRetrieva)

GROUP

优化是会用到,相关函数如下,这块后续深入下

multi_range_read_info_const

ror_scan_selectivity

cost_group_min_max

需优化的地方

1  stat_n_leaf_pages,stat_index_size的统计

Btr_get_size取到的是ret,而不是used.

它们的区别是ret:segment中的所有页,包括一些free页

User: segment中已使用的页

ret >used

Free页即不用于B树页,也不用于externer页,因此stat_index_size不应包括free页。这应该算是个bug吧。应用used统计。

*used = mtr_read_ulint(inode + FSEG_NOT_FULL_N_USED, MLOG_4BYTES, mtr)
+ FSP_EXTENT_SIZE * flst_get_len(inode + FSEG_FULL, mtr)
+ fseg_get_n_frag_pages(inode, mtr); ret = fseg_get_n_frag_pages(inode, mtr)
+ FSP_EXTENT_SIZE * flst_get_len(inode + FSEG_FREE, mtr)
+ FSP_EXTENT_SIZE * flst_get_len(inode + FSEG_NOT_FULL, mtr)
+ FSP_EXTENT_SIZE * flst_get_len(inode + FSEG_FULL, mtr);

2  关于external page

读了这段代码发现,发现external page 属于leaf segment;

total_external_size
+= btr_rec_get_externally_stored_len(
rec, offsets_rec);

external page 和叶子页公用一个segment, external page和叶子页在同一个extent内混合出现,让叶子页在物理上更离散。

1 会影响只查非大字段查询

2  在某些情况下MRR显得很无力

3 统计信息计算external page导致统计偏差

解决方法:external page可以用单独的segment管理

3 关于persistent采样

transient 采样比较简单,但过于随机,极端情况下会出现采集到同一页的情况。

Persistent 方式做到了尽量采集不同的值,并且不会出现采集到同一页的情况。Persistent 方式会读取b树前N(N>0,即不会统计叶子层) 层所有页和记录。

假设100条记录的分布如下

91个0 加上 1  2 3 4 5 6 7 8 9

假设采样10页

按Persistent逻辑采样记录为 0 1 2 3 4 5 6 7 8 9  n_diff=9

按transient逻辑采样很大可能结果为 9个0 加 1        n_diff=2

显然 Persistent会统计比transient统计要精确。

n_recs_to_dive_below = ut_min(N_SAMPLE_PAGES(index),
n_diff_for_this_prefix);

对于n_diff_for_this_prefix< N_SAMPLE_PAGES(index),这时候的采样数为n_diff_for_this_prefix,采样数过少,会导致偏差。

此时应仍然采集N_SAMPLE_PAGES(index)个页,换以下统计方式

1 可以统计n_diff_boundaries之间的区间大小,因为n_diff_for_this_prefix较小,所以这个统计成本较小

2 按区间比例来分配,区间越小,采样的页数相对应更多

3 总共采集N_SAMPLE_PAGES(index)个页

4 以上纯属YY