MySQL 统计信息不准导致的性能问题

表的统计信息错误导致优化器选择错误的执行计划。

一个客户的性能优化案例: 没有修改数据库实例的任何配置参数以及业务代码没有变更的情况下,一条 sql 出现大幅性能下降。

我们来看看出问题的sql 以及他的执行计划:

mysql>explain
->SELECTcount(con.id) ,
->MAX(DAYNAME(con.date)) ,
->now() ,
->pcz.type,
->pcz.c_c
->FROMconAScon
->joinordersooncon.o_id=o.id
->JOINpczASpczONo.d_p_c_z_id=pcz.id
->leftjoincconcon.c_id=c.id
->WHEREcon.date=current_date() andpcz.type="T_D"
->GROUPBYcon.date, pcz.c_c, pcz.type;
+----+-------------+-------+------------+--------+-------------------+----------+---------+----------------------------+------+----------+----------------------------------------------+
|id|select_type|table|partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra|
+----+-------------+-------+------------+--------+-------------------+----------+---------+----------------------------+------+----------+----------------------------------------------+
|1|SIMPLE|pcz|NULL|ALL|PRIMARY|NULL|NULL|NULL|194|10.00|Usingwhere; Usingtemporary; Usingfilesort|
|1|SIMPLE|o|NULL|ref|PRIMARY,dpcz_FK|dpcz_FK|9|custom.pcz.id|1642|100.00|Usingindex|
|1|SIMPLE|con|NULL|ref|FK_order,IDX_date|FK_order|8|custom.o.id|1|4.23|Usingwhere|
|1|SIMPLE|c|NULL|eq_ref|PRIMARY|PRIMARY|8|custom.con.c_id|1|100.00|Usingindex|
+----+-------------+-------+------------+--------+-------------------+----------+---------+----------------------------+------+----------+----------------------------------------------+

执行计划显示 rows examined = (19410%)1642(14.23%)=1347 查看执行计划我们就发现 where 条件 con.date = current_date() 。这个条件看起来更适合作为索引过滤数据。但是 为什么 MySQL 优化器不选择该索引呢?接下来使用 force index 强制执行计划使用 con.date 字段上的索引。执行计划如下:

mysql>explain
->SELECTcount(con.id) ,
->MAX(DAYNAME(con.date)) ,
->now() ,
->pcz.type,
->pcz.c_c
->FROMconASconUSEINDEX(IDX_date)
->joinordersooncon.o_id=o.id
->JOINp_c_zASpczONo.d_p_c_z_id=pcz.id
->leftjoincconcon.c_id=c.id
->WHEREcon.date=current_date() andpcz.type="T_D"
->GROUPBYcon.date, pcz.c_c, pcz.type;
+----+-------------+-------+------------+--------+-----------------+----------+---------+---------------------------------------+--------+----------+---------------------------------+
|id|select_type|table|partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra|
+----+-------------+-------+------------+--------+-----------------+----------+---------+---------------------------------------+--------+----------+---------------------------------+
|1|SIMPLE|con|NULL|ref|IDX_date|IDX_date|3|const|110446|100.00|Usingtemporary; Usingfilesort|
|1|SIMPLE|c|NULL|eq_ref|PRIMARY|PRIMARY|8|custom.con.c_id|1|100.00|Usingindex|
|1|SIMPLE|o|NULL|eq_ref|PRIMARY,dpcz_FK|PRIMARY|8|custom.con.o_id|1|100.00|Usingwhere|
|1|SIMPLE|pcz|NULL|eq_ref|PRIMARY|PRIMARY|8|custom.o.d_p_c_z_id|1|10.00|Usingwhere|
+----+-------------+-------+------------+--------+-----------------+----------+---------+---------------------------------------+--------+----------+---------------------------------+

问题来了 rows examined = 110446*(1*10%)=11045 rows根据计算评估, 第一个执行计划的 1347 大概是 110446 的十分之一 ,至少从表面上看来这个是MySQL 优化器选择第一个执行计划的原因。

