第 08 关 · ★★★

表设计四件套

排序键、分区、分桶与副本:写出一张能过评审的生产表。

已点亮 · 最佳 分

表设计四件套:排序键、分区、分桶与副本

Day 7 决定相同 Key 写入后留什么,今天决定数据在集群里怎么摆

物理层级前缀索引Dynamic / Auto 分区Bucket 估算副本与故障域
1 / 32

Day 8|表设计四件套:排序键、分区、分桶与副本

Day 8 学习总览
Day 8 学习总览

Day 7 解决了一个基础问题:相同业务 Key 再次写入时,Doris 应该保存全部明细、按照聚合规则合并,还是维护一条当前状态。今天继续向下走一层,讨论数据在集群中的实际组织方式。

一张表创建完成以后,数据会持续写入很多年。查询习惯会逐渐稳定,数据量会从 GB 增长到 TB、PB,节点会扩容,磁盘会发生故障,历史数据会进入冷存储。排序键、分区、分桶、副本和 Tablet 数量,会在整个生命周期里持续影响以下结果:

  • 一条查询需要扫描多少数据;
  • 多个 BE 能否同时参与计算;
  • 写入任务会生成多少 Rowset 与 Segment;
  • FE 需要维护多少元数据;
  • Compaction、Balance 和副本修复需要承担多少工作;
  • 单机、磁盘或机房发生故障时,数据还能否继续服务;
  • 未来调整结构时,需要一次轻量变更,还是完整迁移。

表结构设计因此是一项长期工程决策。一个参数看起来只占 DDL 中的一行,后续可能影响几十亿行数据和数百台机器。

本日学习目标

完成本篇后,你应当能够:

  1. 解释 Table → Partition → Tablet → Replica → Rowset → Segment 的层级关系;
  2. 说明排序键、Key 列和前缀索引之间的联系;
  3. 根据查询窗口、增长速度和数据留存周期选择分区粒度;
  4. 区分 Range、List、Dynamic Partition 与 Auto Partition;
  5. 根据等值过滤、Join Key、数据基数和写入模式选择 Hash 或 Random 分桶;
  6. 估算 Bucket、Tablet 与 Replica 数量,并识别元数据膨胀风险;
  7. 使用 replication_numreplication_allocation 设计生产副本策略;
  8. 通过 EXPLAINSHOW PARTITIONSSHOW TABLETS 和副本状态命令验证设计结果;
  9. 完成三类典型业务表的 DDL,并形成可评审的设计说明。

实验前提

本篇沿用 Day 5 的单 FE、单 BE 学习环境。实验表统一设置 replication_num = 1。生产集群通常使用三副本或符合企业容灾要求的标签化副本策略。

版本口径

截至 2026-08-16,Apache Doris 官网将 4.1.3 标记为 Latest,将 4.0.8 标记为 Stable。本文以 4.x 通用能力为主。Storage Format V3 等 4.1 能力会单独标明版本范围。

资料口径说明

原培训材料第 111、117、118 页建立了“分区负责管理、分桶负责物理分布”的基础框架,并给出了 Range/List、动态分区和 Bucket 初始估算方法。当前 4.x 官方文档新增或强化了 Auto Partition、Random Bucketing、BUCKETS AUTO、Tablet 元数据上限和更完整的分桶建议。本文保留原有教学主线,同时使用当前官方口径更新参数与边界。


一、表结构设计需要完成五个相互联动的决定

五个相互联动的决定
五个相互联动的决定

Doris 建表时最容易被忽略的一点,是各项设计并不独立。一个字段可以同时参与数据模型、排序、分区和分桶,但它在每个位置承担的职责不同。

1. 排序键:决定 Tablet 内部怎样排列

Doris 的 Segment 文件内部按排序键组织数据。合理的排序顺序能够形成更好的数据局部性,让前缀索引、ZoneMap、压缩、范围过滤和局部聚合发挥作用。

排序键回答的问题是:在同一个 Tablet 内,哪些行应该彼此靠近。

2. 分区:决定数据的管理边界

分区通常依据日期、时间或有限枚举值划分。每个分区可以独立创建、删除、归档、设置存储策略,也可以在查询规划阶段被整体裁剪。

分区回答的问题是:哪些数据可以作为一个整体管理和跳过。

3. 分桶:决定数据怎样展开到多个 Tablet

每个分区继续被切成若干 Bucket,每个 Bucket 对应一个 Tablet。分桶数量决定了单分区的并行扫描上限,也影响数据在 BE 之间的均衡程度。

分桶回答的问题是:一个分区需要多少个并行工作单元,以及每一行应该进入哪个单元。

4. 副本:决定故障后的服务能力

每个 Tablet 可以拥有多份 Replica。副本分布在不同 BE、磁盘或资源标签上,后台调度器负责健康检查、修复和均衡。

副本回答的问题是:某台机器或某块磁盘失效以后,哪份数据继续提供服务。

5. 表属性:决定长期运行方式

表属性会控制副本数、更新模式、存储格式、压缩、冷却策略、动态分区、Bloom Filter 和其他行为。属性数量很多,生产环境需要保持克制:每个属性都应对应明确的业务目标和验证证据。

6. 推荐的设计顺序

一套更稳定的设计顺序如下:

  1. 明确数据模型和相同 Key 的写入语义;
  2. 收集真实查询条件、Join 关系、TopN 排序和数据留存要求;
  3. 选择管理边界,也就是分区字段与粒度;
  4. 选择物理分布方式,也就是分桶列与 Bucket 数量;
  5. 设计排序键,让分区内部的数据布局贴近高频查询;
  6. 按可用性目标设置副本和故障域;
  7. 增加确有必要的表属性与索引;
  8. 使用真实数据、真实并发和真实写入节奏完成验证。

