暂无图片
暂无图片
2
暂无图片
暂无图片
暂无图片

KingbaseES V9R2C13性能优化能力实测

一、测试环境说明

数据库版本:KingbaseES V009R002C013
测试库:perfdb
核心业务表:orders
数据量:
orders 表约 3000 万行
user_id 基数约 100 万

二、构造测试模型(真实业务模拟)

为了贴近真实业务,我们构造三个典型表:
用户表:users
订单表:orders
订单明细:order_items

1. 创建测试数据库和用户

CREATE DATABASE perfdb;
CREATE USER perf WITH PASSWORD 'Perf@123';
GRANT ALL PRIVILEGES ON DATABASE perfdb TO perf;

连接数据库:

ksql -U perf -d perfdb

2. 建表结构

CREATE TABLE users (
    user_id     BIGINT PRIMARY KEY,
    user_name   VARCHAR(50),
    create_time TIMESTAMP
);

CREATE TABLE orders (
    order_id    BIGINT PRIMARY KEY,
    user_id     BIGINT,
    status      INT,
    amount      NUMERIC(10,2),
    create_time TIMESTAMP
);

CREATE TABLE order_items (
    item_id     BIGINT PRIMARY KEY,
    order_id    BIGINT,
    product_id  BIGINT,
    price       NUMERIC(10,2)
);

三、造测试数据(千万级)

本节目标是构造“数据量足够大、分布足够真实”的测试数据,让优化器、统计视图、执行器优化都能在压力下体现差异。

1. 用户表:100 万

INSERT INTO users
SELECT
    generate_series(1,1000000),
    'user_' || generate_series(1,1000000),
    NOW() - random() * interval '365 days';

2. 订单表:3000 万

INSERT INTO orders
SELECT
    generate_series(1,30000000),
    (random() * 1000000)::BIGINT,
    (random() * 5)::INT,
    round(random() * 1000, 2),
    NOW() - random() * interval '180 days';

3. 明细表:7000 万

INSERT INTO order_items
SELECT
    generate_series(1,70000000),
    (random() * 30000000)::BIGINT,
    (random() * 100000)::BIGINT,
    round(random() * 1000, 2);

四、建立索引(优化基础)

这一节不追求“面面俱到”,只建立最贴近业务访问路径的基础索引,为后续场景提供可对比的执行路径。

CREATE INDEX idx_orders_user ON orders(user_id);
CREATE INDEX idx_orders_time ON orders(create_time);
CREATE INDEX idx_items_order ON order_items(order_id);

五、测试场景一:SQL 参数值统计实测

这个场景要解决的核心问题是:
同一条 SQL,因为参数值不同,返回行数不同,执行路径(Index / Seq Scan)和耗时也可能不同。

1)开启 SQL 采集开关

在数据库配置文件 kingbase.conf 中开启 SQL 统计相关参数,例如:

track_sql = on
track_instance = on
track_counts = on
track_activities = on
track_wait_timing = on
track_io_timing = on

sys_stat_statements.max           = 10000
sys_stat_statements.track         = 'top'
sys_stat_statements.track_utility = off
sys_stat_statements.save          = on

通过 sys_ctl reload 让配置生效,数据库开始为每条 SQL 采集执行统计信息。

2)测试 SQL(逐参数执行)

本次测试使用固定参数:

SELECT * FROM orders WHERE user_id = 300172;

执行结果如下(部分节选):

order_id | user_id | status | amount |        create_time

–––––+———+––––+––––+––––––––––––––
14903693 |  300172 |      4 | 810.64 | 2025-11-26 14:42:05.651282
12990199 |  300172 |      5 | 648.42 | 2025-09-21 15:36:58.382901
14966513 |  300172 |      1 | 121.48 | 2025-12-02 15:00:14.610596
…
5432652 |  300172 |      3 | 303.72 | 2025-11-13 02:20:31.287637
(25 rows)

不同 user_id 返回行数不同,也意味着执行计划(Index / Seq Scan)和耗时也可能不同。

3)查看 SQL 参数执行统计(sys_stat_statements)

执行统计查询:

SELECT
query,
calls,
total_exec_time,
mean_exec_time,
rows
FROM sys_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

4)真实统计结果(Top SQL 实测数据)

(1)复杂聚合 SQL:耗时最高

