解决分区大表统计信息不准确引发的性能问题

探讨分区大表因统计信息不准确导致的性能问题,并提供多种解决策略,助力数据库性能优化。

原文标题:分区大表统计信息不准确引发的性能问题

原文作者:牧羊人的方向

冷月清谈:

本文针对分区大表中因统计信息更新不及时导致的性能问题,以GaussDB数据库为例进行了分析。文章首先介绍了分区表的统计信息自动收集策略,包括分区表级别和表分区级别的统计信息收集。接着,深入探讨了统计信息不准确对动态分区裁剪和跨分区访问的影响,指出陈旧的统计信息会导致连接基数和成本估算偏差,生成次优的执行计划。最后,文章提出了几种解决策略,如自适应的统计信息更新阈值、为大表单独设置更新阈值、定期手动收集统计信息,以及使用Hint或绑定执行计划。文章强调,维护分区大表的统计信息需要从被动依赖转向主动管理,提前发现潜在风险。

怜星夜思:

1、文章提到了多种解决分区大表统计信息不准的方法,例如自适应阈值、单独设置阈值、手动收集等。在实际生产环境中,你会优先考虑哪种方案?为什么?
2、文章提到Hint和绑定执行计划可以解决统计信息不准带来的问题,但同时也增加了维护成本和风险。在你看来,这两种方法更适合哪些场景?应该如何降低其带来的负面影响?
3、文章中提到了分区裁剪,那么在你的实际工作中,你遇到过哪些因为分区裁剪失效导致的性能问题?你是如何排查和解决的?

原文内容

随着数据量的爆炸式增长,分区表已成为关系型数据库管理大型数据集的关键技术,尤其在时间序列、日志记录和交易流水等场景中应用广泛。在实际应用使用过程中,可能因为分区统计信息更新不及时,引发数据库性能问题。本文以GaussDB数据库为例,介绍分区大表由于新分区插入数据未及时更新统计信息导致执行计划走偏的案例

1、分区表的统计信息自动收集策略

分区表(Partitioned Table)指在单节点内对表数据内容按照分区键以及围绕分区键的分区策略对表进行逻辑切分。从数据分区的角度来看是一种水平分区(horizontal partition)策略方式,将表按照指定规则划分为多个数据互不重叠的部分。从数据分布而言,分区表分为主表和子表,主表是一个逻辑上的概念,实际的数据是存放在表分区子表之上,表分区可以增加删除、修改,存放的文件系统也可以不同,主表只需要同步全局的表结构和维护全局分区表的字典信息以及统计信息。

相比较普通表,分区表不仅可以提升数据库查询性能,在分区裁剪的场景下只需要查询指定的分区,不需要对所有分区扫描,从而提升查询效率;另外如果某个分区出现故障,表在其它分区的数据依然可用。

那么分区表的统计信息搜集策略是怎样,有两个维度:

  1. 分区表的维度,当所有分区的增删改达到表统计信息自动收集的阈值,会触发统计信息自动收集,对所有满足条件的分区收集统计信息,并汇总到分区表中;全表级别和字段级别的统计信息分别存放在pg_class和pg_statistic系统视图中。
  2. 表分区维度,当全局表没有达到自动收集的阈值,但是单个分区的增删改达到这个阈值时,也会触发该分区的统计信息自动收集。从数据库的表现来看,当单个分区的统计信息收集后,并没有同步更新到分区表上。分区级统计信息保存在pg_partition视图中。

在前文“”[1]中介绍过GaussDB数据库的统计信息自动收集策略,基于autovacuum_analyze_threshold(默认50行)和autovacuum_analyze_scale_factor(默认10%)参数,当数据变更量(增删改)超过阈值时会自动触发。对于一张分区大表而言,每天新插入到新分区的记录尚未达到全表的更新量阈值,也就不能触发全表统计信息的自动更新。

以一个特定的场景为例:一张按月分区的流水表sales_log,已经存在了3年,共36个分区,总计10亿行数据。由于全表记录数为10亿行,触发全局统计信息更新所需的变化量(10%)为1亿条,假如每天新增记录100w条,那么最近3个月新增的总记录也未达到自动收集统计信息的阈值。这样数据库的全局统计信息,包含总行数、索引基数、字段值分布等,停留在上一次统计信息收集的状态。对于数据库优化器而言是无法知道最新的数据分布情况,在执行计划评估时就可能存在偏差。