这套顺序可以避免两个常见问题:先按照经验写 DDL,随后强迫业务查询适应表结构;或者只追求单条 SQL 的最快结果,忽略写入、元数据、Compaction 和容灾成本。


二、先看清物理层级:Table、Partition、Tablet 与 Replica

Doris 数据组织层级
Doris 数据组织层级

从业务用户视角看,Doris 中只有数据库和表。进入存储层以后,一张表会被展开成多级对象:

Database
└── Table
    ├── Partition 1
    │   ├── Tablet 1
    │   │   ├── Replica on BE-1
    │   │   ├── Replica on BE-2
    │   │   └── Replica on BE-3
    │   ├── Tablet 2
    │   └── ...
    ├── Partition 2
    └── ...

每个 Tablet 内继续包含多个 Rowset;每次导入事务或 Compaction 会形成新的 Rowset;Rowset 下面的 Segment 是实际列式文件。

1. Partition 是逻辑管理单元

一行数据先根据分区表达式找到唯一分区。Range 分区通过范围边界定位,List 分区通过枚举集合定位。查询到来时,FE 根据谓词裁剪无关分区,剩余分区才会进入后续调度。

没有显式写 PARTITION BY 时,Doris 会为表创建一个对用户透明的默认分区。小型维表可以使用默认分区;持续增长的大表通常需要显式规划生命周期。

2. Bucket 与 Tablet 一一对应

一个分区拥有 N 个 Bucket,也就拥有 N 个基础 Tablet。Hash 分桶根据分桶列计算哈希值,Random 分桶按随机或批次策略分配。Tablet 是数据迁移、复制、平衡、修复和并行扫描的重要单位。

3. Replica 是 Tablet 的物理副本

三副本表中,一个 Tablet 通常有三份 Replica,放在不同 BE 上。查询调度会选择健康副本参与扫描;后台 TabletChecker 和 TabletScheduler 持续检查健康状态,并在副本缺失时触发修复。

4. 数量如何计算

只看基础索引时:

基础 Tablet 数 = 分区数 × 每分区 Bucket 数
物理 Replica 数 = 基础 Tablet 数 × 副本数

如果表拥有同步 Rollup 或同步物化索引,每个索引也需要对应的 Tablet 与副本,实际数量会继续增加。因此,评审 Bucket 数时需要同时查看分区数、同步索引数和副本数。

举例:

12 个按月分区 × 32 Buckets = 384 个基础 Tablets
384 Tablets × 3 副本 = 1,152 个物理 Replicas

再增加两个同步 Rollup 后,Tablet 与 Replica 的总规模会继续放大。FE 要维护更多元数据,BE 要处理更多版本、文件、Compaction 与修复任务。

5. Tablet 数量为什么需要控制

当前 4.x 官方文档给出了一组工程边界:单个 BE 承载的 Tablet 数建议低于 20,000;每分区 Bucket 数通常建议低于 128;单 Tablet 数据量常以 1–10 GB 为经验目标。极大规模集群需要结合硬件、表数量、索引数量和写入节奏重新测算。

Tablet 太少会带来以下问题:

  • 大表集中在少量 BE,其他节点空闲;
  • 单查询并行度不足;
  • 单 Tablet 文件和 Compaction 负担过重;
  • 扩容后新节点难以充分参与既有查询。

Tablet 太多同样会产生代价:

  • FE 元数据量持续上升;
  • 每次小批写入分散到大量 Tablet,生成更多小 Rowset;
  • Compaction、Balance 与副本修复任务增多;
  • 建表、Schema Change 和集群恢复时间变长;
  • 高并发写入需要打开更多文件和连接。

表设计的目标是让 Tablet 数量与业务规模匹配,保留合理并行度,同时控制长期运维成本。


三、排序键:先安排 Tablet 内部的数据局部性

排序键与前缀索引
排序键与前缀索引

Doris 将数据按 Sort Key 有序存储。三种数据模型的排序键来源不同:

数据模型 排序键来源
Duplicate Key DUPLICATE KEY(...) 中的列
Aggregate Key AGGREGATE KEY(...) 中的列
Unique Key UNIQUE KEY(...) 中的列

Key 在 Day 7 中已经承担了明细、聚合或主键语义;今天需要继续考虑它的物理排序效果。

在 Doris 建表语法中,Key 列需要位于字段列表前部,字段顺序应与 Key 定义保持一致。Value 列放在 Key 列之后。这项规则既影响 DDL 是否能创建,也让排序语义在 Schema 中保持清晰。

1. 前缀索引怎样工作

Doris 会自动基于排序键构建稀疏 Prefix Index。每个逻辑数据块保留一条索引项,索引内容来自该数据块第一行排序键的前缀,最长为 36 字节。索引规模较小,可以常驻内存,查询通过二分定位快速找到可能命中的数据块。

前缀索引适合:

  • col = value
  • col IN (...)
  • col > valueBETWEEN 等范围查询;
  • 从排序键最左侧开始连续命中的组合条件。

例如排序键为:

(event_date, user_id, event_type, event_time)

以下条件可以较好地利用前缀:

WHERE event_date = '2026-08-16'
  AND user_id = 1001

只写 WHERE user_id = 1001 会跳过最左侧的 event_date,前缀导航能力明显减弱。分区裁剪可能已经缩小日期范围,但 Tablet 内的排序仍需通过真实 Profile 验证。

