数据模型与 Join 性能:选错模型,查询慢十倍

说实话,Doris 的三种数据模型——Duplicate、Aggregate、Unique——很多同学一开始觉得不就是存储方式不一样嘛。我刚开始也这么想,直到有一次线上 Join 查询跑了 30 秒,改了个模型直接降到 1 秒。嗯,从那以后我再也不敢轻视这个选择了。

这一章咱们就聊聊:数据模型到底怎么影响 Join?Key 和 Value 怎么选?分区和分桶又能帮上什么忙?

三种数据模型对 Join 的影响

先看一张图,帮你快速建立整体认知:

Doris 三种数据模型与 Join 性能关系 Duplicate 模型 保留所有原始数据 无聚合、无去重 Aggregate 模型 按 Key 预聚合 Value 列使用聚合函数 Unique 模型 主键去重 本质是 Aggregate 特例 对 Join 的核心影响 数据模型决定 Join 时是否需要实时聚合 / 去重,直接影响 Shuffle 数据量和计算开销 推荐场景 日志明细查询 Join 时无聚合需求 保留所有原始行 推荐场景 报表聚合查询 Join 前已预聚合 减少 Join 数据量 推荐场景 主键更新场景 Join 时按主键去重 适合小表 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,性能提升非常明显。

实战建议:模型 + 分区 + 分桶的组合策略

说了这么多,到底怎么选?我整理了一个决策流程:

  1. 先看数据用途:明细查询用 Duplicate,聚合报表用 Aggregate,主键更新用 Unique
  2. 再看 Join 频率:高频 Join 的字段放 Key,低频的放 Value
  3. 再看数据量:大表按时间分区,小表可以不做分区
  4. 最后看分桶:Join 条件字段做分桶键,桶数设为节点数的 3~5 倍

记住一句话:模型选对,事半功倍;模型选错,查询受罪。我见过太多因为模型选错导致 Join 慢的案例,改个模型就解决了。所以,花 10 分钟想清楚模型,能省下后面 10 小时的调优时间。