SELECT user_id, sum(amount)
FROM orders
GROUP BY user_id
ORDER BY sum(amount) DESC
LIMIT $1
calls:1
total_exec_time:16961 ms
rows:20

(2)我们测试的重点 SQL:参数 user_id 的性能表现

SELECT * FROM orders where user_id=300172;

• calls:3
• total_exec_time:6320 ms
• mean_exec_time:2106 ms
• rows:75

说明:
对 user_id=300172 执行了 3 次,共返回 75 行,平均每次执行约 2 秒。

这是典型的 参数敏感 SQL(Parameter-Sensitive Plan) 行为:
不同参数 → 不同返回行数 → 不同执行耗时。

(3)其他 SQL 的统计信息

例如:

SELECT count(*) FROM orders

• total_exec_time:1674 ms

SELECT * FROM orders WHERE user_id=$1 LIMIT $2

• total_exec_time:736 ms
• rows:10

这些数据与实际 workload 完全一致,已经成功被 sys_stat_statements 捕获。

5)开启 SQL 参数采集后,可以看到什么?

你现在已经可以清晰观察到:

1、哪些参数值触发 Seq Scan

例如:
• user_id 分布极度不均,会导致某些参数触发大量 Block 读取。

你可以进一步通过:

EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE user_id = 300172;

来查看真实计划。

2、 哪些参数走 Index Scan

如果 user_id 有更好的选择度,执行计划会切换为 Index Scan,响应时间会大幅下降。

3、 哪些参数值导致极端慢(高耗时 outlier)

sys_stat_statements 中你看到:

SQL calls total_exec_time mean_exec_time rows
SELECT * FROM orders where user_id=$1 3 6320 ms 2106 ms 75

说明 特定参数造成明显慢查询,而不是 SQL 本身有问题。

六、测试场景二:数据库时间模型视图

1)执行压测

可以使用例如 JMeter、sysbench、自写循环 SQL 等方式进行压力模拟。

例如执行 30 万次 user_id 查询:

DO $$
DECLARE i int;
BEGIN
  FOR i IN 1..300000 LOOP
    PERFORM * FROM orders WHERE user_id = floor(random()*900000);
  END LOOP;
END$$;

2)查询数据库时间模型

执行压测后查询:

SELECT *
FROM sys_stat_dbtime
ORDER BY metric;

      metric       |  calls  | total_time |  avg_time  | dbtime_pct 
-------------------+---------+------------+------------+------------
 Analyze Event     |     168 |      87452 |     520.55 |       0.04
 CommitTransaction |     141 |       2579 |      18.29 |       0.00
 DB CPU            |         |  125614524 |            |      55.21
 DB Time           |         |  227531123 |            |     100.00
 Execute Event     |     139 |  227130255 | 1634030.61 |      99.82
 FG Wait           | 1807238 |  101916599 |      56.39 |      44.79
 InitializePlan    |     105 |      25006 |     238.15 |       0.01
 Net Read          |     156 |       1683 |      10.79 |       0.00
 Net Write         |  271375 |   26889619 |      99.09 |      11.82
 Parse Event       |     175 |      12161 |      69.49 |       0.01
 Plan Event        |     133 |      92939 |     698.79 |       0.04
 PLPGSQL Compile   |       2 |        833 |     416.50 |       0.00
 PLPGSQL Execute   |       2 |      43339 |   21669.50 |       0.02
 Rewrtie Event     |     167 |      16222 |      97.14 |       0.01
 Simple Message    |     152 |  227509433 | 1496772.59 |      99.99
 Wait Message      |     317 |      21690 |      68.42 |       0.01
(16 rows)

七、优化器能力实测

这一节只围绕一个点:
优化器是否能把“业务写法”自动转成“更合理的执行方式”,并且行为可解释。

1. NOT IN 优化:从子查询到 Hash Anti Join

在一些中大型业务系统里,类似下面的语句非常常见:

EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM users
WHERE user_id NOT IN (
SELECT user_id
FROM orders
WHERE status = 0
);

语义很直观:
• users:用户主表
• orders:订单表
• 希望找出没有 status=0 订单的用户

