Doris architecture

ANALYTICAL DATABASE / SOURCE READING / LESSON 17

Table design and data modelling

Translate business access patterns into Doris data models, sort keys, partitioning, bucketing, indexes, and colocate choices.

Reading
55 min
Track
Doris architecture
Source
Chinese source notes

The source notes for this track are currently maintained in Chinese.

预计阅读时间: 55 分钟 前置阅读: doc-00, doc-06, doc-08 下一次阅读: doc-18(慢 SQL 与 Profile 调优)


1. 为什么专家必须先会建模

会读查询链路和存储格式, 还不等于能把 Doris 用好。生产环境里最常见的性能问题, 很多不是优化器或执行引擎的 bug, 而是表设计阶段已经埋下了代价:

  • 分区列没有出现在高频查询条件里, 导致扫描大量无关 Partition。
  • Bucket 太多, FE 调度开销和小文件数量上升。
  • Bucket 太少, Scan 并行度被锁死。
  • Sort Key 把低频字段放在前面, Prefix Index 过滤收益很低。
  • Unique Key 被当成万能 Upsert 表, 导入、Compaction 和查询都被放大。
  • 文本检索、半结构化字段、明细查询都塞进同一张宽表, 索引和治理边界混乱。

Doris 表设计不是把 MySQL 表结构照搬过来, 而是在四个问题之间取平衡:

  1. 数据如何写入: 追加、更新、聚合还是 CDC。
  2. 查询如何过滤: 时间、租户、业务主键、维度组合。
  3. 数据如何分布: Partition 管生命周期, Bucket 管并行和均衡。
  4. 热点如何加速: Sort Key、Prefix Index、Bloom Filter、Bitmap、Inverted Index、物化视图。

Apache Doris 官方 POC 文档也把建模放在第一步: 数据模型、排序键、分区和分桶会直接决定后续导入和查询性能。

参考:


2. 三种主模型的选择

Duplicate Key: 明细事实表的默认选择

Duplicate Key 保留所有行, 适合日志、事件、订单流水、行为明细、Trace Span、指标样本等追加型数据。

CREATE TABLE event_log
(
    event_time DATETIME NOT NULL,
    tenant_id BIGINT NOT NULL,
    service_name VARCHAR(64),
    event_type VARCHAR(64),
    trace_id VARCHAR(64),
    message STRING
)
DUPLICATE KEY(event_time, tenant_id, service_name)
AUTO PARTITION BY RANGE(date_trunc(event_time, 'day')) ()
DISTRIBUTED BY HASH(tenant_id) BUCKETS 24
PROPERTIES (
    "replication_num" = "3"
);

判断标准:

问题如果答案是 yes
同一个业务主键是否允许多条历史记录Duplicate Key
查询是否主要按时间范围扫描Duplicate Key + 时间分区
是否更关心导入吞吐和扫描性能Duplicate Key

Unique Key: CDC 与当前状态表

Unique Key 表表达"同一个 Key 只保留最新值"。它适合用户画像、设备状态、订单当前状态、账户余额等更新型数据。

CREATE TABLE user_profile
(
    user_id BIGINT NOT NULL,
    tenant_id BIGINT NOT NULL,
    status TINYINT,
    level_name VARCHAR(32),
    updated_at DATETIME
)
UNIQUE KEY(user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
    "enable_unique_key_merge_on_write" = "true",
    "replication_num" = "3"
);

Unique Key 的专家判断不是"能不能更新", 而是"更新代价能不能被业务接受":

  • 高频更新会增加写入、Compaction 和版本管理压力。
  • 如果主键分布倾斜, 部分 Tablet 会成为热点。
  • 如果查询经常按非主键过滤, 需要补 Sort Key、索引或物化视图思路。
  • 如果只需要保留历史流水, 不要强行使用 Unique Key。

Aggregate Key: 预聚合指标表

Aggregate Key 适合写入时就能合并的指标, 比如 PV、UV 近似值、总金额、最大/最小值。

CREATE TABLE site_metric_daily
(
    dt DATE NOT NULL,
    site_id BIGINT NOT NULL,
    channel VARCHAR(32) NOT NULL,
    pv BIGINT SUM DEFAULT "0",
    uv HLL HLL_UNION,
    amount DECIMAL(18, 2) SUM DEFAULT "0"
)
AGGREGATE KEY(dt, site_id, channel)
AUTO PARTITION BY RANGE(date_trunc(dt, 'day')) ()
DISTRIBUTED BY HASH(site_id) BUCKETS 16;

使用边界:

  • 聚合函数必须是业务语义的一部分, 不是为了省存储硬凑。
  • 明细追溯需求强时, 保留明细表, 再通过物化视图或异步任务生成聚合表。
  • UV 这类指标要提前决定精确 Bitmap 还是近似 HLL。

3. Sort Key 与 Prefix Index

