外观
分区策略怎么选、怎么验
难度与前置
难度:入门
前置:已读 数仓分层怎么落地;知道离线调度会按日补数/重跑。
读完能做什么:为一条日批主题表选定分区键与写入方式,挂上补数与验收清单。
适用场景
- 日批 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. 按查询模式选分区键(三原则)
- WHERE 里最常出现的字段才配当分区键(十有八九是日期)
- 分区数量可控:单表经验上控制在约 1 万分区以内
- 类型用 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,原始时区进字段;不要用「跑任务的机器本地日」切分区。
常见坑
用服务器本地日切多时区数据
东八区半夜跑任务,把美西晚上的消耗切进「第二天」。对账和留存都会偏。分区日必须来自明确业务规则,不是current_date随手一写。动态分区不设上限,一次回刷扫爆元数据
脏数据里混入错误日期/空值,可能写出海量分区目录。回刷前先SELECT biz_date, COUNT(*)看日期分布,并设max.dynamic.partitions。空分区不告警 / 有数据也不校验
只盯任务成功码:渠道文件缺失时分区空白,日报显示 0 被当成「真没消耗」。应对:分区就绪标志 + 行数/金额同比阈值。补数只重跑 DWD,忘记 ADS
分层依赖断了,明细对了报表不对。补数工单必须带下游级联清单。二级分区过细导致小文件
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 数仓原创文,已改写成可执行清单;未包含未公开公司数据与内部表名。