但是对比实际的查询结果的响应时间,肯定粗问题了。因为执行计划二 的sql 的响应时间在预期之内,但是执行计划一对应的响应时间反而更慢。

进一步来看表 orders 的创建语句以及执行计划1,我们发现 表pcz的确有194行。然后查看 索引 orders.dpcz_FK,表 orders 返回 1642行 ,因为外键约束 orders_ibfk_10 的定义,也就意味着 表 orders 的记录数应该是 194*1642=318548 ,但是实际的行数是 32508150,百倍于执行计划估计的值 318548 。

CREATETABLE`orders` (
`id`bigint(20) NOTNULLAUTO_INCREMENT,
...
`d_p_c_z_id`bigint(20) DEFAULTNULL,
...,
PRIMARYKEY (`id`),
...
KEY`dpcz_FK` (`d_p_c_z_id`),
...
CONSTRAINT`orders_ibfk_10`FOREIGNKEY (`d_p_c_z_id`) REFERENCES`p_c_z` (`id`) ONDELETECASCADEONUPDATECASCADE,
...
) ENGINE=InnoDB....
mysql>select*frommysql.innodb_table_statswheredatabase_name='cutom'andtable_name='orders';
+---------------+------------+---------------------+----------+----------------------+--------------------------+
|database_name|table_name|last_update|n_rows|clustered_index_size|sum_of_other_index_sizes|
+---------------+------------+---------------------+----------+----------------------+--------------------------+
|custom|orders|2022-03-0321:58:18|32508150|349120|697618|
+---------------+------------+---------------------+----------+----------------------+--------------------------+

分析至此,我们可以断定 orders.dpcz_FK 的统计信息是不准确的,于是乎我们使用如下语句确认它的实际数据量:

mysql>select*frommysql.innodb_index_statswheredatabase_name='cutom'andtable_name='orders'andindex_name='dpcz_FK';
mysql>select*frommysql.innodb_index_statswheredatabase_name='custom'andtable_name='orders'andindex_name='dpcz_FK';
+---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+
|database_name|table_name|index_name|last_update|stat_name|stat_value|sample_size|stat_description|
+---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+
|custom|orders|dpcz_FK|2022-02-2812:35:30|n_diff_pfx01|19498|50|d_p_c_z_id|
|custom|orders|dpcz_FK|2022-02-2812:35:30|n_diff_pfx02|32283087|128|d_p_c_z_id,id|
|custom|orders|dpcz_FK|2022-02-2812:35:30|n_leaf_pages|55653|NULL|Numberofleafpagesintheindex|
|custom|orders|dpcz_FK|2022-02-2812:35:30|size|63864|NULL|Numberofpagesintheindex|
+---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+
mysql>selectcount(distinctd_p_c_z_id) fromorders;
+----------------------------------------------+
|count(distinctd_p_c_z_id) |
+----------------------------------------------+
|195|
+----------------------------------------------+

Bingo!从上面来看 表 orders 字段 d_p_c_z_id 的区分度(不一样的值的总数)为 195 。在信息统计表里面 dpcz_FK的 stat_value 值是 19498 ,显然这个值是不准确的并且比实际值大的多,100倍 。索引的 state_value 值应该等于这个字段的在表里面的区分度。

如果使用正确的 索引 dpcz_FK 的值 stat_value 195 去重新评估执行计划的成本,我们将得到执行计划1 的结果 32508150/195=166708 ,并且执行计划预估的扫描的行数应该是 (194*10%)*166708*(1*4.23%)=136804。因为该值是10倍于执行计划2 的值 11045 。MySQL 在没有使用force index的情况下就能走到正确的执行计划 。

这个sql的问题解决了,但是为什么 MySQL 的统计信息会计算错误,我们如何修复它呢?

回答这个问题之前,我们先了解一下 MySQL 是如何收集统计信息以及哪些参数控制 这个动作。

