Skip to content

分区策略怎么选、怎么验 ​

难度与前置

难度:入门
前置:已读 数仓分层怎么落地;知道离线调度会按日补数/重跑。
读完能做什么:为一条日批主题表选定分区键与写入方式,挂上补数与验收清单。

适用场景 ​

  • 日批 ODS / DWD / DWS 主题表,查询几乎都带时间范围
  • 需要按某一天重跑、补数、对账的链路
  • 多时区业务里,必须先说清「分区日」到底是哪一种日期

不适用:

  • 全量很小、全表扫描也秒级的维表(可先不分区,或只做快照分区)
  • 用高基数字段(如 user_id)当分区键——分区会炸
  • 把分区当成「再建一层业务分类」的替代品(业务域应靠库/表名,不靠无限加分区维度)

真实场景(一条) ​

承接 数仓分层 里的投放消耗链路:

  • 调度每天 6:00 跑 dt = T-1 的 DWD / DWS
  • 渠道文件常迟到 1~2 天,要对历史分区补数
  • 运营日报按「入账业务日」看数;偶尔排查又要按「事件发生日」切片

若分区键和写入约定没说清,会出现:

  • 补了 dt=2025-01-10,下游 ADS 没级联,日报仍旧
  • 动态分区一次回刷写出上万空目录,元数据告警
  • 多时区账户按服务器本地日切分区,对账永远差一截

目标:选定分区键、写死写入与补数契约,并能用清单验收。

怎么做(步骤清单) ​

1. 先定「分区日」口径(三种常见,只能选一种当默认) ​

口径含义适合风险
入账业务日财务/结算认的那一天对账、消耗日报与事件日不一致时,排查要另备字段
事件发生日曝光/点击真实发生日漏斗、留存跨时区若不标准化会错切
入库日数据进仓的自然日运维追溯业务分析几乎不该用它当主分区

建议在表注释写死一行,例如:

dt = 入账业务日(yyyy-MM-dd);事件时间见 event_ts(已统一时区);迟到数据重跑对应 dt 分区。

与分层文一致:核心事实表优先入账业务日做分区;事件日留作字段,需要时再二次过滤。

2. 按查询模式选分区键(三原则) ​

  1. WHERE 里最常出现的字段才配当分区键(十有八九是日期)
  2. 分区数量可控:单表经验上控制在约 1 万分区以内
  3. 类型用 STRING(yyyy-MM-dd / HH),避免各引擎 DATE 行为不一致
场景推荐说明
普通日批事实表dt一年约 365 个分区
小时级入仓dt + hr一天 24 个;注意小文件
多区域强隔离且查询常带区域dt + region地区数要少且稳定
❌ user_id / creative_id不要高基数 → 分区爆炸

多级分区先问:二级字段是否几乎每次查询都有?没有就别加。

3. DDL 骨架(可复制) ​

sql
CREATE TABLE IF NOT EXISTS dwd_ad_spend_detail_di (
  bill_id       STRING,
  channel_id    STRING,
  cost_amount   DECIMAL(18,4),
  event_ts      TIMESTAMP COMMENT '事件时间(已统一时区)',
  is_valid      BOOLEAN,
  filter_reason STRING,
  etl_ts        TIMESTAMP
)
COMMENT 'DWD 广告消耗明细;分区 dt=入账业务日'
PARTITIONED BY (
  dt STRING COMMENT '入账业务日 yyyy-MM-dd'
);

小时表示意:

sql
PARTITIONED BY (
  dt STRING COMMENT '业务日 yyyy-MM-dd',
  hr STRING COMMENT '小时 00-23'
)

4. 写入约定:静态为主,动态有上限 ​

日常调度用静态分区(确定、可审计):

sql
INSERT OVERWRITE TABLE dwd_ad_spend_detail_di PARTITION (dt = '${biz_date}')
SELECT
  bill_id, channel_id, cost_amount, event_ts,
  is_valid, filter_reason, current_timestamp() AS etl_ts
FROM tmp_dwd_ad_spend_detail
WHERE biz_date = '${biz_date}';

历史回刷可用动态分区,但必须设上限(Hive 示例):

sql
SET hive.exec.dynamic.partition=true;
SET hive.exec.dynamic.partition.mode=nonstrict;
SET hive.exec.max.dynamic.partitions=2000;
SET hive.exec.max.dynamic.partitions.pernode=200;

INSERT OVERWRITE TABLE dwd_ad_spend_detail_di PARTITION (dt)
SELECT
  bill_id, channel_id, cost_amount, event_ts,
  is_valid, filter_reason, current_timestamp() AS etl_ts,
  biz_date AS dt
FROM tmp_dwd_ad_spend_detail_hist;

写入时顺手控制文件数(避免每分区碎成几百个小文件),例如 Spark 按分区大小估算 repartition,或 Hive DISTRIBUTE BY 控制并行度。经验:单分区文件数 大约 分区数据量 / 128MB。

5. 调度与补数 ​

