OceanBase与金仓复杂SQL执行计划对比及调优实战
1. 从一条慢查询说起:为什么要做这场对比
去年年底接手一个迁移评估项目,源库是集中式部署的金仓(KingbaseES)V8,目标库是OceanBase 4.x的三副本集群。业务方给了一条跑了快两分钟的复杂SQL,涉及七张表关联、两个窗口函数、一个递归CTE,还有几处IN子查询嵌套。这条SQL在金仓上单机跑,执行计划稳定,但并发一上来CPU就顶到90%以上;换到OceanBase上,第一次跑反而更慢,调整分区键和并行度之后才压到秒级。
这件事让我意识到一个问题:同一组复杂SQL,在分布式和集中式两种架构下的执行逻辑根本不是一回事。很多人做选型或者迁移的时候,习惯性地把SQL当成"通用资产",觉得换个库改改语法就行,结果上线才发现性能天差地别。这篇文章就把我踩过的坑、测过的数据、调过的参数完整梳理一遍,围绕OceanBase和金仓这两个在国内政企、金融场景里出现频率很高的数据库,聊聊复杂SQL在两种架构下到底怎么跑、为什么这么跑、怎么调才能跑好。
适合谁看:正在做数据库选型的技术负责人、从集中式往分布式迁移的DBA、需要写跨库兼容SQL的后端开发,以及单纯想搞明白"分布式数据库到底难在哪"的工程师。文章里会有大量实测参数、执行计划对比和调优手法,你可以直接拿去套自己的场景。
2. 架构差异决定了SQL的"跑法"完全不同
2.1 集中式与分布式的本质分野
先把概念说清楚,不然后面的对比没法展开。
金仓V8是典型的集中式关系型数据库,它的架构逻辑和Oracle、PostgreSQL一脉相承:一个主节点管所有数据,SQL解析、优化、执行都在这个节点上完成,存储可以是本地盘也可以是共享存储。它的优势在于执行路径短、事务模型简单、优化器对复杂查询的代价估算成熟。一条七表关联的SQL,优化器能基于完整的统计信息选出它认为最优的Join顺序,因为所有数据都在一个节点上,它"看得见全局"。
OceanBase是原生分布式数据库,数据按照分区键打散到多个OBServer节点上,每个分区有多个副本分布在不同的Zone里。一条SQL进来,首先要在Proxy层做路由解析,判断涉及哪些分区、这些分区在哪些节点上,然后生成分布式执行计划,把子任务下发到各个节点并行执行,最后在某个节点上做结果汇聚。这个过程中,网络开销、数据重分布、跨节点事务协调都是集中式数据库不需要考虑的成本。
打个比方:集中式数据库像一个经验丰富的老会计,所有账本都在他桌上,查什么直接翻;分布式数据库像一个财务共享中心,账本分散在十个工位上,查一笔账要先分工、再汇总,分工合理就快,分工不合理光沟通成本就拖垮了。
2.2 两种架构对复杂SQL的"态度"
复杂SQL的典型特征是:多表关联、子查询嵌套、聚合与窗口函数混用、数据扫描量大。这类SQL在两种架构下的表现差异,主要来自三个维度:
| 维度 | 集中式(金仓V8) | 分布式(OceanBase) |
|---|---|---|
| 执行计划生成 | 单节点全局优化,代价模型成熟 | 分布式优化,需考虑数据分布与网络代价 |
| 数据移动 | 无,本地Join | 可能触发数据重分布(Shuffle) |
| 并行能力 | 依赖单机多核并行 | 天然多节点并行,可水平扩展 |
| 事务协调 | 本地事务,开销低 | 跨节点需两阶段提交或Paxos |
| 统计信息敏感度 | 中等,优化器有兜底 | 极高,分布统计缺失直接导致计划劣化 |
我实测过一条典型的分析型SQL,在金仓上执行计划是Hash Join串行加并行扫描,耗时约12秒;在OceanBase上如果不指定分区键,优化器选择了跨所有分区的全表扫描加远程数据拉取,耗时飙到40秒以上。把分区键对齐到Join条件之后,OceanBase利用分区裁剪和本地Join,耗时降到3秒以内。同一组SQL,跑法不同,性能差一个数量级。
2.3 为什么不能简单地说"谁更快"
经常有人问"OceanBase和金仓哪个快",这个问题本身就不成立。正确的问法是:在什么数据规模、什么并发量、什么查询模式下,哪个更合适。
- 数据量在TB级以下、并发在几百以内、以复杂关联查询为主:金仓这类集中式数据库往往表现更稳,因为省去了分布式协调开销。
- 数据量到几十TB甚至PB级、并发上千、写入吞吐要求高:OceanBase的水平扩展能力是集中式无法比拟的,单机堆硬件总有天花板。
- 混合负载(OLTP+OLAP)场景:OceanBase的LSM-Tree存储引擎和行列混存能力在部分场景有优势,但复杂AP查询仍需针对性调优。
理解了这层,后面的实操才有意义。
3. 复杂SQL在两边的执行计划长什么样
3.1 金仓V8的执行计划解读
在金仓上,我用EXPLAIN (ANALYZE, BUFFERS)看一条七表关联SQL的计划,核心结构是这样的:
金仓的优化器基于PostgreSQL的代价模型,对每张表的行数、选择率、IO代价都有估算。它的执行计划是一棵单机执行树,所有算子都在同一个进程组里跑,通过共享内存传递中间结果。并行查询时,会启动多个Worker进程,但都在同一台机器上。
关键点在于:金仓的优化器能看到所有表的完整统计信息,所以它选Join顺序、选Hash还是Nested Loop、选索引还是全表扫描,依据是充分的。我遇到过一次统计信息过期导致计划劣化的情况,ANALYZE之后计划立刻恢复正常,这说明集中式优化器对统计信息的依赖是"线性"的——信息准,计划就准。
3.2 OceanBase的分布式执行计划
OceanBase的执行计划要复杂得多。用EXPLAIN看同一条SQL,你会看到类似这样的结构:
这里的PX是Parallel Execution的缩写,PX COORDINATOR是协调节点,负责汇总各节点的执行结果;PX PARTITION ITERATOR表示按分区迭代;下面的算子会下发到各个OBServer上并行执行。
OceanBase的执行计划里,数据分布信息是核心。如果Join的两张表分区键一致,可以做Partition-Wise Join,各节点本地完成,没有数据移动;如果分区键不一致,就要插入一个EXCHANGE算子做数据重分布,这个算子的代价往往占整个查询的大头。
我实测过:两张千万级表做Join,分区键对齐时耗时2.8秒,不对齐时耗时19秒,差距主要就在Shuffle上。所以在OceanBase上写复杂SQL,第一要务是让Join条件尽量落在分区键上。
3.3 执行计划差异带来的调优思路差异
因为执行计划结构不同,调优的切入点也完全不同:
- 金仓调优:重点在统计信息、索引、Join顺序、并行度。手段是
ANALYZE、建合适索引、用Hint干预Join顺序、调整max_parallel_workers。 - OceanBase调优:重点在分区键设计、数据分布、并行度、执行计划绑定。手段是调整分区策略、用
/*+ PARALLEL(n) */控制并行度、用/*+ LEADING */干预Join顺序、必要时用Outline绑定计划。
一个很典型的误区是:从金仓迁移到OceanBase时,直接把索引原样搬过去。结果发现OceanBase上索引没被用上,因为分布式优化器认为走索引回表加跨节点拉取的代价,比全表扫描加本地过滤更高。索引不是越多越好,在分布式架构下,索引的收益要减去跨节点访问的成本。
4. 实操对比:一条复杂SQL的完整调优过程
4.1 测试环境与数据准备
为了让对比有参考价值,我搭了一套可复现的环境:
- 金仓V8:单节点,16核32G,SSD存储,数据量约500GB。
- OceanBase 4.x:三节点集群,每节点16核32G,三副本,数据量约500GB。
- 测试表:订单表(2亿行)、用户表(2000万行)、商品表(500万行)、订单明细表(8亿行)等七张表。
- 测试SQL:涉及订单、用户、商品、明细的多维关联,带时间范围过滤、分组聚合、窗口函数排名。
数据导入用的是两边各自的批量导入工具,金仓用COPY,OceanBase用LOAD DATA。导入完成后都执行了统计信息收集:金仓用ANALYZE,OceanBase用ANALYZE TABLE并确保收集了分区级统计。
提示:OceanBase的分区级统计信息非常关键,如果只收集了表级统计而没收集分区级,优化器对数据分布的估算会严重失真,直接导致计划选错。
4.2 第一轮:不做任何调优的裸跑
先看裸跑结果,这是最有参考价值的基线:
| 指标 | 金仓V8 | OceanBase |
|---|---|---|
| 首次执行耗时 | 11.8秒 | 42.3秒 |
| 二次执行耗时 | 10.9秒 | 38.7秒 |
| CPU峰值 | 85% | 各节点约40% |
| 内存峰值 | 12GB | 各节点约6GB |
| 是否走索引 | 是 | 部分走,部分全表 |
OceanBase慢的原因很明确:执行计划里出现了多次EXCHANGE算子,数据在节点间来回搬。而且优化器对分区表的扫描选择了全分区扫描,没有做分区裁剪,因为过滤条件里的时间字段不是分区键。
金仓这边虽然快,但CPU已经接近瓶颈,如果并发上到50以上,响应时间会明显劣化。
4.3 第二轮:针对OceanBase的分区与并行调优
针对裸跑暴露的问题,我做了三件事:
第一,调整分区键。 原表按order_id哈希分区,但查询的过滤条件主要是create_time。我改成按create_time做Range分区,再按order_id做Hash子分区。这样时间范围过滤能直接触发分区裁剪,扫描的分区数从全部128个降到8个。
第二,控制并行度。 默认并行度是自动计算的,在这条SQL上给到了32,但实际节点只有3个,过高的并行度反而增加了协调开销。我用Hint显式指定:
12是3节点乘以每节点4个并行Worker,实测这个值最平衡。
第三,干预Join顺序。 用LEADING Hint把过滤性最强的表放在最前面:
调优后OceanBase的耗时降到4.2秒,比裸跑快了近十倍。
4.4 第三轮:金仓的并行与索引优化
金仓这边虽然基线不差,但还有优化空间。我做了两件事:
第一,调整并行度。 默认max_parallel_workers_per_gather是2,对于这种大表关联明显不够。调到8之后,扫描和Join都并行了,耗时降到6.5秒。
第二,优化索引。 原有一个复合索引(user_id, create_time),但查询里create_time的范围过滤选择性更高。我新建了(create_time, user_id)的索引,让时间过滤先走索引,再回表关联。耗时进一步降到5.1秒。
最终对比:
| 指标 | 金仓V8(调优后) | OceanBase(调优后) |
|---|---|---|
| 执行耗时 | 5.1秒 | 4.2秒 |
| CPU峰值 | 92% | 各节点约55% |
| 并发50时P99 | 18秒 | 6秒 |
| 水平扩展能力 | 受单机限制 | 可加节点线性提升 |
这个结果说明:单条复杂SQL的极限性能,两者调优后差距不大;但并发场景下,分布式的扩展优势就体现出来了。
4.5 参数配置速查
把关键参数整理成表,方便直接抄:
| 参数 | 金仓V8 | OceanBase | 说明 |
|---|---|---|---|
| 并行度 | max_parallel_workers_per_gather | parallel_degree_policy + Hint | 控制并行Worker数 |
| 统计信息 | ANALYZE | ANALYZE TABLE + 分区统计 | 必须定期执行 |
| 内存限制 | work_mem | ob_sql_work_area_percentage | 影响Hash Join和排序 |
| 计划干预 | pg_hint_plan | Outline / Hint | 分布式更依赖Hint |
| 分区裁剪 | 无(集中式) | 分区键设计决定 | 分布式核心优化点 |
5. 迁移与兼容:那些文档里不会写的坑
5.1 SQL语法兼容性不是最大问题
很多人担心迁移时SQL语法不兼容,实际上金仓和OceanBase都高度兼容标准SQL和Oracle/MySQL语法。真正的问题在语义差异和执行行为差异。
举个例子:ROWNUM在金仓里是Oracle兼容模式下的伪列,在OceanBase的Oracle模式下也支持,但两者的执行时机不同。金仓是在结果集生成后应用ROWNUM,OceanBase在分布式执行时可能在各节点先应用再汇总,导致结果不一致。这类问题文档里很少提,但实际迁移中经常遇到。
我的做法是:迁移前用两边的测试环境跑一遍全量SQL回归,重点比对结果集而不是执行时间。结果集不一致的SQL,逐条分析执行计划差异。
5.2 事务模型的差异要提前设计
金仓是本地事务,BEGIN...COMMIT之间的操作都在一个节点上,ACID保证直接。OceanBase的跨分区事务需要走两阶段提交,延迟明显更高。如果业务里有大量跨分区的小事务,在OceanBase上性能会受影响。
应对策略有两个:一是尽量让事务落在单个分区内,比如按用户ID分区,同一用户的操作天然在一个分区;二是把大事务拆小,减少跨节点协调的窗口。
5.3 慢SQL的排查路径完全不同
在金仓上排查慢SQL,路径是:看执行计划 → 看统计信息 → 看索引 → 看资源等待。工具用pg_stat_statements和EXPLAIN ANALYZE就够了。
在OceanBase上,排查路径多了一层:先看数据分布,再看执行计划,最后看节点负载。因为慢的原因可能是某个分区数据倾斜,导致单个OBServer成为瓶颈。OceanBase提供了GV$OB_SQL_AUDIT和GV$OB_PLAN_CACHE_PLAN_STAT等视图,能看到每个算子在哪个节点上花了多少时间。
我整理了一个排查对照表:
| 现象 | 金仓排查方向 | OceanBase排查方向 |
|---|---|---|
| 执行慢 | 统计信息、索引、Join顺序 | 分区裁剪、数据倾斜、Shuffle量 |
| 结果不对 | 事务隔离级别、锁 | 分布式执行计划、副本一致性 |
| 并发差 | 连接数、work_mem | 分区数、Leader分布、并行度 |
| 写入慢 | WAL、检查点 | 副本同步、LSM合并 |
5.4 几个实操避坑技巧
- OceanBase上不要盲目建索引。每建一个索引,写入时就要同步维护,分布式环境下索引维护的代价比集中式高。我一般只对高频过滤字段建索引,且尽量让索引包含分区键。
- 金仓上要定期VACUUM。长事务会导致死元组堆积,统计信息失真,计划劣化。设置自动VACUUM并监控
pg_stat_user_tables的n_dead_tup。 - OceanBase的Hint要慎用。Hint绑定太死,数据分布变化后计划可能不再最优。我一般先用Hint验证思路,稳定后再考虑用Outline固化。
- 两边都要做执行计划回归。数据库版本升级、数据量增长、统计信息变化都会导致计划翻转,定期跑一遍核心SQL的执行计划对比,能提前发现问题。
6. 选型建议:什么场景选什么
6.1 集中式仍然不可替代的场景
不是所有业务都需要分布式。以下场景,金仓这类集中式数据库反而更合适:
- 数据量在TB级以下,且增长可预期。单机加SSD和内存就能扛住,没必要引入分布式复杂度。
- 复杂分析查询为主,并发不高。集中式优化器对复杂查询的处理更成熟,执行路径短。
- 团队运维能力有限。集中式数据库的运维门槛明显低于分布式,出问题排查路径清晰。
- 强事务一致性要求,且事务跨表频繁。本地事务的开销远低于分布式事务。
6.2 分布式真正发挥价值的场景
- 数据量持续增长,单机存储和计算到瓶颈。OceanBase可以加节点线性扩展,这是集中式的硬伤。
- 高并发写入,且要求高可用。三副本Paxos协议保证了节点故障时的自动切换,RPO为0。
- 混合负载,OLTP和OLAP都要跑。OceanBase的LSM-Tree和行列混存对混合负载有一定优势。
- 多地多活部署。分布式架构天然支持多副本跨地域部署。
6.3 混合架构也是一种选择
我最近做的一个项目就是混合架构:核心交易走OceanBase保证扩展性和高可用,报表分析走金仓保证复杂查询性能,两边通过数据同步工具做准实时同步。这种架构的复杂度在于数据一致性维护,但收益是各取所长。
选型没有标准答案,关键是想清楚业务的数据规模、并发量、一致性要求和团队能力,再匹配架构。盲目上分布式,可能带来的是运维成本飙升而不是性能提升。
7. 我个人在实际操作中的几点体会
折腾完这一轮对比,有几个感受比较深。
第一,分布式数据库的调优,七分在设计,三分在调参。分区键设计错了,后面怎么调Hint都是事倍功半。我现在的习惯是,建表之前先把核心查询的过滤条件和Join条件列出来,反推分区键,而不是建完表再想办法。
第二,集中式数据库的优化器比想象中聪明,但也比想象中依赖统计信息。金仓上很多"慢SQL"其实不是SQL写得差,而是统计信息过期了。定期ANALYZE这个动作,看起来简单,但坚持做能省掉大量排查时间。
第三,跨架构迁移,最大的成本不是改SQL,而是改思维。从集中式到分布式,要习惯"数据不在一个地方"这个前提,所有的SQL设计、索引设计、事务设计都要围绕这个前提重新思考。我见过太多团队把集中式的表结构原样搬到分布式,然后抱怨性能不行,问题往往就出在这里。
最后分享一个小技巧:做架构对比测试时,一定要用同一份数据、同一组SQL、同一套硬件规格,并且记录首次执行和二次执行两个指标。首次执行反映的是冷启动和计划生成的开销,二次执行反映的是缓存命中后的稳定性能,两个指标结合看,才能得出靠谱的结论。我见过只测二次执行就下结论的,结果上线后首次请求超时,就是忽略了冷启动这一环。