OceanBase VS 金仓:同一组复杂 SQL,分布式与集中式架构怎么跑

0 阅读14分钟

OceanBase 和金仓都能跑同一条 SQL,但复杂查询的延迟差在哪里,往往要到订单、客户、支付流水和区域维度放在一起的报表里才看得出来。简单的单表查询两边都能返回结果,差异出在这几处:数据是不是按查询条件落在少数分区,关联时要不要跨节点取数,中间结果在哪里聚合,窗口函数的排序由谁来承担。

这篇不拿一组没有实际环境支撑的耗时数字来排座次,而是把 OceanBase 的分布式架构和 KingbaseES 的集中式、共享存储对照路径放在同一张 SQL 试卷上,看延迟差在哪一段环节。OceanBase 官方文档把分区、副本和 Multi-Paxos 作为分布式集群的基础;KingbaseES 的产品资料则同时覆盖单实例、读写分离和共享存储集群。对比前先把对照形态说清楚,结论才不会把某一种部署方式扩大成整个产品的结论。

先分清比较的两种架构

OceanBase 以分区组织数据。一个分区有自己的主副本和备副本,副本之间用 Paxos 体系同步日志;SQL 到达集群后,还要经过分区位置和租户路由,找到能够处理这部分数据的节点。官方文档对分区、副本和路由关系有具体说明。把一张表按客户编号分成多个分区,能够把不同客户的请求分散到多个节点,但一条 SQL 如果同时访问多个分区,后面的 Join、排序和聚合就不能只看单节点的执行时间。

KingbaseES 这里选择单实例或共享存储集群作为对照。单实例的查询路径更直接,所有表、索引和中间结果都在同一个数据库实例内完成;共享存储集群可以由多个节点提供服务,但数据文件位于共享存储,节点之间仍需处理缓存、锁和全局一致性。它和 OceanBase 的 Shared-Nothing 分布式模型不是同一条扩展路线,也不能仅凭“节点更多”判断复杂 SQL 一定更快或更慢。

把这条边界落到延迟上会更清楚。OceanBase 的延迟是几段拼起来的:命中分区内的扫描与聚合、分区路由、跨节点数据交换、最终合并。查询命中的分区越少、数据越能和分区键对齐,后三段就越接近可以忽略;一旦跨分区,整体延迟就由参与节点里最慢的那一个、以及交换的数据量决定。金仓数据库单实例的延迟构成更短,扫描、排序、缓存命中都在一个实例内完成,索引选择、统计信息和排序空间这几项能调的手段,效果会直接落到延迟上,中间没有分区路由和跨节点合并这两段。共享存储集群是多节点部署,节点之间要处理缓存、锁和全局一致性,这部分开销来自部署形态,不在单实例的链路上。两者延迟的根本差异,不在于谁更快,而在于延迟里多出来的那一段由什么触发:OceanBase 由跨分区触发,共享存储集群由多节点之间的协调触发。

两边的架构边界可参考 OceanBase 分区和副本说明KingbaseES 高可用架构说明

OceanBase 与 KingbaseES 的架构对照

实际项目里容易忽略的是,数据库架构服务于业务访问方式。写入是否集中在少数热点客户,查询是否总带分区键,报表是否跨越全部区域,都会改变架构优势能否发挥出来。

先用同一套表结构和口径

测试模型不需要堆很多表,订单主表、订单明细、客户和支付流水已经能覆盖大多数报表查询。两边的列名、数据类型、主键和索引保持一致;数据文件也使用同一份,不能一边重新随机生成一遍。

CREATE TABLE demo_customer (
    customer_id    BIGINT PRIMARY KEY,
    customer_name  VARCHAR(80) NOT NULL,
    customer_level VARCHAR(16) NOT NULL,
    region_code    VARCHAR(16) NOT NULL
);
​
CREATE TABLE demo_order (
    order_id      BIGINT PRIMARY KEY,
    customer_id   BIGINT NOT NULL,
    order_time    TIMESTAMP NOT NULL,
    order_status  VARCHAR(16) NOT NULL,
    order_amount  DECIMAL(14, 2) NOT NULL,
    channel_code  VARCHAR(16) NOT NULL
);
​
CREATE TABLE demo_order_item (
    order_id    BIGINT NOT NULL,
    line_no     INTEGER NOT NULL,
    product_id  BIGINT NOT NULL,
    quantity    INTEGER NOT NULL,
    unit_price  DECIMAL(14, 2) NOT NULL,
    PRIMARY KEY (order_id, line_no)
);
​
CREATE TABLE demo_payment (
    payment_id     BIGINT PRIMARY KEY,
    order_id       BIGINT NOT NULL,
    payment_time   TIMESTAMP NOT NULL,
    payment_status VARCHAR(16) NOT NULL,
    paid_amount    DECIMAL(14, 2) NOT NULL
);