Doris 的排序键决定数据在 Segment 内的组织顺序, Prefix Index 会基于排序键前缀加速定位。真正的设计规则不是"把主键放前面", 而是:

  1. 高频过滤列靠前。
  2. 固定长度类型优先, 例如 DATE、INT、BIGINT。
  3. 高选择性列优先, 但不要破坏时间裁剪。
  4. VARCHAR 放太前会让 Prefix Index 早早截断。
  5. 没进入前缀范围的字段, 用 Bloom Filter、Bitmap 或 Inverted Index 补位。

示例:

-- 常见查询:
-- WHERE event_time >= ? AND event_time < ? AND tenant_id = ? AND service_name = ?

DUPLICATE KEY(event_time, tenant_id, service_name)
AUTO PARTITION BY RANGE(date_trunc(event_time, 'day')) ()
DISTRIBUTED BY HASH(tenant_id) BUCKETS 24;

如果业务更常按租户看近期数据:

DUPLICATE KEY(tenant_id, event_time, service_name)

区别:

排序键适合查询风险
(event_time, tenant_id)全站时间范围分析单租户查询可能扫描更多块
(tenant_id, event_time)租户隔离报表全局时间报表可能不如前者
(trace_id, event_time)Trace 点查时间聚合不适合作为主表排序

专家做法通常是: 主明细表服务最主要的扫描模式, 其他查询通过索引、Rollup、物化视图或字典表补齐。


4. Partition: 生命周期与裁剪

Partition 的第一职责是管理数据生命周期, 第二职责才是查询裁剪。日志、指标、行为事件一般按时间分区。

自动分区

AUTO PARTITION BY RANGE(date_trunc(event_time, 'day')) ()

适合:

  • 数据源不断产生新日期。
  • 不希望人工提前创建分区。
  • POC 或早期业务阶段。

动态分区

动态分区按规则滚动创建和删除分区, 常用于 TTL。

PROPERTIES (
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "DAY",
    "dynamic_partition.start" = "-30",
    "dynamic_partition.end" = "3",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.buckets" = "24"
);

但官方文档也提示, Auto Partition 是动态分区的后继能力。新表优先考虑 Auto Partition, 老系统再评估是否保留 Dynamic Partition。

分区设计反例

设计问题
按用户 ID 分区分区数量不可控, 生命周期难管理
按小时分区但数据量很小Tablet 和小文件过多
按月分区但每天查询每次查询扫大量无关数据
查询条件没有分区列Partition Pruning 失效

5. Bucket: 并行度与均衡

Bucket 决定 Tablet 数量和数据分布。它直接影响三个东西:

  1. 查询并行度。
  2. 导入并发。
  3. FE/BE 元数据和调度开销。

一个实用估算:

单分区压缩后数据量 / 目标单 Tablet 大小 = bucket 数

经验目标:

  • Duplicate Key: 单 Tablet 压缩后通常控制在 1GB 到 20GB。
  • Unique Key: 更新和 Compaction 更敏感, 目标更保守。
  • Bucket 数通常取 BE 数量的整数倍。
  • 不要为了"更并行"把每个小分区切成几百个 Bucket。

检查命令:

SHOW TABLETS FROM event_log;
SHOW PARTITIONS FROM event_log;

关注:

  • 同一分区下 Tablet 大小是否接近。
  • 是否存在单个 Tablet 明显大于其他 Tablet。
  • 是否存在大量很小 Tablet。
  • 扩容后新旧数据是否均衡。

6. Colocate 与 Join 建模

如果两张大表经常 Join, 且 Join Key 稳定, 可以考虑 Colocate Group, 让相同 Key 的 Bucket 落在相同 BE, 减少 Shuffle。

CREATE TABLE orders
(
    order_id BIGINT,
    user_id BIGINT,
    dt DATE,
    amount DECIMAL(18, 2)
)
DUPLICATE KEY(order_id, user_id)
PARTITION BY RANGE(dt) (...)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
    "colocate_with" = "user_group"
);

CREATE TABLE user_profile
(
    user_id BIGINT,
    city VARCHAR(64),
    level_name VARCHAR(32)
)
UNIQUE KEY(user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
    "colocate_with" = "user_group"
);

适合:

  • Join Key 相同。
  • Bucket 数一致。
  • 大表之间 Join 频繁。
  • 集群节点稳定, 不频繁扩缩容。

不适合:

  • 小维表 Join, 直接 Broadcast 更简单。
  • Join Key 经常变化。
  • 表生命周期不同, 经常重建。

7. 建模评审清单

上线前, 至少回答这些问题:

维度问题
写入是追加、Upsert、聚合还是 CDC
查询Top 10 SQL 的 WHERE、JOIN、GROUP BY 是什么
分区分区列是否出现在高频查询条件中
分桶Bucket 数是否匹配 BE 数和单分区数据量
排序Sort Key 是否服务最高频过滤路径
索引Prefix Index 覆盖不到的列是否需要补索引
变更分区列、分桶列、Key 列未来是否稳定
运维Tablet 数、版本数、Compaction 压力是否可控

一句话总结:

Doris 专家的建模能力, 是把业务访问模式翻译成 Partition、Bucket、Sort Key、索引和物化视图之间的组合, 而不是单独背某一种建表语法。