0
0
1
0
博客/.../

什么是数据库统计信息和索引?TiDB 优化器如何选择最佳执行计划

 Billmay表妹  发表于  2026-06-02
原创

摘要

数据库优化器是 SQL 性能的核心引擎,而统计信息(Statistics)和索引(Index)是优化器做出正确决策的两个关键输入。统计信息告诉优化器"数据长什么样",索引告诉优化器"数据在哪里"。本文详解 TiDB 统计信息的收集与维护机制、索引类型与选择策略、优化器的执行计划选择逻辑,以及统计信息不准时的诊断与处理方法。本文适合需要优化 SQL 性能、理解 TiDB 执行计划的 DBA 和后端开发工程师。


一、统计信息对优化器的重要性

1.1 优化器为什么需要统计信息

关系数据库的 SQL 声明式查询需要优化器自动选择执行路径。这个过程类似导航系统规划路线——导航系统需要知道道路的车流量(统计信息)和道路连接关系(索引)才能规划最优路线。

优化器基于成本估算模型(Cost-Based Optimizer, CBO)计算每种执行路径的成本,选择成本最低的方案。统计信息直接影响成本估算的准确性:

统计信息类型 影响的优化器决策 示例
表行数(Count) 是否选择索引扫描 vs 全表扫描 行数少 → 全表扫描
列基数(NDV) 选择哪个索引更高效 高基数列更适合索引
直方图(Histogram) 精确估算 WHERE 条件的过滤比例 `age > 30` 匹配多少行
NULL 比例 是否跳过 NULL 值的索引扫描 NULL 多 → 可能不选索引
列相关性(Correlation) 是否使用索引有序扫描 数据有序 → 索引范围扫描更优
TopN 值 常量等值查询的行数估算 `status = 'active'` 匹配多少行

1.2 统计信息不准确会怎样

示例:orders 表有 1 亿行数据,但统计信息显示只有 1 万行

SELECT * FROM orders WHERE customer_id = 123 AND order_date = '2025-06-01';

优化器的错误决策:
  ✗ 选择全表扫描(认为数据量小,扫描成本低)
  ✓ 实际应选择索引扫描(数据量大,索引能快速定位)

结果:查询耗时从毫秒级变为秒级甚至分钟级

二、TiDB 统计信息收集机制

2.1 手动收集统计信息

-- 收集整个表的统计信息
ANALYZE TABLE orders;

-- 收集指定列的统计信息
ANALYZE TABLE orders COLUMNS customer_id, order_date, status;

-- 收集索引的统计信息
ANALYZE TABLE orders INDEX idx_customer_date;

-- 指定采样比例(默认自动确定)
ANALYZE TABLE orders WITH 200000 SAMPLES;

2.2 自动收集统计信息

TiDB 提供了自动统计信息收集功能,默认开启:

参数 默认值 说明
`tidb_auto_analyze_ratio` 0.5 表数据变化超过该比例触发自动收集
`tidb_auto_analyze_start_time` `00:00 +0000` 自动收集允许开始时间
`tidb_auto_analyze_end_time` `23:59 +0000` 自动收集允许结束时间
-- 查看自动收集配置
SHOW VARIABLES LIKE 'tidb_auto_analyze%';

-- 手动触发一次全量收集
ANALYZE ALL;

自动收集触发条件:

  • 表的修改行数超过 `tidb_auto_analyze_ratio * 总行数`
  • 在配置的时间窗口内执行
  • 不与手动 ANALYZE 冲突

2.3 统计信息内容详解

-- 查看表的统计信息
SHOW STATS_META WHERE db_name = 'mydb' AND table_name = 'orders';
-- 返回:行数、修改行数、版本号等

-- 查看列的统计信息
SHOW STATS_HISTOGRAMS WHERE table_name = 'orders';
-- 返回:列名、NDV、NULL 比例、直方图桶数等

-- 查看直方图详情
SHOW STATS_BUCKETS WHERE column_name = 'customer_id';
-- 返回:上界、下界、重复次数、NDV 等桶信息

-- 查看索引的统计信息
SHOW STATS_HISTOGRAMS WHERE table_name = 'orders' AND is_index = 1;
统计信息指标 含义 获取方式
Count 表的总行数 `SHOW STATS_META`
NDV(Number of Distinct Values) 列的不同值数量 `SHOW STATS_HISTOGRAMS`
Null Count NULL 值数量 `SHOW STATS_HISTOGRAMS`
Correlation 列值与物理位置的有序相关性 `SHOW STATS_HISTOGRAMS`
TopN 出现频率最高的值及其计数 `SHOW STATS_TOP_N`
Histogram 等高直方图桶 `SHOW STATS_BUCKETS`

三、索引类型与选择策略

3.1 TiDB 支持的索引类型

