
做数据库优化的人绕不开统计信息这四个字。执行计划该走索引却全表扫描、两张表关联顺序反了、一条平时只要几十毫秒的SQL突然变成秒级这些让人挠头的现象背后多半是统计信息缺失、过期或者采样不准。统计信息说白了就是数据库给优化器准备的数据画像一张表有多少行、多少个不同的值、字段值大致怎么分布、索引有多少个数据页。没有这张画像优化器判断代价全靠蒙执行计划自然不靠谱。这篇文章我想把统计信息的查看和收集从头到尾拆一遍既有原理层面的解释也有可以直接抄走的SQL和操作步骤。适合三类人看正在被慢查询折磨的DBA、接手存量数据库调优的开发者、还有看到告警面板上“统计信息过期”提示却不知道从哪下手的运维同学。不管你用的是哪种主流关系型数据库统计信息的理念都是相通的只是查看和收集的语法稍有差异我会把几个主流库的常见写法都列出来做对比。1. 统计信息的底层逻辑优化器凭什么做决定1.1 统计信息到底存了哪些内容要理解统计信息的查看和收集先得知道它装的是什么。不同数据库存放的位置不一样Oracle放在数据字典里PostgreSQL存在pg_statistic和pg_class两张系统表里MySQL放在innodb_table_stats和innodb_index_stats里但内容大体是同一套东西表级信息行数、数据块数或页数、平均行长度。行数是最关键的基础数据它决定全表扫描的相对代价也参与关联顺序的生成。列级信息非空值数量、不同值个数NDV、最大值最小值、平均字段长度。这些数据被优化器用来估算单列过滤条件的选择率。直方图信息把列的值分布切成若干个桶每个桶记录边界值和频次。直方图的存在让优化器不必强行假设数据是均匀分布的。索引级信息索引层数B树高度、叶子块数、聚簇因子。聚簇因子影响回表代价的估算直接影响优化器是否愿意选择某个索引。把数据库比作一辆车统计信息就是仪表盘上的油量、转速和路况数据。没有这些数据车况再好你也开不稳。理解了这一点就会明白统计信息不只是DBA手册里的一个名词而是执行计划质量的地基。1.2 优化器拿统计信息做什么计算优化器的本质工作是把一条SQL翻译成多个候选执行计划然后估算每个计划的代价挑一个代价最小的。代价估算的依据就是统计信息里的行数、页数、分布数据用它们去推算扫描成本、关联成本、排序成本。举个例子。一张订单表里有1000万行数据status字段有3个值未支付、已支付、已取消。如果统计信息准确优化器知道status等于已支付大概会返回800万行那它肯定不会选择在这个字段的单列索引上做回表因为回表800万行的成本远高于全表扫描反过来如果统计信息里记录status有10万个不同值分布均匀优化器会认为过滤性很好高高兴兴走索引。这就是同一条SQL在统计信息新鲜和过期时执行计划完全不同的原因。关联顺序也是同一个道理。两张表做关联经验法则是小表驱动大表但“小”和“大”由谁来判断就是统计信息里估算的行数。一旦某张表的行数统计翻了几倍或者原地不动优化器算出来的关联代价就全部失真执行计划自然乱套。直方图的作用更直观。假设某个客户ID字段90%的记录都集中在少数几个客户上。要是没有直方图优化器默认每个值出现的频率差不多等于把偏斜数据当成均匀数据去算。结果就是一条只会返回几万行的查询被安排成一次昂贵的全表扫描。这个场景在真实业务里非常常见后面排查实录部分我会专门展开。2. 查看统计信息从表到列的立体排查2.1 先看表级统计信息查统计信息的第一步永远是确认表和索引的基础画像。不同数据库的查看方式差别比较大这里把三种主流数据库的常用写法都列出来。PostgreSQL里表级统计信息写在pg_class系统表中SELECT relname, relpages, reltuples FROM pg_class WHERE relname orders;relpages是表的页数reltuples是估算行数。需要留意的是reltuples来自最近一次ANALYZE或VACUUM它不保证是实时的只能反映最近一次分析的快照。Oracle里最常用的是dba_tables视图SELECT table_name, num_rows, blocks, avg_row_len, last_analyzed FROM dba_tables WHERE table_name ORDERS;num_rows是最近一次统计信息记录的行数blocks是数据块数last_analyzed告诉你它上一次是什么时候被分析的。如果last_analyzed是空说明这张表大概率从没收集过统计信息执行计划质量可想而知。MySQL里最简单是查information_schemaSELECT TABLE_NAME, TABLE_ROWS, DATA_LENGTH, UPDATE_TIME FROM information_schema.TABLES WHERE TABLE_NAME orders;这里有个常见的误解需要澄清MySQL在InnoDB引擎下的TABLE_ROWS是估算值来自对采样页的计算不是精确值。如果发现它和真实行数差距很大别急着怀疑数据出了问题先看采样配置。2.2 列级统计信息与直方图怎么检查表级数据只是骨架真正决定过滤条件估算质量的是列级统计信息。PostgreSQL直接查pg_stats就很方便它已经把内部编码转成了可读格式SELECT attname, null_frac, n_distinct, most_common_vals, most_common_freqs, histogram_bounds FROM pg_stats WHERE tablename orders;most_common_vals和most_common_freqs就是直方图内容能看到高频值和对应的频率。如果极端值出现频率很高这两列会非常直观。n_distinct为负数时表示不同值个数占行数的比例正数才表示绝对个数。Oracle需要查dba_tab_col_statistics和dba_histogramsSELECT column_name, num_distinct, num_nulls, density, histogram, last_analyzed FROM dba_tab_col_statistics WHERE table_name ORDERS;histogram字段会显示NONE、FREQUENCY或HEIGHT BALANCED。FREQUENCY直方图适合低基数列HEIGHT BALANCED适合高基数但有偏斜的列。如果发现一个明显偏斜的字段显示NONE那就该考虑重新收集并指定method_opt策略。MySQL的列级信息相对有限可以查information_schema.STATISTICS看索引的基数SELECT INDEX_NAME, COLUMN_NAME, CARDINALITY FROM information_schema.STATISTICS WHERE TABLE_NAME orders;CARDINALITY是索引列不同值个数的估算。如果CARDINALITY相对于行数非常小说明索引区分度很低优化器大概率不会用它。这个指标对判断索引是否值得保留很有参考价值。2.3 如何判断哪些表的统计信息已经过期不要等到慢查询出现了才去查统计信息。日常巡检中我会用下面几个条件筛选出有风险的表PostgreSQL场景对比pg_class的reltuples和pg_stat_user_tables的n_live_tup如果差距超过一定比例说明表数据变化没有被及时反映也可以看last_autoanalyze和last_analyze的时间结合批量任务时间判断。Oracle场景查dba_tab_modifications可以看到自上次分析以来的INSERT、UPDATE、DELETE次数配合dba_tables的last_analyzed做判断。注意dba_tab_modifications需要提前开启监控或者手动执行DBMS_STATS.FLUSH_DATABASE_MONITORING_INFO才能及时写入。MySQL场景把information_schema.TABLES的UPDATE_TIME和业务批量任务时间对齐如果经常发生大批量写入但UPDATE_TIME很久没变说明自动重算统计可能没有生效。这些检查不需要写得多精确目的先圈出存在风险的表再逐张核对执行计划走向。记住一个原则统计信息的新鲜度从来不取决于你上次是什么时候跑的而取决于表数据多久没被重新画像。一张每天翻几倍数据的表哪怕昨天刚收集过今天也可能已经过期。3. 收集统计信息手动执行与自动化策略3.1 手动收集的常用命令先说最直接的手段手动收集。PostgreSQL的ANALYZE命令负责收集统计信息ANALYZE orders;如果希望同时回收死元组占用的空间并更新统计信息可以执行VACUUM ANALYZE orders;值得区分的是VACUUM本身不收集统计信息但VACUUM ANALYZE会先清理后分析。对经常更新删除的表这种方式比单独ANALYZE更解渴。Oracle使用DBMS_STATS包EXEC DBMS_STATS.GATHER_TABLE_STATS( ownname APP, tabname ORDERS, cascade TRUE, estimate_percent 100 );cascade表示同时收集表上的索引统计信息estimate_percent设为100表示全量采样。对核心小表全量采样通常没问题对大表后面会说怎么权衡。MySQL InnoDB下直接执行ANALYZE TABLE orders;这个命令会重新计算索引基数并将统计信息持久化到内部表。需要注意MySQL的ANALYZE TABLE在InnoDB下会请求元数据锁虽然持锁时间通常很短但大表上仍可能造成轻微阻塞低峰期操作更稳妥。3.2 采样率怎么权衡全量还是抽样收集统计信息时最纠结的问题就是采样率。全量最准但大表上可能等于一次几十分钟的IO风暴抽样快但估算结果可能偏差很大。PostgreSQL的ANALYZE默认采样30000行这个采样行数由default_statistics_target控制默认值是100。对中小表绰绰有余对几千万行的大表就不太够特别是高基数列容易被低估。想提高精度可以先调整参数再重新ANALYZE但也要准备接受更长的分析时间。Oracle的estimate_percent可以填1到100。对几百GB的大表全量采样意味着什么资源消耗心里要有数。实操中我倾向于把主表全量采样放在维护窗口平时巡检交给自动任务的默认采样率。最关键的过滤列可以单独指定100%全量收集保证直方图的准确性。MySQL InnoDB通过innodb_stats_sample_pages控制采样页数不同版本默认值有差异。如果发现索引基数估算经常不准可以适当调大代价是ANALYZE时间变长。反过来如果系统对ANALYZE时间很敏感就该接受一定程度的估算误差。这里有个生活化的类比全量采样等于人口普查数据准但工程量大抽样等于抽查几万人统计趋势速度快但可能会有误差。生产环境不需要每张表都做普查但关键表关键列必须值得。3.3 自动收集机制和手动收集的平衡主流数据库都有内置的自动收集机制但绝不意味着可以完全撒手不管。PostgreSQL里autovacuum进程不仅做清理也会在表变化量达到阈值后触发autoanalyze。默认阈值大概是1000条加上表行数的20%。也就是说500万行的表要累计变化超过100万行左右才会触发自动分析这个阈值对于很多大表来说相当迟钝。Oracle默认有周一到周五晚上十点以及周末全天的自动统计信息任务。现实问题是如果业务每天晚上都跑批量任务而批量任务恰好在自动任务之前结束统计信息能捕捉到如果批量任务持续到凌晨自动任务可能只收集到一半状态的数据。更麻烦的是长时间运行的批量任务阻塞了自动任务的情况也时有发生。MySQL的innodb_stats_auto_recalc在表数据变化超过10%左右会自动触发重算但它是后台异步完成的并发高峰期可能被延迟统计信息更新不一定跟得上业务节奏。我的思路是自动收集保底线手动收集保关键。核心大表、经常变化的维度表、跑批后立刻查询的报表表都应该建立自己的手动收集脚本放在批处理流程的固定环节里而不是指望后台任务偶发地照顾到你。这套思路在多个项目里验证下来对执行计划稳定性帮助很大。3.4 分区表收集与并发控制分区表是统计信息收集的重灾区。一个分区表动辄几十个分区新分区每天往里灌数据如果每次都整表全量分析资源浪费肉眼可见。Oracle可以在收集时只处理当前活跃分区EXEC DBMS_STATS.GATHER_TABLE_STATS( ownname APP, tabname ORDERS_PART, partname ORDERS_202501, cascade TRUE );PostgreSQL如果用的是声明式分区可以对单独分区执行ANALYZE也可以对分区表整体执行获取全局视图。细节是分区级视图和全局视图都有价值查询只涉及某个分区时优化器可以用分区统计信息涉及全表时全局统计信息更重要。很多时候两者都需要有。并发控制方面不少DBA图省事把所有表丢给一个脚本从头跑到尾结果触发大量并发IO和锁竞争。更好的做法是分批处理先收集小表再收集大表把大表按表名拆成多批串行跑避免IO被打满。收集过程中可以顺便关注数据库的锁等待视图如果发现DDL操作被长期阻塞及时终止慢速的收集任务。4. 统计信息导致的慢查询排查思路与实战记录4.1 数据量暴涨但统计信息没跟上模拟项目中遇到过这样一个场景某个访问日志表平时每天新增几十万行某次业务活动一天新增了上千万行。当天晚上监控显示很多报表查询从毫秒级变成秒级应用层第一时间怀疑SQL写得不对实际上真正原因是统计信息里记录的行数还停留在几天前。排查路径是这样的先抓出慢SQL的执行计划发现优化器估算的行数和实际返回行数差了上千倍再看表级统计信息last_analyzed显示是三天前而表的真实行数已经翻了几倍。重新收集统计信息后执行计划马上恢复正常查询耗时也跟着回落。这个案例说明一个很基础但经常被忽略的要点统计信息排查的关键不只是跑一遍ANALYZE而是先确认优化器眼里到底看到了什么。看执行计划时如果发现Estimated Rows和Actual Rows对不上号相差几个数量级第一怀疑对象就应该是统计信息。4.2 数据倾斜与直方图缺失另一个高频问题来自数据倾斜。某业务系统里有一张订单表business_type字段正常状态下95%是普通订单5%是预售订单。某次活动后预售订单涨到30%由于列上的直方图还是旧数据优化器把普通订单的选择率估算得过高导致不少查询明明改走索引更划算却被安排成了全表扫描。这种问题靠默认的ANALYZE不一定能解决尤其是Oracle的method_opt默认是for all columns size auto它只对认为值得的列生成直方图判断依据是列的使用频率和数据分布。一直不参与查询或者分布均匀的字段可能压根不会生成直方图。处理方式也不复杂对关键过滤列重新收集并强制生成直方图。Oracle可以这样写EXEC DBMS_STATS.GATHER_TABLE_STATS( ownname APP, tabname ORDERS, method_opt for columns size skewonly business_type );PostgreSQL则可以把default_statistics_target调大再重新ANALYZE让直方图桶更细。但注意调大后要关注内存开销和收集时间不是越大越好我一般从200到500的范围试观察执行计划稳定性再定。4.3 几个关键参数的调优对照把常用的统计信息相关参数整理成一张对照表方便直接收藏参考。数据库关键参数作用什么时候需要调PostgreSQLdefault_statistics_target控制ANALYZE采样桶数量间接影响采样密度高基数列估算偏差大时适当调大Oracleestimate_percent控制采样百分比100为全量大表但直方图要求高时指定全量Oraclemethod_opt控制哪些列生成直方图以及类型偏斜字段估算不准时指定skewonly或size autoMySQLinnodb_stats_sample_pages控制索引基数采样的页数索引基数估算偏差大时调大长期维护的经验是给核心表建立一个统计信息健康度检查的例行巡检项。每周跑一次排查SQL把统计信息较旧、变更量大、行数估算偏差大的表拎出来再针对执行计划不稳定的关键SQL做定向分析。我自己还会在每次大版本升级或者数据迁移后强制对所有迁移后的表做一次全量收集因为迁移工具很多时候不会同步统计信息尤其是索引统计信息。新建索引后也别想当然。PostgreSQL创建索引后一般会自动更新相关统计信息但更复杂的表达式索引和部分索引需要手动ANALYZEOracle在创建索引时会默认收集索引统计信息但已有的直方图不会因为建了索引就自动更新MySQL在InnoDB下创建索引后通常会自动更新但在某些配置下不会立即生效。所以建完索引后顺手跑一次统计信息刷新宁可多跑一次也不要在执行计划上猜。最后再分享一个实操体会。我长期维护系统下来最大的感受是统计信息不是一次性工作而是一条持续迭代的数据链路。你可以不必记得每一条SQL是怎么写的但你不能不知道系统里哪几张表的统计信息最容易过期该在什么时间窗口收集批量任务之后应该先刷新哪几张表。每次大版本升级、每月跑批结束之后我都习惯性地做一轮统计信息巡检这个最不起眼的活儿往往能挡住最难缠的慢查询。