InnoDB 是如何收集表的统计信息

我们可以通过显式的方式或者系统自动采集表的统计信息 。

通过开启参数innodb_stats_auto_recalc =on (默认也是打开的) 以便在表的数据发生重大变化以后来自动收集表的统计信息。比如当表中的10% 的行发生变化 ,InnoDB 将重新计算统计信息。或者我们可以使用ANALYZE TABLE显式地重新计算统计信息。

InnoDB 使用随机采样技术的方法采集统计信息– 随机抽取索引页,估计索引的基数。参数innodb_stats_persistent_sample_pages 控制采样页面的数量。参考​​https://dev.mysql.com/doc/refman/5.7/en/innodb-persistent-stats.html​​

根据代码和描述,随机抽样并不是完全随机的。采样页面实际上是根据采样算法选择的。最终,不同键值的总数,即索引的 stat_value 将通过以下公式计算
N * R * N_DIFF_AVG_LEAF。其中

N : 叶页数
R : levelLA上不同key值的个数与levelLA上记录总数的比值
`N_DIFF_AVG_LEAF`:在所有A叶页中找到的不同键值的平均数。

采样算法代码的详细信息可以在链接中找到:​​https://github.com/mysql/mysql-server/blob/6846e6b2f72931991cc9fd589dc9946ea2ab58c9/storage/innobase/dict/dict0stats.cc​​

基于上面的介绍,我们知道当一个表的索引发生分裂时,无论是叶子页数(N),还是 层LA 上不同键值的个数占 层LA 总记录数的比值(R ) 变得越来越不准确,因此 stat_value 的计算可能不正确。一旦发生这种情况,除非更改参数innodb_stats_persistent_sample_pages或重建索引,否则显式重新计算(手动运行 ANALYZE TABLE)将无法生成正确的 stat_value。

解决方法

我们怎么修正表的统计信息 ,并且阻止这类情况进一步发生。

经过前面的分析和讨论,我们知道 有两个因素影响数据库收集表的统计信息 ,

innodb_stats_persistent_sample_pages: A索引的组织方式

为了能够让 InnoDB 得到正确的 统计信息,我们需要 调整innodb_stats_persistent_sample_pages 或者重建索引 。
通过命令 analyze table 不重建的方式,保持
innodb_stats_persistent_sample_pages =128,stat_value 略微更改为 19582,接近原始不正确的 19498,仍然关闭。索引中的叶子页数从 55653 略微更改为 55891,索引中的页数也从 63864 略微更改为 64248

mysql>showvariables='innodb_stats_persistent_sample_pages;
+--------------------------------------+-------+
|Variable_name|Value|
+--------------------------------------+-------+
|innodb_stats_persistent_sample_pages|128|
+--------------------------------------+-------+
mysql>analyzetableorders;
+---------------+---------+----------+----------+
|Table|Op|Msg_type|Msg_text|
+---------------+---------+----------+----------+
|custom.orders|analyze|status|OK|
+---------------+---------+----------+----------+
mysql>select*frommysql.innodb_index_statswheredatabase_name='custom'andtable_name='orders'andindex_name='dpcz_FK';
+---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+
|database_name|table_name|index_name|last_update|stat_name|stat_value|sample_size|stat_description|
+---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+
|custom|orders|dpcz_FK|2022-03-0321:58:18|n_diff_pfx01|19582|50|d_p_c_z_id|
|custom|orders|dpcz_FK|2022-03-0321:58:18|n_diff_pfx02|32425512|128|d_p_c_z_id,id|
|custom|orders|dpcz_FK|2022-03-0321:58:18|n_leaf_pages|55891|NULL|Numberofleafpagesintheindex|
|custom|orders|dpcz_FK|2022-03-0321:58:18|size|64248|NULL|Numberofpagesintheindex|
+---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+