2、分区大表统计信息不准确的影响
2.1 对动态分区裁剪的影响

分区裁剪分为静态分区裁剪和动态分区裁剪。当WHERE子句中对分区键的过滤条件为常量时为静态分区裁剪,例如“WHERE partition_key = ‘2026-03-21’”,优化器在查询解析阶段就能精确地确定需要访问哪些分区,因此静态分区裁剪受表级别的统计信息影响较小。动态分区裁剪是当WHERE子句中对分区键的过滤条件来自于与其他表连接的结果时发生,在复杂的多表关联查询中经常出现。例如以下语句:

SELECT s.*
FROM sales_log s
JOIN promotion_campaigns p ON s.campaign_id = p.id
WHERE p.campaign_type = 'NewYearSpecial';

在这个查询中,sales_log表需要访问哪些分区,取决于promotion_campaigns表中哪些活动属于NewYearSpecial类型,以及这些活动的id对应到sales_log表中的哪些分区。优化器必须在执行阶段,通常是在执行JOIN操作时,才能确定要裁剪掉哪些分区。

上述例子中如果分区大表sales_log的全局统计信息过于陈旧,最终会导致动态分区裁剪的性能远低于预期:

  1. 连接基数估算:在上面的例子中,优化器首先需要估算promotion_campaigns表中满足campaign_type = 'NewYearSpecial’的行数
  2. 连接成本估算:接下来,优化器需要决定sales_log和promotion_campaigns的连接顺序和方法。它会参考sales_log的全局统计信息。由于统计信息是旧的,优化器并不知道最新的分区p_2026_03中可能包含了大量与NewYearSpecial相关的销售记录。
  3. 生成次优的执行计划:基于以上成本估算,优化器可能会认为sales_log 表相对较小,从而选择了一个不恰当的连接算法,比如嵌套循环连接(Nested Loop Join)。嵌套循环先从promotion_campaigns表中找出活动ID,然后对每个ID,去 sales_log 表中探测一次。

在嵌套循环时,虽然每次探测sales_log都会进行分区裁剪,但整个过程是“驱动表驱动被动表”的模式。如果优化器因为错误的统计信息选择了错误的驱动表,那么即使每次探测都做了分区裁剪,整体的I/O和计算开销也会非常大。

2.2 跨分区访问的影响

跨分区的大表访问在分析统计类的业务场景下经常遇到,比如以下SQL统计2026年第一季度的销售总额:

SELECT product_category,SUM(sale_amount) as total_sales
FROM sales_log
WHERE status=’T’ and sale_date >= '2026-01-01' AND sale_date < '2026-04-01'
GROUP BY product_category
ORDER BY total_sales DESC;

该查询需要访问p_2026_01、p_2026_02和p_2026_03三个分区的数据。由于新增分区的数据量未达到全表的统计信息自动收集,全局的统计信息中未包含该三个分区的统计信息。优化器在执行计划成本估算的时候会出现偏差,有可能认为全表扫描这几个分区的数据比走索引的成本更低,最终导致性能上的劣化。

3、如何解决大表统计信息不准的问题

不仅仅是GaussDB数据库,其它如MySQL系、TiDB等数据库都存在问题。那么对于分区大表的统计信息如何进行更为精细化的管理和维护?

1)自适应的统计信息更新阈值

目前大多数数据库的统计信息自动更新阈值是固定的,比如更新量超过一定的pages或者整表数据量的10%等。对于一些大表而言会存在统计信息更新不及时的问题,因此需要数据库底层支持自适应的阈值更新,根据表数据量的级别进行动态的适配调整,比如1000w数据量的阈值为10%、1亿条数据的阈值为1%等。

2)大表单独设置更新阈值