2. 36 字节与 VARCHAR 截断规则

前缀索引累计排序列的前 36 字节。遇到 VARCHAR 时会在该列结束位置截断,后续列不再进入前缀索引。第一列就是短 VARCHAR 时,即使没有用满 36 字节,后面的整型或时间列也无法进入这一组前缀。

因此,常见设计建议包括:

  • 高频等值或范围过滤列靠前;
  • 固定长度的 BIGINTINTDATEDATETIME 优先于长字符串;
  • VARCHAR 不宜过早出现;
  • 排序列数量保持克制,通常围绕最核心的两到四个过滤方向设计;
  • 时间分区已经完成粗裁剪后,排序键可以进一步优化分区内部的服务名、用户、地区或事件时间过滤。

3. 低基数列应该放在前面吗

答案取决于查询组合。低基数列放在前部可以让同类数据聚集,提高压缩和局部扫描效果;它的选择性有限,单独过滤时仍可能读取大量数据。高基数列定位更精确,但数据写入和 Join 设计也需要同步考虑。

评审时可以用三个问题判断:

  1. 该列出现在多少比例的查询中?
  2. 它通常单独使用,还是与左侧列一起使用?
  3. 过滤后预计保留多少行?

4. 一张表只有一组排序键

当业务存在多套完全不同的高频过滤路径时,可以考虑:

  • 为其他列增加倒排索引;
  • 创建列顺序不同的同步物化视图;
  • 为隔离明显的工作负载建立独立服务表;
  • 通过异步物化视图形成面向应用的汇总层。

5. 如何验证排序键效果

使用 EXPLAIN 只能看到整体 Scan 结构,最终效果应结合 Query Profile。Prefix Index 相关指标中,RowsKeyRangeFiltered 可以反映排序键范围过滤的贡献。还要同时观察:

  • RowsReadRowsReturned
  • ZoneMap、Bloom Filter、倒排索引分别过滤了多少行;
  • Scan 的 I/O、解压和 CPU 时间;
  • 同一查询在冷缓存与热缓存下的差异。

四、分区:围绕查询窗口与生命周期划定边界

分区模式全景
分区模式全景

分区是粗粒度的数据管理机制。它最适合表达稳定、可解释、可批量管理的边界,例如日期、月份、地区、业务线和有限租户组。

1. Range Partition

Range 按连续范围划分,时间数据最常使用。一个月一个分区的示例:

PARTITION BY RANGE(order_date) (
    PARTITION p202607 VALUES ["2026-07-01", "2026-08-01"),
    PARTITION p202608 VALUES ["2026-08-01", "2026-09-01"),
    PARTITION p202609 VALUES ["2026-09-01", "2026-10-01")
)

它适合:

  • 订单、行为、日志、指标等随时间增长的事实数据;
  • 查询普遍带有明确时间范围;
  • 需要按天、月或季度删除历史数据;
  • 冷热分层和备份策略与时间相关。

2. List Partition

List 按枚举值集合划分:

PARTITION BY LIST(region) (
    PARTITION p_cn VALUES IN ("CN"),
    PARTITION p_us VALUES IN ("US"),
    PARTITION p_eu VALUES IN ("DE", "FR", "IT", "ES")
)

它适合有限且稳定的离散维度。地区、业务线或少量监管域可以采用 List。用户 ID、设备 ID、订单 ID 等高基数值不适合作为一值一分区,否则分区数量会快速失控。

3. 分区粒度怎样确定

分区粒度需要同时考虑四组数据:

查询窗口

多数查询看最近一天,按天分区有利于准确裁剪;多数查询按月汇总,月分区通常更简洁。小时级分区只适合吞吐极高、查询窗口很短、留存策略也按小时执行的场景。

单分区数据量

分区太小会增加分区元数据和写入分散度;分区太大会导致删除、迁移、备份和扫描范围过粗。可以先估算压缩后体积,再结合 Bucket 数量控制单 Tablet 大小。

写入集中度

一次导入同时触达大量历史分区,会在许多 Tablet 上产生小 Rowset,放大 Compaction 压力。实时流通常集中写当前少量分区;大范围历史回补应控制批次和分区跨度。

生命周期

保留 30 天日志、保留 25 个月经营数据、保留 7 年财务明细,对应不同分区粒度和归档策略。删除过期分区属于元数据操作,成本远低于在大表上逐行执行条件删除。

4. 分区列的约束

分区列必须属于 Key 列。Unique Key 表中,分区列还必须是 Unique Key 的子集,否则同一主键可能进入不同分区,破坏去重语义。

这项约束会影响“当前状态表”的设计。订单主键只有 order_id 时,直接按 create_date 分区会迫使日期进入 Unique Key,主键语义随之变化。常见处理方法包括:

  • 当前状态表保持单分区,通过合理分桶承载;
  • 业务主键天然包含租户或日期时,将这些字段纳入完整主键;
  • 历史事件进入按时间分区的 Duplicate 表,当前状态进入独立 Unique 表;
  • 超大状态表在 POC 中验证分区、主键和更新语义后再落地。

5. 分区裁剪怎样验证

查询条件尽量直接作用于分区列:

WHERE event_time >= '2026-08-16 00:00:00'
  AND event_time <  '2026-08-17 00:00:00'

复杂函数、隐式类型转换和非等价表达式可能降低裁剪能力。运行:

EXPLAIN SELECT ...;

关注 Scan 节点中的:

partitions=1/365

它表示 365 个分区只保留了 1 个。实际字段名称随版本和 Explain 模式有所差异,判断原则保持一致:扫描分区数应符合业务时间范围。


