什么?!我们竟然可以自己决定MySQL的查询计划!!!

1,731 阅读20分钟

导读

在《MySQL为什么选择执行计划A而不选择B》的上下篇中我详细讲解了MySQL选择执行计划的策略,其中,讲到了3种查询成本计算方式:

  1. 基于索引扫描的查询成本计算
  2. 基于索引统计的查询成本计算
  3. 基于全表扫描的查询成本计算

我们都希望能有一个计算查询成本效率又高,又准确的方案,而目前MySQL只支持这3种查询成本计算方案,那么,结合我们的期望,再看看上面3种方案是否有可能通过某种方法实现我们的期望呢?

  1. 基于索引扫描的查询成本计算

    通过《MySQL为什么选择执行计划A而不选择B(上)?》的讲解,我们知道这是一个效率低,但是计算最准确的方案。

    而方案的效率低是由于必须扫描索引树来确定查询成本,如果索引树的分叉很多,势必会降低扫描的效率,从而降低了查询成本计算的效率。所以,不能满足我们的期望。

  2. 基于索引统计的查询成本计算

    通过《MySQL为什么选择执行计划A而不选择B(上)?》的讲解,我们知道这是一个效率高,但是计算不准确的方案。

    而方案的计算不准确是由于MySQL是通过采样部分索引树的节点,然后对这些节点做相关计算,从而生成索引统计结果,最后,得出查询成本的。所以,也不能满足我们的期望。

    那么,如果我们可以提升采样节点的数量,是不是就可以更加精准地计算查询成本了?

的确,MySQL给了我们相关的参数优化,可以让我们更加精准地计算查询成本。计算结果更准确了,该方案效率又高,正好可以满足我们的期望。

  1. 基于全表扫描的查询成本计算

    通过《MySQL为什么选择执行计划A而不选择B(下)?》中全表扫描的查询成本计算过程,我们可以知道该方案是计算效率高,但是计算结果不准确的方案。

    而在大多数情况下,它的成本计算结果往往都比上面两个基于索引计算查询成本的方案要差很多,所以,对这个的方案的进一步分析的意义不是很大,我在这里就不做更进一步的分析和讲解了。

综上所述,只有第2种方案,即基于索引统计的查询成本计算是有进一步优化空间的,所以,下面,我将详细讲解在基于索引统计的查询成本计算中,MySQL是如何生成索引相关统计结果的,在讲解过程中,我会告诉大家MySQL的哪个参数可以提升查询成本计算的准确性?

INFORMATION_SCHEMA

在讲解MySQL是如何生成索引相关统计结果前,我先讲解一个数据库:INFORMATION_SCHEMA,因为它是MySQL生成索引统计结果时需要的基础数据来源。

现在主流使用的存储引擎就是InnoDB,所以,我就详细讲一下在INFORMATION_SCHEMA这个数据库中,跟InnoDB引擎有关的几张表,这几表,一般我们把它们叫做InnoDB数据字典(Data Dictionary),其中,索引统计结果的基础信息主要来源于2张字典表:INNODB_SYS_TABLESINNODB_SYS_INDEXES,我以user表为例详细讲解这两张表:

为了方便理解2张字典表的列的概念,我把user表结构和表记录再贴一下:

user表结构:

image-20201128170737576.png

user表记录

image-20201128212422722.png

INNODB_SYS_TABLES

这张表存放了MySQL中所有表的基础信息,我们来看一下user表的信息:

image-20201128210559739.png

我们来看一下该表中的核心字段:

列名说明
TABLE_ID所有的表自增排序的一个序号,每个表都是唯一的,比如上图:user表序号为40
NAME表名,结构为数据库/表,比如上图,user_center/user,表示user_center数据库中的user表
N_COLS表中的列的个数,其中包含3个MySQL自带的隐藏列,即DB_ROW_ID、DB_TRX_ID和DB_ROLL_PTR,比如,上面user表结构中看到,user表包含7列,加上隐藏列,一共是10列
SPACE表所在的表空间ID,每个表空间在一个MySQL实例中是唯一的,比如上图,user表的表空间ID为57
ROW_FORMAT行记录格式,主要包含4种:Compact, Redundant, Dynamic或Compressed,比如上图,user表的行记录格式为Dynamic
INNODB_SYS_INDEXES

