数据模型与 Join 性能:选错模型,查询慢十倍
说实话,Doris 的三种数据模型——Duplicate、Aggregate、Unique——很多同学一开始觉得不就是存储方式不一样嘛。我刚开始也这么想,直到有一次线上 Join 查询跑了 30 秒,改了个模型直接降到 1 秒。嗯,从那以后我再也不敢轻视这个选择了。
这一章咱们就聊聊:数据模型到底怎么影响 Join?Key 和 Value 怎么选?分区和分桶又能帮上什么忙?
三种数据模型对 Join 的影响
先看一张图,帮你快速建立整体认知:
Duplicate 模型:最灵活,但小心数据膨胀
Duplicate 模型说白了就是「原样存储」。你导入多少行,它就存多少行,不做任何聚合或去重。我个人的习惯是:只要没有明确的聚合需求,优先用 Duplicate。
对 Join 的影响:
- Join 时不需要额外的聚合计算,直接拿原始数据匹配
- 但如果数据量巨大,Shuffle 阶段会传输大量重复行
- 适合事实表、日志表这类明细数据
我的经验:有一次做用户行为分析,事实表用了 Aggregate 模型,结果 Join 维表时发现很多聚合后的数据对不上。后来改成 Duplicate,虽然存储多了 30%,但 Join 结果完全正确了。记住:明细查询 + 多表关联,首选 Duplicate。
Aggregate 模型:预聚合的威力
Aggregate 模型会在导入时按 Key 列做预聚合。Value 列需要指定聚合函数,比如 SUM、MAX、MIN、REPLACE 等。
对 Join 的影响:
- 数据量大幅减少——Join 时 Shuffle 的数据量也小了
- 但 Join 条件如果包含 Value 列,需要小心聚合语义
- 适合「先聚合、再关联」的场景,比如每日汇总表 Join 维度表
避坑指南:我曾经在 Aggregate 模型上做 Join,发现结果比预期小。查了半天,原来是 Join 条件里用了一个 Value 列,而那个列用了 REPLACE 聚合函数,导致部分数据被覆盖了。所以记住:Join 条件尽量只用 Key 列。
Unique 模型:主键去重的双刃剑
Unique 模型本质上是 Aggregate 模型的一个特例——它用 REPLACE 聚合函数实现主键去重。你想想看,如果一张表需要按主键更新,那 Unique 就是天然的选择。
对 Join 的影响:
- Join 时自动按主键去重,省去了手动去重的麻烦
- 但去重本身有开销,如果 Join 的右表很大,这个开销会放大
- 适合「小表 Join 大表」且小表需要去重的场景
注意:Unique 模型在批量导入时,如果同一个主键有多条记录,只有最后一条会保留。如果你需要保留历史变更,那就别用 Unique,改用 Duplicate + 时间戳。
Key 与 Value 的选择策略
很多同学问我:Key 和 Value 到底怎么分?其实规则很简单:
| 角色 | 特点 | Join 中的影响 |
|---|---|---|
| Key 列 | 排序、去重、分区的依据 | Join 条件尽量用 Key 列,效率最高 |
| Value 列 | 存储指标数据,可聚合 | Join 时尽量避免用 Value 列做关联 |
我的选择原则:
- Join 条件中出现的字段,一定要放在 Key 里
- 过滤条件中高频出现的字段,也放 Key 里
- 指标类字段(金额、数量、分数)放 Value
- 文本描述类字段(名称、备注)放 Value
举个例子:订单表 Join 用户表,订单表的 user_id 是 Join 条件,必须放在 Key 里。如果放在 Value 里,Join 时 Doris 需要扫描全表,性能直接崩掉。我见过一个案例,就因为 Key 放错了,查询从 200ms 变成了 8s。
分区与分桶对 Join 的优化作用
分区和分桶,说白了就是「把数据切小」。切得越合理,Join 时扫描的数据就越少。
分区:按时间或地域裁剪数据
分区最常见的用法是按时间。比如每天一个分区,Join 时只扫描最近 7 天的数据。
-- 按天分区
CREATE TABLE order_detail (
order_id BIGINT,
user_id BIGINT,
order_date DATE,
amount DECIMAL(18,2)
) ENGINE=OLAP
DUPLICATE KEY(order_id, user_id)
PARTITION BY RANGE(order_date) (
PARTITION p202401 VALUES LESS THAN ('2024-02-01'),
PARTITION p202402 VALUES LESS THAN ('2024-03-01'),
PARTITION p202403 VALUES LESS THAN ('2024-04-01')
)
DISTRIBUTED BY HASH(user_id) BUCKETS 16;
分区对 Join 的优化:
- 如果两张表按相同字段分区,Join 时可以做分区裁剪
- 分区裁剪能减少 90% 以上的数据扫描量
- 我建议:事实表和维表尽量按相同粒度分区
分桶:让数据在节点间均匀分布
分桶是 Doris 的「分布式切分」手段。分桶键的选择直接影响 Join 时的数据倾斜。
| 分桶策略 | 适用场景 | Join 效果 |
|---|---|---|
| 按 Join 条件分桶 | 两表 Join 条件相同 | 可做 Bucket Shuffle,避免数据移动 |
| 按高基数字段分桶 | 数据分布均匀 | 避免数据倾斜 |
| 按低基数字段分桶 | 数据量小 | 容易产生热点 |
我的经验:如果两张表经常按 user_id 做 Join,那两张表的分桶键都设为 user_id。这样 Doris 可以在本地完成 Join,不需要跨节点传输数据。这个优化叫 Bucket Shuffle Join,性能提升非常明显。
实战建议:模型 + 分区 + 分桶的组合策略
说了这么多,到底怎么选?我整理了一个决策流程:
- 先看数据用途:明细查询用 Duplicate,聚合报表用 Aggregate,主键更新用 Unique
- 再看 Join 频率:高频 Join 的字段放 Key,低频的放 Value
- 再看数据量:大表按时间分区,小表可以不做分区
- 最后看分桶:Join 条件字段做分桶键,桶数设为节点数的 3~5 倍
记住一句话:模型选对,事半功倍;模型选错,查询受罪。我见过太多因为模型选错导致 Join 慢的案例,改个模型就解决了。所以,花 10 分钟想清楚模型,能省下后面 10 小时的调优时间。