五、Dynamic Partition 与 Auto Partition

Dynamic 与 Auto Partition
Dynamic 与 Auto Partition

两种能力都能自动创建分区,驱动逻辑不同。

1. Dynamic Partition:按照当前时间滚动

Dynamic Partition 由调度器周期执行,根据当前日期和配置偏移量创建未来分区、删除过期分区。它适合实时连续写入和固定时间窗口管理。

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" = "16"
)

以上配置表达:保留过去 30 天的时间窗口,并预创建未来 3 天分区。具体保留边界、历史分区数量和系统参数应在目标版本中验证。

Dynamic Partition 的主要限制包括:

  • 只支持 DATE / DATETIME 上的 Range 分区;
  • 只支持单分区键;
  • 它围绕系统当前时间推进,历史回补和离散枚举值并不自然;
  • dynamic_partition.buckets 会覆盖 DISTRIBUTED 中的 Bucket 数,需要避免两处配置不一致。

2. Auto Partition:按照实际写入值创建

Auto Partition 在写入时检查分区值。目标分区不存在时,Doris 自动创建它。

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

Auto Range 支持 DATEDATETIME;Auto List 可按照一个或多个枚举列创建分区。它适合:

  • 历史数据回补;
  • 数据日期与当前时间无关;
  • 分区值难以提前枚举;
  • 离散租户、地区或业务值按需出现;
  • 手工创建分区的工作量过大。

当前 4.x 官方文档将 Auto Partition 作为自动分区管理的推荐主线,并将其视为 Dynamic Partition 的后续方案。Dynamic 的 TTL 与未来分区预创建仍然具备明确价值。

3. 生命周期优先使用 retention_count

技术上,Auto Partition 与 Dynamic Partition 仍可组合;当前 4.x 官方文档已经不再推荐新项目采用这条组合。AUTO RANGE 表可以使用 partition.retention_count 保留分区值最大的 N 个历史分区,当前和未来分区继续保留。

PROPERTIES (
    "partition.retention_count" = "30"
)

“保留 30 天”需要先确定当天是否计入。每天一个分区、当天计入 30 天窗口时,可以保留 29 个历史分区并加上当前分区。存在未来数据、跨时区写入和迟到数据时,还要单独核对边界。已有 Dynamic Partition 表运行稳定时,可以继续维护;迁移到 Auto Partition 前应验证回补、回收、分区命名和下游任务兼容性。

4. 自动化也需要治理

自动建分区降低了人工成本,也可能放大脏数据:

  • 异常年份 2099-01-01 会创建意外分区;
  • 空格、大小写或编码差异会生成多个 List 分区;
  • 高基数租户值会导致分区爆炸;
  • 回补任务可能在短时间创建大量分区和 Tablet;
  • 保留策略缺失会让历史分区持续增长。

因此需要在导入前设置日期范围、枚举白名单、数据质量检查和保留规则,并为分区数量配置监控告警。


六、分桶:为数据均衡、并行扫描和 Join 协同提供基础

Hash 分桶与 Tablet 分布
Hash 分桶与 Tablet 分布

分区解决“读哪些时间或业务范围”,分桶解决“这些数据由多少个工作单元并行处理”。

1. Hash Bucketing

DISTRIBUTED BY HASH(user_id) BUCKETS 32

Doris 对分桶列计算哈希值并映射到一个 Bucket。Hash 适合以下情况:

  • 存在高频等值过滤,如 user_id = ?order_id IN (...)
  • 多张大表经常按同一 Key Join;
  • 分桶列基数高、数据分布相对均匀;
  • 希望通过 Bucket Pruning 只扫描少量 Tablet;
  • 需要为 Bucket Shuffle Join 或 Colocate Join 建立一致布局。

当查询包含 Hash 分桶列的等值条件时,FE 可以直接计算目标 Bucket。EXPLAIN 中可能看到:

tablets=1/32

范围条件通常无法定位到单一 Hash Bucket,因为连续业务值经过哈希后会分散到不同 Tablet。

2. Random Bucketing

DISTRIBUTED BY RANDOM BUCKETS 32

Random 将数据分散到多个 Bucket,不依赖业务列。当前 4.x POC 指南将其作为 Duplicate Key 追加型表的常用选择,特别适合缺少稳定等值过滤列、写入均衡优先的日志和事件数据。

Random 的特点:

  • 不会因为某个业务 Key 过热而集中到单一 Bucket;
  • 不提供基于业务列的 Bucket Pruning;
  • 不能依靠分桶列完成 Colocate 对齐;
  • 适用范围受表模型限制,当前主要面向 Duplicate Key;
  • 可结合 load_to_single_tablet 优化小批写入,但需要评估数据均衡和后续查询。

3. 分桶列怎样选择

好的 Hash 分桶列通常具备:

  • 较高基数;
  • 分布稳定,没有少数值占据大部分数据;
  • 经常出现在等值过滤或 Join 条件中;
  • 写入时能够稳定获得;
  • 未来业务扩展后仍能保持均匀。

需要谨慎的列包括:

  • 性别、状态、渠道等低基数列;
  • 少数超级客户占据大部分数据的租户列;
  • 大量 NULL 或默认值的列;
  • 经常发生语义变化的临时字段;
  • 只在极少数查询中使用的高基数列。

复合 Hash 分桶可以改善单列倾斜,例如 HASH(tenant_id, order_id)。代价是只有完整或可推导的组合条件才能获得更精准的 Bucket Pruning,Join 对齐也需要保持相同列顺序和 Bucket 数量。

4. 分桶与 Join 的关系