这张表存放了MySQL中所有索引的基础信息,我们来看一下user表的索引信息:

image-20201128210759662.png

列名说明
INDEX_ID索引ID,在一个MySQL实例中,索引ID是唯一的,比如上图,user表的primary主键索引ID=41,index_age_birth索引ID=42
NAME索引名,其中,主键索引名为PRIMARY,还有一个名为index_age_birth的辅助索引
TABLE_ID索引所在的表ID,比如上图,user表中的两个索引所在的表ID都为40,即user表
TYPE索引类型,包含聚簇索引、辅助索引、唯一索引、普通索引等等。比如上图,user表中primary主键索引,它的类型为3,即聚簇索引。而索引index_age_birth类型为0,即辅助索引
N_FIELDS索引包含的列数,比如上图,primary主键索引包含1列,即id。index_age_birth索引包含2列,见表结构图中的INDEXES部分,即agebirthday
PAGE_NO索引所在的B+Tree的根节点号。比如上图,主键索引所在B+Tree的根节点号为3,index_age_birth索引所在的B+Tree的根节点号为4
SPACE索引所在的表空间ID,比如上图,主键索引和index_age_birth索引都在user表中,所以,对应user表的表空间ID为57

索引统计表

接着,我们再看一下MySQL的索引统计表信息,因为这张表存储MySQL生成的索引统计结果。

我们还是以InnoDB引擎为例,InnoDB引擎的索引统计表在mysql数据库下面,执行如下SQL查询:

SELECT * FROM mysql.innodb_index_stats WHERE table_name = 'user'

我们以user表为例,看下索引统计表innodb_index_stats的数据:

image-20201128210015534.png

其中,列database_nametable_name分别表示某个数据库下的哪张表,我就不详细说了。重点看一下下面这些列:

  • index_name

    该列记录索引名,如上图,user表中包含两个索引,PRIMARY主键索引和index_age_birth索引。

  • stat_name/stat_value

    针对index_name列相同的行,stat_name表示针对该索引的统计项名称,stat_value展示的是该索引在该统计项上的值。我们来具体看一下一个索引都有哪些统计项:

    • n_leaf_pages:表示该索引中叶子节点的数量。

      比如,user表primary主键索引叶子节点数为1,index_age_birth索引叶子节点数也为1

    • size:表示该索引所有节点的数量。

      比如,user表primary主键索引所有节点数为1,index_age_birth索引所有节点数也为1

    • n_diff_pfxNN:表示对应的索引列不重复的值有多少。其中的NN是什么意思呢?

      其实NN可以被替换为010203... 这样的数字。比如上图,对于index_age_birth索引来说:

      • n_diff_pfx01表示该索引中age这单个列不重复的记录有多少。结合上面user表记录图,我们发现,user表中age列不重复的记录有6条。
      • n_diff_pfx02表示该索引中age,birthday这两个列组合起来不重复的记录有多少。结合上面user表记录的图,我们发现,user表中age,birthday列组合后不重复的记录有9条。
      • n_diff_pfx03表示该索引中age,birthday,id这三个列组合起来不重复的记录有多少。结合上面user表记录的图,我们发现,user表中age,birthday,id列组合后不重复的记录有10条。
  • sample_size

    在计算某些索引列中包含多少不重复的记录时,需要对一些叶子节点进行采样,sample_size列就表明了采样的叶子节点的数量是多少。如上图,

    • primary主键索引id的采样叶子节点数为1
    • index_age_birth索引中age列的采样叶子节点数为1
    • index_age_birth索引中age,birthday列组合的采样叶子节点数为1
    • index_age_birth索引中age,birthday,id列组合的采样叶子节点数为1

构建InnoDB字典缓存

在《INFORMATION_SCHEMA》这一部分中,我讲到MySQL索引统计结果中的基础数据来源于InnoDB数据字典,其中主要来源于两张表INNODB_SYS_TABLESINNODB_SYS_INDEXES。现在,我们来看一下这个场景:

由于INNODB_SYS_TABLESINNODB_SYS_INDEXES是持久化在磁盘上的,为了保证索引统计结果的准确性,MySQL在构建索引统计结果时,为了获取基础数据,必须从磁盘上频繁读取这两张表,此时,磁盘IO会增加,导致MySQL性能下降。这一定不是我们想看到的。