ANALYZE TABLE 不重建,但将innodb_stats_persistent_sample_pages 从 128 增加到 512,使 stat_value 到192非常接近实际基数 195。索引中的叶页数发生了很大变化,从 55653 到 44188。索引中的页数也从也发生了巨大变化,从 63864 变为 50304。

mysql>showvariableslike'%persistent_sample%';
+--------------------------------------+-------+
|Variable_name|Value|
+--------------------------------------+-------+
|innodb_stats_persistent_sample_pages|512|
+--------------------------------------+-------+
mysql>analyzetableorders;
+---------------+---------+----------+----------+
|Table|Op|Msg_type|Msg_text|
+---------------+---------+----------+----------+
|custom.orders|analyze|status|OK|
+---------------+---------+----------+----------+
mysql>select*frommysql.innodb_index_statswheredatabase_name='custom'andtable_name='orders'andindex_name='dpcz_FK';
+---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+
|database_name|table_name|index_name|last_update|stat_name|stat_value|sample_size|stat_description|
+---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+
|custom|orders|dpcz_FK|2022-03-0906:54:29|n_diff_pfx01|192|179|d_p_c_z_id|
|custom|orders|dpcz_FK|2022-03-0906:54:29|n_diff_pfx02|31751321|512|d_p_c_z_id,id|
|custom|orders|dpcz_FK|2022-03-0906:54:29|n_leaf_pages|44188|NULL|Numberofleafpagesintheindex|
|custom|orders|dpcz_FK|2022-03-0906:54:29|size|50304|NULL|Numberofpagesintheindex|
+---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+

重建表,保持innodb_stats_persistent_sample_pages 为128,同样得到了正确的 stat_value 187,接近真实基数195。索引中的叶子页数大幅变化,从55653变为43733,索引中的页数也从63864变化到 50111。

mysql>showvariables='innodb_stats_persistent_sample_pages';
+--------------------------------------+-------+
|Variable_name|Value|
+--------------------------------------+-------+
|innodb_stats_persistent_sample_pages|128|
+--------------------------------------+-------+
mysql>altertableordersengine=innodb;
QueryOK, 0rowsaffected (11min16.37sec)
mysql>select*frommysql.innodb_index_statswheredatabase_name='custom'andtable_name='orders'andindex_name='dpcz_FK';
+---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+
|database_name|table_name|index_name|last_update|stat_name|stat_value|sample_size|stat_description|
+---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+
|custom|orders|dpcz_FK|2022-03-0718:44:43|n_diff_pfx01|187|128|d_p_c_z_id|
|custom|orders|dpcz_FK|2022-03-0718:44:43|n_diff_pfx02|31531493|128|d_p_c_z_id,id|
|custom|orders|dpcz_FK|2022-03-0718:44:43|n_leaf_pages|43733|NULL|Numberofleafpagesintheindex|
|custom|orders|dpcz_FK|2022-03-0718:44:43|size|50111|NULL|Numberofpagesintheindex|
+---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+

在更正表统计数据后,MySQL 优化器也会选择正确的执行计划:

mysql>explain
SELECTcount(con.id) ,
MAX(DAYNAME(con.date)) ,
now() ,
pcz.type,
pcz.c_c
FROMconAScon
joinordersooncon.order_id=o.id
JOINp_c_zASpczONo.d_p_c_z_id=pcz.id
leftjoincconcon.c_id=c.id
WHEREcon.date=current_date()
andpcz.type="T_D"
GROUPBYcon.date, pcz.c_c, pcz.type;
+----+-------------+-------+------------+--------+-------------------+----------+---------+---------------------------------------+------+----------+---------------------------------+
|id|select_type|table|partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra|
+----+-------------+-------+------------+--------+-------------------+----------+---------+---------------------------------------+------+----------+---------------------------------+
|1|SIMPLE|con|NULL|ref|FK_order,IDX_date|IDX_date|3|const|3074|100.00|Usingtemporary; Usingfilesort|
|1|SIMPLE|c|NULL|eq_ref|PRIMARY|PRIMARY|8|custom.con.c_id|1|100.00|Usingindex|
|1|SIMPLE|o|NULL|eq_ref|PRIMARY,dpcz_FK|PRIMARY|8|custom.con.order_id|1|100.00|Usingwhere|
|1|SIMPLE|pcz|NULL|eq_ref|PRIMARY|PRIMARY|8|custom.o.d_p_c_z_id|1|10.00|Usingwhere|
+----+-------------+-------+------------+--------+-------------------+----------+---------+---------------------------------------+------+----------+---------------------------------+
4rowsinset, 1warning (0.01sec)

