批量导入、历史数据清理和业务状态集中修改,可能会改变数据分布,反映在业务上大多数是医院收费窗口查询变慢,这时候就需要对执行计划和统计信息进行检查。在日常增量变化交给自动 ANALYZE,同时还要巡检确认任务是否完成,必要进行手工补采。本文以 TiDB 8.5 自建集群为范围,医院测试库 his_lab、模拟医院日常业务场景和对应的 SQL 。
统计信息与执行计划
优化器通过统计信息估算一个条件能筛出多少行,再计算索引访问、表扫描和表连接的成本。表行数、不同值数量、直方图和 TopN 都会参与估算。统计信息落后于数据变化时,估算行数可能偏差很大,进而影响索引和连接顺序的选择。
门诊收费表的历史记录大多已结算,未收费记录主要集中在近期。查 charge_status = 'UNPAID' 时,是否加日期条件,涉及的数据量可能相差很大。需要结合执行计划检查状态、日期、字段关联、索引设计和查询写法。
版本与配置
SELECT TIDB_VERSION();
SHOW GLOBAL VARIABLES
WHERE Variable_name IN (
'tidb_enable_auto_analyze',
'tidb_auto_analyze_ratio',
'tidb_auto_analyze_start_time',
'tidb_auto_analyze_end_time',
'tidb_auto_analyze_concurrency',
'tidb_enable_auto_analyze_priority_queue',
'tidb_analyze_version',
'tidb_analyze_column_options',
'tidb_persist_analyze_options'
);
SELECT @@SESSION.tidb_analyze_version AS session_analyze_version;
默认值见下表。维护时以实际配置为准,升级集群可能保留原值。
| 变量 | 默认值或版本差异 | 维护时的含义 |
|---|---|---|
tidb_enable_auto_analyze |
ON |
自动收集统计信息的开关 |
tidb_auto_analyze_ratio |
0.5 |
已有统计信息的表, 变更比例超过阈值时进入自动收集判断 |
tidb_auto_analyze_start_time |
00:00 +0000 |
自动收集窗口的开始时间 |
tidb_auto_analyze_end_time |
23:59 +0000 |
自动收集窗口的结束时间 |
tidb_auto_analyze_concurrency |
v8.5.0~v8.5.6 为 1;v8.5.7 起为 3 |
集群自动 ANALYZE 任务的并发度; 升级到 v8.5.7 时保留原值 |
tidb_enable_auto_analyze_priority_queue |
ON |
按优先队列调度自动收集任务 |
tidb_analyze_version |
2 |
收集的统计信息版本, 具有 SESSION 和 GLOBAL 作用域 |
tidb_analyze_column_options |
新建 v8.5.0~v8.5.4 集群为 PREDICATE;
新建 v8.5.5 及以后集群为 ALL |
Version 2 的默认采集列范围 |
tidb_persist_analyze_options |
ON |
保存表的采集配置,供后续收集沿用 |
除 tidb_analyze_version 外,上表变量均为 GLOBAL 作用域。tidb_auto_analyze_ratio 在 8.5 中的取值范围为 (0, 1];要关闭自动收集,使用 tidb_enable_auto_analyze。
tidb_analyze_version 控制采集版本,本文手工示例使用 Version 2。修改变量不会立即转换已有统计信息,还需要重新收集。Version 2 使用直方图和 TopN 等信息,不再收集 Count-Min Sketch;从 v8.5.6 起,Version 1 已被标记为废弃。
自动 ANALYZE 的触发条件
普通非分区表已有统计信息时,TiDB 会根据累计变更量判断是否需要重新收集。默认阈值为 0.5,比较条件是变更比例大于阈值。Modify_count 是累计变更计数,同一行多次修改会继续累加。
自动收集还需满足开关已开启、当前时间在允许窗口内、统计信息未锁定等条件。默认情况下,少于 1000 行的表不会触发自动 ANALYZE。满足条件后仍需等待调度,巡检要确认任务是否完成。
未收集过统计信息的表、缺少统计信息的新索引,也会进入自动收集判断,无需等到变更比例超过 0.5。表规模、时间窗口和调度条件仍然有效。新表或新索引承载重要 SQL 时,需要在上线前确认统计信息就绪。
SHOW STATS_META 的 Row_count 是随 DML 更新的行数。8.5.0 的实现计算变更比例时,会优先使用上次收集得到的行数基数,有可用基数时不直接使用当前 Row_count。巡检脚本可以用 Modify_count / Row_count 辅助观察变化,但不要据此精确复刻自动调度条件;健康度也直接读取系统结果。
凌晨 1 点至 5 点的窗口配置:
SET GLOBAL tidb_auto_analyze_start_time = '01:00 +0800';
SET GLOBAL tidb_auto_analyze_end_time = '05:00 +0800';
维护窗口按门诊、急诊和夜间批处理负载确定。缩短窗口后,检查任务能否完成。这两个变量只约束自动收集,手工执行 ANALYZE TABLE 需另行安排。
健康度、元信息与任务状态
检查业务库:
SHOW STATS_HEALTHY
WHERE Db_name = 'his_lab';
SHOW STATS_META
WHERE Db_name = 'his_lab';
SHOW STATS_HEALTHY 返回 Db_name、Table_name、Partition_name 和 Healthy。Healthy 的范围为 0~100,用于粗略判断统计信息受数据变更影响的程度。分数高不等于每个查询条件的估算都准确。
用以下查询筛选待检查的表。60 是本文的巡检筛选值,TiDB 没有规定以此作为手工 ANALYZE 阈值。
SHOW STATS_HEALTHY
WHERE Db_name = 'his_lab'
AND Healthy < 60;
查看收费表的元信息:
SHOW STATS_META
WHERE Db_name = 'his_lab'
AND Table_name = 'outpatient_charge';
| 字段 | 检查内容 |
|---|---|
Db_name、Table_name |
确认库表 |
Partition_name |
区分分区统计信息与表级统计信息;非分区表通常为空 |
Update_time |
统计元信息更新时间,DML 引起的计数更新也会改变它 |
Modify_count |
累计修改行数,用于观察收集后发生了多少变更 |
Row_count |
统计信息维护的表行数,不是现场执行 COUNT(*) 的结果 |
Last_analyze_time |
最近一次收集统计信息的时间 |
上次收集时间看 Last_analyze_time。持续写入时,Update_time 可能很新,直方图仍可能是之前收集的。
元信息存在持久化和缓存更新过程,批量操作刚结束时,连续查询可能看到不同计数。结合任务完成时间复查,不为核对行数扫描整张收费明细表。健康度为 0 时,结合 Last_analyze_time 和任务记录,区分“尚未收集”与“已有统计信息但变更较多”。
低健康度持续多次巡检未改善时,查近期任务和统计信息锁定状态:
SHOW ANALYZE STATUS
WHERE Table_schema = 'his_lab'
AND Table_name = 'outpatient_charge';
SHOW STATS_LOCKED
WHERE Db_name = 'his_lab'
AND Table_name = 'outpatient_charge';
SHOW ANALYZE STATUS 使用 Table_schema,SHOW STATS_* 使用 Db_name。状态为 failed 时查看 Fail_reason;有 running 任务时确认进度,避免重复提交。统计信息被锁定的表会跳过收集,手工 ANALYZE 也不能绕过,处理前需查明锁定原因。
没有任务记录时,核对开关、时间窗口和表规模。调整过统计任务运行节点的,还需检查各 TiDB 实例的 tidb_enable_stats_owner,该变量只影响所在实例。配置项 stats-lease 设为 0 会停止统计信息自动更新。
哪些情况需要手工补采
按表判断是否补采,不安排固定的每日全库 ANALYZE。
| 现场情况 | 处理方式 |
|---|---|
| 批量导入收费、住院费用等数据 | 导入提交后检查统计信息;尚未收集或仍反映导入前分布, 且业务启用前等不到自动任务时,安排手工收集 |
| 大批量删除、归档,或集中修正收费状态 | 检查受影响表的变更量和关键 SQL;分布明显变化时,在合适窗口重新收集 |
| 执行计划异常,估算行数与实际行数明显不符 | 检查相关表、列和索引的统计信息;确认缺失或陈旧后补采,并复查原 SQL |
| 健康度持续下降,自动任务迟迟未完成 | 查明失败、锁定或窗口不足的原因;关键表不能继续等待时, 评估负载后手工处理 |
| 新表完成数据装载 | 业务启用前确认已有统计信息;空表不必反复 ANALYZE |
| 新索引要用于上线 SQL | 检查索引统计信息是否已生成;缺失时补采,已有可用统计信息时无需重复执行 |
| 不足 1000 行的小表参与重要连接 | 自动收集通常不会处理;存在估算问题时手工收集 |
官方文档将大批量数据变更和查询计划不合理列为 ANALYZE 的使用场景。维护时间结合业务时限确定。历史病案表一个月没有变化,仅因 Last_analyze_time 较早无需重采;收费状态当天集中变化,即使整表健康度较高,也要检查相关查询。
收费查询的估算检查
用测试表 his_lab.outpatient_charge 检查待收费查询,涉及 charge_no、total_amount、charge_status、created_at:
EXPLAIN
SELECT charge_no, total_amount, created_at
FROM his_lab.outpatient_charge
WHERE charge_status = 'UNPAID'
AND created_at >= '2026-09-22 00:00:00'
AND created_at < '2026-09-23 00:00:00'
ORDER BY created_at
LIMIT 100;
执行计划重点看访问路径、扫描范围和 estRows。stats:pseudo 表示该处使用伪统计信息,需检查统计信息是否缺失、过旧或未及时加载。数据量较小或读取比例较大时,TableFullScan 可能合理,不能单凭它判断统计信息异常。
查询开销可接受时,执行:
EXPLAIN ANALYZE
SELECT charge_no, total_amount, created_at
FROM his_lab.outpatient_charge
WHERE charge_status = 'UNPAID'
AND created_at >= '2026-09-22 00:00:00'
AND created_at < '2026-09-23 00:00:00'
ORDER BY created_at
LIMIT 100;
EXPLAIN ANALYZE 会实际执行 SQL,SELECT 同样消耗查询资源。LIMIT 100 只限制返回行数,不保证只扫描 100 行。按同一算子比较 estRows 和 actRows,重点看扫描、过滤处的偏差,顶层 Limit 的行数不足以判断。
偏差集中在状态、时间范围或连接条件时,检查相关列是否已收集。新索引检查索引统计信息:
SHOW STATS_HISTOGRAMS
WHERE Db_name = 'his_lab'
AND Table_name = 'outpatient_charge'
AND Is_index = 1;
Is_index = 1 表示索引统计信息,Column_name 显示索引名。收集后用相同查询条件复查。估算接近实际但耗时仍高时,继续查索引访问开销、锁等待或集群负载,无需反复 ANALYZE。
手工 ANALYZE
收集收费表的统计信息:
ANALYZE TABLE his_lab.outpatient_charge;
使用独立的维护连接,确认业务数据已提交,并检查运行中的自动、手工任务。收费、住院费用、EMR 等大表逐张处理。
以下并发参数均支持 SESSION 和 GLOBAL 作用域。临时维护优先修改会话值。
| 变量 | 8.5 默认值 | 控制内容 |
|---|---|---|
tidb_build_stats_concurrency |
2 |
手工收集时构建统计信息的任务并发 |
tidb_build_sampling_stats_concurrency |
2 |
合并采样等内部处理并发 |
tidb_analyze_partition_concurrency |
2 |
保存 TopN、直方图等结果的并发 |
tidb_analyze_distsql_scan_concurrency |
4 |
ANALYZE 扫描 TiKV Region 的并发 |
需要降低本次资源开销时,在维护连接中调整并发。下面的 1、2 为取值示例,需按现场负载调整:
SET SESSION tidb_analyze_version = 2;
SET SESSION tidb_build_stats_concurrency = 1;
SET SESSION tidb_analyze_distsql_scan_concurrency = 2;
ANALYZE TABLE his_lab.outpatient_charge;
SHOW WARNINGS;
tidb_build_stats_concurrency = 1 不代表任务只有一个线程,Region 扫描、采样处理和结果写入有各自的并发。tidb_auto_analyze_concurrency 控制自动任务,手工并发连接需单独控制。
收集期间监控 TiDB 内存、TiKV CPU、磁盘读流量和业务查询延迟。采样也有资源开销,降低采样率不保证磁盘读取量同比下降。tidb_mem_quota_analyze 可以限制收集任务的内存占用,单位为字节,默认 -1 表示不限制。
采集范围
ANALYZE TABLE 未指定列选项时,采集范围受 tidb_analyze_column_options 和表上已保存配置影响。查询模式固定的表可用:
ANALYZE TABLE his_lab.outpatient_charge PREDICATE COLUMNS;
Version 2 下,这会收集已记录的谓词列,同时收集索引列和所有索引的统计信息。新 SQL 使用的列可能尚未进入已记录范围,需要全部列时使用 ALL COLUMNS。EMR 等含大字段的表,还需核对 tidb_analyze_skip_column_types 排除的类型。
ANALYZE TABLE his_lab.outpatient_charge ALL COLUMNS;
两种采集方式按需选择,无需连续执行。tidb_persist_analyze_options 默认开启。Version 2 下,显式指定的列范围、采样率等配置可以保存,后续自动收集或未指定配置的手工收集会沿用。临时调整采集范围时,需检查对后续任务的影响。
假设测试表已有索引 idx_status_created,指定索引的语法为:
ANALYZE TABLE his_lab.outpatient_charge INDEX idx_status_created;
在 tidb_analyze_version = 2 下,这条语句会收集索引列和所有索引的统计信息。
医院大表的维护安排
门诊收费表持续写入和更新状态,保留自动收集,巡检待收费、收费明细查询对应的统计信息。历史费用补录或状态集中修正后,加查相关 SQL;日常正常写入不固定追加全表 ANALYZE。
按月分区的住院费用明细,可针对变更较大的分区维护。假设 inpatient_fee_detail 存在月分区 p202609:
ANALYZE TABLE his_lab.inpatient_fee_detail PARTITION p202609;
SHOW WARNINGS;
SHOW STATS_META
WHERE Db_name = 'his_lab'
AND Table_name = 'inpatient_fee_detail';
SHOW STATS_HEALTHY
WHERE Db_name = 'his_lab'
AND Table_name = 'inpatient_fee_detail';
动态裁剪模式下,同时检查 Partition_name = 'global' 的全局统计信息。更新分区可能触发全局统计信息合并,带来额外开销。其他分区缺少统计信息时,需结合警告和实际配置检查,不能只确认本月分区完成。
EMR 宽表重点检查过滤、关联列。文书状态、就诊标识、科室、创建时间等列参与查询时,需要相应的统计信息。正文等大字段结合跳过类型和采集范围处理。病案分析、科研检索的查询种类较多,采集范围需按实际查询确定,不能直接照搬固定交易 SQL 的谓词列采集方式。
维护窗口避开收费高峰,并核对夜间结算、医保对账、备份和报表任务的时间。凌晨仍有急诊业务,大表收集按当时的业务延迟和资源余量安排。
收集后的复查
ANALYZE TABLE 返回后立即查看 SHOW WARNINGS,随后检查任务记录,也可通过另一连接查看:
SHOW ANALYZE STATUS
WHERE Table_schema = 'his_lab'
AND Table_name = 'outpatient_charge';
按开始时间、表名和分区名定位本次任务。finished 表示成功,failed 需查原因。SHOW ANALYZE STATUS 展示集群任务及有限的历史记录;更早的近期记录可查 mysql.analyze_jobs,保留最近 7 天历史。
复查 Last_analyze_time、健康度和原 SQL 的执行计划。持续写入时健康度可能低于 100,更新统计信息后也可能继续使用原索引。维护记录保留触发原因、采集范围、参数、任务状态及原 SQL 的前后对比。SQL 仍慢时,继续查具体算子的耗时。