
目录
- 前言
- [KingbaseES 查询优化器整体架构](#KingbaseES 查询优化器整体架构)
- 统计信息:CBO优化器的数据基石
- 代价模型核心原理:成本如何计算
- 执行计划常见问题定位与调优实战
- CBO优化器高频认知误区汇总
- 总结
1. 前言
在企业级业务系统长期迭代过程中,SQL性能波动一直是开发、DBA日常工作中频繁遇到的难题。不少工程师碰到过这类诡异现象:数据表结构、索引定义完全没有改动,仅仅更换查询条件,一条SQL的响应时间就出现数十倍差距;部分索引明明已经创建完成,优化器却始终不会选用,只有人工改写SQL之后查询效率才能恢复正常。大量线上故障复盘之后可以发现,绝大多数这类性能异常,根源都和数据库查询优化器的执行策略直接相关。
KingbaseES内置两种查询优化实现:基于规则的优化器(RBO)与基于代价的优化器(CBO)。RBO依靠系统预设固定规则生成执行路径,适配场景十分有限,一旦面对多表复杂关联、海量数据查询、数据倾斜业务场景,极易产出低效执行计划。目前几乎所有生产环境,都会默认启用CBO优化模式。
CBO的核心逻辑通俗易懂:数据库预先采集数据表、索引、字段的数据分布特征,通过内置代价计算公式,枚举多条可行的查询执行路径并完成成本估算,最终挑选代价最低的方案作为最终执行计划。完整链路包含统计信息采集、候选路径枚举、代价估算、最优执行计划生成四大核心环节。
很多一线运维人员做SQL调优时,工作往往局限于简单查看执行计划、新增索引,并不理解CBO代价估算底层逻辑。当遭遇执行计划漂移、索引无法命中、大查询性能突降等问题时,只能盲目修改SQL语句,无法从根源定位问题。本文结合大量可以直接上机复现的SQL示例,逐层拆解KingbaseES CBO运行机制,讲解统计信息日常维护手段、代价模型核心逻辑,同时落地生产环境可直接复用的执行计划调优方案。
2. KingbaseES 查询优化器整体架构
一条客户端下发的SQL语句,从发送至数据库服务端,直到最终返回查询结果,会依次经历语法解析、语义分析、查询重写、优化器处理、执行器执行几个核心阶段。CBO优化器的工作,就处于整条处理链路的中间位置。
- 语法解析:解析原始SQL文本,校验语法合法性,构建初始抽象语法树;
- 语义分析:校验表、字段、访问权限、数据类型合法性,消除语法树中的语义错误;
- 查询重写:按照内置规则改写查询树,自动展开视图、化简常量表达式、合并冗余过滤条件;
- 查询优化(CBO核心阶段):生成多条可行执行路径,计算每条路径执行代价,筛选最优执行计划;
- 执行器:按照选定的执行计划,调用底层存储引擎完成数据读取、关联、聚合等运算。
CBO优化阶段内部又划分三个紧密协作模块:统计信息管理模块、代价估算模块、路径生成与计划选择模块。三者存在强依赖关系:统计信息作为代价计算的基础输入数据,代价模型评估所有候选执行路径优劣,最终输出最优执行计划交付执行器。
这里需要注意一个关键点:优化器不会无限制穷举全部执行方案。多表关联场景下,表关联顺序、关联算法组合数量呈指数级上涨。为了控制SQL优化阶段CPU消耗,KingbaseES内置搜索边界阈值。当参与关联的表数量超过阈值,优化器会切换为启发式搜索策略,适度牺牲找到全局最优解的可能性,保障优化过程不会占用过长时间。
基础测试环境准备
后续全文所有验证案例统一使用业务测试表,先执行下述建表语句,搭建基础测试数据集。
sql
-- 创建客户信息主表
CREATE TABLE t_customer (
c_id BIGINT PRIMARY KEY,
c_name VARCHAR(128) NOT NULL,
c_phone VARCHAR(32),
c_level INT,
create_time TIMESTAMP,
update_time TIMESTAMP
);
-- 创建订单业务表,和客户表通过c_id关联
CREATE TABLE t_order (
order_id BIGINT PRIMARY KEY,
c_id BIGINT NOT NULL,
order_amount NUMERIC(16,2),
order_status INT,
pay_time TIMESTAMP,
CONSTRAINT fk_order_customer FOREIGN KEY(c_id) REFERENCES t_customer(c_id)
);
-- 创建订单明细表
CREATE TABLE t_order_item (
item_id BIGINT PRIMARY KEY,
order_id BIGINT NOT NULL,
goods_id BIGINT,
item_num INT,
item_price NUMERIC(16,2),
CONSTRAINT fk_item_order FOREIGN KEY(order_id) REFERENCES t_order(order_id)
);
-- 创建单列普通索引
CREATE INDEX idx_t_order_cid ON t_order(c_id);
CREATE INDEX idx_t_order_status ON t_order(order_status);
CREATE INDEX idx_t_item_oid ON t_order_item(order_id);
建表完成后,执行插入语句写入基础测试数据。大规模测试场景下,可以编写存储过程批量生成海量样本数据,简单功能验证直接使用如下插入语句:
sql
-- 插入客户基础数据
INSERT INTO t_customer(c_id,c_name,c_phone,c_level,create_time,update_time)
VALUES
(1,'张三','13800001111',1,'2025-01-01 10:20:00','2025-01-01 10:20:00'),
(2,'李四','13800002222',2,'2025-01-02 09:10:00','2025-01-02 09:10:00'),
(3,'王五','13800003333',1,'2025-01-03 14:35:00','2025-01-03 14:35:00');
-- 订单数据写入
INSERT INTO t_order(order_id,c_id,order_amount,order_status,pay_time)
VALUES
(10001,1,299.00,1,'2025-01-01 10:30:00'),
(10002,1,599.50,2,'2025-01-05 16:12:00'),
(10003,2,129.00,1,'2025-01-06 11:22:00');
-- 订单明细数据
INSERT INTO t_order_item(item_id,order_id,goods_id,item_num,item_price)
VALUES
(20001,10001,5001,1,299.00),
(20002,10002,5002,2,299.75),
(20003,10003,5003,1,129.00);
后续所有执行计划验证,全部基于上面三张数据表开展。日常查看执行计划依靠EXPLAIN系列指令,两条最常用语法:
sql
-- 仅输出预估执行计划,不会真实执行SQL
EXPLAIN SELECT * FROM t_order WHERE c_id = 1;
-- 真实执行SQL,输出预估指标与实际运行指标对比,调优首选
EXPLAIN ANALYZE SELECT * FROM t_order WHERE c_id = 1;
3. 统计信息:CBO优化器的数据基石
CBO所有代价估算结果是否具备参考价值,完全取决于统计信息的准确程度。优化器不会实时扫描全表采集数据特征,而是直接读取预先采集、持久化存储的统计元数据。一旦统计信息和数据表真实数据分布出现明显脱节,代价评估结果会彻底失真,直接导致生成低效执行计划。
3.1 KingbaseES统计信息组成
统计信息整体分为表级统计信息、字段级统计信息、索引统计信息三大类。
-
表级统计信息
数据表预估总行数、占用磁盘页数、空页数量、平均行长度、数据表最近一次统计采集时间。优化器依靠预估总行数,结合过滤条件估算筛选之后剩余元组数量,也就是选择性。
-
字段级统计信息
字段唯一值估算数量、空值占比、字段值域上下边界、数据分布直方图。直方图是字段统计信息中最重要的数据。举一个典型业务场景:订单状态order_status,数值1代表已支付,占据全部数据85%;数值2代表已取消,仅占15%。如果缺少直方图,优化器会默认数据均匀分布,预估结果和真实情况偏差巨大。
-
索引统计信息
索引占用页面数量、索引区分度、索引元组平均长度,用来辅助判断索引扫描、全表顺序扫描二者成本高低。
所有统计信息保存在系统内置数据字典视图,日常排查可以直接执行查询语句查看:
sql
-- 查询数据表基础统计信息
SELECT relname,reltuples,relpages,last_vacuum_analyze
FROM sys_class
WHERE relname IN ('t_order','t_customer','t_order_item');
-- 查询字段统计信息
SELECT attname,n_distinct,null_frac
FROM sys_stats
WHERE relname='t_order';
简单说明核心字段含义:
- reltuples:数据表预估行数,并非实时精确行数;
- relpages:数据表占用物理磁盘页;
- n_distinct:字段唯一值估算数量;
- null_frac:字段空值所占比例。
3.2 统计信息自动采集机制
KingbaseES自带自动分析后台进程autovacuum analyze,持续监控数据表的数据变更规模。当数据表新增、修改、删除记录达到设定阈值,后台自动触发analyze操作刷新统计信息。
自动分析触发阈值计算公式:
autovacuum_analyze_threshold + autovacuum_analyze_scale_factor * 表预估行数
参数默认配置:
- autovacuum_analyze_threshold:基础变更行数阈值,全局默认50;
- autovacuum_analyze_scale_factor:变更比例系数,默认0.1。
举个例子:一张预估10000行的数据表,触发自动analyze条件 = 50 + 10000 * 0.1 = 1050行。累计DML变更达到1050条,后台自动执行统计采集。
这套自动化机制存在明显短板,也是线上大量性能问题的源头:
- 超大表场景,0.1的比例系数意味着需要海量数据变更才会触发分析;如果业务只是少量高频更新,长期达不到触发阈值,统计信息持续陈旧;
- 批量导入数据完成瞬间,不会立刻刷新统计信息;大量新数据写入后,优化器依旧沿用旧统计数据;
- 分区表自动分析逻辑和普通表存在差异,各个子分区需要单独触发统计采集。
3.3 手动执行Analyze维护统计信息
生产环境运维规范中,不能单纯依赖自动analyze,必须配置定期手动分析任务。基础执行语法:
sql
-- 采集整张表所有字段统计信息
ANALYZE t_order;
-- 仅采集指定字段统计信息,减少资源开销
ANALYZE t_order(order_status,c_id);
-- VERBOSE 打印执行详细日志,方便观察进度
ANALYZE VERBOSE t_customer;
-- 单独分析分区表某个子分区
ANALYZE t_order PARTITION p202607;
很多开发人员很容易混淆VACUUM和ANALYZE功能,这里明确区分:
- VACUUM:清理表内死亡元组,回收磁盘存储空间;不会更新统计信息;
- ANALYZE:采集数据统计元数据,不负责清理存储碎片;
组合命令 VACUUM ANALYZE 可以同时完成碎片清理+统计信息刷新:
sql
VACUUM ANALYZE t_order_item;
3.4 统计信息失效复现案例
我们模拟一个生产高频场景:t_order表初始一万条数据,order_status=1占绝大多数。执行查询语句:
sql
EXPLAIN SELECT * FROM t_order WHERE order_status=2;
统计信息准确时,优化器判断筛选后数据量很小,选择索引扫描。
接下来模拟业务大批量清理旧数据,批量写入大量order_status=2的新订单:
sql
DELETE FROM t_order WHERE order_status=1;
-- 批量插入上万条 order_status=2 业务数据
INSERT INTO t_order(order_id,c_id,order_amount,order_status,pay_time)
VALUES (10100,3,199.00,2,'2026-07-01 09:20:00');
大批量操作结束后,尚未达到自动分析阈值,统计信息依旧保留旧的数据分布记录。再次执行相同查询:
sql
EXPLAIN SELECT * FROM t_order WHERE order_status=2;
优化器错误预估符合条件数据很少,选择索引扫描。但真实数据表中绝大多数记录都是order_status=2,索引扫描需要大量随机IO,成本远高于全表扫描,直接造成查询性能退化。
手动执行ANALYZE刷新统计信息后,执行计划就会自动恢复正常。这也是线上执行计划漂移最典型诱因。大批量DML操作完成后,运维脚本建议主动触发analyze。
3.5 直方图采样参数调优
采集字段统计信息时,数据库不会读取全量数据,采用随机采样方式完成。采样样本规模由参数 default_statistics_target 控制,系统默认值100。数值越大,采样样本更多,直方图精度更高,但analyze执行消耗的IO、CPU资源同步上升。
查看与临时调整参数语句:
sql
-- 查看当前参数值
SHOW default_statistics_target;
-- 会话级别临时调高采样数量,针对数据倾斜字段
SET default_statistics_target=500;
ANALYZE t_order(order_status);
SET default_statistics_target=100;
针对数据高度倾斜的热点字段,例如订单状态、业务类型、区域编码,建议单独调高采样目标,采集精度更高的直方图。
4. 代价模型核心原理:成本如何计算
CBO中提到的执行代价,不等于SQL真实运行耗时,是一套无量纲虚拟成本数值。优化器依靠统一代价单位横向对比多条执行路径,代价数值只用于方案对比,不能直接等同于毫秒延迟。
4.1 基础代价常量定义
KingbaseES预设一组基准代价常量,作为整套代价计算公式的基础:
- seq_page_cost:顺序读取磁盘页面成本,默认1.0;
- random_page_cost:随机读取磁盘页面成本,默认4.0;
- cpu_tuple_cost:处理单条元组CPU开销,默认0.01;
- cpu_index_tuple_cost:索引扫描单条索引项CPU开销;
- cpu_operator_cost:单次条件比较运算CPU开销。
重要调优知识点:机械硬盘环境random_page_cost维持默认4即可;SSD固态存储随机IO性能更强,可以下调至1.5~2.0;纯内存访问场景可以调整为1.0。
查询当前全局代价参数配置:
sql
SHOW seq_page_cost;
SHOW random_page_cost;
SHOW cpu_tuple_cost;
4.2 单表扫描代价计算逻辑
单表查询存在两种基础访问路径:顺序扫描(全表扫描)、索引扫描。优化器分别计算两种方案总成本,择优选择。
顺序扫描代价公式
总代价 = IO代价(读取全部数据页) + CPU代价(遍历所有元组+条件过滤运算)
sql
EXPLAIN SELECT * FROM t_order;
执行计划输出中可以看到启动代价、总代价两个指标。顺序扫描启动代价通常为0,代表不需要预先加载大量资源就可以返回第一条数据。
索引扫描代价公式
索引扫描完整流程:随机IO读取索引页面→定位数据物理地址→回表读取数据页面。整个流程伴随大量随机IO,因此random_page_cost参数对索引扫描成本影响极大。
当过滤条件返回数据集很大时,大量回表随机IO会让索引扫描总成本高于顺序扫描,优化器主动放弃索引,也就是大家常说的索引失效。这属于优化器理性选择,并不是数据库故障。
案例直观验证:
sql
-- 筛选少量数据,优化器选用索引扫描
EXPLAIN SELECT * FROM t_order WHERE c_id = 1;
-- 筛选大批量数据,优化器选择全表顺序扫描
EXPLAIN SELECT * FROM t_order WHERE c_id <= 10000;
4.3 多表关联三种算法代价对比
多表关联查询场景,KingbaseES支持三类关联算法:嵌套循环连接(Nested Loop)、哈希连接(Hash Join)、合并连接(Merge Join),三者拥有独立的代价计算模型。
测试关联SQL:
sql
EXPLAIN ANALYZE
SELECT t1.c_name,t2.order_amount
FROM t_customer t1
JOIN t_order t2 ON t1.c_id = t2.c_id
WHERE t1.c_level = 1;
-
嵌套循环 Nested Loop
逻辑:驱动表筛选完成后,每一条结果循环遍历被驱动表匹配记录。
适用场景:驱动表结果集很小;被驱动表关联字段存在索引。
优势:启动代价低,可以快速返回第一条数据;
短板:驱动表数据量大时,循环次数激增,性能快速恶化。
-
哈希连接 Hash Join
逻辑:选择两张表中小表构建内存哈希表,遍历大表进行哈希匹配。
适用场景:没有合适索引、中间结果集数据量偏大;
限制:仅支持等值连接;哈希表超出work_mem内存限制时,数据溢写到磁盘生成临时文件,代价大幅上涨。
-
合并连接 Merge Join
逻辑:两张表数据按照关联键排序,有序双向遍历完成匹配。
适用场景:两张表结果集规模都很大;关联字段天然有序;
额外开销:原始数据无序的情况下,需要额外增加Sort排序代价。
CBO会分别估算三种连接方式执行成本,自动挑选最优方案。我们可以临时关闭某种连接算法用于测试,观察执行计划变化。
注意:仅允许测试环境使用,禁止线上全局长期修改!
sql
-- 临时关闭哈希连接,观察执行计划切换
SET enable_hashjoin = off;
EXPLAIN SELECT * FROM t_customer t1 JOIN t_order t2 ON t1.c_id = t2.c_id;
SET enable_hashjoin = on;
4.4 选择性(Selectivity)对代价的决定性作用
选择性代表满足WHERE过滤条件的元组占数据表总行数比例,是预估结果行数的核心输入。
预估行数 = 表预估总行数 × 条件选择性
选择性计算高度依赖统计信息直方图,统计信息过时,选择性估算出现偏差,后续扫描、关联全部代价计算都会失真。
日常开发容易引发选择性估算异常的场景:
- 隐式类型转换:
WHERE char_column = 123,字段为字符串类型,常量传入数字,直方图无法正常使用; - 多条件AND组合查询,优化器默认各个条件相互独立,无法识别字段相关性;
- 前缀模糊查询
%关键词,无法利用索引,选择性只能保守估算。
5. 执行计划常见问题定位与调优实战
掌握统计信息与代价模型底层逻辑之后,我们落地日常SQL标准化调优方案,梳理高频问题处理思路。
5.1 读懂EXPLAIN执行计划核心指标
统一阅读规则:执行计划由内向外、自下而上执行 ,缩进层级越深,越优先执行。
常用标识:
Seq Scan:顺序扫描(全表扫描)Index Scan / Index Only Scan:索引扫描、仅索引扫描- Nested Loop、Hash Join、MergeJoin:关联算法
- rows:预估返回行数;actual rows:真实返回行数
预估行数和真实行数差距巨大是高危信号 ,优先怀疑统计信息异常,第一时间执行ANALYZE刷新统计。
复现案例:统计信息陈旧导致预估严重偏差
sql
EXPLAIN ANALYZE SELECT * FROM t_order WHERE order_status=2;
观察输出,如果预估rows=10,实际返回rows=8000,偏差明显。修复方式:
sql
ANALYZE t_order(order_status);
刷新统计信息之后再次查看执行计划,预估行数会回归合理区间。
5.2 统计信息常态化运维方案
- 大批量数据导入、删除业务结束后,通过业务脚本主动执行ANALYZE;
- 每日业务低峰时段,针对核心大表定时执行ANALYZE VERBOSE;
- 监控平台增加指标监控:数据表last_vacuum_analyze时间,长期未更新统计信息的表配置告警;
- 存在数据倾斜的热点字段,单独提升default_statistics_target采样精度采集统计信息。
5.3 索引优化适配CBO代价选择逻辑
很多开发人员碰到慢查询第一反应新建索引,但忽略代价模型约束,新增索引依旧不会被优化器选用。索引设计遵循下面原则:
- 等值过滤字段放在联合索引前置位置;
- 尽可能构建Index Only Scan覆盖索引,消除回表随机IO,降低索引扫描整体代价;
覆盖索引实操示例:
sql
-- 普通索引,查询需要回表读取数据
CREATE INDEX idx_order_cid ON t_order(c_id);
EXPLAIN SELECT order_amount FROM t_order WHERE c_id=1;
-- 创建覆盖索引,INCLUDE附加查询字段,避免回表
DROP INDEX IF EXISTS idx_order_cid;
CREATE INDEX idx_order_cid_cover ON t_order(c_id) INCLUDE(order_amount,order_status);
EXPLAIN SELECT order_amount FROM t_order WHERE c_id=1;
通过INCLUDE子句附加非索引字段构建覆盖索引,减少随机IO开销,更容易被CBO选中。
5.4 多表关联SQL调优重点
- 在子查询提前过滤数据,缩小驱动表结果集,更加适配Nested Loop;
- 尽量避免单条SQL关联超过5张数据表,优化器搜索空间受限,容易选出次优执行计划;
- 关联字段前后数据类型保持完全一致,杜绝隐式转换干扰统计估算;
反面案例(隐式类型转换):
sql
-- c_id字段类型BIGINT,传入字符串常量,触发隐式转换
SELECT * FROM t_order WHERE c_id = '1';
字段与常量类型不一致,优化器无法使用直方图精准估算选择性,极易生成劣质执行计划。
5.5 执行计划固化适用场景(生产谨慎使用)
极少数极端业务场景,数据分布周期性变化,CBO持续在优劣差异巨大的两套执行计划来回切换,引发业务周期性性能波动。
数据库支持执行计划绑定,固定最优执行路径。但是优先采用治本方案:先确认统计信息稳定性、优化SQL写法、完善索引。计划固化只能作为兜底手段,一旦后续业务数据分布发生重大变化,固化后的执行计划会持续劣化,存在潜在风险。
6. CBO优化器高频认知误区汇总
结合大量线上故障复盘,整理开发、运维人员普遍存在认知误区:
-
误区1:只要创建索引,查询就必须走索引
纠正:CBO依靠代价模型自动判断,大批量数据查询场景,全表扫描代价更低,不走索引属于合理行为,不建议强制Hint干预。
-
误区2:VACUUM命令可以刷新统计信息
纠正:VACUUM仅负责清理存储碎片,想要更新统计元数据,必须搭配ANALYZE。
-
误区3:开启自动analyze,就不需要手动维护统计信息
纠正:自动分析存在触发阈值,超大表、数据倾斜业务场景很容易长期无法触发,手动运维必不可少。
-
误区4:执行计划预估行数轻微偏差,不会影响查询性能
纠正:单表查询偏差影响有限;多表关联场景中,上游行数估算错误会层层放大,最终直接选错关联算法。
-
*误区5:临时调整enable_join参数可以彻底修复慢SQL
纠正:修改参数只是强制限制可选路径,没有解决统计信息、索引、SQL结构根本问题,流量变化后故障会再次复现。
7. 总结
KingbaseES CBO优化器完整工作链路可以简单概括:采集真实数据分布特征(统计信息)→依靠统一代价公式估算多条可行路径成本→选出最低代价执行方案。整套机制稳定运行的前提,就是统计信息保持准确。线上绝大多数异常执行计划问题,溯源之后基本都指向统计信息失效、数据倾斜、隐式类型转换三类问题。
日常SQL性能调优建议建立标准化流程:
第一步:使用EXPLAIN ANALYZE对比预估行数与真实行数,判断统计信息是否正常;
第二步:检查过滤条件、关联字段是否存在隐式类型转换;
第三步:基于业务查询模式合理设计索引,优先构建覆盖索引;
第四步:调整SQL结构,提前过滤中间数据集,简化多表关联;
第五步:全部常规优化手段无效时,再考虑代价参数微调、执行计划绑定这类高级方案。
数据库优化器属于动态决策系统,不存在一次性优化永久生效的方案。业务数据分布会持续发生变化,运维人员需要持续监控统计信息状态、持续观测执行计划变化,搭建常态化巡检机制,提前规避潜在SQL性能风险。