所以,为了解决这个问题,MySQL对InnoDB数据字典做了缓存,那么,在构建索引统计结果时,MySQL就可以从缓存中读取基础数据,相比磁盘读取,提升了读取的性能。

下面,我们就来看下MySQL是如何构建InnoDB数据字典缓存的?

缓存结构

在讲解缓存构建过程之前,我们先看一下这个数据字典缓存长什么样?我以user表为例,看下INNODB_SYS_TABLESINNODB_SYS_INDEXES两张字典表的缓存结构。

INNODB_SYS_TABLES缓存

INNODB_SYS_TABLES缓存主要包含3个结构:table_hashtable_id_hashtable_LRU

我们先看一下table_hash

image-20201129213439853.png

如上图,table_hash是一个hash表,它的结构主要包含:

  • cells:hash表的node节点列表,每个node节点的结构如下:

    • node节点的hash值通过对表名做fold运算得到。如上图左侧:

      • 第一个node节点hash值通过对t1表名做fold(t1)运算得到。

      • 第二个node节点hash值通过对user表名做fold(user)运算得到。

      • 第三个node节点hash值通过对t2表名做fold(t2)运算得到。

    • node节点存储table对象的单向链表,链表中的每个节点存储table对象,该对象包含table的基础信息。存储单向链表是为了解决hash冲突

      • 第一个node节点的链表中,头部节点存储t1表对象,中间节点存储t2表对象,最后一个节点存储t3表对象。

      • 第二个node节点的链表中,头部节点存储user表对象,中间节点存储user2表对象,最后一个节点存储user3表对象。

下面,我在看一下table_id_hash结构:

image-20201129213800598.png

如上图,与table_hash结构相似,唯一的区别就是node节点的hash值是通过对table_id,即图中的30,40和45做fold运算得到,而不是对table表名做fold运算得到。

最后,我们看一下table_LRU结构:

image-20201130164911950.png

table_LRU是一个最少使用的table逐出链表,这是一个双向链表,当有table长期不使用,MySQL会将其从该链表中移除,同时,将其对应的table_hashtable_id_hash中的节点删除,保证内存的有效利用率。

如上图,该链表从前到后,包含usert1t2三个表对象。

关于逐出算法的机制,因为不是本章的重点,我就先不详细说了。

INNODB_SYS_INDEXES缓存

接着,我们再看一下INNODB_SYS_INDEXES缓存的结构。

image-20201129235314249.png

MySQL为每张表的索引维护了一个双向链表,我们把它叫做INNODB_SYS_INDEXES链表,如上图,有2张表的索引组成的双向链表,该链表的结构如下:

  1. 链表节点内存储索引相关信息。如上图,

    • table1表的链表中包含4个节点,从前到后分别存储primary主键索引信息、Index2索引信息、Index3索引信息和Index4索引信息。

    • user表的链表中包含2个节点,从前到后分别存储primary主键索引信息和index_age_birth索引信息。

  2. 链表的头部节点一定是存储table的主键索引信息。如上图,

    • table1表的链表的头部节点存储primary主键索引。

    • user表的链表头部节点存储primary主键索引。

  3. 链表有一个start指针指向头部节点,一个end指针指向尾部节点。如上图,

    • table1表的链表的start指针指向头部的primary主键索引节点,end指针指向尾部的Index4索引节点。

    • user表的链表的start指针指向头部的primary主键索引节点,end指针指向尾部的index_age_birth索引节点。

现在,我们来看一下InnoDB数据字典缓存构建的过程。该过程主要有以下两种方式:

Alert/Create触发构建

第一种方式是在我们执行创建或修改表/索引的SQL语句时,触发构建InnoDB数据字典缓存。

创建表时触发

先来看一下创建表时,INNODB_SYS_TABLES字典表缓存是如何构建的?

我以创建user表的SQL语句为例,讲解一下INNODB_SYS_TABLES缓存的构建过程。

CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `user_id` int(8) DEFAULT NULL COMMENT '用户id',
  `user_name` varchar(29) DEFAULT NULL COMMENT '用户名',
  `user_introduction` varchar(498) DEFAULT NULL COMMENT '用户介绍',
  `sex` tinyint(1) DEFAULT NULL COMMENT '性别',
  `age` int(3) DEFAULT NULL COMMENT '年龄',
  `birthday` date DEFAULT NULL COMMENT '生日',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