两张表使用相同分桶列、Bucket 数量和 Colocate Group 时,相同 Key 的数据可以被放到同一组 BE,Join 有机会避免跨节点 Shuffle。这个策略适合稳定、高频、数据规模较大的 Join。

它也会增加约束:

  • 多张表需要共同维护 Bucket 布局;
  • 扩容与再平衡需要整体考虑;
  • Bucket 数早期估算过小会限制长期并行度;
  • 数据倾斜会同时影响多张表。

因此,Colocate 属于在真实负载证明 Join Shuffle 成本较高后使用的专项优化,不宜在所有表上默认开启。


七、Bucket 数怎样估算

Bucket 与 Tablet 规模估算
Bucket 与 Tablet 规模估算

原培训材料给出了“总数据量 ÷ 副本数 ÷ 1GB”的初始公式。这个公式便于课堂快速理解,但在生产设计中需要进一步修正。副本数决定物理存储总量,每份 Replica 都保存完整 Tablet;Bucket 数主要由单分区单份逻辑数据量和目标 Tablet 大小决定。

更适合 4.x 的估算流程如下。

1. 估算单分区压缩后数据量

假设订单明细每天原始数据 300 GB,Doris 压缩后约 80 GB。按天分区时,单分区压缩量约为 80 GB。

2. 选择目标 Tablet 大小

官方文档给出了两组相互补充的经验:

  • 基础概念文档建议单 Tablet 约 1–10 GB;
  • POC 指南建议普通表压缩后单 Bucket 不超过 20 GB,Unique Key 表不超过 10 GB。

生产初始设计可以把 5–10 GB 作为常见目标,再根据数据模型、磁盘、查询并行度和写入压力调整。

3. 得到初始 Bucket 数

初始 Bucket 数 = ceil(单分区压缩数据量 ÷ 目标 Tablet 大小)

例如:

80 GB ÷ 5 GB ≈ 16 Buckets
80 GB ÷ 10 GB ≈ 8 Buckets

4. 结合 BE 数量调整

Bucket 数通常设置为 BE 数量的整数倍,便于均匀分布。8 个 BE 的集群可以从 8、16、32 等候选值中压测。节点扩容后,后台 Balance 会重新分布副本,但原有 Bucket 数不会自动增加。

5. 检查每分区上限与长期增长

官方建议每分区 Bucket 数通常低于 128。需要更高并行度时,可以先评估:

  • 分区粒度是否过粗;
  • 单分区数据量是否已经达到数 TB;
  • 查询是否真的能够使用更多并行度;
  • 写入是否会被大量 Tablet 拖慢;
  • FE 和 BE 的 Tablet 总量是否可控。

6. 使用 BUCKETS AUTO

无法准确预测 Bucket 数时,可以使用:

DISTRIBUTED BY HASH(user_id) BUCKETS AUTO
PROPERTIES (
    "estimate_partition_size" = "80G"
)

FE 会结合估算分区大小、BE 数量和磁盘情况选择 Bucket 数。这个功能减少了手工猜测,estimate_partition_size 仍需尽量接近真实压缩后规模。极大表和长期高速增长场景仍应通过 POC 验证。

7. 物理容量还要乘以副本

基础数据物理容量 ≈ 压缩后逻辑容量 × 副本数

还需预留:

  • Rowset 与 Segment 元数据;
  • 索引文件;
  • Delete Bitmap;
  • Compaction 临时空间;
  • Schema Change 临时副本;
  • 数据增长和故障修复空间。

生产容量规划通常需要保留明显余量,避免磁盘高水位时 Balance、Compaction 和副本修复相互争抢空间。


八、副本:从机器级故障走向故障域设计

副本与故障域
副本与故障域

每个 Tablet 可以拥有多个副本。单 BE 实验环境只能设置一副本;生产环境常用三副本,具体数量由可用性、成本、写入吞吐和监管要求决定。

1. replication_num

PROPERTIES (
    "replication_num" = "3"
)

这个属性设置默认副本数量。三副本会把同一 Tablet 的三份数据放到不同 BE 上,系统根据节点健康、磁盘状态和调度策略进行分配。

2. replication_allocation

企业可能需要跨机架、可用区、资源组或业务域放置副本。可以先给 BE 配置标签,再使用:

PROPERTIES (
    "replication_allocation" =
        "tag.location.az1:1,tag.location.az2:1,tag.location.az3:1"
)

FE 在建表时会检查标签下是否存在足够的 BE。资源不足时 DDL 会失败,这类失败能够提前暴露容灾布局与实际资源不匹配的问题。

3. 副本数带来的收益与代价

收益包括:

  • 单 BE 或单磁盘故障时仍有健康副本;
  • 查询可以选择可用副本;
  • 后台可以从健康副本克隆并修复缺失数据;
  • 维护和滚动升级具备更大操作空间。

代价包括:

  • 存储容量按副本数放大;
  • 写入需要维护多份数据;
  • 网络流量和修复流量增加;
  • 跨可用区部署可能增加时延与带宽费用;
  • 副本越多,Tablet 元数据和调度对象也越多。

4. 副本不等同于备份

副本用于集群内高可用。误删表、错误覆盖、逻辑污染和操作失误可能同步影响所有副本。生产系统仍需要独立的备份、快照、对象存储副本或灾备集群,并定期演练恢复流程。

5. 查看副本状态

常用命令包括:

SHOW REPLICA DISTRIBUTION FROM db_name.table_name;
SHOW REPLICA STATUS FROM db_name.table_name;
SHOW TABLET <tablet_id>;