结论

MySQL优化器依赖于表的统计信息的准确性来选择最优的执行计划。我们可以通过更改参数innodb_stats_persistent_sample_pages 来控制系统采集表统计信息的准确性。

我们还可以选择通过在对索引进行碎片整理的同时重建/重建表来强制重新计算表统计信息,这有助于提高表统计信息的准确性。重构表,我们可以直接用 alter table xx; 修改表或者使用 pt-online-schema-change 达到同样的效果。

文章来源网络,作者:管理,如若转载,请注明出处:https://shuyeidc.com/wp/308425.html<

(0)
管理的头像管理
上一篇2025-05-27 14:08
下一篇 2025-05-27 14:09

相关推荐

  • 云服务器和云虚拟主机怎么选?云服务器和虚拟主机区别

    云服务器适合业务增长快、需弹性扩展的场景,而云虚拟主机适合预算有限、技术门槛低的小型静态网站或测试环境,二者核心区别在于资源独享性与运维复杂度,核心差异解析:从底层架构到使用体验很多人容易混淆这两者,觉得它们都是“买空间建站”,它们的底层逻辑完全不同,云服务器(ECS)就像是你租了一整栋别墅,水电网络独立,你想……

    2026-06-29
    0
  • 赣州智慧旅游招聘是真的吗?赣州旅游人才招聘信息

    中级岗位(3-5年经验)月薪范围通常在6000-10000元,这类岗位需要独立负责项目模块,如独立运营一个抖音账号,或维护一个景区小程序的功能迭代,具备成功案例的候选人议价能力较强,高级岗位(5年以上经验)月薪范围通常在10000-20000元,部分核心管理岗可达更高,这类人才需要具备战略规划能力,如制定整个景……

    2026-06-29
    0
  • 赣州智能物联网车位锁如何管理?智能车位锁管理系统多少钱

    赣州智能物联网车位锁管理的核心在于通过云端平台实现远程控锁、状态实时监控及自动计费,彻底解决传统车位“被占难管”与“找位难”的痛点,在赣州这样的城市,随着机动车保有量的持续增长,老旧小区、商业综合体以及私人固定车位的资源矛盾日益凸显,传统的机械地锁或简易遥控锁,不仅操作繁琐,更无法实现数据化管理,引入智能物联网……

    2026-06-29
    0
  • 赣州智能消防栓好用吗,智能消防栓多少钱一个

    赣州智能消防栓通过物联网技术实现实时监测与远程报警,能显著降低火灾响应时间并提升城市消防安全管理水平,是目前智慧城市建设中不可或缺的基础设施,赣州智能消防栓的核心价值与应用场景传统消防栓往往存在“看不见、摸不着、用不了”的痛点,在赣州这样地形复杂、老城区与新城区并存的区域,传统设施的管理难度极大,智能消防栓的出……

    2026-06-29
    0
  • 云服务器和物理机到底有啥区别?

    云服务器本质上是虚拟化资源池中的弹性实例,而传统物理服务器是独占的硬件实体,前者胜在弹性与运维便捷,后者强在物理隔离与性能稳定,具体选择取决于业务对成本、扩展性及安全合规的权衡,很多人初次接触服务器时,容易把“云服务器”和“传统物理服务器”混为一谈,觉得它们都是用来跑网站或存数据的盒子,这两者的底层逻辑完全不同……

    2026-06-29
    0

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注