数据装好后两边各查一次行数、金额范围和状态分布,确认输入确实相同:

SELECT 'demo_customer' AS table_name,
       COUNT(*) AS row_count
FROM demo_customer
UNION ALL
SELECT 'demo_order', COUNT(*)
FROM demo_order
UNION ALL
SELECT 'demo_order_item', COUNT(*)
FROM demo_order_item
UNION ALL
SELECT 'demo_payment', COUNT(*)
FROM demo_payment;
​
SELECT order_status,
       COUNT(*) AS row_count,
       SUM(order_amount) AS amount_sum,
       MIN(order_time) AS first_time,
       MAX(order_time) AS last_time
FROM demo_order
GROUP BY order_status
ORDER BY order_status;

索引也要按同一套逻辑准备。订单时间和状态用于报表过滤,客户编号用于关联,支付表按订单和支付状态查找:

CREATE INDEX idx_demo_order_status_time
    ON demo_order (order_status, order_time);
​
CREATE INDEX idx_demo_order_customer_time
    ON demo_order (customer_id, order_time);
​
CREATE INDEX idx_demo_payment_order_status
    ON demo_payment (order_id, payment_status);

OceanBase 如果按 customer_id 分区,分区键的选择要和主要访问条件一起看;KingbaseES 单实例或共享存储路径则更关注索引选择、统计信息和内存中的排序空间。索引名称抄成一样,两边的访问成本也不相同。

后面三条 SQL 都会经过过滤、连接、聚合和排序这几个环节,只是各自把成本压在哪一步不同。

三条复杂 SQL 的执行环节与两种架构的成本位置

第一条 SQL:多表关联先看数据在哪里汇合

常见的一条日报查询:筛选某一天支付成功的订单,关联客户和支付流水,按区域统计订单数和金额。时间条件用左闭右开,避免把下一天零点重复算进来。

SELECT c.region_code,
       COUNT(DISTINCT o.order_id) AS order_count,
       SUM(o.order_amount) AS order_amount,
       SUM(p.paid_amount) AS paid_amount
FROM demo_order AS o
JOIN demo_customer AS c
  ON c.customer_id = o.customer_id
JOIN demo_payment AS p
  ON p.order_id = o.order_id
 AND p.payment_status = 'SUCCESS'
WHERE o.order_status = 'PAID'
  AND o.order_time >= TIMESTAMP '2026-08-01 00:00:00'
  AND o.order_time <  TIMESTAMP '2026-08-02 00:00:00'
GROUP BY c.region_code
ORDER BY c.region_code;

在 OceanBase 中,日期条件可以先缩小订单分区内的数据,再参与关联;如果订单按客户分区,而查询只给了时间条件,就可能需要访问更多分区。客户表和支付表的分区方式不同,还会增加数据交换和中间结果汇总的成本。分布式架构把数据和计算拆开了,查询也就要承担路由和跨分区协调。

KingbaseES 单实例里,上面的 Join、分组和排序由同一个优化器选择访问顺序;共享存储集群还要观察节点之间的缓存与锁协调。这里不能简单写成“集中式没有网络开销”,因为共享存储和集群节点本身也有协调成本,只是成本出现的位置不同。

同一条查询的 EXPLAIN 输出:

EXPLAIN
SELECT c.region_code,
       COUNT(DISTINCT o.order_id) AS order_count,
       SUM(o.order_amount) AS order_amount,
       SUM(p.paid_amount) AS paid_amount
FROM demo_order AS o
JOIN demo_customer AS c
  ON c.customer_id = o.customer_id
JOIN demo_payment AS p
  ON p.order_id = o.order_id
 AND p.payment_status = 'SUCCESS'
WHERE o.order_status = 'PAID'
  AND o.order_time >= TIMESTAMP '2026-08-01 00:00:00'
  AND o.order_time <  TIMESTAMP '2026-08-02 00:00:00'
GROUP BY c.region_code;

要记录的不只是总耗时,还包括扫描了多少行、是否命中时间索引、Join 顺序、聚合发生在分区内还是汇总节点。两边计划文本的节点名字可能不同,拿节点名称逐个对齐没有意义,应该对照实际访问范围和中间结果规模。

第二条 SQL:嵌套子查询会把中间结果放大

经营分析里另一类高频写法是同一个结果:本区域消费额高于区域平均值的客户。用嵌套子查询直接表达最直观,外层每一行客户都要回一次订单表算自己的金额,还要再算一次所属区域的均值:

SELECT c.region_code,
       c.customer_id,
       (SELECT SUM(o.order_amount)
        FROM demo_order AS o
        WHERE o.customer_id = c.customer_id
          AND o.order_status = 'PAID') AS total_amount
FROM demo_customer AS c
WHERE (SELECT SUM(o.order_amount)
       FROM demo_order AS o
       WHERE o.customer_id = c.customer_id
         AND o.order_status = 'PAID')
      >
      (SELECT AVG(t.region_amount)
       FROM (
           SELECT SUM(o2.order_amount) AS region_amount
           FROM demo_order AS o2
           JOIN demo_customer AS c2
             ON c2.customer_id = o2.customer_id
           WHERE o2.order_status = 'PAID'
             AND c2.region_code = c.region_code
           GROUP BY c2.customer_id
       ) AS t)
ORDER BY c.region_code, total_amount DESC;

这条 SQL 读起来别扭,代价也直接写在结构里:region_code 这个关联条件把内层子查询钉成了逐行相关,外层客户有多少行,内层就要重复计算多少次。优化器未必会照原样执行,子查询解关联、提升成一次聚合都是常见手段,但能不能做到、要不要物化中间结果,两边给出的路径不一定相同。

把它改成先聚合、再连接,重复计算就消失了:

WITH customer_amount AS (
    SELECT c.region_code,
           c.customer_id,
           SUM(o.order_amount) AS total_amount
    FROM demo_customer AS c
    JOIN demo_order AS o
      ON o.customer_id = c.customer_id
    WHERE o.order_status = 'PAID'
    GROUP BY c.region_code, c.customer_id
), region_average AS (
    SELECT region_code,
           AVG(total_amount) AS average_amount
    FROM customer_amount
    GROUP BY region_code
)
SELECT ca.region_code,
       ca.customer_id,
       ca.total_amount,
       ra.average_amount
FROM customer_amount AS ca
JOIN region_average AS ra
  ON ra.region_code = ca.region_code
WHERE ca.total_amount > ra.average_amount
ORDER BY ca.region_code, ca.total_amount DESC;

两种写法返回同一个结果,差别落在扫描范围和中间结果规模上:

相关子查询与预聚合改写的对照

代价落在哪里,还是要看数据分布。嵌套子查询在 OceanBase 里如果按 customer_id 相关,每个涉及的分区都可能独立跑一遍内层聚合;客户和订单按同一个键保持局部性时,聚合可以尽量靠近数据完成,而区域平均值仍要跨分区汇总,最后经过一次全局聚合。金仓数据库单实例中,代价更多体现为子查询被重复求值的次数、聚合前的扫描和排序、以及内存占用。同一条 SQL 在两边都能返回结果,但重复计算被消除在哪一层,要拿计划确认。

把结果压成摘要,客户端传输对计时的影响会小一些:

WITH customer_amount AS (
    SELECT c.region_code,
           c.customer_id,
           SUM(o.order_amount) AS total_amount
    FROM demo_customer AS c
    JOIN demo_order AS o
      ON o.customer_id = c.customer_id
    WHERE o.order_status = 'PAID'
    GROUP BY c.region_code, c.customer_id
), region_average AS (
    SELECT region_code,
           AVG(total_amount) AS average_amount
    FROM customer_amount
    GROUP BY region_code
)
SELECT COUNT(*) AS customer_count,
       SUM(ca.total_amount) AS amount_sum
FROM customer_amount AS ca
JOIN region_average AS ra
  ON ra.region_code = ca.region_code
WHERE ca.total_amount > ra.average_amount;

如果只比较一条返回明细很多的 SQL,客户端网络和结果输出会遮住数据库本身的差异。用同样的摘要结果验证口径,再把计划和扫描量放在一起看。

第三条 SQL:窗口函数最容易暴露排序成本

“每个区域消费额最高的十个客户”是窗口函数的常见场景。它既要保留客户明细,又要在区域内排序,不能简单用一个全局 LIMIT 代替。

WITH customer_amount AS (
    SELECT c.region_code,
           c.customer_id,
           c.customer_name,
           SUM(o.order_amount) AS total_amount
    FROM demo_customer AS c
    JOIN demo_order AS o
      ON o.customer_id = c.customer_id
    WHERE o.order_status = 'PAID'
    GROUP BY c.region_code, c.customer_id, c.customer_name
), ranked_customer AS (
    SELECT region_code,
           customer_id,
           customer_name,
           total_amount,
           ROW_NUMBER() OVER (
               PARTITION BY region_code
               ORDER BY total_amount DESC, customer_id
           ) AS region_rank
    FROM customer_amount
)
SELECT region_code,
       customer_id,
       customer_name,
       total_amount,
       region_rank
FROM ranked_customer
WHERE region_rank <= 10
ORDER BY region_code, region_rank;