关注:

  • 副本是否分布在不同 BE;
  • 版本是否完整;
  • 是否存在 Bad 或缺失副本;
  • 某些磁盘或节点是否集中承载过多副本;
  • Balance 与 Repair 是否长期积压。

九、表属性与结构演进:减少无依据的参数堆叠

表属性应服务于可描述的目标。以下几类与 Day 8 最相关。

1. 副本与存储属性

属性 作用 评审重点
replication_num 设置副本数 学习环境与生产环境分开配置
replication_allocation 按标签分配副本 资源标签、故障域和节点数量是否满足
storage_policy 配置远程冷却策略 热冷边界、对象存储成本、冷查询延迟
storage_format 选择存储格式 4.1 的 V3 主要面向超宽表、Variant 和远程存储

Doris 4.1 的 Storage Format V3 将列元数据按需加载,适合数百到数千列的宽表、Variant 表和对象存储场景。普通几十列的稳定表可以继续采用成熟默认格式,升级前应通过真实数据验证兼容性与收益。远程分层存储与 Unique Key MoW 等能力存在版本和模式限制,启用前需按目标版本逐项核对。

2. 自动分区属性

Dynamic Partition 的时间单位、开始偏移、结束偏移、前缀和 Bucket 数必须形成完整生命周期。配置时同步回答:

  • 过去保留多久;
  • 未来预创建多久;
  • 迟到数据允许回补多远;
  • 历史分区是否进入冷存储;
  • 自动删除是否符合审计与合规要求。

3. 索引属性

Bloom Filter 适合高基数等值与 IN 查询;倒排索引适合多列过滤、全文和复杂搜索;前缀索引由排序键自动生成。索引会增加写入、存储和 Schema Change 成本,因此每个索引都需要对应明确 SQL 和验收指标。

4. 结构变更的边界

分区列和分桶列无法直接修改。排序 Key 的重大变化通常需要重写与重排数据。建表以后发现分区、分桶或模型选择错误,常见处理流程是:

  1. 新建目标表;
  2. 使用 INSERT INTO SELECT 或外部任务回灌历史数据;
  3. 双写或持续同步增量;
  4. 对账行数、金额、时间范围和关键指标;
  5. 回归查询与写入性能;
  6. 低峰切换表名、视图或应用配置;
  7. 保留回滚窗口,再清理旧表。

表结构评审应在首次上线前投入足够时间。后期迁移仍然可行,成本会随着数据规模和业务并发持续增长。


十、三类典型业务表怎样设计

三类业务表设计示例
三类业务表设计示例

下面使用事件明细、订单当前状态和门店日汇总三个场景串联 Day 7 与 Day 8。

场景一:用户行为事件明细

特点:追加写、数据量大、按时间和事件类型查询、需要长期追溯。

CREATE TABLE ods_user_event (
    event_date   DATE         NOT NULL,
    event_type   VARCHAR(32)  NOT NULL,
    service_name VARCHAR(64)  NOT NULL,
    event_time   DATETIME(3)  NOT NULL,
    event_id     BIGINT       NOT NULL,
    user_id      BIGINT       NOT NULL,
    properties   VARIANT      NULL
)
DUPLICATE KEY(event_date, event_type, service_name, event_time)
AUTO PARTITION BY RANGE (date_trunc(event_date, 'day')) ()
DISTRIBUTED BY HASH(user_id) BUCKETS AUTO
PROPERTIES (
    "estimate_partition_size" = "80G",
    "replication_num" = "3"
);

设计说明:

  • event_date 支持按天自动分区和粗裁剪;
  • event_type、service_name、event_time 形成分区内部常用过滤顺序;
  • user_id 高基数,适合均匀 Hash,也支持用户等值查询;
  • BUCKETS AUTO 根据单日估算规模计算 Bucket 数;
  • Variant 承载长尾属性,核心过滤字段继续使用实体列。

如果查询很少按 user_id 等值过滤,且用户热点明显,可以对比 Random Bucketing 的写入均衡和查询表现。

场景二:订单当前状态表

特点:主键 Upsert、查询经常按订单 ID 点查、状态字段持续更新。

CREATE TABLE dwd_order_current (
    order_id       BIGINT        NOT NULL,
    tenant_id      BIGINT        NOT NULL,
    user_id        BIGINT        NOT NULL,
    order_status   VARCHAR(32)   NOT NULL,
    amount         DECIMAL(18,2) NOT NULL,
    update_time    DATETIME(3)   NOT NULL
)
UNIQUE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 64
PROPERTIES (
    "enable_unique_key_merge_on_write" = "true",
    "replication_num" = "3"
);

设计说明:

  • order_id 同时承担主键、排序和分桶职责;示例中的 64 Buckets 只代表一种生产候选值,最终数量需按 BE 数、容量和并发完成 POC;
  • 当前状态表没有按日期分区,避免日期进入 Unique Key 后改变主键语义;
  • 超大规模时需要通过 POC 验证单分区 Tablet 数、FE 元数据、部分更新和点查性能;
  • 完整订单历史进入独立的 Duplicate 事件表;
  • CDC 乱序由 Sequence Column 或源端版本字段治理。

如果业务主键天然为 (tenant_id, order_id),可以将两列共同作为 Unique Key,并重新评估分桶与故障隔离。

场景三:门店日经营汇总

特点:固定粒度、主要查询天/月趋势、指标可以稳定合并。

