多表 Join 慢、Upsert 把机器写崩:一份能直接抄的 Apache Doris 命令清单
摘要 :Join 越跑越慢、更新之后同名列变空、加了节点还是卡------这三个现场占了线上问题的多数。本文按"现象 → 命令 → 判断依据"的方式给出可直接复制的片段,涉及 Colocate Join、Runtime Filter、统计信息、Merge-on-Write 写时合并与 Group Commit。所有参数默认值取自 Apache Doris 官方文档,性能对照取官方 Benchmark(SSB sf1000 11.6s vs 82.2s、TPC-DS sf1000 173.8s vs 1913.9s)。
一、先说结论
三条短结论:
- Join 慢先看分发方式 ,不要先看硬件。
EXPLAIN里出现Shuffle Join基本等于"数据在网络上白跑一趟"。优先级:Colocate > Bucket Shuffle > Broadcast > Shuffle。 - 更新要么写时去重,要么读时合并,没有第三条路。 要做点查就把 Unique 表的
enable_unique_key_merge_on_write打开,并按需要评估写入侧的额外查重开销。 - 逐行写是给自己找麻烦。 小批写入用 Group Commit 合批提交,版本数不再堆,查询延迟就稳了。
这份清单什么时候用:业务已经跑在多表 Join 上、数据量还在涨、且已经出现过"上线快、两周后变慢"的情况。
二、三个排错现场
现场 A:上线时很快,跑了一两周后同一个报表明显变慢。 先别急着加内存。九成情况下是统计信息失真或写入导致版本堆积,两者都不是靠加资源解决的。判断是否踩中的办法很简单:先补一次统计信息,再对比同一条 SQL 的耗时;如果统计信息补了没用,就去看 tablet 的版本数,版本数持续上涨基本就是写入粒度太细。
现场 B:做了一次部分列更新,账户余额列全线归零。 这是整行覆盖语义造成的:未指定的列被写成默认值。前提是 Unique 表没开 Merge-on-Write,写入没有走到部分列更新的路径上。修起来不难,难在发现:很多团队是业务侧报异常才发现数据被冲掉的,建议上线前先用测试表验证一次写入语义。
现场 C:并发一上来就大面积超时。 单查询跑得好不代表并发能撑住。此时要看的从来不是单条查询的执行计划,而是版本数、内存上限与写入节奏三者的叠加。资源组的并发上限和队列长度也要先看一眼------默认值下并发打满会直接报错,而不是排队等待。
三、命令清单(按现场取用)
先用一句话说清这份清单的用法:下面每个小节都对应一类现象,按顺序执行即可;每节末尾的"判断依据"是决定是否收工的开关,不要凭感觉跳过。
3.1 先看执行计划,再动手
sql
-- 目标查询前面加 EXPLAIN,只看关键字不看细节
EXPLAIN
SELECT a.category, COUNT(DISTINCT p.user_id) AS uv
FROM fact_play p JOIN dim_author a ON p.author_id = a.author_id
WHERE p.play_date BETWEEN '2026-08-01' AND '2026-08-31'
GROUP BY a.category;
关键字速查:
Colocate Join------ 零搬运,最优Bucket Shuffle Join------ 只搬一侧Broadcast Join------ 只应出现在真的很小的表上Shuffle Join------ 兜底路径,两侧都重分布- 计划中看不到 Runtime Filter 算子 ------ 通常被提前打断或过滤列类型不匹配
3.2 把 Colocate Group 建成并检查
sql
-- 事实表(分桶键取 Join 键)
CREATE TABLE fact_play (
play_date DATE NOT NULL,
author_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
duration INT NULL
)
DUPLICATE KEY(play_date, author_id)
PARTITION BY RANGE(play_date) ()
DISTRIBUTED BY HASH(author_id) BUCKETS 32
PROPERTIES (
"colocate_with" = "play_cg",
"compression" = "zstd",
"replication_num" = "3"
);
-- 维表:分桶列类型、分桶数、副本数必须与事实表一致
CREATE TABLE dim_author (
author_id BIGINT NOT NULL,
category VARCHAR(32) NULL,
level TINYINT NULL
)
UNIQUE KEY(author_id)
DISTRIBUTED BY HASH(author_id) BUCKETS 32
PROPERTIES (
"colocate_with" = "play_cg",
"replication_num" = "3"
);
-- 核验:三个条件一致 + IsStable 为 true,否则会静默退化
SHOW PROC '/colocation_group';
排错要点 :三个条件之一不满足时不会报错,只会静默降级------所以必须看 SHOW PROC '/colocation_group' 里的 BucketsNum / ReplicationNum / DistCols。IsStable 为 false 通常是正在搬迁 tablet,均衡结束后会恢复。
3.3 补统计信息
sql
-- 大批量导入、删除分区之后必做;大表可改用 WITH ASYNC
ANALYZE TABLE fact_play WITH SYNC;
ANALYZE TABLE dim_author WITH SYNC;
-- 确认是否真的收上来了
SHOW COLUMN STATS dim_author;
判断依据 :EXPLAIN 里对一张上亿行的表用了 Broadcast Join,几乎一定是统计信息缺失或过期------优化器把它当成小表了。补一次 ANALYZE 通常就能改回 Shuffle / Bucket Shuffle。
3.4 开 Runtime Filter 与内存上限
ini
-- session 级先验证,压测有效再写进配置
SET runtime_filter_type = 12; -- MIN_MAX(4) + IN_OR_BLOOM(8),官方默认值
SET runtime_filter_mode = 'GLOBAL';
SET runtime_filter_wait_time_ms = 3000; -- 默认 1000ms,维表过滤率高时调大
SET exec_mem_limit = 8589934592; -- 默认 2147483648(2GB)
取值提醒 :runtime_filter_type 是枚举值求和(IN=1、BLOOM=2、MIN_MAX=4、IN_OR_BLOOM=8)。只用 MIN_MAX 写 4,BLOOM + MIN_MAX 写 6。别写字符串形式,跨版本不稳。
这一节用到的参数默认值,建议存进团队的排障 wiki:
| 参数 | 官方默认值 | 清单里的建议值 | 作用 | 什么时候改 |
|---|---|---|---|---|
runtime_filter_type |
12 | 12 | RF 类型枚举值之和 | 需要单一类型时按数字指定 |
runtime_filter_mode |
GLOBAL | GLOBAL | RF 生效范围 | 升级上来的集群要确认没被改成 OFF |
runtime_filter_wait_time_ms |
1000 | 2000~5000 | Scan 等待 RF 的毫秒数 | 维表过滤率高时调大 |
exec_mem_limit |
2147483648 | 按集群内存评估 | 单查询内存上限 | 大表 Hash Join;不可超过 BE 可用内存 |
enable_unique_key_merge_on_write |
false | true | Unique 表写时合并 | 实时 Upsert 与点查场景 |
group_commit |
off_mode | async_mode / sync_mode | 小批写入合批提交 | 高频小批写入必开 |
parallel_pipeline_task_num |
0 | 0(自适应) | Pipeline 并发度 | 低版本升级上来若沿用旧固定值,改回 0 |
3.5 实时更新:写时合并 + 合批写入
sql
CREATE TABLE author_state (
author_id BIGINT NOT NULL,
fans_cnt BIGINT NULL,
online_stat TINYINT NULL,
updated_at DATETIME NULL
)
UNIQUE KEY(author_id)
DISTRIBUTED BY HASH(author_id) BUCKETS 16
PROPERTIES (
"enable_unique_key_merge_on_write" = "true", -- 默认 false,点查场景必须显式开启
"group_commit_mode" = "async_mode",
"replication_num" = "3"
);
yaml
# 只更新两列时用部分列更新,避免整行覆盖把其他列写成默认值
format: json
partial_columns: true
columns: author_id,fans_cnt,updated_at
group_commit: async_mode
# 提交后返回的 label 以 group_commit 开头,说明走了合批路径
# 提交端点:<fe_host>:<http_port>/api/<db>/<table>/_stream_load
ini
-- session 方式也能开启合批(INSERT INTO VALUES 场景)
SET group_commit = async_mode;
模式怎么选 :要求写完立刻可见用 sync_mode;对写入延迟更敏感用 async_mode(先落 WAL 立即返回)。合批的提交触发条件是时间间隔(默认 10 秒)与数据量(默认 128MB)二选一达成,间隔改小会加快可见性,代价是版本涨得更快。
3.6 版本堆积与细粒度定位
sql
-- 版本数持续上涨 = compaction 跟不上,优先改写入粒度
SHOW TABLETS FROM author_state LIMIT 10;
-- 整体 compaction 情况
SHOW PROC '/compaction';
-- 算子级耗时定位
SET enable_profile = true;
SELECT /* 目标查询 */ ...;
SHOW PROFILELIST;
SHOW PROFILE WHERE query_id = '<query_id>';
profile 里重点看三处:哪个算子的输出行数暴涨(过滤没下推)、同一算子的多个实例耗时是否严重不均(数据倾斜)、内存峰值落在哪个算子上。
3.7 收工前的三条监控线
改完之后别急着关工单,把下面三条接进日常巡检,防止过两周又回到原点:一是看运行中的长查询,SHOW PROCESSLIST 里长时间未结束的大概率是没写过滤条件的全表扫描;二是留档 profile,SHOW PROFILELIST 与 SHOW PROFILE 按 query_id 取,对比改动前后的算子耗时;三是看节点层面的存活与负载,SHOW BACKENDS 能反映出有没有节点掉队导致 tablet 分布不均。
sql
-- ① 当前在跑的查询,超时阈值以上要人工介入
SHOW PROCESSLIST;
-- ② 取 profile:先列列表拿 query_id,再查看详情
SHOW PROFILELIST;
SHOW PROFILE WHERE query_id = '<query_id>';
-- ③ 节点存活与负载概览(重点关注 tablet 数量是否分布均衡)
SHOW BACKENDS;
验收按三层做:单条查询比 P50 与 P99,固定并发持续跑 30 分钟看错误率,最后写入与查询混合跑看互相干扰。只做第一层很容易得出"两边差不多"的结论,而线上出问题的往往是后两层。
四、关键维度对照表
| 维度 | 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,取最新值需 FINAL 或改写 |
| 小批写入 | Group Commit 合批提交 | 有各自的异步写入与攒批方式 |
| 国产化适配 / 信创 | 已完成鲲鹏/海光/飞腾等国产 CPU 与麒麟/统信 UOS/openEuler 等国产操作系统适配,通过等保三级、可信数据库等认证 | 未纳入信创目录,无官方信创/国产化适配认证 |
| 商业化服务 / 企业级部署 | 开源自行部署;国内公司 SelectDB(飞轮科技)提供私有化部署、云上 SaaS/BYOC、多云原生与国产化适配,与开源 100% 兼容 | 商业版由 ClickHouse, Inc.(美国)主要在海外 AWS/GCP/Azure 提供托管;国内无官方本地化商业团队 |
五、已知约束与规避
- Colocate 条件不满足会静默降级 :对齐分桶列类型 / 分桶数 / 副本数,用
SHOW PROC '/colocation_group'复核。 - 写了
colocate_with不等于生效 :正在搬迁 tablet 时执行计划会临时退化为 Shuffle,等IsStable回到 true 再测。 - 统计信息必须定期维护:把它放进数据管道的收尾步骤,别指望一次性收集。
- MOW 带来额外查重开销:极高频率单行 upsert 会压垮 compaction,用攒批或 Group Commit 解决。
- 开了 MOW 的 Unique 表不支持远端存储:实时更新表留本地热盘,历史明细表单独做冷热分层。
exec_mem_limit只限制爆炸半径:真正该降的是进入 Join 的数据量。
六、常见问题(FAQ)
Q:部分列更新为什么有时候会失败?
Unique 表需要开启 Merge-on-Write 才走这条路径。没开的话按整行覆盖处理,未指定的列会被写成默认值------这是"更新完发现余额清零"的直接原因。检查办法是 SHOW CREATE TABLE 看 enable_unique_key_merge_on_write 的值。另外要注意:只有写了这批字段的行会被更新,其余不在批次里的行不受影响;如果上游发来的 JSON 本身就缺字段,那属于数据源的问题,需要往上游追溯。上线前建议用一个测试表先跑一遍写入语义,确认未指定列是否被保留。
Q:怎么判断写入有没有真的走 Group Commit?
看写入返回的 label:以 group_commit 开头说明进入了合批路径。另外长期观察版本数,启用后如果版本仍在持续上涨,说明触发条件(默认 10 秒 / 128MB)与业务节奏不匹配。
Q:并发上来就超时,是内存不够吗?
多数情况是这类叠加造成的:版本堆积导致单次扫更多文件、单查询内存上限过低、写入与查询抢同一批资源。先把版本数压下来,再看资源组隔离(max_concurrency 与 max_queue_size 要一起配,否则并发打满会直接报错而不是排队)。
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 官方文档:Runtime Filter 参数与取值规则、查询 Profile 分析
- Apache Doris 官方文档:Colocation Join(建组条件、SHOW PROC '/colocation_group' 字段含义)
- Apache Doris 官方文档:Unique Key 模型、Merge-on-Write 与部分列更新
- Apache Doris 官方文档:Group Commit 手册(三种模式、提交触发条件默认值、返回 label 规则)