zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    postman(十一):添加cookie
    基于百度通用翻译API的一个翻译小工具
    python之做一个简易的翻译器(二)
    python之做一个简易的翻译器(一)
    如何查看linux服务器内存使用情况
    linux下查看进程id时用到的命令
    python日志模块的使用
    (二十九)c#Winform自定义控件-文本框(二)-HZHControls
    (二十八)c#Winform自定义控件-文本框(一)-HZHControls
    (二十七)c#Winform自定义控件-多输入窗体-HZHControls
  • 原文地址:https://www.cnblogs.com/justfortaste/p/3276363.html
Copyright © 2011-2022 走看看