CREATE TABLE ads_store_daily (
    dt           DATE          NOT NULL,
    store_id     BIGINT        NOT NULL,
    channel      VARCHAR(16)   NOT NULL,
    order_count  BIGINT        SUM DEFAULT "0",
    sales_amount DECIMAL(20,2) SUM DEFAULT "0",
    max_order    DECIMAL(18,2) MAX DEFAULT "0"
)
AGGREGATE KEY(dt, store_id, channel)
AUTO PARTITION BY RANGE (date_trunc(dt, 'month')) ()
DISTRIBUTED BY HASH(store_id) BUCKETS AUTO
PROPERTIES (
    "estimate_partition_size" = "20G",
    "replication_num" = "3"
);

设计说明:

  • 月分区便于按月管理,日维度保留在 Aggregate Key 中;
  • store_id 分桶有利于门店查询和分布均衡;
  • SUMMAX 具备稳定可合并性;
  • 明细仍保留在上游事实表,汇总口径变化时可以重新生成;
  • 热点报表还可以增加异步物化视图或服务层表。

十一、验证与故障定位:让设计结论有证据

表结构问题定位链路
表结构问题定位链路

表结构上线前至少完成以下验证。

1. 检查最终 DDL

SHOW CREATE TABLE db_name.table_name;

确认:

  • 数据模型与 Key 顺序;
  • 分区表达式和边界;
  • Hash/Random 分桶方式;
  • Bucket 数或 Auto Bucket 参数;
  • 副本数与标签;
  • 关键表属性和索引。

2. 检查分区

SHOW PARTITIONS FROM db_name.table_name;

检查分区数量、边界、数据量、Bucket 数、存储介质和最近更新时间。Auto Partition 场景还要检查异常值是否生成意外分区。

3. 检查 Tablet 分布

SHOW TABLETS FROM db_name.table_name;

观察各 Tablet 的数据量、行数、版本数和 BE 分布。可以计算最大值与最小值之比,判断倾斜程度。某个 Tablet 显著大于其他 Tablet 时,优先检查分桶列的基数和热点值。

4. 检查裁剪

EXPLAIN
SELECT SUM(amount)
FROM orders
WHERE order_date >= '2026-08-01'
  AND order_date <  '2026-09-01'
  AND user_id = 1001;

理想情况下,Explain 能同时显示较少的 partitionstablets。如果分区裁剪正常、Tablet 裁剪未发生,需要检查查询是否对 Hash 分桶列使用了等值条件。

5. 检查 Query Profile

重点观察:

  • RowsReadRowsReturned
  • RowsKeyRangeFiltered
  • Scan 的 I/O、解压和 CPU 时间;
  • 每个 Instance 读取的行数是否接近;
  • 是否出现一个明显慢于其他实例的 Straggler;
  • Join 中的 Shuffle Bytes 和 Runtime Filter 效果;
  • 冷缓存与热缓存差异。

6. 检查副本

SHOW REPLICA DISTRIBUTION FROM db_name.table_name;
SHOW REPLICA STATUS FROM db_name.table_name;

确认副本健康、版本完整、分布均匀,标签化副本是否真的落入目标故障域。

7. 四类常见故障

查询扫描大量分区

常见原因:查询条件没有直接约束分区列、分区粒度与查询窗口不匹配、隐式转换或函数表达式影响裁剪。

处理方向:改写谓词,调整分区表达式;结构已经固化且收益明显时,新建表迁移。

Tablet 严重倾斜

常见原因:低基数分桶列、热点租户、NULL 集中、复合 Key 顺序不合理。

处理方向:分析频率分布,评估高基数列、复合 Hash 或 Random Bucketing。

写入和 Compaction 压力高

常见原因:Bucket 过多、单批数据太小、一次写入触达大量分区、频繁短事务。

处理方向:合并批次、减少 Bucket、集中写当前少量分区、调整导入并发,并观察 VersionNum 与 Compaction Score。

副本长期缺失

常见原因:BE 或磁盘不可用、目标标签下节点不足、磁盘空间不足、调度队列积压。

处理方向:恢复资源,检查标签和磁盘高水位;必要时提高指定表的修复优先级。


十二、常见反模式与改进建议

反模式 1:按每个用户或租户建分区

高基数值会生成海量分区和 Tablet,FE 元数据与建表成本快速上升。多数租户隔离场景更适合 Hash 分桶、行列权限、资源组或分层表设计。

反模式 2:用状态、性别、渠道做 Hash 分桶列

低基数列只能把数据集中到少数 Bucket,无法充分利用所有 BE。可以选择用户 ID、订单 ID、设备 ID 等高基数列,或者使用 Random Bucketing。

反模式 3:所有大表固定一个 Bucket

单 Tablet 会限制并行度,并把数据集中在少量节点。小维表可以使用少量 Bucket;持续增长的事实表需要按分区规模和集群节点数估算。

反模式 4:每分区创建几百甚至上千 Bucket

小批写入会产生大量小文件,Compaction 和元数据成本显著增加。官方建议通常将每分区控制在 128 以内;达到上限前先评估分区粒度。

反模式 5:把长 VARCHAR 放在排序键第一列

前缀索引会在 VARCHAR 处截断,后续列无法进入该前缀。固定长度的高频过滤列可以放在更前面,文本检索交给倒排索引。

反模式 6:只关注平均延迟

分桶倾斜、缓存冷热和副本位置常常影响 P95、P99。验收应覆盖冷缓存、热缓存、峰值写入、读写混合和节点故障。

反模式 7:生产表使用单副本

单副本只适合开发、临时或可从源端完整重建的数据。生产主数据应根据 SLA 设置多副本,并提供独立备份。

反模式 8:把副本当成灾备

逻辑错误会同步进入所有副本。备份、跨集群复制和恢复演练仍然必需。

