文章
数仓建模体系01
问题:基于上面的讨论,帮我输出一个完整版本的数据建模体系,包含每一层做的具体工作,比如ods到dwd做数据清洗、退化等 列出具体事项
以下是为你量身定制的 高企咨询服务场景下的完整离线数仓建模体系,覆盖从 ODS → DWD → DWS → DWT → ADS 的每一层,明确每层职责、具体工作项、输入输出、关键规范和示例,可直接作为团队交付标准或建模规范文档使用。
🧱 高企咨询数仓建模体系(完整版)
适用场景:企业资质评估、客户画像、销售跟进、资源匹配
核心实体:企业、联系人、专利/软著、联系方式、来源、标签
技术栈:Doris(支持 Array、物化视图、倒排索引)
🗃️ 一、ODS 层(Operational Data Store)—— 贴源层
✅ 定位:
- 保留原始业务系统数据,不做任何业务逻辑处理
- 仅做技术性抽取、分区、压缩
🛠️ 具体工作:
事项 | 说明 | 示例 |
---|---|---|
1. 数据接入 | 全量/增量同步源系统表(MySQL/API/文件) | ods_company ,ods_contact ,ods_patent |
2. 分区存储 | 按天分区(dt 字段),保留365天 | PARTITION BY dt |
3. 保留原始结构 | 字段名、类型、顺序与源系统一致 | 不改gmt_create 为create_time |
4. 保留逻辑删除标记 | 不过滤is_deleted=1 数据 | 保留脏数据供审计 |
5. 添加技术字段 | etl_time ,source_system ,dt | 记录抽取时间与来源 |
6. 数据压缩 | 使用 ZSTD/LZ4 压缩节省存储 | Doris 默认支持 |
⚠️ 禁止事项:
- ❌ 不做字段清洗、标准化
- ❌ 不做关联、聚合、计算
- ❌ 不删字段、不改名
🧱 二、DWD 层(Data Warehouse Detail)—— 明细建模层
✅ 定位:
- 面向分析主题的原子粒度建模
- 清洗、标准化、退化常用维度、统一口径
- 为上层提供“干净、可关联、可下钻”的明细数据
🛠️ 具体工作:
- 数据清洗 & 过滤
事项 | 说明 | 示例 |
---|---|---|
过滤逻辑删除 | WHERE is_deleted = 0 | |
处理空值 | 字符串→'' ,数值→-1 或0 ,布尔→0/1 | COALESCE(phone, '') |
去重 | 根据业务主键去重(如company_id + update_time ) | ROW_NUMBER() OVER (PARTITION BY ...) |
异常值处理 | 过滤非法值(如手机号长度≠11、邮箱无@) | WHERE LENGTH(phone)=11 |
标准化 & 映射
事项 | 说明 | 示例 |
---|---|---|
字段重命名 | 统一命名规范(snake_case,业务语义) | credit_code →unified_social_credit_code |
单位统一 | 金额→“分”或“万元”,时间→yyyy-MM-dd HH:mm:ss | registered_capital / 10000 AS registered_capital_wan |
码值映射 | 通过字典表映射标准 code | dict_map('province', raw_province) AS province_code |
布尔标准化 | 统一为 0/1 | CASE WHEN status = 'Y' THEN 1 ELSE 0 END |
维度退化(关键!)
事项 | 说明 | 示例 |
---|---|---|
退化高频维度 | 冗余维度表的常用描述字段,减少 JOIN | province_code +province_name |
仅退化稳定维度 | 不退化易变字段(如价格、状态) | 退化“行业名称”,不退化“最新评分” |
退化到事实表 | 在事实表中直接存储维度属性 | dwd_company_base 中存industry_name |
复杂类型处理(Array)
事项 | 说明 | 示例 |
---|---|---|
多值字段用 Array | 来源、标签等一对多关系 | source_codes ARRAY<VARCHAR(10)> |
存储 ID(code),非 name | 保证语义稳定 | ['1', '3'] 而非['爬虫', '展会'] |
预计算常用标志 | 基于 Array 计算布尔字段 | is_valid AS ARRAY_CONTAINS(tag_codes, 'valid') |
表结构设计(按业务过程)
表名 | 粒度 | 核心字段示例 | 说明 |
---|---|---|---|
dwd_company_base_inc | 企业 | company_id, company_name, province_code, province_name, industry_code, ... | 企业基础属性 + 退化维度 |
dwd_contact_base_inc | 联系人 | contact_id, company_id, company_name, contact_name, position_code, ... | 联系人属性 + 退化企业维度 |
dwd_contact_channel_inc | 联系方式 | channel_id, contact_id, company_id, channel_type, channel_value, source_codes, tag_codes, is_valid, ... | 方式值 + Array来源/标签 + 有效性标记 |
dwd_patent_fact_inc | 专利 | patent_id, company_id, apply_date, grant_date, patent_type_code, patent_status_code, ... | 专利原子事实,不聚合 |
分区 & 索引
事项 | 说明 |
---|---|
按天分区 | PARTITION BY data_date |
分桶 | DISTRIBUTED BY HASH(company_id) BUCKETS 10 |
倒排索引 | 对source_codes ,tag_codes 建INVERTED INDEX |
📊 三、DWS 层(Data Warehouse Summary)—— 轻度聚合层
✅ 定位:
- 按主题、维度、时间进行轻度聚合
- 支持多维分析、趋势统计、指标计算
- 不存储明细,只存聚合结果
🛠️ 具体工作:
- 聚合计算
事项 | 说明 | 示例 |
---|---|---|
COUNT/SUM/AVG | 基础聚合 | COUNT(*) AS patent_count |
时间窗口 | 近7/30天统计 | COUNT_IF(DATEDIFF(NOW(), apply_date) <= 30) |
分组维度 | 按企业、行业、地区等分组 | GROUP BY company_id, patent_type_code |
表结构设计(按主题)
表名 | 聚合维度 | 核心字段示例 | 说明 |
---|---|---|---|
dws_company_patent_stat | 企业×专利 | company_id, total_patent_count, invention_count, last_apply_date, ... | 企业专利统计 |
dws_company_contact_stat | 企业×联系人 | company_id, total_contact_count, valid_contact_count, wechat_coverage_rate, ... | 联系有效性统计 |
dws_contact_channel_dist | 联系方式×来源/标签 | company_id, source_code, tag_code, channel_count, ... | 渠道分布分析 |
dws_company_trend_7d | 企业×时间 | company_id, stat_date, patent_apply_7d, contact_add_7d, ... | 近7天趋势 |
更新策略
事项 | 说明 |
---|---|
增量更新 | 只更新有变更的企业/联系人 |
全量覆盖 | 每日快照,简单可靠 |
🖼️ 四、DWT 层(Data Warehouse Theme)—— 主题画像层
✅ 定位:
- 企业/联系人画像宽表
- 只包含稳定、高频、通用的画像特征
- 不是大宽表,而是“数据产品”
🛠️ 具体工作:
- 画像特征构建
事项 | 说明 | 示例 |
---|---|---|
只选核心指标 | 高频、稳定、跨部门通用 | ✔️total_patent_count ❌ 昨日新增专利 |
特征工程 | 基于DWS/DWD计算标签/分层 | CASE WHEN patent_count > 10 THEN '高潜力' ELSE '普通' END |
存储ID非name | 维度字段仍存code,名称在查询时映射 | industry_code 而非industry_name |
表结构设计
表名 | 主体 | 核心字段示例 | 说明 |
---|---|---|---|
dwt_company_profile | 企业画像 | company_id, company_name, industry_code, total_patent_count, contact_valid_rate, customer_value_score, ... | 企业特征快照 |
dwt_contact_profile | 联系人画像 | contact_id, contact_name, position_code, is_key_person, main_channel_valid, intent_level, tag_codes, ... | 联系人特征快照 |
技术实现
事项 | 说明 |
---|---|
使用物化视图 | CREATE MATERIALIZED VIEW ... 自动同步 |
倒排索引 | 对tag_codes 建索引加速查询 |
分区 | PARTITION BY data_date ,保留365天 |
治理规范
字段准入评审 | 新增字段需评审:是否高频?是否稳定? |
生命周期管理 | 每季度清理低频字段 |
文档化下钻路径 | 注释中说明“深度分析请查 dws_xxx” |
📈 五、ADS 层(Application Data Store)—— 应用层
✅ 定位:
- 面向报表、API、BI、模型的最终数据出口
- 按需组合 DWT + DWD + DWS
🛠️ 具体工作:
- 数据服务方式
场景 | 数据源 | 说明 |
---|---|---|
固定报表/看板 | DWT | 高性能,直接查询画像宽表 |
临时分析/下钻 | DWS + DWD | 灵活组合,支持复杂 JOIN |
探索性分析 | DWD | 原始明细,支持任意维度下钻 |
API/模型输入 | DWT 或DWS | 按需封装,提供稳定接口 |
示例查询
-- 场景1:销售日报(查DWT)
SELECT company_name, total_patent_count, customer_value_score
FROM dwt_company_profile
WHERE customer_value_score > 80 AND data_date = '2025-09-22';
-- 场景2:专利类型×联系人职位分析(查DWS+DWD)
SELECT
dwd.position_name,
dws.patent_type_code,
s.patent_type_name,
COUNT(*) as cnt
FROM dwd_contact_base dwd
JOIN dws_company_patent_by_type dws ON dwd.company_id = dws.company_id
JOIN dim_patent_type s ON dws.patent_type_code = s.patent_type_code
WHERE dws.data_date = '2025-09-22'
GROUP BY dwd.position_name, dws.patent_type_code, s.patent_type_name;
性能优化
事项 | 说明 |
---|---|
Doris 查询改写 | 自动路由到物化视图 |
结果缓存 | 对高频查询结果做 Redis 缓存 |
分页下推 | BI 工具查询时下推 LIMIT/OFFSET |
📜 六、配套规范 & 治理
1. 命名规范
ODS | ods_系统名_表名 | ods_crm_company |
DWD | dwd_业务域_表名_更新模式 | dwd_company_base_inc |
DWS | dws_主题_维度_时间粒度 | dws_company_patent_1d |
DWT | dwt_主体_类型 | dwt_company_profile |
ADS | ads_应用场景_表名 | ads_sales_dashboard |
字段规范
类型 | 命名 | 示例 |
---|---|---|
ID | xxx_id | company_id |
名称 | xxx_name | company_name |
编码 | xxx_code | province_code |
数量 | xxx_count | patent_count |
金额 | xxx_amount | order_amount |
比率 | xxx_rate | contact_valid_rate |
时间 | xxx_time /xxx_date | create_time ,stat_date |
数据质量监控
指标 | 监控方式 | 阈值 |
---|---|---|
主键非空率 | COUNT(*) / COUNT(company_id) | > 99.9% |
枚举值合规率 | COUNT_IF(source_code IN ('1','2','3')) / COUNT(*) | = 100% |
数据波动率 | ABS(today_count - yesterday_count) / yesterday_count | < 30% |
时效性 | MAX(etl_time) - NOW() | < 1小时 |
🧩 七、架构全景图
[源系统]
↓ 全量/增量同步
[ODS] —— 贴源、分区、保留原始结构
↓ 清洗、标准化、退化维度、Array存code
[DWD] —— 原子事实表(企业、联系人、专利、联系方式)
↓ 按主题聚合(企业×专利、企业×联系人、渠道×标签)
[DWS] —— 轻度聚合表
↓ 特征工程、构建画像(只存稳定高频特征)
[DWT] —— 企业/联系人画像宽表(物化视图)
↓ 按需组合
[ADS] —— 报表/API/BI/模型
├─ 固定需求 → DWT
├─ 临时分析 → DWS + DWD
└─ 探索分析 → DWD
✅ 总结:你的数仓体系已具备企业级能力
通过以上设计,你的数仓将实现:
- ✅ 清晰分层:各司其职,避免职责混乱
- ✅ 高性能:Doris + 物化视图 + 倒排索引
- ✅ 高灵活:DWT 做画像,DWD/DWS 做深度分析
- ✅ 高可维护:命名规范、数据质量监控、治理机制
问题:忘了提一点,我们数仓分层没有 表分区,也就是分析的都是截至当前的存量数据,也就是dwt层表没有历史都是当前存量算的。这样就没有增加dwt维度还要算历史分区数据的问题了
太好了!你补充的这一点非常关键 —— “数仓无分区,所有分析基于当前存量数据”,这直接决定了我们整个建模体系的设计重心和优化方向。
这意味着:
✅ DWT 层无需处理历史快照,只维护“当前最新画像”
✅ DWD/DWS 也无需按天分区,全量覆盖更新即可
✅ ETL 逻辑更简单,资源消耗更低,查询性能更高
✅ “画像层”可以更轻量、更实时、更聚焦当前业务状态
🧱 更新版:无分区存量数仓建模体系(高企咨询场景)
📌 核心原则:所有表均为“当前最新全量”,无历史分区,无时间旅行
🗃️ 一、ODS 层(贴源层)—— 仍保留原始结构
✅ 职责不变:
- 全量/增量同步源表
- 保留逻辑删除数据
- 添加
etl_time
,source_system
🔄 更新策略:
- 全量覆盖:每日或每小时全量抽取最新数据
- 无分区:单表存储最新状态
-- 示例:ods_company (无分区,全量最新)
CREATE TABLE ods_company (
company_id BIGINT,
company_name STRING,
is_deleted TINYINT,
gmt_modified DATETIME,
etl_time DATETIME,
source_system STRING
) ENGINE=OLAP
DUPLICATE KEY(company_id); -- Doris 用 DUPLICATE 模型存明细
🧱 二、DWD 层(明细建模层)—— 当前最新原子数据
✅ 核心变化:
- 不按天分区,只存当前最新状态
- ETL 每次全量重建或增量合并(推荐全量重建,简单可靠)
🛠️ 具体工作(更新版):
1. 数据清洗 & 过滤
事项 | 说明 |
---|---|
过滤逻辑删除 | WHERE is_deleted = 0 |
去重 | ROW_NUMBER() OVER (PARTITION BY company_id ORDER BY gmt_modified DESC) = 1 → 取最新一条 |
空值/异常处理 | 同前,标准化处理 |
标准化 & 退化维度
事项 | 说明 |
---|---|
字段重命名、单位统一、码值映射 | 同前 |
退化维度 | 仍退化province_name ,industry_name 等高频维度 |
Array 存储(关键!)
事项 | 说明 |
---|---|
存储来源/标签ID(code) | source_codes ARRAY<VARCHAR(10)> 存['1','3'] |
预计算标志位 | is_valid AS ARRAY_CONTAINS(tag_codes, 'valid') |
表结构示例(无分区)
-- dwd_company_base (当前最新全量)
CREATE TABLE dwd_company_base (
company_id BIGINT,
company_name STRING,
unified_social_credit_code STRING,
province_code VARCHAR(10),
province_name STRING, -- 退化维度
industry_code VARCHAR(10),
industry_name STRING, -- 退化维度
registered_capital_wan DECIMAL(10,2),
is_high_tech TINYINT,
etl_time DATETIME
) ENGINE=OLAP
DUPLICATE KEY(company_id)
DISTRIBUTED BY HASH(company_id) BUCKETS 10;
-- dwd_contact_channel (当前最新全量)
CREATE TABLE dwd_contact_channel (
channel_id BIGINT,
contact_id BIGINT,
company_id BIGINT,
channel_type VARCHAR(20),
channel_value STRING,
source_codes ARRAY<VARCHAR(10)>, -- 存ID,如 ['1','2']
tag_codes ARRAY<VARCHAR(20)>, -- 存ID,如 ['valid','not_company']
is_valid TINYINT AS ARRAY_CONTAINS(tag_codes, 'valid'), -- 虚拟列或ETL计算
is_primary TINYINT,
etl_time DATETIME
) ENGINE=OLAP
DUPLICATE KEY(channel_id)
DISTRIBUTED BY HASH(company_id) BUCKETS 10;
💡 Doris 提示:
- 用
DUPLICATE KEY
模型存储明细数据 - 用
DISTRIBUTED BY HASH
分桶提升查询性能 - 对
source_codes
,tag_codes
建倒排索引加速查询
📊 三、DWS 层(轻度聚合层)—— 当前最新聚合
✅ 核心变化:
- 无时间维度,只计算当前总量/状态
- 每次全量重建,简单高效
🛠️ 具体工作:
1. 聚合计算(当前状态)
事项 | 说明 | 示例 |
---|---|---|
总数统计 | COUNT(*) | total_patent_count |
分布统计 | GROUP BY patent_type_code | invention_count ,utility_count |
覆盖率 | SUM(is_valid) / COUNT(*) | contact_valid_rate |
表结构示例
-- dws_company_patent_stat (当前最新)
CREATE TABLE dws_company_patent_stat (
company_id BIGINT,
total_patent_count BIGINT,
invention_count BIGINT,
software_copyright_count BIGINT,
last_apply_date DATE,
etl_time DATETIME
) ENGINE=OLAP
UNIQUE KEY(company_id) -- 每个企业一条最新记录
DISTRIBUTED BY HASH(company_id) BUCKETS 10;
-- dws_company_contact_effectiveness
CREATE TABLE dws_company_contact_effectiveness (
company_id BIGINT,
total_contact_count BIGINT,
valid_contact_count BIGINT,
wechat_contact_count BIGINT,
contact_valid_rate DECIMAL(5,4),
etl_time DATETIME
) ENGINE=OLAP
UNIQUE KEY(company_id)
DISTRIBUTED BY HASH(company_id) BUCKETS 10;
💡 Doris 提示:
- 用
UNIQUE KEY
模型,自动覆盖旧数据 - 每次 ETL 全量 INSERT OVERWRITE,或用 MERGE 操作
🖼️ 四、DWT 层(主题画像层)—— 当前最新画像(无历史包袱!)
✅ 重大利好:
- 无需考虑“新增字段要重算历史”问题 → 加字段只需跑一次当前全量!
- 画像表结构可灵活调整,无历史兼容压力
- ETL 超级简单:JOIN 当前 DWS + DWD → 生成最新画像
🛠️ 具体工作:
1. 画像特征构建
事项 | 说明 |
---|---|
只选当前核心指标 | 如total_patent_count ,contact_valid_rate |
特征工程 | 基于当前数据打标签,如CASE WHEN patent_count > 10 THEN '高潜力' |
存储 ID 非 name | 仍存industry_code , 查询时再 JOIN 名称 |
表结构示例(当前最新全量)
-- dwt_company_profile (当前最新画像)
CREATE TABLE dwt_company_profile (
company_id BIGINT,
company_name STRING,
industry_code VARCHAR(10),
province_code VARCHAR(10),
total_patent_count BIGINT,
total_copyright_count BIGINT,
contact_valid_rate DECIMAL(5,4),
customer_value_score DECIMAL(5,2), -- RFM 或自定义评分
company_scale_level TINYINT, -- 1~5 分层
is_high_potential TINYINT, -- 是否高潜力
profile_update_time DATETIME,
etl_time DATETIME
) ENGINE=OLAP
UNIQUE KEY(company_id) -- 每个企业一条最新画像
DISTRIBUTED BY HASH(company_id) BUCKETS 10;
ETL 逻辑(超级简单!)
-- 每次全量重建最新画像
INSERT OVERWRITE dwt_company_profile
SELECT
dwd.company_id,
dwd.company_name,
dwd.industry_code,
dwd.province_code,
dws1.total_patent_count,
dws2.total_copyright_count,
dws3.contact_valid_rate,
-- 特征工程
(dws1.total_patent_count * 0.4 + dws3.contact_valid_rate * 0.6) * 100 AS customer_value_score,
CASE
WHEN dws1.total_patent_count > 10 THEN 5
WHEN dws1.total_patent_count > 5 THEN 4
ELSE 3
END AS company_scale_level,
CASE WHEN dws1.total_patent_count > 5 AND dws3.contact_valid_rate > 0.8 THEN 1 ELSE 0 END AS is_high_potential,
NOW() AS profile_update_time,
NOW() AS etl_time
FROM dwd_company_base dwd
LEFT JOIN dws_company_patent_stat dws1 ON dwd.company_id = dws1.company_id
LEFT JOIN dws_company_copyright_stat dws2 ON dwd.company_id = dws2.company_id
LEFT JOIN dws_company_contact_effectiveness dws3 ON dwd.company_id = dws3.company_id;
优势:
- 新增字段?改一下 SELECT 语句,跑一次全量即可!
- 无历史数据重算压力
- 画像永远是最新状态,业务用得放心
📈 五、ADS 层(应用层)—— 直接查当前最新数据
✅ 查询方式:
-- 场景1:查高潜力企业(直接查DWT)
SELECT company_name, total_patent_count, customer_value_score
FROM dwt_company_profile
WHERE is_high_potential = 1
ORDER BY customer_value_score DESC;
-- 场景2:分析专利类型分布(查DWS)
SELECT
s.patent_type_name,
SUM(dws.patent_count) as total_count
FROM dws_company_patent_by_type dws
JOIN dim_patent_type s ON dws.patent_type_code = s.patent_type_code
GROUP BY s.patent_type_name;
✅ 性能优化:
- Doris 自动下推谓词、列裁剪
- 对高频查询字段建索引
- BI 工具直连 Doris,无需中间层
📜 六、配套规范(更新版)
1. 命名规范(去掉时间标识)
层级 | 命名规则 | 示例 |
---|---|---|
ODS | ods_系统名_表名 | ods_crm_company |
DWD | dwd_业务域_表名 | dwd_company_base (去掉_inc ) |
DWS | dws_主题_维度 | dws_company_patent_stat (去掉_1d ) |
DWT | dwt_主体_类型 | dwt_company_profile |
ADS | ads_应用场景 | ads_sales_dashboard |
更新策略
层级 | 策略 | 说明 |
---|---|---|
ODS | 全量覆盖 | 每日/每小时全量同步最新 |
DWD | 全量重建 | 每次 ETL 重建最新明细 |
DWS | 全量重建 | JOIN DWD 重新聚合 |
DWT | 全量重建 | JOIN DWS + DWD 重新生成画像 |
💡 为什么推荐全量重建?
- 无历史分区,数据量可控(千万级以内)
- 逻辑简单,不易出错
- Doris 写入性能强,全量重建很快
🧩 七、架构全景图(无分区版)
[源系统]
↓ 全量同步
[ODS] —— 当前最新全量,保留原始结构
↓ 全量重建:清洗、标准化、退化、Array存code
[DWD] —— 当前最新原子事实(企业、联系人、专利、联系方式)
↓ 全量重建:按主题聚合
[DWS] —— 当前最新轻度聚合(专利统计、联系有效性)
↓ 全量重建:特征工程、构建画像
[DWT] —— 当前最新企业/联系人画像(无历史包袱!)
↓ 直接查询
[ADS] —— 报表/API/BI
├─ 固定需求 → DWT
└─ 临时分析 → DWS + DWD
✅ 总结:无分区存量数仓的“三大优势”
- 架构更简单:无分区、无历史、无时间旅行
- 维护更轻松:加字段、改逻辑,只需跑一次全量
- 性能更优:Doris + 全量 + 倒排索引,查询飞快
你的数仓设计已经非常务实高效 —— 不是所有企业都需要“历史快照”,聚焦当前业务状态才是王道!