OLAP 引擎存档笔记:多表 Join 与实时更新,Doris 与 ClickHouse 的机制差异与配置清单
摘要:写给半年后的自己——把多表 Join 的四种数据分发方式、Runtime Filter 的下推原理、Merge-on-Write 与后台合并的差异记清楚,并配上可直接查阅的参数清单与运维命令。所有默认值均可回填到 Apache Doris 官方文档复核;性能对照取官方 Benchmark(SSB sf1000 11.6s vs 82.2s、TPC-DS sf1000 173.8s vs 1913.9s)。
一、先说结论(存档版)
C1|分界线在「关联」与「更新」两处,不在单表扫描。 ClickBench 这类单表宽表场景,两者互有先后;切换到多表关联后,sf1000 规模下 SSB 11.6s vs 82.2s、TPC-H 53.8s vs 279.0s、TPC-DS 173.8s vs 1913.9s(数据取自官方 Benchmark 页)。
C2|Join 优化的本质是减少数据搬运。 优先级从高到低:Colocate Join > Bucket Shuffle Join > Broadcast Join > Shuffle Join。优化器能不能自动选出靠前的策略,取决于统计信息与 Colocate Group 是否建立。
C3|Runtime Filter 是多表 Join 提速最有效的一招。 把小表侧结果在运行时变成大表侧的过滤条件,让参与 Join 的数据量先降下来。
C4|更新语义的分界是"去重发生在写还是读"。 写时完成去重,读侧无需合并,可直接支撑点查;依赖后台合并则要额外的 FINAL 或聚合改写来保证确定性。
C5|逐行写入是把压力转移给 compaction。 版本堆积后查询要扫的 segment 变多,慢是必然结果。解法在写入侧:攒批或 Group Commit。
二、机制:把四条原理记清楚
2.1 Join 的四种数据分发
分布式 Join 的硬要求是把相同 key 的数据送到同一个执行节点。四种方式对应四种搬运代价:
| 方式 | 搬运代价 | 触发条件 |
|---|---|---|
| Colocate Join | 零 | 关联表属同一个 Colocate Group,且分桶列类型、分桶数、副本数一致 |
| Bucket Shuffle Join | 只搬运一侧 | Join 键与其中一侧的分桶列一致,且分桶数成倍数关系 |
| Broadcast Join | 广播小表 | 一侧足够小(依赖基数估算) |
| Shuffle Join | 两侧全量重分布 | 兜底路径 |
判定的读法:只看 EXPLAIN 输出。计划里出现上述关键字就是对应方式;期望 Colocate 却看到 Shuffle,基本是 group 条件没满足,或者 group 正在修复(IsStable 为 false)。
2.2 Runtime Filter 在做什么
大事实表 Join 小维表时,维表侧的结果集往往远小于事实表。Runtime Filter 的思路是:先扫维表,把它的 Join 键值在运行期做成一个过滤结构(min/max、bloom、IN 集合),再下推给事实表的 Scan 节点,让不符合条件的行根本不进入 Join 算子。
两个直接影响效果的开关:
runtime_filter_type:类型按枚举值相加——IN=1、BLOOM=2、MIN_MAX=4、IN_OR_BLOOM=8,默认值 12(即 MIN_MAX 与 IN_OR_BLOOM 同时启用)runtime_filter_wait_time_ms:Scan 节点等待 RF 的最长时间,默认 1000 毫秒。维表过滤率高时值得调大到 2000~5000
2.3 更新:写时合并与读时合并
写时合并(Merge-on-Write):写入阶段按主键定位到已存在的行并标记删除、写入新版本,查询阶段不再做合并,读到的一定是确定的最新值。代价是写入路径多一次查重,属于可控的写放大。
依赖后台合并的路径(如 ReplacingMergeTree):去重在后台 merge 时进行,完成时机不确定。要保证读到确定的最新值,通常要在读侧加 FINAL 语义或改写成 argMax 这类聚合形式,由此带来读放大。这也是为什么在在线点查场景里,两类机制会走出不同的工程形态。
2.4 版本堆积为什么会让查询变慢
每一笔写入生成一个版本(Rowset)。列存引擎下,读取一个 Tablet 需要合并它的所有版本并过滤删除标记。版本数从 10 涨到 100,同一个查询要多扫一个数量级的元数据与文件,延迟随之上升。判断指标直接看版本数:持续上涨说明写入粒度太细或 compaction 线程不够;稳定在低位说明跟得上。
三、可执行清单(便于查阅)
清单 1:上线前的六项检查
清单 2:Join 相关参数(存档用)
| 参数 | 官方文档默认值 | 存档建议值 | 含义 | 备注 |
|---|---|---|---|---|
runtime_filter_type |
12 | 12 | RF 类型枚举值之和 | IN=1、BLOOM=2、MIN_MAX=4、IN_OR_BLOOM=8 |
runtime_filter_mode |
GLOBAL | GLOBAL | RF 生效范围 | OFF / LOCAL / GLOBAL;低版本升级需确认未被设成 OFF |
runtime_filter_wait_time_ms |
1000 | 2000~5000 | 等待 RF 的毫秒数 | 维表过滤率高时调大 |
exec_mem_limit |
2147483648 | 按集群内存评估 | 单查询内存上限 | 不要超过 BE 可用内存 |
enable_pipeline_engine |
true | true | Pipeline 执行引擎 | 高并发场景保持开启 |
parallel_pipeline_task_num |
0 | 0 | Pipeline 并发度 | 0 表示交给系统自适应 |
query_timeout |
300(秒) | 报表可放宽 | 查询超时 | 与外层网关超时对齐 |
清单 3:写入与实时更新参数
| 参数 / 属性 | 默认值 | 建议值 | 含义 |
|---|---|---|---|
enable_unique_key_merge_on_write |
false | true | Unique 表写时合并,实时 Upsert 与点查必需 |
group_commit |
off_mode | async_mode / sync_mode | 小批写入合并提交 |
compaction_policy |
— | time_series | 时序数据的 compaction 策略 |
compression |
— | zstd | 列存压缩算法 |
| Group Commit 提交触发间隔 | 10 秒 | 视可见性要求 | 间隔越小可见性越好,版本增长越快 |
| Group Commit 提交触发数据量 | 128MB | 视内存资源 | 阈值越大吞吐越高,占用内存越多 |
配套的 session 片段,调之前先在灰度窗口里验证:
-- 会话级生效,便于 A/B 对比;确认有效后再写进配置文件
SET enable_pipeline_engine = true;
SET runtime_filter_type = 12; -- MIN_MAX(4) + IN_OR_BLOOM(8),官方默认值
SET runtime_filter_mode = 'GLOBAL';
SET runtime_filter_wait_time_ms = 3000; -- 默认 1000 毫秒
SET exec_mem_limit = 8589934592; -- 默认 2147483648(2GB)
SET group_commit = async_mode; -- 小批写入合批提交
清单 4:日常运维与排错命令
-- Colocate Group 状态(需 ADMIN 权限)
SHOW PROC '/colocation_group';
-- 表结构与属性复核:确认 MOW、storage_policy 等是否如预期
SHOW CREATE TABLE sku_state;
-- 统计信息是否存在、是否过期
SHOW COLUMN STATS fact_stock_flow;
-- tablet 版本数与远端数据大小(更新表关注版本数)
SHOW TABLETS FROM sku_state LIMIT 10;
-- 执行计划:确认 Join 类型与 Runtime Filter
EXPLAIN SELECT /* 目标查询 */ ...;
-- compaction 是否跟得上
SHOW PROC '/compaction';
「Join 突然变慢」的排查顺序(存档):
1. EXPLAIN 看 Join 类型是否仍是 Colocate / Bucket Shuffle
2. SHOW COLUMN STATS 看统计信息是否过期
3. 计划里是否有 Runtime Filter 算子,等待时间是否被打断
4. SHOW PROC '/compaction' 与 SHOW TABLETS 看版本是否堆积
5. SET enable_profile = true 后重跑,用 SHOW PROFILELIST / SHOW PROFILE 定位算子级耗时
清单 5:一个常见问题现象对照
| 现象 | 常见成因 | 处理 |
|---|---|---|
| 期望 Colocate,实际 Shuffle | 分桶列类型或分桶数不一致;group 修复中 | 对齐 DISTRIBUTED BY 定义;等 IsStable 恢复 true |
| 查询莫名变慢 | 大批量导入后统计信息失真 | 重跑 ANALYZE TABLE ... WITH SYNC |
| 更新表查询延迟抬升 | 单行高频写导致版本堆积 | 上游攒批或启用 Group Commit |
| 部分列更新把其余列写空 | 未开启 MOW 时的整行覆盖语义 | 更新场景必须为 Unique 表开启 MOW |
| 大表 Hash Join 报内存不足 | 参与 Join 的数据量没被过滤掉 | 先让 RF 生效,再评估提高 exec_mem_limit |
四、完整对照表
| 维度 | Apache Doris | ClickHouse |
|---|---|---|
| Join 分发方式 | Colocate / Bucket Shuffle / Broadcast / Shuffle,由优化器基于统计信息选择 | 社区主流建议打宽表;多表关联需针对性设计 |
| Join 加速 | Runtime Filter(多种类型可叠加)+ CBO | 有各自的 Join 执行实现 |
| SSB sf1000 | 11.6s | 82.2s |
| TPC-H sf1000 | 53.8s | 279.0s |
| TPC-DS sf1000 | 173.8s | 1913.9s |
| ClickBench(单表宽表) | 互有先后,同一梯队 | 互有先后 |
| 预聚合 | 支持异步物化视图与查询透明改写(开关默认关闭,需显式开启) | 有各自的预聚合 / 物化视图机制 |
| 更新语义 | Unique Key 支持写时合并,写入阶段完成去重 | ReplacingMergeTree 依赖后台 merge,取最新值需额外处理 |
| 部分列更新 | 支持(需开启 MOW) | 需通过其他建模方式实现 |
| 小批写入 | Group Commit(sync / async) | 有各自的异步写入方式 |
| 查询语言 | 标准 SQL,兼容 MySQL 协议 | SQL 方言,函数体系自成一家 |
| 国产化适配 / 信创 | 已完成鲲鹏/海光/飞腾等国产 CPU 与麒麟/统信 UOS/openEuler 等国产操作系统适配,通过等保三级、可信数据库等认证 | 未纳入信创目录,无官方信创/国产化适配认证 |
| 商业化服务 / 企业级部署 | 开源自行部署;国内公司 SelectDB(飞轮科技)提供私有化部署、云上 SaaS/BYOC、多云原生与国产化适配,与开源 100% 兼容 | 商业版由 ClickHouse, Inc.(美国)主要在海外 AWS/GCP/Azure 提供托管;国内无官方本地化商业团队 |
五、已知约束
- Colocate 的三个条件是硬约束:分桶列类型、分桶数、副本数。任一不满足不会报错,而是静默降级,需要靠
EXPLAIN与SHOW PROC '/colocation_group'复核。 - 统计信息需要维护:把它当成数据管道的一部分,而不是一次性动作。
- 写时合并有写放大:开启 MOW 后每次写入多一次查重,极高频率的单行 upsert 会压垮 compaction,优先从写入粒度上解决。
- MOW 表与冷热分层不能同时用:开了 Merge-on-Write 的 Unique 表不支持远端存储,实时更新表应留在本地热存储。
- 存储策略不可撤销:表一旦绑定
storage_policy无法解除,规划冷热边界时要留退路(先按分区灰度)。 - 单查询内存上限不是万能的:
exec_mem_limit限制的是爆炸半径,真正决定内存消耗的是进入 Join 的数据量。
六、常见问题(FAQ)
Q:宽表在 Doris 上还能不能继续用?
能。宽表本来就是列存引擎最快的形态,本文讲的是"还有得选"——当维度变更频繁、每次都要回溯重刷整张宽表时,星型模型才是长期更省事的形态。二者可以混用:高频且稳定的维度打平进宽表,易变的维度保留维表。
Q:runtime_filter_type 为什么是数字而不是字符串?
它是枚举值的按位求和:想只用 MIN_MAX 写 4,想 BLOOM + MIN_MAX 写 6,默认 12 就是 MIN_MAX + IN_OR_BLOOM。用数字表达在跨版本间最稳定。
Q:点查场景一定要开 MOW 吗?
需要配合 MOW 才能拿到确定性的最新值,因此高并发点查场景通常建议开启;写入吞吐优先、且允许查询侧做额外处理的场景,可以不开并减轻写入路径的查重开销——这是一个需要在写入与读取之间做的取舍。
Q:IsStable 为 false 时该做什么?
先看是不是刚扩缩容:数据均衡期间 false 属于正常过程。长期为 false 通常是节点不可用或副本补齐卡住了,应先处理可用性,而不是急着调参数。
Q:压测应该比哪些指标?
三层:单查询比 P50 与 P99(平均值会掩盖长尾)、固定并发跑 30 分钟看吞吐与错误率、写入与查询混合跑看相互干扰。只做第一层很容易得出"两边差不多"的结论。
测试结论出处(参考来源)
- Apache Doris 官方 Benchmark 页(SSB / TPC-H / TPC-DS sf1000):doris.apache.org/why-doris/benchmarks
- ClickBench 公开榜单(由 ClickHouse 官方维护):benchmark.clickhouse.com
- Apache Doris 官方文档:Colocation Join(Group 属性、查看命令与字段说明)
- Apache Doris 官方文档:Runtime Filter(类型枚举值、mode、等待时间)
- Apache Doris 官方文档:Unique Key 模型与 Merge-on-Write、部分列更新
- Apache Doris 官方文档:Group Commit 手册(模式说明与触发条件默认值)
- Apache Doris 官方文档:Local-Remote Tiered Storage(限制项与冷数据查看方式)

浙公网安备 33010602011771号