本节摘要:前面三节讲了原则,这一节讲原则打架时怎么办。我们完整复盘一张生产级订单宽表从需求澄清、两轮方案争论到上线验收的全过程——包括那个差点让 GMV 口径翻倍的分桶失误。所有 DDL 与估算数字都取自真实项目(细节已脱敏),你可以把它当作一份可对照的施工图。
阅读完本节,你应当能够:
场景是一家家居电商的数据中台重构:旧链路是 Hive 离线层加一套 MySQL 汇总库,大屏 P99 超过八秒,运营自助取数完全靠人肉导表。新目标用 Doris 承接订单域全部分析服务。
第一周没有写一行 DDL,产出的是三份口径文档:
把"实时"两个字拆开问,得到的真实诉求其实是两级:大屏十五分钟内反映变化即可,风控侧的当日欺诈指标则要求分钟级。这决定了后面导入方式的两条通道——纯实时的 Stream Load 全量通道对它并不必要,Routine Load 分钟级延迟绰绰有余,成本还低一截。
平台组主张大宽表(事实加全部维度冗余),BI 组主张标准星型(事实表关联维表)。两边都对了一半:
| 方案 | 大屏固定看板 | 自助即席分析 | 口径变更 | 存储开销 |
|---|---|---|---|---|
| 纯大宽表 | 极快(免 Join) | 维度不全需回源 | 改口径要刷全量宽表 | 高(冗余列) |
| 纯星型 | 受 Join 影响 | 维度自由组合 | 只改维表或临时层 | 低 |
| 两级混合 | 快 | 快 | 中间层重跑 | 中 |
最终落点是一个三层结构:ODS 明细常驻 Doris 的 Duplicate 表承接回溯,DWD 层做轻度清洗,面向高频看板的 DWS 大宽表由调度任务每日两次刷新。看板直查 DWS,自助分析允许打 DWD 加维表的星型查询。解决争论的不是权威而是分层——每一方在自己的层里都是对的。

财务的诉求是"月末报表按最终状态",运营则要看"当时的实时漏斗"。一个 Unique 主键表无法同时表达两者——覆盖式更新会抹掉中间状态。最终方案是把语义拆开:
教训写入制度:任何 Unique 表立项时必须书面回答"被覆盖掉的中间态谁还需要"。本案若不做流水层,三个月后 BI 团队必然绕开中台自建影子表。
初版 DWD 以 user_id % 城市数 思路选了「城市 ID」单列分桶。上线两周后沿海大仓所在城市的 Tablet 体量达到小城市场景的一百二十倍,BE 磁盘水位开始劈叉。修复路径如下,全程不锁写:
-- 第一步 新表用均匀性验证过的复合键重建 CREATE TABLE dwd_order_detail_v2 LIKE dwd_order_detail; ALTER TABLE dwd_order_detail_v2 DISTRIBUTED BY HASH(order_id) BUCKETS 64; -- 第二步 首次近七天数据走实时通道 其余历史分批迁 INSERT INTO dwd_order_detail_v2 SELECT * FROM dwd_order_detail WHERE dt >= '2026-08-20'; -- 第三步 历史按月分批 双写比对三天后切换读流量 INSERT INTO dwd_order_detail_v2 SELECT * FROM dwd_order_detail WHERE dt BETWEEN '2026-08-01' AND '2026-08-19';
第三天的对账 SQL 显示两侧行数与聚合金额完全一致后,上游写入指向新表,旧表保留两周作为保险再删除。整件事的真正成本不是那几十行迁移语句,而是评审会上没人要求出示分桶键的分布证明——此后它成了我们的强制附件。
CREATE TABLE dws_order_wide ( dt DATE NOT NULL COMMENT '下单日期', order_id BIGINT NOT NULL, user_id BIGINT, category_l1 VARCHAR(32), province VARCHAR(32), channel VARCHAR(16), member_level TINYINT, pay_amount DECIMAL(16,2) SUM COMMENT 'MoW下作展示 列值随最后版本', status VARCHAR(16), pay_time DATETIME, index_idx_bloom (`channel`) USING BLOOMFILTER ) UNIQUE KEY(dt, order_id) PARTITION BY RANGE(dt) () DISTRIBUTED BY HASH(order_id) BUCKETS 48 PROPERTIES ( "replication_num" = "3", "dynamic_partition.enable" = "true", "dynamic_partition.time_unit" = "DAY", "dynamic_partition.start" = "-550", "dynamic_partition.end" = "1", "dynamic_partition.buckets" = "48" );
验收当天跑的四道检查,后来沉淀为我们团队的标准清单:
EXPLAIN 抽查 Top20 看板语句,确认每个都命中当日单分区裁剪;💡 关键直觉:复盘的价值不在复刻这张表的参数,而在带走四个问题模板——覆盖掉的中间态谁要?分桶键拿什么证明自己均匀?容量按峰值还是均值算?挂掉一台机器谁兜底?
表已成型、案例在手。下一章处理它的另一半生命——数据如何持续不断地、正确地、无感地流进来。