有些数据库厂商是支持对单表设置统计信息更新阈值的,比如在GaussDB数据库中通过语句“ALTER TABLE xx SET (autovacuum_analyze_scale_factor=0.01)”对大表设置不同的阈值,比如0.01对于1亿大表更新量超过100w即自动收集统计信息。这样的好处是大表的统计信息能及时的更新到,不过这个阈值该怎么定义、频繁触发统计信息收集比如在业务高峰期的性能影响等,都需要事先评估确认清楚。有些数据库厂商建议这种做法,虽然会增加一定的维护成本。

3)定期手动收集统计信息

对于分区大表新增分区已经更新统计信息但全局表未及时收集的,通过SQL语句可以定期排查出清单,在周末等业务低峰期进行手动的统计信息收集。定期收集统计信息在传统DB2、Oracle数据库中使用较多,也是增加了运维成本。

4)应用使用Hint或者绑定执行计划

在SQL中指定hint方式强制走某个索引如SELECT /*+ INDEX(sales_log idx_date) */ …,防止因为统计信息不准确导致的执行计划走偏进而引发性能问题。这种方式对应用具有侵入性并且需要修改业务代码。另外就是使用执行计划绑定方式,在统计信息准确时,捕获一个查询的“良好”执行计划,并将其“绑定”到这条SQL语句上。之后,无论统计信息如何变化,只要这条SQL被执行,优化器都会优先使用这个被绑定的计划。不过这两种方法都会增加维护成本,并且随着数据量的变化或者复杂的多表关联查询语句,之前的hint或执行计划并不是最优的,会带来一定的风险。

综合而言,分区大表统计信息不准确是一个常见的数据库性能劣化场景,也没有通用的实施优化标准。有些厂商建议使用表级别的统计信息收集阈值、有些会使用hint方式、也有定期收集统计信息维护策略。根本上还是需要从被动依赖向主动管理转变,提前发现潜在的风险和问题。

参考资料:

同意楼上的观点。Hint和绑定执行计划可能会限制优化器的选择,使其无法根据最新的数据分布情况选择最优的执行计划。在数据量变化较大或者查询模式发生变化的情况下,之前绑定的执行计划可能不再是最优的,甚至会变得更差。所以,在使用Hint和绑定执行计划之前,一定要进行充分的测试和评估。

个人觉得,Hint和绑定执行计划就像是“止痛药”,只能暂时缓解问题,不能从根本上解决问题。在统计信息长期不准确的情况下,频繁使用Hint和绑定执行计划,可能会掩盖更深层次的性能问题,甚至导致未来的性能风险。所以,应该尽量避免长期依赖这种方法。

其实,除了手动收集,还可以考虑使用一些第三方工具。有些工具可以自动分析数据库的性能瓶颈,并根据分析结果智能地收集统计信息。虽然这些工具可能需要一定的成本,但如果能显著提高数据库性能,降低运维成本,也是值得考虑的。

我的想法是,可以结合业务特点,制定更合理的统计信息收集策略。比如,对于新增数据量较大的分区,可以更频繁地收集统计信息;对于历史分区,可以适当降低收集频率。另外,还可以根据查询模式,有针对性地收集特定字段的统计信息,避免全表扫描。

这个问题提得好!不同数据库的架构和统计信息管理机制不同,解决这个问题的方式也会有所差异。比如MySQL可能更依赖于手动维护和更细粒度的统计信息收集,而TiDB作为分布式数据库,可能需要考虑数据分布和跨节点统计信息同步的问题。个人感觉没有绝对的优势劣势,具体还是要看应用场景和DBA的经验。

从我的理解来看,MySQL的优势在于生态完善,有很多成熟的监控和管理工具,可以辅助DBA进行统计信息的分析和维护。劣势在于,MySQL本身的统计信息机制相对简单,对于复杂查询的支持可能不够好。TiDB的优势在于分布式架构,可以更好地处理大规模数据,但劣势在于其统计信息的收集和同步可能更加复杂,需要更多的运维成本。

这个问题问到了点子上。手动收集统计信息的确比较繁琐,但可以通过一些方法来提高效率。比如,可以编写自动化脚本,定期分析分区表的统计信息,找出需要更新的分区,然后批量执行统计信息收集命令。另外,还可以利用数据库提供的管理工具,比如GaussDB的“gs_gather_stats”,来简化统计信息收集的过程。