但在传统数据库实现里,这种写法有两个经典的坑:
1. 性能问题:
早期实现经常会走 Nested Loop + 子查询,外层 users 一行行去扫 orders,在数据量大的时候非常慢。
2. NULL 语义问题:
一旦子查询返回的 user_id 列里混入了 NULL,NOT IN 的三值逻辑会让结果变得“出人意料”。

1.1 测试环境准备(示意)

假设我们有:
• users 表:100 万用户
• orders 表:3000 万订单(上一节已经准备好的那张大表)

1.2 原始 SQL:NOT IN 子查询

我们先直接执行原始写法,并观察执行计划:

EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM users
WHERE user_id NOT IN (
    SELECT user_id
    FROM orders
    WHERE status = 0
);

在 KingbaseES V9 上,优化器如果判断数据量足够大、统计信息完善,会尝试把它改写成 Anti Join 形式。

理想情况下,你应该能在执行计划中看到类似这样的节点(计划文本略有差异没关系):

--------------------------------------------------------------------------------------------------------------------------------
 Hash Anti RSNA Join  (cost=1085654.98..1149259.12 rows=51114 width=27) (actual time=8271.625..11100.020 rows=49433 loops=1)
   Hash Cond: (users.user_id = orders.user_id)
   Buffers: shared hit=17956 read=241556, temp read=16413 written=16413
   I/O Timings: read=1360.948
   ->  Seq Scan on users  (cost=0.00..17408.00 rows=1000000 width=27) (actual time=0.015..197.510 rows=1000000 loops=1)
         Buffers: shared hit=2 read=7406
         I/O Timings: read=51.565
   ->  Hash  (cost=627101.25..627101.25 rows=2999002 width=8) (actual time=8265.331..8265.332 rows=3001680 loops=1)
         Buckets: 131072  Batches: 64  Memory Usage: 2858kB
         Buffers: shared hit=17951 read=234150, temp written=10069
         I/O Timings: read=1309.383
         ->  Seq Scan on orders  (cost=0.00..627101.25 rows=2999002 width=8) (actual time=0.015..6385.056 rows=3001680 loops=1)
               Filter: (status = 0)
               Rows Removed by Filter: 26998320
               Buffers: shared hit=17951 read=234150
               I/O Timings: read=1309.383
 Planning Time: 2.039 ms
 Execution Time: 11103.603 ms
(18 rows)

1. 计划解读:优化器已经在“帮你改写”

这份计划里,有几个关键点可以直接写进文章总结优化器能力:
1). NOT IN → Hash Anti Join
• 顶层节点是:Hash Anti RSNA Join
• 说明优化器并不是“死磕子查询”,而是把 NOT IN 自动等价改写成了 反连接(Anti Join):
• 外表:users
• 内表:orders(status = 0)
• 语义:找出在 orders(status=0) 中不存在的 users.user_id
也就是说,这条简单的业务 SQL,优化器已经自动做了:
• 子查询重写 → Anti Join
• 再选择具体算法:Hash Anti Join
2). 外层 users:顺序扫描成本可控

Seq Scan on users
actual time=0.015…197.510 rows=1000000

• 全表 100 万行,197ms 扫完
• 说明 users 体量相对可控,即便 Seq Scan 整体开销也不算太大
• 在这样的大表对大表场景里,让 users 当外表做 Anti Join 是合理的选择

3). 内层 orders:status=0 过滤 + Hash 构建

Seq Scan on orders
actual time=0.015…6385.056 rows=3001680
Filter: (status = 0)
Rows Removed by Filter: 26998320

• orders 共约 3000 万行,其中 status = 0 命中约 300 万行
• 这 300 万行被用于构建 Hash 表(Hash 节点),供上层 Anti Join 使用
• Hash 节点的 Buckets: 131072 Batches: 64 + temp read/write,说明:
• 数据量较大
• Hash 构建需要分批(多 Batch),并在磁盘上产生了临时文件(temp read=16413 written=16413)

4). I/O 主导的 11 秒执行时间
• 总执行时间:11.1 秒
• 其中:
• orders 顺序扫描 IO 时间:I/O Timings: read=1309.383 ms
• 总体 IO 时间(包括 Hash 等):read=1360.948 ms
• 结合 Buffers: shared hit / read 可以看出:
• 这是典型的 “大表全扫 + 大量磁盘读 + Hash 需要落盘” 场景
• 性能瓶颈主要在 存储 IO + Hash 过程的外部批处理,而不是优化器策略错误

  1. 从优化器视角看:这算“合格”还是“优秀”?