索引类型 定义 适用场景 示例
主键索引(Primary Key) `_tidb_rowid` 或聚簇索引 唯一标识行,默认创建 `id BIGINT PRIMARY KEY`
二级索引(Secondary Index) 非主键列的 B+ 树索引 等值查询、范围查询 `CREATE INDEX idx_status ON orders(status)`
复合索引(Composite Index) 多列组合索引 多条件 AND 查询 `CREATE INDEX idx_cus_date ON orders(customer_id, order_date)`
唯一索引(Unique Index) 保证列值唯一的索引 防重复、快速等值查找 `CREATE UNIQUE INDEX idx_email ON users(email)`
函数索引(Functional Index) 对表达式/函数结果建立索引 函数条件查询 `CREATE INDEX idx_lower_name ON users((LOWER(name)))`
前缀索引(Prefix Index) 对字符串前 N 个字符建索引 长 VARCHAR 列等值查询 `CREATE INDEX idx_name ON users(name(20))`
表达式索引(Expression Index) TiDB 6.6+ 支持的表达式索引 复杂条件查询 `CREATE INDEX idx_expr ON t((a + b))`

3.2 索引设计原则

原则 1:高选择性列优先

-- 查看列选择性(NDV / 总行数,越接近 1 越好)
SELECT COUNT(DISTINCT customer_id) / COUNT(*) AS selectivity
FROM orders;
-- selectivity > 0.1 才值得建索引

原则 2:复合索引遵循最左前缀原则

CREATE INDEX idx_abc ON t(a, b, c);

-- 以下查询可以使用索引:
WHERE a = 1;                    -- 使用 a
WHERE a = 1 AND b = 2;          -- 使用 a, b
WHERE a = 1 AND b = 2 AND c = 3; -- 使用 a, b, c

-- 以下查询不能使用该索引:
WHERE b = 2;                     -- 跳过 a
WHERE c = 3;                     -- 跳过 a, b
WHERE b = 2 AND c = 3;           -- 跳过 a

原则 3:覆盖索引减少回表

-- 查询只需索引列时,无需回表(Covering Index Scan)
SELECT customer_id, order_date FROM orders WHERE customer_id = 100;
-- 如果 idx_cus_date (customer_id, order_date) 存在,无需读取主表数据

-- 查看执行计划确认
EXPLAIN SELECT customer_id, order_date FROM orders WHERE customer_id = 100;
-- 注意看操作符是否为 IndexReader__(表示索引覆盖)

3.3 TiDB 索引的特殊考虑

特性 说明
聚簇索引 TiDB 支持聚簇主键(`CLUSTERED`),主键值直接存储在 KV 中
索引回表 非聚簇索引需要回表获取完整行数据,可能产生大量随机读
索引维护成本 每个索引增加写入开销(Raft 复制 + 索引更新)
索引数量建议 单表建议不超过 5-7 个二级索引
TiFlash 索引 TiFlash 不使用二级索引,依赖列式存储和 MPP 并行
-- TiDB 聚簇主键示例
CREATE TABLE users (
    id BIGINT PRIMARY KEY CLUSTERED,  -- 聚簇主键,数据按主键有序存储
    name VARCHAR(100),
    email VARCHAR(200) UNIQUE
);

四、优化器如何使用统计信息和索引

4.1 执行计划选择流程

SQL 解析 → 逻辑计划生成 → 规则优化 → 成本估算 → 物理计划选择 → 执行
                                  ↑
                          统计信息 + 索引信息

TiDB 优化器在物理计划选择阶段,基于统计信息估算每种访问路径的成本:

访问路径 成本计算要素 适用条件
全表扫描(TableFullScan) 总行数 × 单行读取成本 过滤比例高、无合适索引
索引范围扫描(IndexRangeScan) 匹配行数 × 索引查找成本 + 回表成本 等值或范围条件、选择性高
索引查找(IndexLookUp) 索引扫描成本 + N 次回表成本 索引不覆盖所有查询列
索引全扫描(IndexFullScan) 索引总条目 × 扫描成本 查询条件不在索引前列但需要排序
TiFlash 全扫描 总列存储大小 × 列扫描成本 分析型查询、MPP 加速

4.2 使用 EXPLAIN 分析执行计划

-- 基础执行计划
EXPLAIN SELECT * FROM orders WHERE customer_id = 100;

-- 查看实际执行成本
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 100;

-- 查看绑定(执行计划固化)
SHOW BINDINGS;

-- 使用 Hint 引导优化器选择
SELECT /*+ USE_INDEX(orders, idx_customer_id) */ *
FROM orders WHERE customer_id = 100;

SELECT /*+ IGNORE_INDEX(orders, idx_customer_id) */ *
FROM orders WHERE customer_id = 100;

EXPLAIN 关键字段解读:

字段 含义
id 算子编号
estRows 优化器估算的行数(基于统计信息)
actRows 实际执行行数(EXPLAIN ANALYZE)
task `root`(TiDB 节点执行)或 `cop`(TiKV/TiFlash 执行)
operator info 算子详细信息

4.3 执行计划绑定(SPM)