有一说一,统计信息这块儿,PostgreSQL及其衍生品(比如GaussDB)相对来说做得更好一些。PostgreSQL的统计信息更加丰富,优化器也更聪明。MySQL嘛,可能更需要DBA人工干预。TiDB这种分布式数据库,挑战更大,需要考虑数据分布和一致性。

从我的经验来看,可以考虑在以下情况下谨慎使用Hint和绑定执行计划:1. 数据集非常稳定,不太可能发生大的变化;2. 查询语句非常关键,性能要求极高,并且经过充分测试,确认Hint或绑定的执行计划是最优的;3. DBA团队有足够的经验和能力来维护Hint和绑定执行计划。

有一次,一个按时间分区的日志表,查询最近一周的数据非常慢。检查SQL发现WHERE条件中包含了时间范围,理论上应该可以进行分区裁剪。但实际执行计划显示全表扫描。后来发现,WHERE条件中的时间字段类型是字符串,而分区键是日期类型。由于隐式类型转换,导致分区裁剪失效。解决方法是将SQL中的字符串类型转换为日期类型。

我倾向于优先考虑“定期手动收集统计信息”这个方案。虽然它增加了运维成本,但可控性最高。自适应阈值依赖于数据库自身的实现,可能不够透明;单独设置阈值需要精确评估,否则可能频繁触发或仍然更新不及时。手动收集可以根据业务特点,在低峰期进行,确保统计信息的准确性,从而保证查询性能的稳定。

作为一个DBA,我会选择“大表单独设置更新阈值”结合监控。首先,单独设置阈值能够针对性地解决大表更新不及时的问题。其次,我会设置监控,观察统计信息收集的频率以及对数据库性能的影响。如果发现频繁触发影响业务,可以适当调整阈值。这样既能保证统计信息的时效性,又能控制对业务的影响。关键在于“监控”和“调整”。

分区裁剪失效也可能是权限问题导致的。如果用户对分区表有查询权限,但对某些分区没有权限,优化器可能会选择全表扫描,而不是进行分区裁剪。解决方案是授予用户对所有分区的查询权限。另外,还可以检查一下是否有其他因素干扰了优化器的决策,比如复杂的函数调用、子查询等。

Hint适合于快速修复紧急的性能问题。比如,某个SQL突然变慢,但排查发现是统计信息不准确导致优化器选择了错误的执行计划。这时,可以使用Hint强制走正确的索引,立即恢复性能。但Hint只是临时方案,后续需要排查统计信息问题并移除Hint。
绑定执行计划适合于相对稳定的查询,且已经找到一个“良好”的执行计划。但要定期评估绑定计划的有效性,如果数据分布发生较大变化,可能需要重新绑定。

降低负面影响的关键在于“可控性”。对于Hint,要做好注释,说明原因和有效期,方便后续维护。对于绑定执行计划,要做好版本管理,记录每次绑定的时间、原因和效果,方便回滚。另外,可以考虑使用SQL Profile等工具,辅助分析和优化执行计划。

我认为这两种方法都属于“手术刀”级别的优化,风险较高。Hint会绕过优化器,可能导致其他潜在问题;绑定执行计划则限制了优化器的灵活性。因此,只应在性能瓶颈非常明显,且其他优化方法无效时才考虑使用。使用前,一定要进行充分的测试和评估,并在监控中密切关注。

我觉得应该根据实际情况来选择。如果数据库版本较新,且自适应阈值效果较好,可以优先考虑。如果对数据库的控制力要求较高,或者有明确的业务低峰期,手动收集是更好的选择。单独设置阈值适合对业务了解很深入,能够准确评估阈值大小的场景。没有银弹,要具体问题具体分析。

我之前遇到过动态分区裁剪失效的情况,原因是JOIN条件中使用的字段类型不一致。主表的分区键是INT类型,关联表中的对应字段是BIGINT类型。虽然数据上可以匹配,但优化器无法识别,导致动态分区裁剪失效。解决方法是修改关联表的字段类型,使其与主表的分区键类型一致。