可以在文章里给出你作为 DBA 的判断:
• 优化策略层面:是“合格甚至优秀”的
• ✅ NOT IN 能够自动改写为 Hash Anti Join,而不是 naïve 的子查询嵌套循环
• ✅ 正确识别“大表 orders + 相对较小的 users”的场景,让 orders 当内表构建 Hash
• ✅ 结合过滤条件 status = 0 先做筛选再 Hash,避免把 3000 万行全部写入 Hash

1.3. OR 自动改写为 UNION ALL

在 orders(约 3000 万行)表上,测试如下 SQL:

SELECT *
FROM orders
WHERE status = 1 OR user_id = 100;

以及同字段 OR 的写法:

SELECT *
FROM orders
WHERE status = 1 OR status = 2;

即使在 status、user_id 上均已建立索引,执行计划依然选择:

Seq Scan on orders
Filter: ((status = 1) OR (user_id = 100))

或:

Seq Scan on orders
Filter: ((status = 1) OR (status = 2))

从执行结果可以看到:
• OR 条件命中行数占比高(约 40%)
• SELECT * 导致走索引时需要大量随机回表
• 拆分为 UNION ALL 后的索引路径综合成本高于顺序扫描

因此,优化器基于成本模型选择了 Seq Scan + Filter,而不是 OR → UNION ALL 改写。

这说明:OR 条件并不会“必然触发” UNION ALL 改写,是否改写取决于数据分布、选择性以及访问路径成本。

1.4. UNION 外层条件下推

测试 SQL:

SELECT * FROM (
SELECT * FROM orders WHERE status = 1
UNION ALL
SELECT * FROM orders WHERE status = 2
) a
WHERE create_time > NOW() - interval '7 days';

很多人在写 UNION ALL 时,会把时间条件放在最外层。逻辑正确,但性能是否好,取决于优化器能不能把外层谓词 提前下推 到各分支内部。

(1)实测执行计划:谓词已经被下推到每个分支

执行计划中最关键的证据是:外层的时间条件不再“最后过滤”,而是直接变成两个分支的联合过滤条件:
• 分支 1(status = 1):

Filter: ((status = 1) AND (create_time > (now() - ‘7 days’::interval)))
Rows Removed by Filter: 9967536

• 分支 2(status = 2):

Filter: ((status = 2) AND (create_time > (now() - ‘7 days’::interval)))
Rows Removed by Filter: 14951402

这说明优化器已经把原本外层的:

WHERE create_time > now() - interval ‘7 days’

自动下推到了 UNION ALL 的每个子查询。

这就是典型的 Predicate Pushdown(谓词下推):
把“最后过滤”提前到“每个分支先过滤”。

(2)并行执行 + Append 合并:典型的大表优化路径

计划顶层结构是:

Gather
-> Parallel Append
-> Parallel Seq Scan on orders (status=1 AND create_time>7days)
-> Parallel Seq Scan on orders (status=2 AND create_time>7days)

说明三件事:
1. UNION ALL 被实现为 Append(合并结果集,不做去重)
2. 两个分支各自并行扫描(Workers Planned/Launched = 2)
3. 合并阶段由 Gather 汇总输出

(3)收益点:提前过滤让“参与合并的数据”显著变少

你这次实测的最终结果行数:
• 总输出:约 194,589 行
• 但是在两个分支中,被提前过滤掉的行数分别是:
• status=1:过滤掉 9,967,536 行
• status=2:过滤掉 14,951,402 行

也就是说:如果不下推,UNION ALL 的合并阶段会吞下接近 2500 万行,再在外层过滤;而现在优化器把过滤提前做掉,让合并阶段只处理“7 天内的数据”。

执行时间上,你这次的整体耗时约:
• 6.1 秒(第一条 SQL)
• I/O read 时间约 2.8 秒(read=2775ms)

DBA 视角:
这类优化的价值不在于“语法更优雅”,而在于减少参与 UNION/Append 的数据规模,从而让大表查询更稳定。

(4)NOT MATERIALIZED 对比:这次差异不大,但结论一致

你额外跑了:

WITH a AS NOT MATERIALIZED (…)
SELECT * FROM a WHERE create_time > …

结果同样显示谓词在分支内生效,且总体耗时在 6.3 秒 左右,和第一条差异不大。

这也说明:
• 本次是否物化并不是瓶颈核心
• 关键点仍然是:谓词已经下推成功

八、自治事务性能实测

在合规审计、运维留痕、金融风控等系统里,有一个很现实的要求:

业务事务可以失败回滚,但审计日志必须可靠落库。

如果审计和业务绑定在同一个事务里,一旦回滚,日志也会一起消失,导致“留痕链条断裂”。
KingbaseES 的 自治事务(Autonomous Transaction),就是为了解决这个问题。

1)准备:创建审计日志表

CREATE TABLE audit_log (
id          BIGINT PRIMARY KEY,
msg         TEXT,
create_time TIMESTAMP
);

2)创建自治事务过程:write_audit_log

关键点:使用 LANGUAGE plsql 并通过 $$…$$ 一次性提交完整块,避免客户端拆句解析。

CREATE OR REPLACE PROCEDURE write_audit_log(p_id BIGINT, p_msg TEXT)
AS $$
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
INSERT INTO audit_log(id, msg, create_time)
VALUES (p_id, p_msg, now());
COMMIT;
END;
$$ LANGUAGE plsql;

执行结果:

CREATE PROCEDURE

3)模拟业务失败:主事务回滚

BEGIN;
CALL write_audit_log(1, 'biz do something, then rollback');
ROLLBACK;

输出:

CALL
ROLLBACK

4)验证:业务回滚不影响审计落库

SELECT * FROM audit_log WHERE id = 1;

输出(核心证据):

id |               msg               |        create_time
----+---------------------------------+----------------------------
1 | biz do something, then rollback | 2025-12-13 06:24:59.996775
(1 row)


结论非常清晰:
• 主事务已回滚(ROLLBACK 生效)
• 审计日志仍然成功提交(自治事务独立提交)

十、DBA 总结

这次测试覆盖了四类 DBA 最关心的能力点:可观测、可拆解、可解释、可兜底。

1)SQL 性能不再靠猜

通过 SQL 统计与参数采集,sys_stat_statements 能把同一条 SQL 在不同参数下的真实执行行为记录下来。对 DBA 来说,最直接的价值是:
• 性能抖动到底是 SQL 问题,还是参数分布导致的,不再靠经验判断。

2)数据库时间模型把“慢”拆成结构

压测后查询 sys_stat_dbtime,能把 DB Time 拆成 CPU、等待、网络、解析、执行等维度。这样 DBA 在进入细节优化之前,就能先回答一个更关键的问题:
• 慢主要耗在哪一类时间上?

3)优化器行为更像工程决策,而不是机械改写

•	NOT IN 能改写为 Hash Anti Join,说明优化器愿意为业务写法做等价重写;
•	OR 条件没有强制改写为 UNION ALL,说明优化器不是“看到 OR 就改”,而是基于成本模型做选择;
•	UNION 外层条件下推,说明优化器能把“最后过滤”提前到“分支先过滤”,减少参与合并的数据量,让大表查询更稳定。

4)自治事务证明:失败路径也能可靠留痕

主事务回滚,但审计日志仍然提交成功,这是典型的“失败路径兜底能力”。对审计、合规、运维留痕类系统来说,这类能力的意义往往比单条 SQL 的快慢更大。

性能优化不是把参数调到极限,而是让问题可被观测、路径可被解释、结果可被复现,异常路径也能被兜住。

作者:Digital Observer(施嘉伟)
Oracle ACE Pro
PostgreSQL ACE Partner
Oracle OCM、KCM、PGCM、YCP、DB2 、MySQL OCP、PCTP、PCSD、OCI、PolarDB技术专家、达梦师资认证,从业11年+
ITPUB认证专家、崖山YVP、PolarDB开源社区技术顾问、HaloDB技术顾问、TiDB社区技术布道师、青学会MOP技术社区专家顾问、国内某高校企业实践指导教师
公众号/墨天轮/金仓社区/IF Club:Digital Observer;CSDN/PGfans:施嘉伟;ITPUB:sjw1933

hhh7.jpg

最后修改时间:2025-12-15 09:47:06
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论