具体过程如下:

  1. 解析SQL语句,得到表相关信息,比如,表名、列名、列信息等,对应上面的SQL,即user、7个列和对应的列信息。
  2. 生成user表table_id
  3. 根据表名usertable_id、7个列名、7个列信息等构建user表对象,该对象包含前面的这些基础信息
  4. user表对象写入table_hashtable_id_hash
  5. user表对象添加到table_LRU链表头部
创建索引时触发

下面我们再看一下创建索引时,INNODB_SYS_INDEXES缓存是如何构建的?

我以创建索引index_age_birth的SQL语句为例,讲解一下INNODB_SYS_INDEXES缓存的构建过程。

ALTER TABLE `user` ADD UNIQUE INDEX `index_age_birth` (`age`, `birthday`);

具体过程如下:

  1. 解析SQL语句,得到索引及表相关信息,比如,索引名、索引列、表名等,对应上面的SQL,即index_age_birth(age, birthday)user
  2. 根据表名usertable_hash中取出表对象
  3. 根据索引名index_age_birth、索引列(age, birthday)等信息构建索引基础信息
  4. 将索引基础信息更新到user表对象
  5. user表对象重新写入table_hashtable_id_hash
  6. 将索引基础信息添加到user表对应的INNODB_SYS_INDEXES链表的末尾
启动时构建

第二种方式是当MySQL重启时,InnoDB数据字典缓存会丢失,所以,MySQL会扫描InnoDB数据字典表,将数据加载进数据字典缓存。具体过程如下:

  1. 扫描InnoDB数据字典表,逐张字典表读取相关数据,并加载到缓存

    (1) 逐行读取INNODB_SYS_TABLES表中记录,根据记录构建table对象,逐个将对象写入table_hashtable_id_hash,以及将对象添加到table_LRU链表头部

    (2) 逐行读取INNODB_SYS_INDEXES表中记录:

    • 根据记录中的table->id,根据table_idtable_id_hash中获取table对象

    • 将记录中的索引信息写入该table对象

    • 将记录中的索引信息添加到该table对象对应的INNODB_SYS_INDEXES链表尾部

    • 该table对象重新写入table_hashtable_id_hash

我们发现通过上面两种方式写InnoDB数据字典缓存,可以有效保障缓存中总是拥有最新的数据字典,从而保证了后续更新索引统计表的时效性。

索引统计表构建

现在,我们最后看一下MySQL是如何生成索引统计结果,并更新索引统计表的?

MySQL采用两种方式更新索引统计表:

  1. 定时更新方式:每隔10秒更新一次索引统计表。
  2. ANALYZE TABLE语句方式:手动执行该语句更新一次索引统计表。

从上面的讲解中,我们知道索引统计表的基础信息来源于两个InnoDB数据字典缓存:INNODB_SYS_TABLES缓存INNODB_SYS_INDEXES缓存。现在结合索引统计表定时更新方式,我们看一下这个场景:

  1. MySQL扫描所有table对应的INNODB_SYS_INDEXES链表
  2. 逐个读取index索引,获取索引基础信息
  3. 逐个根据索引基础信息计算索引统计项
  4. 将统计项写入索引统计表

仔细看下这个过程,我们发现如果INNODB_SYS_INDEXES链表随着表越来越多,早期更新的表可能要花很长时间才能扫描到,导致该表更新索引统计表周期变长,该表对应的索引统计表统计项更新变慢,最终影响该表相关SQL的查询的性能。因为在本章《导读》中,我说过索引统计项更新越快,统计结果越准,MySQL根据统计结果选择某条SQL执行计划就越准确,该条SQL执行的效率越高。

为了解决上面这个问题,MySQL引入了recalc_pool这个东西,可以让MySQL总是从最早变化的表取出索引的基础信息,然后,用这些信息计算,得到索引统计结果。

所以,我们就先来看一下这个recalc_pool

recalc_pool

image-20201129234909913.png

recalc_pool结构如下:

recalc_pool是一个列表,列表内存储表id。如上图,从前到后依次存储user表id: 40,t1表id: 45和t2表id: 80。

recalc_pool的作用:将最近变更的table->id添加到recalc_pool尾部,定时任务从recalc_pool中取最早变更的table->id进行索引统计。

定时更新