反模式 9:上线后再考虑分区与分桶

分区列和分桶列无法原地修改。新表迁移需要回灌、追平、对账和切换,数据规模越大,项目成本越高。


十三、本日实践任务

Day 8 实验闭环
Day 8 实验闭环

完整包中的 sql/day08-table-design-lab.sql 提供了可执行实验。建议按照以下顺序完成。

任务 1:建立 Hash 分桶与 Random 分桶对照表

两张 Duplicate Key 表使用相同字段、相同分区和相同 Bucket 数,只改变分桶方式。加载相同数据后对比:

  • 数据在 Tablet 间的分布;
  • user_id = ? 查询的 Tablet 裁剪;
  • 无用户条件的时间范围聚合;
  • 小批写入与大批写入的差异。

任务 2:验证 Auto Partition

写入三个不同日期,其中一个日期远离当前时间。确认 Doris 在写入时创建对应分区,并检查异常日期的治理策略。

任务 3:验证 Unique Key 当前状态表

针对同一 order_id 连续写入多个状态,确认最终只保留一个可见版本。随后检查:

  • 主键点查;
  • Bucket 分布;
  • 副本状态;
  • 与 Duplicate 事件表的职责区别。

任务 4:验证分区与 Bucket 裁剪

分别执行:

  1. 只带时间范围;
  2. 只带 Hash 分桶列等值条件;
  3. 同时带时间和 Hash 等值条件;
  4. 对分区列套用函数的查询。

记录 Explain 中的 partitions=x/ytablets=x/y

任务 5:完成容量估算

基于你的真实业务填写:

  • 每日或每月原始数据量;
  • 压缩比假设;
  • 单分区压缩后数据量;
  • 目标 Tablet 大小;
  • 初始 Bucket 数;
  • 分区数量;
  • 同步索引数量;
  • 副本数量;
  • 预计 Tablet 与 Replica 总数;
  • 三年增长后的容量与元数据规模。

任务 6:输出设计评审结论

完整包提供 reference/production-table-review-template.md。评审记录至少包含:

  • 业务写入语义与数据模型;
  • Top 查询与过滤列频次;
  • 分区字段、粒度和保留期;
  • 分桶方式、分桶列和 Bucket 数;
  • 排序键与前缀索引预期;
  • 副本数与故障域;
  • Tablet 总量和三年增长;
  • Explain、Profile、倾斜和故障测试证据;
  • 迁移、切换与回滚方案。

十四、Knowledge Check

1. 分区与分桶分别解决什么问题?

**答案要点:**分区负责范围裁剪、生命周期和管理;分桶负责 Tablet 分布、并行度、数据均衡和部分 Join 优化。

2. 一个分区有 32 个 Bucket、3 个副本,基础数据对应多少个 Tablet 和 Replica?

**答案要点:**32 个基础 Tablet,96 个物理 Replica。同步 Rollup 会继续增加对应 Tablet 与副本。

3. Prefix Index 为什么容易被 VARCHAR 截断?

**答案要点:**前缀索引最长取排序键前 36 字节,遇到 VARCHAR 会在该列结束处截断,后续排序列不进入这一组前缀。

4. Dynamic Partition 与 Auto Partition 的主要差异是什么?

**答案要点:**Dynamic 围绕当前系统时间周期滚动;Auto 根据实际写入值按需创建,可覆盖历史回补和离散枚举值。

5. 哪类列适合 Hash 分桶?

**答案要点:**高基数、分布均匀、经常用于等值过滤或 Join 的稳定列。

6. Random Bucketing 的主要代价是什么?

**答案要点:**无法依据业务列执行 Bucket Pruning,也不便于依赖同一分桶列做 Colocate 对齐。

7. Bucket 数为什么不能只依据 CPU 核数设置?

**答案要点:**Bucket 同时决定 Tablet 大小、写入分散度、元数据、Compaction 和副本数量,需要结合单分区压缩量、BE 数、增长和查询并行度。

8. 为什么 Unique Key 表按日期分区需要格外谨慎?

**答案要点:**分区列必须是 Unique Key 的子集;随意加入日期可能改变主键语义,导致同一业务对象落入多个逻辑主键。

9. 三副本能否替代备份?

**答案要点:**不能。副本解决集群内硬件故障,逻辑误删和错误写入会影响全部副本。

10. 怎样确认一条查询同时命中了分区裁剪和 Bucket 裁剪?

**答案要点:**查看 Explain 中 partitions=x/ytablets=x/y,并结合 Profile 验证实际读取行数与 Scan 成本。


十五、本日总结

今天完成了 Doris 表结构的五项核心设计:

  1. 排序键安排 Tablet 内部数据顺序,为前缀索引和局部扫描建立条件;
  2. 分区划定管理边界,让查询能够整体跳过无关时间或枚举范围;
  3. 分桶把分区展开成多个 Tablet,提供均衡、并行和 Bucket 裁剪;
  4. 副本把 Tablet 放到不同 BE 或故障域,提供硬件故障后的服务与修复能力;
  5. 表属性控制长期运行行为,需要每项配置都能被业务目标和实验证据解释。

一张高质量 Doris 表可以用三句话检验:

查询先通过分区缩小范围,再通过 Tablet 与索引减少扫描;写入集中到合理数量的分区和 Bucket;副本、元数据和 Compaction 规模能够随数据增长稳定运行。

Day 9 将进入数据导入体系,系统学习 Stream Load、Group Commit、Broker Load、TVF、INSERT 等批量导入方式,并把今天设计的 Partition 与 Tablet 布局带入真实写入路径。


官方资料索引