推荐契约:

text
参数 biz_date = T-1(或补数指定日)
ODS(dt) → DWD(dt) → DWS(dt) → ADS(dt)   -- 同分区键对齐
  • 补数:只重跑出问题的 dt 列表,上游成功后再级联下游同 dt
  • INSERT OVERWRITE 同分区 → 幂等;禁止「只 insert 不 overwrite」导致重跑翻倍
  • 空分区也要有策略:允许空(业务日无消耗)时,用「分区已落地」成功标记,而不是「行数 > 0」才算成功
  • 生命周期:热数据保留 N 天;冷数据归档或删分区(ALTER TABLE ... DROP PARTITION)前确认无未完成审计

6. 和分层时间口径对齐 ​

层分区建议
ODS与采集切片一致;若源按文件日到达,可先按到达日,但要在 DWD 归到业务日
DWD / DWS / ADS同一业务日口径(本文默认入账业务日),禁止三层各用各的日切

跨时区:在 DWD 用统一后的业务日写 dt,原始时区进字段;不要用「跑任务的机器本地日」切分区。

常见坑 ​

  1. 用服务器本地日切多时区数据
    东八区半夜跑任务,把美西晚上的消耗切进「第二天」。对账和留存都会偏。分区日必须来自明确业务规则,不是 current_date 随手一写。

  2. 动态分区不设上限,一次回刷扫爆元数据
    脏数据里混入错误日期/空值,可能写出海量分区目录。回刷前先 SELECT biz_date, COUNT(*) 看日期分布,并设 max.dynamic.partitions。

  3. 空分区不告警 / 有数据也不校验
    只盯任务成功码:渠道文件缺失时分区空白,日报显示 0 被当成「真没消耗」。应对:分区就绪标志 + 行数/金额同比阈值。

  4. 补数只重跑 DWD,忘记 ADS
    分层依赖断了,明细对了报表不对。补数工单必须带下游级联清单。

  5. 二级分区过细导致小文件
    dt+hr+channel 组合爆炸,每目录几 MB。先保证查询真需要该维度;否则合并粒度,并加定期合并任务。

如何验收(可量化) ​

合并前 checklist:

  • [ ] 表注释写明:dt 口径(入账/事件/入库三选一)、格式、与 event_ts 关系
  • [ ] 分区键出现在高频查询 WHERE 中;单表分区规模可估算且可控
  • [ ] 日常任务静态分区 + INSERT OVERWRITE;动态分区有上限
  • [ ] 补数按 dt 列表级联下游;重跑同分区行数/金额不翻倍
  • [ ] 空分区与异常跌量有监控(不能只靠「任务绿勾」)
  • [ ] 抽查:指定 dt 查询有分区裁剪(执行计划 / 扫描字节远小于全表)
    • 可照做:对带 WHERE dt = '${biz_date}' 的查询跑 EXPLAIN(Hive)或 EXPLAIN / EXPLAIN EXTENDED(Spark),确认只扫目标分区路径,而不是全表目录

快速核对示例:

sql
-- 1) 分区是否落地(元数据)
SHOW PARTITIONS dwd_ad_spend_detail_di PARTITION (dt = '${biz_date}');

-- 2) 补数幂等:重跑前后金额应一致(同一过滤条件)
SELECT SUM(cost_amount) AS cost_sum, COUNT(*) AS cnt
FROM dwd_ad_spend_detail_di
WHERE dt = '${biz_date}' AND is_valid = true;

-- 3) 与 DWS 同分区对齐(过滤条件必须与 DWS 一致,见分层文)
SELECT
  d.cost_dwd - s.cost_dws AS diff
FROM (
  SELECT SUM(cost_amount) AS cost_dwd
  FROM dwd_ad_spend_detail_di
  WHERE dt = '${biz_date}' AND is_valid = true
) d
CROSS JOIN (
  SELECT SUM(cost_amount) AS cost_dws
  FROM dws_ad_channel_play_1d
  WHERE dt = '${biz_date}'
) s;

相关面试题 ​

题库小程序链接待定;先用站内锚点占位。

  • [ ] 题:dt 该取入账日、事件日还是入库日?注释里要写清什么?(考察点:dt 口径三选一写进注释)→ 占位:#todo-interview-partitioning-dt-semantics
  • [ ] 题:如何用 EXPLAIN 证明分区裁剪生效?(考察点:只扫目标分区路径/字节远小于全表)→ 占位:#todo-interview-partitioning-prune-explain
  • [ ] 题:同分区重跑两次如何验收不翻倍?(考察点:行数/金额一致)→ 占位:#todo-interview-partitioning-rerun-idempotent
  • [ ] 题:空分区与任务绿勾有何差别?怎样监控?(考察点:空分区/跌量监控,不靠任务绿)→ 占位:#todo-interview-partitioning-empty-vs-green

改写说明:原则与踩坑来自 Wood 数仓原创文,已改写成可执行清单;未包含未公开公司数据与内部表名。