下面,我们来看一下定时更新索引统计表的过程。

recalc_pool的作用来看,我们发现,它主要包含两部分:添加table->id到recalc_pool和从recalc_pool中取table->id。

添加recalc_pool

那么,我们先看一下添加table->idrecalc_pool尾部的过程:

image-20201129235135542.png

如上图,假设用户A现在对user表执行insert操作,插入20条记录,用户B对account表做update操作,更新表中一条记录,此时,MySQL会做如下判断:

如果变更表的记录数 > 表总记录数 * 10%,那么,将该表的id添加到recalc_pool

结合之前user表记录这张图,我们知道user表总记录数为10,现在,要插入20条记录,20 > 10 * 10%,所以,如上图,user表的id,即40,添加到recalc_pool尾部。

account表更新操作只影响一条记录,假设account表总共有20条记录,1 < 20 * 10%,所以,如上图,account表的id不添加到recalc_pool

读取recalc_pool

下面,我们再看一下从recalc_pool读取table->id,并计算索引统计项的过程,还是以user表为例,见下图:

image-20201130105401209.png

  1. recalc_pool中读取第一条table->id,即图中从head头部读取user表id: 40。

  2. 根据user表id从table_id_hash读取user表对象。即图中上面从table_id_hash中取出user。

  3. 如果当前时间 - 上次计算user表的时间 > 10秒,更新索引统计项,否则,继续将user表id添加到recalc_pool尾部

  4. 更新索引统计项

    (1) 由于在上文创建索引时触发时,已经将user表的所有索引更新到user表对象,所以,根据user表对象,我们可以获取该表所有索引

    (2) 遍历INNODB_SYS_INDEXES缓存中user表的链表,获取每个索引,即图中左下方,依次读取primaryindex_age_birth索引,这里我就以index_age_birth索引为例,开始计算索引统计项,即图中compute部分,我们结合上文索引统计表的统计项来看下面的计算流程:

    • 开启index_age_birth索引级别mtr,关于mtr,我会在《MySQL是如何平衡日志写入的性能和数据可靠性的?》详细讲解。

    • 根据index_age_birth索引id找到内存中的对应的索引树,即图中右下角,计算该索引树总节点的个数,将个数写入stat_index_size,所以,index_age_birth索引的stat_index_size = 1

    • 根据index_age_birth索引id找到内存中的对应的索引树,即图中右下角,计算该索引树叶子节点的个数,将个数写入stat_n_leaf_pages,所以,index_age_birth索引的stat_n_leaf_pages = 1

    • 根据index_age_birth索引id找到内存中的对应的索引树,即图中右下角,计算该索引树叶子节点的个数:

      • 如果index_age_birth索引树的叶子节点数 < 采样的叶子节点数参数 * 索引列个数stat_n_sample_sizes为该索引树的所有叶子节点个数。其中采样的叶子节点数参数默认为20,可以通过调整innodb_stats_persistent_sample_pages参数变更。后面我会讲到变更方法

        比如,现在通过遍历index_age_birth索引树叶子节点,我们知道该索引树的总叶子节点数为1,所以,

        • index_age_birth索引总叶子节点数1 < 20 * 1,其中,右边的1代表1个索引列age,所以, 该索引列agestat_n_sample_sizes为1

        • index_age_birth索引总叶子节点数1 < 20 * 2,其中,右边的2代表1个索引列组合(age,birthday),所以,该索引列组合(age,birthday)stat_n_sample_sizes为1

        • index_age_birth索引总叶子节点数1 < 20 * 3,其中,右边的3代表1个索引列组合(age,birthday,id),所以,该索引列组合(age,birthday,id)stat_n_sample_sizes为1

    • 根据index_age_birth索引id找到内存中的对应的索引树,即图中右下角,扫描所有叶子节点记录的链表:

      • 遍历index_age_birth索引列的个数,从1->2->3(分别代表ageage,birthdayage,birthday,id):

        • 如果前一条记录和后一条记录匹配列的个数小于1,说明两条记录在age列上的值不相同,则stat_n_diff_key_vals[0] + 1

        • 如果前一条记录和后一条记录匹配列的个数小于2,说明两条记录在age,birthday列组合上的值不相同,则stat_n_diff_key_vals[1] + 1

        • 如果前一条记录和后一条记录匹配列的个数小于3,说明两条记录在age,birthday,id列组合上的值不相同,则stat_n_diff_key_vals[2] + 1

    • 得到stat_n_diff_key_vals[0] = 6

      stat_n_diff_key_vals[1] = 9

      stat_n_diff_key_vals[2] = 10

    • 关闭index_age_birth索引级别mtr

  5. 持久化索引统计表,按照如下统计项对应关系,更新n_leaf_pagessizesample_sizen_diff_pfxNN到索引统计表中,即图中右边写统计表。

    stat_index_size => size

    stat_n_leaf_pages => n_leaf_pages

    stat_n_sample_sizes => sample_size

    stat_n_diff_key_vals[0] => n_diff_pfx01

    stat_n_diff_key_vals[1] => n_diff_pfx02

    stat_n_diff_key_vals[2] => n_diff_pfx03