当优化器因统计信息问题选择不当的执行计划时,可以使用 SPM(SQL Plan Management)固化正确的计划:

-- 创建执行计划绑定
CREATE BINDING FOR
  SELECT * FROM orders WHERE customer_id = 100
USING
  SELECT /*+ USE_INDEX(orders, idx_customer_id) */ *
  FROM orders WHERE customer_id = 100;

-- 查看已有绑定
SHOW BINDINGS;

-- 删除绑定
DROP BINDING FOR
  SELECT * FROM orders WHERE customer_id = 100;

五、统计信息不准的诊断与处理

5.1 诊断统计信息准确性

-- 方法 1:对比估算行数与实际行数
EXPLAIN ANALYZE SELECT COUNT(*) FROM orders WHERE status = 'pending';
-- 对比 estRows 和 actRows,差异超过 10 倍说明统计信息不准

-- 方法 2:查看统计信息年龄
SELECT modify_count, count FROM mysql.stats_meta
WHERE table_name = 'orders';
-- 如果 modify_count 很大,说明数据变化后未重新收集

-- 方法 3:强制使用不同索引对比性能
SELECT /*+ USE_INDEX(orders, idx_a) */ ...;  -- 索引 A
SELECT /*+ USE_INDEX(orders, idx_b) */ ...;  -- 索引 B
-- 对比执行时间判断哪个更优

5.2 处理统计信息不准

方法 操作 适用场景
手动 ANALYZE `ANALYZE TABLE t;` 常规更新
增加采样量 `ANALYZE TABLE t WITH 500000 SAMPLES;` 大表统计不准
收集特定列 `ANALYZE TABLE t COLUMNS col1, col2;` 只需更新部分列
删除错误统计信息 `DROP STATS t; ANALYZE TABLE t;` 统计信息损坏
使用 SPM 绑定 `CREATE BINDING ...` 优化器持续选错计划
调整直方图桶数 修改 `tidb_analyze_column_options` 列值分布不均匀
-- 强制全量重新收集
ANALYZE TABLE orders WITH 0 SAMPLES;  -- 0 表示全量扫描

-- 锁定统计信息版本(避免自动更新覆盖)
LOCK STATS orders;

-- 解锁统计信息
UNLOCK STATS orders;

六、FAQ

Q1:TiDB 统计信息多久需要收集一次?

A:TiDB 默认开启自动收集,当表数据变化超过 50%(`tidb_auto_analyze_ratio`)时自动触发。对于写入频繁的表,建议根据业务模式调整收集频率。可以在低峰期手动执行 `ANALYZE` 确保统计信息准确,或通过 TiDB Cloud 控制台监控统计信息健康状态。

Q2:为什么 ANALYZE 后执行计划没有改善?

A:可能原因包括:(1)采样量不足,建议增加采样行数;(2)直方图桶数不够,无法准确表达数据分布;(3)查询条件中的值不在直方图覆盖范围内;(4)优化器的成本模型对特定算子的估算存在偏差。建议通过 `EXPLAIN ANALYZE` 对比 `estRows` 和 `actRows`,并考虑使用 Hint 或 SPM 绑定。

Q3:TiDB 的二级索引存储在 TiKV 还是 TiFlash?

A:TiDB 的二级索引数据存储在 TiKV 中(以特殊格式的 KV 存储),不存储在 TiFlash。TiFlash 只存储表的列式数据,不存储索引结构。因此 OLTP 查询的索引加速依赖 TiKV,OLAP 查询依赖 TiFlash 的列式扫描和 MPP 并行执行。

Q4:如何判断一个索引是否被使用?

A:通过 `EXPLAIN ANALYZE` 查看执行计划中是否出现了 `IndexReader`、`IndexLookUp` 等算子。也可以通过 `information_schema.statements_summary` 查看历史 SQL 的执行计划摘要,或使用 TiDB Dashboard 的 SQL 诊断面板查看索引使用情况。


七、总结

统计信息和索引是 TiDB 优化器选择最佳执行计划的两个核心输入。统计信息描述数据分布特征,索引定义数据访问路径。保持统计信息准确、设计合理的索引结构,是确保 SQL 查询性能的基础工作。

建议建立以下运维习惯:

  • 定期检查统计信息健康状态,关注 `estRows` 与 `actRows` 的偏差
  • 合理设计索引,遵循高选择性、最左前缀、覆盖索引原则
  • 善用 SPM 处理优化器持续选错计划的情况
  • 监控慢查询,及时发现因统计信息变化导致的性能退化

七、下一步行动

行动 链接
下载 TiDB SQL 优化与索引设计指南(PDF) https://pingkai.cn/docs
30 分钟试用 TiDB 体验索引和统计信息功能 https://tidb.com/try
获取 TiDB 执行计划分析工具(TiDB Dashboard) https://pingkai.cn/docs
查看统计信息管理最佳实践 https://pingkai.cn/docs
申请 TiDB 性能优化咨询服务 https://pingkai.cn/contact

七、相关资源

0
0
1
0

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论