ORDER BY total_amount DESC, customer_id 里的第二个字段不能省。金额相同时,客户编号提供稳定的次序,分页或反复执行时不会因为并列值改变返回顺序。窗口函数的分区键是 region_code,而订单连接键是 customer_id,这两个键不一致时,分布式执行是否需要重新分布数据,就值得重点看计划。

KingbaseES 单实例中,窗口排序通常表现为本地排序和内存/临时空间消耗;OceanBase 则还要看窗口计算之前数据是否已经按分区和区域聚拢。查询带上区域过滤时,分区裁剪可能明显减少参与排序的数据;不带过滤时,分区数量越多,最终合并阶段越值得关注。

先改 SQL,再把架构差异说清楚

同一条 SQL 在两边都能跑,离跑出好计划还有距离。

过滤条件尽量写在最早能生效的位置,避免先把全量订单与客户、支付流水连接后再过滤:

WITH paid_order AS (
    SELECT order_id,
           customer_id,
           order_amount,
           order_time
    FROM demo_order
    WHERE order_status = 'PAID'
      AND order_time >= TIMESTAMP '2026-08-01 00:00:00'
      AND order_time <  TIMESTAMP '2026-08-02 00:00:00'
)
SELECT c.region_code,
       COUNT(DISTINCT o.order_id) AS order_count,
       SUM(o.order_amount) AS order_amount
FROM paid_order AS o
JOIN demo_customer AS c
  ON c.customer_id = o.customer_id
GROUP BY c.region_code;

明细表只取需要的列,报表 SQL 里不要用 SELECT *。列越多,跨分区传输、缓存读取和临时排序都可能增加:

SELECT o.order_id,
       o.customer_id,
       o.order_time,
       o.order_amount
FROM demo_order AS o
WHERE o.order_status = 'PAID'
  AND o.order_time >= TIMESTAMP '2026-08-01 00:00:00'
  AND o.order_time <  TIMESTAMP '2026-08-02 00:00:00';

重复使用的聚合结果可以考虑物化或按业务周期预计算,但要把刷新时间和数据新鲜度写进验收条件,否则报表 SQL 看起来更快,代价是统计结果不再更新。

EXPLAIN 或对应版本的实际执行计划检查改写前后扫描范围、连接顺序、聚合节点和排序节点。性能变化要和计划变化对应起来,单个耗时数字说明不了问题:

EXPLAIN
WITH paid_order AS (
    SELECT order_id, customer_id, order_amount
    FROM demo_order
    WHERE order_status = 'PAID'
      AND order_time >= TIMESTAMP '2026-08-01 00:00:00'
      AND order_time <  TIMESTAMP '2026-08-02 00:00:00'
)
SELECT c.region_code,
       COUNT(DISTINCT o.order_id),
       SUM(o.order_amount)
FROM paid_order AS o
JOIN demo_customer AS c
  ON c.customer_id = o.customer_id
GROUP BY c.region_code;

把改写前后的计划并排放在一起,能对照的就是这几处差异:

复杂 SQL 改写前后:扫描范围与中间结果规模对照

结论不能脱离数据分布

OceanBase 的优势在于把数据、计算和副本能力扩展到多个节点,但复杂 SQL 是否得到好处,取决于分区键、数据局部性和查询是否经常跨分区。KingbaseES 的单实例或共享存储路径在关系查询、复杂 Join 和窗口函数上也有成熟的本地优化空间,是否需要上更复杂的集群形态,要由并发、容量、容灾和维护边界决定。

因此,“OceanBase VS 金仓谁更快”不是一条 SQL 能回答的问题。更可靠的比较方式是固定同一份数据、同一套索引和同一组 SQL,核对结果和执行计划之后,再谈冷缓存、热缓存以及并发下的响应分位数。架构不同,瓶颈出现的位置也不同:OceanBase 需要留意分区路由和跨节点汇聚。

回到延迟本身:分布式架构并不天然更快。它把单节点上的串行工作拆成可以并行的多段,代价是多了路由、数据交换和协调。查询能落在少数分区、参与节点返回的数据量又比较均衡时,并行收益大于协调开销;查询跨越大量分区、或者某个参与节点要返回的数据明显更多时,最慢的那一段决定整体延迟,协调开销反而被放大。单实例路径没有数据交换这一项,延迟基本由扫描范围、排序空间和缓存命中决定,而这几项恰好是靠索引、统计信息和内存参数能直接调的部分。

讨论延迟差异,先要问清查询命中了几个分区、交换了多少行,再去看集群有多少节点。只拿一条没有分区键的窗口查询,或者只跑一次热缓存的简单 Join,就给两种数据库排出高下,结论和实际业务对不上。