ANALYZE TABLE

该方式就是手动触发并执行《定时更新》这部分内容讲到的更新索引统计项和持久化索引统计表两个步骤。

以user表为例,我们看一下用该语句更新索引统计表的写法:

ANALYZE TABLE user;
参数配置

现在我们再回到《导读》提到的一个问题:针对基于索引统计的查询成本分析,如果我们可以提升采样节点的数量,是不是就可以更加精准地计算查询成本了?

通过上面内容的讲解,我们清楚地了解了MySQL索引统计结果的计算和构建过程,其中,采样叶子节点的数量是由MySQL参数innodb_stats_persistent_sample_pages决定的,所以,如果我们可以调大这个参数,就可以保证精确计算索引统计表中的各统计项,从而使得MySQL能够更加正确地选择执行计划。

调整方式如下:

SET GLOBAL innodb_stats_persistent_sample_pages=30;

小结

本章详细讲解了基于索引统计分析查询成本前,索引统计项构建的过程,通过这个过程的讲解,你应该知道了以下内容:

  1. 两张核心的数据字典表:INNODB_SYS_TABLESINNODB_SYS_INDEXES

    表名说明
    INNODB_SYS_TABLES存放了MySQL中所有表的基础信息
    INNODB_SYS_INDEXES存放了MySQL中所有索引的基础信息
  2. 两张核心的数据字典表缓存:INNODB_SYS_TABLES缓存INNODB_SYS_INDEXES缓存

    缓存名说明
    INNODB_SYS_TABLES缓存主要包含3个结构:table_hash、table_id_hash和table_LRU
    INNODB_SYS_INDEXES缓存MySQL为每张表的索引维护了一个双向链表
  3. 索引统计表

    索引统计表存储MySQL生成的索引统计结果。看到这张表,似乎和《MySQL为什么选择执行计划A而不选择B(上)?》基于索引统计的查询这个部分内容的统计表长得不一样,其实,后者的数据来源于前者,所以,从数据上看是一样的。

  4. 两张核心的数据字典表缓存的构建过程

    构建方式说明
    创建表时构建创建表时触发构建INNODB_SYS_TABLES缓存
    创建索引时构建创建索引时触发构建INNODB_SYS_INDEXES缓存
    MySQL重启时构建在MySQL重启时,会全量加载INNODB_SYS_TABLES和INNODB_SYS_INDEXES表数据到缓存中
  5. 索引统计表的构建过程

    • recalc_pool:一个存储table->id的列表,将最近变更的table->id添加到recalc_pool尾部,定时任务从recalc_pool头部取最早变更的table->id进行索引统计

    • 构建过程

      过程说明
      定时更新每隔10秒更新一次索引统计表
      ANALYZE TABLE手动执行该语句更新一次索引统计表
    • 参数配置

      参数作用
      innodb_stats_persistent_sample_pages调整该参数可以变更MySQL采样索引树叶子节点的个数

思考题

在《定时更新》部分中,有一个步骤是计算stat_n_sample_sizes的流程,其中,我讲解了索引树总叶子节点数 < 采样的叶子节点数参数 * 索引列个数时,stat_n_sample_sizes的计算过程。

那么,如果索引树总叶子节点 > 采样的叶子节点数参数 * 索引列个数时,MySQL是如何计算stat_n_sample_sizes的?

提示:结合《MySQL为什么选择执行计划A而不选择B(上)?》中,我讲到的基于扫描索引树的查询成本分析的过程,看一下怎么回答这个问题。