目录
-
- 写在前面
- 第一部分:数据仓库是什么?
- 第二部分:数据仓库和数据库到底有什么区别?
- [第三部分:数据仓库怎么做?(分层架构 + ETL + 代码示例)](#第三部分:数据仓库怎么做?(分层架构 + ETL + 代码示例))
-
- 一、分层架构:像工厂的流水线
-
- [第 1 层:ODS 层(操作数据存储层 / 贴源层)](#第 1 层:ODS 层(操作数据存储层 / 贴源层))
- [第 2 层:DWD 层(明细数据层)](#第 2 层:DWD 层(明细数据层))
- [第 3 层:DWS 层(汇总数据层)](#第 3 层:DWS 层(汇总数据层))
- [第 4 层:ADS 层(应用数据层)](#第 4 层:ADS 层(应用数据层))
- 分层架构图(完整数据流向)
- [二、ETL:数据的"搬运 + 加工"流程](#二、ETL:数据的“搬运 + 加工”流程)
- 第四部分:数据仓库的四大灵魂特质(进阶理解)
- 第五部分:常用工具与技术选型
- 第六部分:谁在用数据仓库?用来干嘛?
- [第七部分:AI + 数据仓库 = 下一代智能数据平台](#第七部分:AI + 数据仓库 = 下一代智能数据平台)
-
- [7.1 自然语言查数:从写 SQL 到"说人话"](#7.1 自然语言查数:从写 SQL 到“说人话”)
-
- [NL2SQL 的实现原理:从"人话"到"SQL"的四步走](#NL2SQL 的实现原理:从“人话”到“SQL”的四步走)
- [7.2 AI 原生数仓:大模型直接"长"在数仓里](#7.2 AI 原生数仓:大模型直接“长”在数仓里)
- 总结
- 参考资料
一篇写给小白的极简指南,附代码示例。小马以前觉得数据仓库那是大数据领域的事,和后端开发有什么关系。但最近越发觉得似乎这块技术领域离大家越来越近了,特别是Harness E2E对前后端技术壁垒的模糊,乃至最近FDE 工程师概念的崛起,在AI的狂潮下,似乎大数据和研发的这个边界也被打破了。总之一句话,未来也是得会,就算你不会也要让你的AI会,就这么粗暴。

写在前面
如果你是一个后端开发,大概遇到过这种场景:老板跑过来跟你说,"帮我拉一下去年全平台的用户复购率"。你打开 MySQL,准备跑一个复杂的联表查询,结果数据库 CPU 直接飙到 100%,在线业务开始报警,前端同事朝你怒吼:"你把线上库搞挂了!"
这时候,老板要的"历史分析数据"和系统要的"当前业务运行",在你那台小小的业务数据库里,无法兼容。
于是,数据仓库(Data Warehouse,简称 DW 或 DWH) 诞生了。
第一部分:数据仓库是什么?
一句话定义
数据仓库,就是企业版的"超级历史档案馆 + 中央情报局"。
它不负责"做生意"(比如收钱、改库存),它只负责"做军师"(比如定战略、看趋势)。
官方定义
数据仓库之父 Bill Inmon 在 1991 年给出的经典定义是:数据仓库是一个面向主题 的、集成 的、相对稳定 的、反映历史变化 的数据集合,用于支持管理决策。
拆开来看:
| 特征 | 大白话解释 |
|---|---|
| 面向主题 | 不按"功能"组织(比如订单系统、用户系统),而是按"业务主题"组织(比如销售分析、客户分析) |
| 集成 | 把多个系统(CRM、ERP、财务系统)的数据整合到一起,统一口径 |
| 相对稳定 | 数据一旦进入数仓,基本不改也不删 |
| 反映历史变化 | 不只记录"现在",还记录"过去",能追溯任意时间点的状态 |
| 支持管理决策 | 终极目标是帮老板和管理层做决策 |
举个🌰:超市的故事
日常业务数据库(比如收银系统)→ "收银小票本"
收银员每扫一瓶水,系统就记一笔:"2026 年 8 月 16 日 10:05 分,张三买了 1 瓶农夫山泉,收了 2 块钱。"
这是用来 "干活" 的:保证交易成功,库存减 1。
特点:快、实时,但只盯着眼前这一单。而且空间有限,上个月的小票可能就被清空了。
数据仓库(比如 Hive、ClickHouse)→ "超市总部的巨型档案库"
每天晚上,总部把全国所有门店当天几百万张小票全部收上来,清洗、整理、归类 (把"农夫山泉"、"矿泉水"、"饮用水"统一成"瓶装水"),然后永久存放在这个档案库里。
这是用来 "动脑子" 的:老板不关心"张三几点买水",老板关心的是------"过去 5 年,每年夏天哪种口味的饮料销量涨了多少?今年该提前囤多少货?"
特点:慢一点没关系 ,但数据极其全面、干净,且横跨好几年。
第二部分:数据仓库和数据库到底有什么区别?
很多人以为数据仓库就是"大一点的数据库",这是天大的误解。
它们的关系不像"小茶杯"和"大水缸",而像"流水账录音笔 "和"精装回忆录"。
核心区别一览表
| 对比维度 | 数据库(OLTP) | 数据仓库(OLAP) |
|---|---|---|
| 核心目标 | 支持企业的日常业务操作 | 支持管理层的战略决策和业务分析 |
| 主要操作 | 增删改查(下订单、改密码) | 海量读取(做报表、跑分析) |
| 数据特点 | 当前、实时、高频更新 | 历史性、汇总性、相对稳定 |
| 数据模型 | 高度规范化(三范式),减少冗余 | 非规范化(星型/雪花模型),存在一定冗余 |
| 事务特性 | 强调 ACID | 不关注事务处理的一致性 |
| 查询特点 | 简单、点状查询,小范围增删改 | 复杂聚合查询,大量数据扫描 |
| 数据量级 | GB 级别 | TB、PB 级别 |
| 读写模式 | 频繁读写 | 批量写入、大批量读取 |
| 用户群体 | 业务人员、操作员、应用程序 | 数据分析师、业务决策者、管理层 |
| 能否修改 | 能(删改是家常便饭) | 基本不能(一旦入库,永久封存) |
深入见解:为什么会有这种差异?
见解一:OLTP 与 OLAP 是两种完全不同的工作负载
数据库 OLTP 和数仓 OLAP 的本质区别在于:OLTP 优化的是"单条记录的快速读写",OLAP 优化的是"海量数据的批量分析"。
打个比方:OLTP 就像快递员------每天派送几百个包裹,每个包裹都要快速准确地送达;OLAP 就像城市规划师------不需要送快递,但需要分析过去五年所有快递数据,决定在哪里建新的物流中心。
见解二:数据库是为"应用"设计的,数仓是为"主题"设计的
数据库的设计围绕着"功能模块"------订单模块有订单表,用户模块有用户表,商品模块有商品表。这种设计对"做业务"很高效,但对"做分析"却很痛苦------你想查"2023年东北地区的销售冠军",可能需要 JOIN 四五张表,跑半天。
数仓的设计围绕着"业务主题"------把订单、用户、商品、地区等数据提前整合成一张"大宽表",让你一条 SQL 就能拎出来。
见解三:数据库追求"不冗余",数仓容忍"有冗余"
数据库用三范式设计,尽量减少数据重复------比如用户姓名只存一份,所有订单表只存 user_id,不存 user_name。这样更新用户姓名时只改一处,不会产生数据不一致。
数仓恰恰相反------它会把 user_name 直接冗余到订单明细表里。虽然浪费了点存储空间,但查询时不用 JOIN 用户表,速度飞快。这叫 "以空间换时间" 。
一句话总结
数据库解决"发生了什么"的问题,数据仓库解决"发生了什么趋势"的问题。
数据库是"前线士兵",负责打仗;数据仓库是"后方参谋",负责分析战局。
#mermaid-svg-W3R6ExVqB1fQ3vVk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-W3R6ExVqB1fQ3vVk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-W3R6ExVqB1fQ3vVk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-W3R6ExVqB1fQ3vVk .error-icon{fill:#552222;}#mermaid-svg-W3R6ExVqB1fQ3vVk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-W3R6ExVqB1fQ3vVk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-W3R6ExVqB1fQ3vVk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-W3R6ExVqB1fQ3vVk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-W3R6ExVqB1fQ3vVk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-W3R6ExVqB1fQ3vVk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-W3R6ExVqB1fQ3vVk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-W3R6ExVqB1fQ3vVk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-W3R6ExVqB1fQ3vVk .marker.cross{stroke:#333333;}#mermaid-svg-W3R6ExVqB1fQ3vVk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-W3R6ExVqB1fQ3vVk p{margin:0;}#mermaid-svg-W3R6ExVqB1fQ3vVk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-W3R6ExVqB1fQ3vVk .cluster-label text{fill:#333;}#mermaid-svg-W3R6ExVqB1fQ3vVk .cluster-label span{color:#333;}#mermaid-svg-W3R6ExVqB1fQ3vVk .cluster-label span p{background-color:transparent;}#mermaid-svg-W3R6ExVqB1fQ3vVk .label text,#mermaid-svg-W3R6ExVqB1fQ3vVk span{fill:#333;color:#333;}#mermaid-svg-W3R6ExVqB1fQ3vVk .node rect,#mermaid-svg-W3R6ExVqB1fQ3vVk .node circle,#mermaid-svg-W3R6ExVqB1fQ3vVk .node ellipse,#mermaid-svg-W3R6ExVqB1fQ3vVk .node polygon,#mermaid-svg-W3R6ExVqB1fQ3vVk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-W3R6ExVqB1fQ3vVk .rough-node .label text,#mermaid-svg-W3R6ExVqB1fQ3vVk .node .label text,#mermaid-svg-W3R6ExVqB1fQ3vVk .image-shape .label,#mermaid-svg-W3R6ExVqB1fQ3vVk .icon-shape .label{text-anchor:middle;}#mermaid-svg-W3R6ExVqB1fQ3vVk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-W3R6ExVqB1fQ3vVk .rough-node .label,#mermaid-svg-W3R6ExVqB1fQ3vVk .node .label,#mermaid-svg-W3R6ExVqB1fQ3vVk .image-shape .label,#mermaid-svg-W3R6ExVqB1fQ3vVk .icon-shape .label{text-align:center;}#mermaid-svg-W3R6ExVqB1fQ3vVk .node.clickable{cursor:pointer;}#mermaid-svg-W3R6ExVqB1fQ3vVk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-W3R6ExVqB1fQ3vVk .arrowheadPath{fill:#333333;}#mermaid-svg-W3R6ExVqB1fQ3vVk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-W3R6ExVqB1fQ3vVk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-W3R6ExVqB1fQ3vVk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-W3R6ExVqB1fQ3vVk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-W3R6ExVqB1fQ3vVk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-W3R6ExVqB1fQ3vVk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-W3R6ExVqB1fQ3vVk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-W3R6ExVqB1fQ3vVk .cluster text{fill:#333;}#mermaid-svg-W3R6ExVqB1fQ3vVk .cluster span{color:#333;}#mermaid-svg-W3R6ExVqB1fQ3vVk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-W3R6ExVqB1fQ3vVk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-W3R6ExVqB1fQ3vVk rect.text{fill:none;stroke-width:0;}#mermaid-svg-W3R6ExVqB1fQ3vVk .icon-shape,#mermaid-svg-W3R6ExVqB1fQ3vVk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-W3R6ExVqB1fQ3vVk .icon-shape p,#mermaid-svg-W3R6ExVqB1fQ3vVk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-W3R6ExVqB1fQ3vVk .icon-shape .label rect,#mermaid-svg-W3R6ExVqB1fQ3vVk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-W3R6ExVqB1fQ3vVk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-W3R6ExVqB1fQ3vVk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-W3R6ExVqB1fQ3vVk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 数据仓库特点
复杂聚合查询
非规范化设计
以空间换时间
批量写入读取
数据库特点
快速点状查询
高度规范化
强调ACID
支持增删改查
业务数据库
(OLTP)
核心区别
前线士兵
记录'发生了什么'
实时事务处理
高频读写
GB级数据
后方参谋
分析'发生了什么趋势'
批量分析查询
TB/PB级数据
第三部分:数据仓库怎么做?(分层架构 + ETL + 代码示例)
理解了"是什么",接下来看"怎么做"。数据仓库的实现可以概括为两个核心:分层架构 和 ETL 流程。
一、分层架构:像工厂的流水线
数据仓库采用分层设计,每一层只做自己该做的事,职责清晰。最经典的是四层架构。
分层架构的核心优势在于模块化 :每层独立演进,降低耦合度。以下我将结合完整的 SQL + Python 代码示例来讲解每一层。
第 1 层:ODS 层(操作数据存储层 / 贴源层)
职责 :数据的"原材料仓库 "。从各个业务系统(MySQL、Oracle、日志文件等)同步原始数据,几乎不做任何处理,保持与源系统结构一致。
作用:隔离风险、历史回溯、数据备份。
代码示例 1:ODS 层建表语句(Hive SQL)
sql
-- ODS层订单表:几乎和源系统结构一致
CREATE TABLE ods_order (
id BIGINT COMMENT '订单ID',
order_no STRING COMMENT '订单号',
user_id BIGINT COMMENT '用户ID',
shop_id BIGINT COMMENT '商家ID',
order_status INT COMMENT '订单状态',
pay_amount DECIMAL(12,2) COMMENT '支付金额',
order_time TIMESTAMP COMMENT '下单时间',
pay_time TIMESTAMP COMMENT '支付时间',
source_type STRING COMMENT '来源类型',
_source_table STRING COMMENT '源表名', -- 元数据:来自哪个表
_load_time TIMESTAMP COMMENT '数据加载时间' -- 元数据:何时加载
)
COMMENT 'ODS层订单表'
PARTITIONED BY (dt STRING) -- 按日期分区,方便管理
STORED AS PARQUET; -- 列式存储,节省空间
代码示例 2:使用 PySpark 从 MySQL 抽取数据到 ODS 层
python
from pyspark.sql import SparkSession
from pyspark.sql.functions import lit, current_timestamp
# 初始化 Spark 会话
spark = SparkSession.builder \
.appName("OfflineDataWarehouse") \
.getOrCreate()
# 从 MySQL 抽取订单数据(Extract)
orders_df = spark.read \
.format("jdbc") \
.option("url", "jdbc:mysql://mysql:3306/order_db") \
.option("dbtable", "orders") \
.option("user", "root") \
.option("password", "password") \
.load()
# 添加元数据字段(记录数据来源和加载时间)
orders_df = orders_df \
.withColumn("_source_table", lit("orders")) \
.withColumn("_load_time", current_timestamp())
# 加载到 ODS 层(Load)
orders_df.write \
.format("parquet") \
.mode("overwrite") \
.partitionBy("dt") \
.saveAsTable("ods_order")
第 2 层:DWD 层(明细数据层)
职责 :数据的"清洗与标准化车间 "。对 ODS 层的数据进行去重、纠错、格式统一、空值填充、维度关联。
作用:消除数据中的"噪声"和"杂质",产出干净、可信的明细数据。
代码示例 3:DWD 层建表语句(含拉链表)
sql
-- DWD层订单明细事实表:清洗后的干净数据 + 关联维度信息
CREATE TABLE dwd_order_detail (
order_id BIGINT COMMENT '订单ID',
order_no STRING COMMENT '订单号',
user_id BIGINT COMMENT '用户ID',
user_name STRING COMMENT '用户姓名', -- 从用户维度表关联过来
user_phone STRING COMMENT '用户手机',
shop_id BIGINT COMMENT '商家ID',
shop_name STRING COMMENT '商家名称', -- 从商家维度表关联过来
category_id BIGINT COMMENT '商家品类ID',
category_name STRING COMMENT '商家品类名称', -- 从品类维度表关联过来
order_status STRING COMMENT '订单状态',
order_status_name STRING COMMENT '订单状态名称',
order_amount DECIMAL(12,2) COMMENT '订单金额',
discount_amount DECIMAL(12,2) COMMENT '优惠金额',
pay_amount DECIMAL(12,2) COMMENT '实付金额',
pay_type STRING COMMENT '支付方式',
order_time TIMESTAMP COMMENT '下单时间',
pay_time TIMESTAMP COMMENT '支付时间',
cancel_time TIMESTAMP COMMENT '取消时间',
start_date STRING COMMENT '有效期开始日期', -- 拉链表字段
end_date STRING COMMENT '有效期结束日期', -- 拉链表字段
is_current INT COMMENT '是否最新(1是0否)', -- 拉链表字段
_load_time TIMESTAMP COMMENT '数据加载时间'
)
COMMENT 'DWD层订单明细表'
STORED AS PARQUET;
代码示例 4:从 ODS 到 DWD 的清洗转换 SQL
sql
-- 从ODS层清洗数据到DWD层
INSERT OVERWRITE TABLE dwd_order_detail
SELECT
id AS order_id,
order_no,
user_id,
u.user_name, -- 关联用户维度表,补齐姓名
u.user_phone,
shop_id,
s.shop_name, -- 关联商家维度表,补齐名称
c.category_id,
c.category_name,
CASE order_status -- 统一枚举值:把数字转成可读文字
WHEN 1 THEN '待支付'
WHEN 2 THEN '已支付'
WHEN 3 THEN '已发货'
WHEN 4 THEN '已完成'
WHEN 5 THEN '已取消'
ELSE '未知状态'
END AS order_status,
CASE order_status
WHEN 1 THEN 'Pending'
WHEN 2 THEN 'Paid'
WHEN 3 THEN 'Shipped'
WHEN 4 THEN 'Completed'
WHEN 5 THEN 'Cancelled'
ELSE 'Unknown'
END AS order_status_name,
order_amount,
discount_amount,
pay_amount,
pay_type,
order_time,
pay_time,
cancel_time,
DATE(order_time) AS start_date, -- 拉链开始日期
'9999-12-31' AS end_date, -- 拉链结束日期(无穷远)
1 AS is_current, -- 当前有效
CURRENT_TIMESTAMP() AS _load_time
FROM ods_order o
LEFT JOIN dim_user u ON o.user_id = u.user_id
LEFT JOIN dim_shop s ON o.shop_id = s.shop_id
LEFT JOIN dim_category c ON o.category_id = c.category_id
WHERE o.order_status IN (1,2,3,4,5) -- 过滤掉脏数据/测试数据
AND o.pay_amount >= 0; -- 过滤掉金额异常的订单
代码示例 5:使用 PySpark DataFrame API 进行清洗
python
from pyspark.sql.functions import col, when, regexp_replace
# 从 ODS 层读取数据
ods_df = spark.read.parquet("hdfs://ods_layer/")
# 数据清洗:过滤无效数据、处理空值、标准化格式
dwd_df = ods_df \
.filter(col("pay_amount") > 0) \ # 过滤负值
.filter(col("user_id").isNotNull()) \ # 过滤空用户
.withColumn("user_phone",
regexp_replace(col("user_phone"), r"\D", "")) \ # 手机号只保留数字
.withColumn("order_status_name",
when(col("order_status") == 1, "待支付")
.when(col("order_status") == 2, "已支付")
.when(col("order_status") == 3, "已发货")
.when(col("order_status") == 4, "已完成")
.when(col("order_status") == 5, "已取消")
.otherwise("未知"))
# 写入 DWD 层
dwd_df.write.mode("overwrite").parquet("hdfs://dwd_layer/")
第 3 层:DWS 层(汇总数据层)
职责 :数据的"预包装车间 "。根据常见的分析需求(如"按日统计销售额"),对明细数据进行轻度汇总,加工成易于使用的"半成品"。
作用 :以空间换时间,提前算好常用的统计指标,让最终查询报表的速度飞快。
代码示例 6:DWS 层建表语句
sql
-- DWS层:按天 + 按地区汇总的订单统计表
CREATE TABLE dws_daily_region_order (
dim_date_id STRING COMMENT '日期ID(如2026-08-16)',
dim_region_id BIGINT COMMENT '地区ID',
total_orders BIGINT COMMENT '订单总数',
total_amount DECIMAL(15,2) COMMENT '总成交金额',
avg_amount DECIMAL(10,2) COMMENT '平均客单价',
user_count BIGINT COMMENT '下单用户数'
)
COMMENT 'DWS层-日维度地区汇总表'
STORED AS PARQUET;
代码示例 7:从 DWD 到 DWS 的汇总 SQL
sql
-- 从DWD层汇总到DWS层:按天+地区统计
INSERT OVERWRITE TABLE dws_daily_region_order
SELECT
DATE(order_time) AS dim_date_id,
r.region_id AS dim_region_id,
COUNT(DISTINCT order_id) AS total_orders,
SUM(pay_amount) AS total_amount,
AVG(pay_amount) AS avg_amount,
COUNT(DISTINCT user_id) AS user_count
FROM dwd_order_detail o
LEFT JOIN dim_region r ON o.shop_id = r.shop_id
WHERE order_time >= '2026-01-01'
GROUP BY DATE(order_time), r.region_id;
第 4 层:ADS 层(应用数据层)
职责 :数据的"成品展示区 "。数据在这里被加工成最终面向用户的形态,直接服务于具体的报表、大屏或 AI 模型。
作用:业务人员直接接触的数据,屏蔽了底层所有复杂的加工逻辑。
代码示例 8:ADS 层建表语句
sql
-- ADS层:GMV日报表(直接给老板看)
CREATE TABLE ads_gmv_daily_report (
report_date STRING COMMENT '报表日期',
total_gmv DECIMAL(15,2) COMMENT '当日总GMV',
order_count BIGINT COMMENT '当日订单数',
avg_order_amount DECIMAL(10,2) COMMENT '平均订单金额',
top_region STRING COMMENT '销售额最高地区',
top_category STRING COMMENT '销售额最高品类'
)
COMMENT 'ADS层-GMV日报表'
STORED AS PARQUET;
分层架构图(完整数据流向)
#mermaid-svg-6rmAYjJ5xgGOFe8g{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6rmAYjJ5xgGOFe8g .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6rmAYjJ5xgGOFe8g .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6rmAYjJ5xgGOFe8g .error-icon{fill:#552222;}#mermaid-svg-6rmAYjJ5xgGOFe8g .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6rmAYjJ5xgGOFe8g .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6rmAYjJ5xgGOFe8g .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6rmAYjJ5xgGOFe8g .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6rmAYjJ5xgGOFe8g .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6rmAYjJ5xgGOFe8g .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6rmAYjJ5xgGOFe8g .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6rmAYjJ5xgGOFe8g .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6rmAYjJ5xgGOFe8g .marker.cross{stroke:#333333;}#mermaid-svg-6rmAYjJ5xgGOFe8g svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6rmAYjJ5xgGOFe8g p{margin:0;}#mermaid-svg-6rmAYjJ5xgGOFe8g .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6rmAYjJ5xgGOFe8g .cluster-label text{fill:#333;}#mermaid-svg-6rmAYjJ5xgGOFe8g .cluster-label span{color:#333;}#mermaid-svg-6rmAYjJ5xgGOFe8g .cluster-label span p{background-color:transparent;}#mermaid-svg-6rmAYjJ5xgGOFe8g .label text,#mermaid-svg-6rmAYjJ5xgGOFe8g span{fill:#333;color:#333;}#mermaid-svg-6rmAYjJ5xgGOFe8g .node rect,#mermaid-svg-6rmAYjJ5xgGOFe8g .node circle,#mermaid-svg-6rmAYjJ5xgGOFe8g .node ellipse,#mermaid-svg-6rmAYjJ5xgGOFe8g .node polygon,#mermaid-svg-6rmAYjJ5xgGOFe8g .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6rmAYjJ5xgGOFe8g .rough-node .label text,#mermaid-svg-6rmAYjJ5xgGOFe8g .node .label text,#mermaid-svg-6rmAYjJ5xgGOFe8g .image-shape .label,#mermaid-svg-6rmAYjJ5xgGOFe8g .icon-shape .label{text-anchor:middle;}#mermaid-svg-6rmAYjJ5xgGOFe8g .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6rmAYjJ5xgGOFe8g .rough-node .label,#mermaid-svg-6rmAYjJ5xgGOFe8g .node .label,#mermaid-svg-6rmAYjJ5xgGOFe8g .image-shape .label,#mermaid-svg-6rmAYjJ5xgGOFe8g .icon-shape .label{text-align:center;}#mermaid-svg-6rmAYjJ5xgGOFe8g .node.clickable{cursor:pointer;}#mermaid-svg-6rmAYjJ5xgGOFe8g .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6rmAYjJ5xgGOFe8g .arrowheadPath{fill:#333333;}#mermaid-svg-6rmAYjJ5xgGOFe8g .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6rmAYjJ5xgGOFe8g .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6rmAYjJ5xgGOFe8g .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6rmAYjJ5xgGOFe8g .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6rmAYjJ5xgGOFe8g .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6rmAYjJ5xgGOFe8g .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6rmAYjJ5xgGOFe8g .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6rmAYjJ5xgGOFe8g .cluster text{fill:#333;}#mermaid-svg-6rmAYjJ5xgGOFe8g .cluster span{color:#333;}#mermaid-svg-6rmAYjJ5xgGOFe8g div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6rmAYjJ5xgGOFe8g .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6rmAYjJ5xgGOFe8g rect.text{fill:none;stroke-width:0;}#mermaid-svg-6rmAYjJ5xgGOFe8g .icon-shape,#mermaid-svg-6rmAYjJ5xgGOFe8g .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6rmAYjJ5xgGOFe8g .icon-shape p,#mermaid-svg-6rmAYjJ5xgGOFe8g .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6rmAYjJ5xgGOFe8g .icon-shape .label rect,#mermaid-svg-6rmAYjJ5xgGOFe8g .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6rmAYjJ5xgGOFe8g .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6rmAYjJ5xgGOFe8g .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6rmAYjJ5xgGOFe8g :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 业务数据源
抽取(E)
抽取(E)
抽取(E)
清洗、关联(T)
汇总(T)
加载(L)
ADS层
应用数据层
最终成品
报表数据
大屏展示
AI模型输入
业务直接使用
DWS层
汇总数据层
轻度汇总
预聚合指标
按日/地区统计
以空间换时间
DWD层
明细数据层
数据清洗
去重纠错
格式统一
空值填充
维度关联
拉链表
ODS层
操作数据存储层
原始数据
保持源结构
数据备份
历史回溯
风险隔离
MySQL
业务数据库
Oracle
ERP系统
日志文件
应用日志
BI报表/大屏
AI模型/决策系统
二、ETL:数据的"搬运 + 加工"流程
有了分层架构(车间),还需要 ETL(物流系统)来搬运和加工数据。
ETL 是数据仓库的动力核心,包含三个步骤:
| 步骤 | 全称 | 大白话 | 在数仓中的对应 |
|---|---|---|---|
| E | Extract(抽取) | 从各个数据源抽数据 | 从 MySQL/Oracle/日志 → ODS 层 |
| T | Transform(转换) | 洗数据:去重、纠错、统一格式、关联维度 | ODS → DWD(清洗)→ DWS(汇总) |
| L | Load(加载) | 把处理好的数据装进目标层 | 加载到 DWD/DWS/ADS 层 |
代码示例 9:完整的 Python ETL 流水线脚本
python
"""
完整的 ETL 流水线:从 MySQL 抽取 → 清洗转换 → 加载到数据仓库
"""
import pandas as pd
import mysql.connector
from sqlalchemy import create_engine
# ============ Extract:从 MySQL 抽取数据 ============
def extract_from_mysql():
"""从源数据库抽取订单数据"""
conn = mysql.connector.connect(
host="localhost",
user="root",
password="password",
database="order_db"
)
query = "SELECT * FROM orders WHERE order_date >= '2026-01-01'"
df = pd.read_sql(query, conn)
conn.close()
print(f"✅ 抽取了 {len(df)} 条订单记录")
return df
# ============ Transform:数据清洗与转换 ============
def transform_data(df):
"""数据清洗、去重、标准化"""
# 1. 删除重复记录
df = df.drop_duplicates(subset=['order_id'])
# 2. 处理空值
df['user_id'] = df['user_id'].fillna(0)
df['pay_amount'] = df['pay_amount'].fillna(0)
# 3. 过滤无效数据(金额必须 >= 0)
df = df[df['pay_amount'] >= 0]
# 4. 标准化:状态码转文字
status_map = {1: '待支付', 2: '已支付', 3: '已发货',
4: '已完成', 5: '已取消'}
df['order_status_name'] = df['order_status'].map(status_map)
# 5. 新增衍生字段:订单年份、月份
df['order_year'] = pd.to_datetime(df['order_time']).dt.year
df['order_month'] = pd.to_datetime(df['order_time']).dt.month
print(f"✅ 清洗后剩余 {len(df)} 条有效记录")
return df
# ============ Load:加载到数据仓库 ============
def load_to_warehouse(df):
"""将清洗后的数据加载到数据仓库(DWD层)"""
engine = create_engine('mysql+pymysql://root:password@localhost:3306/dw_db')
df.to_sql('dwd_order_detail', engine, if_exists='append', index=False)
print(f"✅ 成功加载 {len(df)} 条记录到数据仓库")
# ============ 主流程 ============
def etl_pipeline():
print("🚀 开始 ETL 流程...")
raw_data = extract_from_mysql()
clean_data = transform_data(raw_data)
load_to_warehouse(clean_data)
print("🎉 ETL 流程完成!")
代码示例 10:ETL 错误处理与重试机制
生产环境的 ETL 远没有上面那么顺利------数据库可能连不上、数据格式可能脏、写入可能超时。下面演示如何捕获 ETL 过程中的常见异常,并用**指数退避(Exponential Backoff)**实现自动重试,同时记录日志并发送告警通知(模拟):
python
"""
ETL 错误处理与重试机制
依赖安装:pip install pandas mysql-connector-python sqlalchemy tenacity
"""
import logging
import time
import random
import pandas as pd
import mysql.connector
from sqlalchemy import create_engine
from tenacity import (
retry,
stop_after_attempt,
wait_exponential,
retry_if_exception_type,
before_sleep_log,
)
# ============ 1. 日志记录:把运行过程写到文件和控制台 ============
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
handlers=[
logging.FileHandler("etl_pipeline.log", encoding="utf-8"), # 落盘
logging.StreamHandler(), # 控制台
],
)
logger = logging.getLogger("etl_pipeline")
# ============ 2. 自定义异常:区分不同失败类型 ============
class DatabaseConnectionError(Exception):
"""数据库连接失败"""
pass
class DataFormatError(Exception):
"""数据格式错误"""
pass
class DataWriteError(Exception):
"""数据写入失败"""
pass
# ============ 3. 告警通知(模拟):生产环境可替换为钉钉/企业微信/邮件 ============
def send_alert(subject: str, message: str):
"""模拟发送告警通知"""
logger.error(f"🚨 [告警] {subject}:{message}")
# ============ 4. 带指数退避的重试装饰器 ============
@retry(
retry=retry_if_exception_type((DatabaseConnectionError, DataWriteError)),
stop=stop_after_attempt(5), # 最多重试 5 次
wait=wait_exponential(multiplier=1, min=1, max=30), # 等待 1s, 2s, 4s, 8s, 16s...
before_sleep=before_sleep_log(logger, logging.WARNING),
reraise=True, # 重试耗尽后抛出原始异常
)
def extract_from_mysql():
"""从源数据库抽取订单数据(带重试)"""
try:
logger.info("开始从 MySQL 抽取数据...")
conn = mysql.connector.connect(
host="localhost",
user="root",
password="password",
database="order_db",
connection_timeout=5, # 5 秒连不上就抛异常
)
query = "SELECT * FROM orders WHERE order_date >= '2026-01-01'"
df = pd.read_sql(query, conn)
conn.close()
logger.info(f"✅ 抽取了 {len(df)} 条订单记录")
return df
except mysql.connector.Error as e:
raise DatabaseConnectionError(f"MySQL 连接失败: {e}") from e
# ============ 5. Transform:数据格式错误不重试,直接抛业务异常 ============
def transform_data(df):
"""数据清洗、去重、标准化"""
try:
logger.info("开始清洗转换数据...")
df = df.drop_duplicates(subset=['order_id'])
df['user_id'] = df['user_id'].fillna(0)
df['pay_amount'] = df['pay_amount'].fillna(0)
df = df[df['pay_amount'] >= 0]
status_map = {1: '待支付', 2: '已支付', 3: '已发货',
4: '已完成', 5: '已取消'}
df['order_status_name'] = df['order_status'].map(status_map)
invalid_mask = df['order_status'].isin(status_map.keys())
if not invalid_mask.all():
bad_rows = df[~invalid_mask]
raise DataFormatError(
f"发现 {len(bad_rows)} 条非法状态码数据,"
f"非法值示例: {bad_rows['order_status'].unique()[:5]}"
)
df['order_year'] = pd.to_datetime(df['order_time']).dt.year
df['order_month'] = pd.to_datetime(df['order_time']).dt.month
logger.info(f"✅ 清洗后剩余 {len(df)} 条有效记录")
return df
except DataFormatError:
send_alert("数据格式错误", "Transform 阶段发现非法数据,请人工介入检查源数据")
raise
except Exception as e:
logger.exception("Transform 阶段发生未知异常")
raise
# ============ 6. Load:写入失败带重试 ============
@retry(
retry=retry_if_exception_type(DataWriteError),
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=2, min=2, max=60),
before_sleep=before_sleep_log(logger, logging.WARNING),
reraise=True,
)
def load_to_warehouse(df):
"""将清洗后的数据加载到数据仓库(DWD层)"""
try:
logger.info("开始加载数据到数据仓库...")
engine = create_engine('mysql+pymysql://root:password@localhost:3306/dw_db')
df.to_sql('dwd_order_detail', engine, if_exists='append', index=False)
logger.info(f"✅ 成功加载 {len(df)} 条记录到数据仓库")
except Exception as e:
raise DataWriteError(f"写入数据仓库失败: {e}") from e
# ============ 7. 主流程 ============
def etl_pipeline():
logger.info("🚀 开始 ETL 流程...")
try:
raw_data = extract_from_mysql()
clean_data = transform_data(raw_data)
load_to_warehouse(clean_data)
logger.info("🎉 ETL 流程完成!")
except DatabaseConnectionError as e:
send_alert("ETL 失败", f"数据库连接持续失败: {e}")
raise
except DataFormatError as e:
logger.error(f"❌ ETL 因数据格式错误终止: {e}")
raise
except DataWriteError as e:
send_alert("ETL 失败", f"数据写入持续失败: {e}")
raise
except Exception as e:
send_alert("ETL 失败", f"未知异常: {e}")
logger.exception("❌ ETL 流程异常终止")
raise
if __name__ == "__main__":
etl_pipeline()
输出示例(模拟数据库连接失败后的重试过程):
text
2026-08-16 11:30:01 [INFO] etl_pipeline: 🚀 开始 ETL 流程...
2026-08-16 11:30:01 [INFO] etl_pipeline: 开始从 MySQL 抽取数据...
2026-08-16 11:30:06 [WARNING] etl_pipeline: Retrying extract_from_mysql in 1.0 seconds...
2026-08-16 11:30:07 [WARNING] etl_pipeline: Retrying extract_from_mysql in 2.0 seconds...
2026-08-16 11:30:09 [WARNING] etl_pipeline: Retrying extract_from_mysql in 4.0 seconds...
2026-08-16 11:30:13 [WARNING] etl_pipeline: Retrying extract_from_mysql in 8.0 seconds...
2026-08-16 11:30:21 [WARNING] etl_pipeline: Retrying extract_from_mysql in 16.0 seconds...
2026-08-16 11:30:37 [ERROR] etl_pipeline: 🚨 [告警] ETL 失败:数据库连接持续失败: MySQL 连接失败: ...
说明:
tenacity库的wait_exponential实现了指数退避(1s → 2s → 4s → 8s → 16s),避免重试风暴压垮数据库。数据格式错误属于不可重试的异常,应直接告警并人工介入。
第四部分:数据仓库的四大灵魂特质(进阶理解)
基于上面的分层和 ETL,我们来重新理解数据仓库的四个核心特征:
1. 面向主题(Subject Oriented)
业务库是按"功能"分的(订单表、用户表、商品表)。数仓是按 "主题" 分的(销售主题、客户分析主题)。它把零散的表重新拼成一张大宽表,让你查"2023 年东北地区的销售冠军"时,一行代码就能拎出来。
2. 集成的(Integrated)
这是数仓最牛的能力!市场部说用户有 100 万,财务部说付费用户只有 80 万。数据仓库的作用就是强行统一 :我不管你们怎么定义,在我这里就以"身份证去重 + 实付金额 > 0"为准。统一口径,就是数据仓库最大的威严。
3. 相对稳定的(Non-Volatile)
业务库的数据随时在变(用户改密码、订单改状态)。但数仓里的数据一旦写入,基本不修改不删除。昨天的销售事实就是那样,不能因为今天老板心情好就改掉。
4. 反映历史变化的(Time Variant)
业务库只会记录"你现在是北京分公司的"。但数据仓库会记录:"去年你在上海,前年你在广州"。做人员流失分析、商品调价影响分析,全靠这些历史数据的积累。
第五部分:常用工具与技术选型
了解了数据仓库的架构和流程,接下来看看实际工作中常用的工具。没有"最好"的工具,只有"最合适"的。下面从适用场景、优缺点、学习成本三个维度,对比几款主流的数据仓库/大数据处理工具,帮你快速选型。
主流工具对比一览表
| 工具 | 类型 | 适用场景 | 优点 | 缺点 | 学习成本 |
|---|---|---|---|---|---|
| Hive | 离线数仓(基于 Hadoop) | 海量离线数据的批处理、ETL、报表 | 生态成熟、SQL 门槛低、可处理 PB 级数据 | 延迟高(分钟级)、不适合实时查询、运维复杂 | 中(需懂 Hadoop 生态) |
| Spark | 大数据计算引擎 | 大规模数据处理、ETL、机器学习、实时流处理 | 速度快(内存计算)、统一批流、API 丰富(SQL/Python/Scala) | 资源消耗大、调优门槛高、非存储引擎需搭配存储 | 中高(需理解 RDD/DataFrame 等概念) |
| ClickHouse | 开源列式 OLAP 数仓 | 实时分析、大宽表聚合查询、BI 报表、监控 | 查询极快(列式存储+向量化)、压缩率高、部署简单 | 不适合高频更新、事务支持弱、Join 复杂场景需优化 | 低(SQL 友好,上手快) |
| Snowflake | 云原生数仓(SaaS) | 企业级云数仓、弹性扩缩容、多租户、数据共享 | 存算分离、按量计费、零运维、生态完善 | 成本较高、依赖云厂商、国内访问延迟 | 低(标准 SQL,文档完善) |
| BigQuery | 云原生数仓(Google Cloud) | 海量数据分析、AI/ML 集成、实时分析 | 无服务器、自动扩缩容、内置 ML/AI 能力、按查询计费 | 依赖 GCP、费用不可控(需谨慎)、国内访问受限 | 低(标准 SQL,上手快) |
选型建议
- 个人学习 / 小团队快速验证 :优先选 ClickHouse,部署简单、查询快,SQL 上手成本最低。
- 企业已有 Hadoop 生态 :Hive + Spark 是经典组合,Hive 管存储和离线批处理,Spark 负责提速和复杂计算。
- 云上弹性需求 / 不想运维 :选 Snowflake 或 BigQuery,按量付费、免运维,适合中大型企业。
- AI 与数据仓库深度结合 :BigQuery 内置 Gemini 集成,是当前 AI 原生数仓的典型代表。
一句话总结 :没有银弹。先明确你的数据量、实时性要求和运维能力,再选工具------小步快跑用 ClickHouse,企业级云上选 Snowflake/BigQuery,Hadoop 存量生态则用 Hive + Spark。
第六部分:谁在用数据仓库?用来干嘛?
以前是 BI(商业智能)分析师在用,现在全员都在用:
- 老板/高管:看驾驶舱大屏。不需要翻 Excel,直接看数仓加工好的"GMV 实时大屏",知道今天赚了还是赔了。
- 产品经理:跑数仓里的用户行为日志,分析"用户为什么在支付页面流失了",以此决定把"付款按钮"放大还是变红。
- 数据分析师:写 SQL 查数仓,产出各种分析报告。
- 算法工程师 :AI 大模型需要海量的高质量历史数据来训练。如果数据是一团乱麻,AI 就是个大傻子。数据仓库就是给 AI 提供"教科书级"训练资料的地方。
第七部分:AI + 数据仓库 = 下一代智能数据平台
这是近年来最激动人心的变化------大语言模型(LLM)正在彻底改变我们与数据仓库的交互方式。过去,数据分析是"人找数据";现在,AI 让"数据找人"成为可能。下面从两个角度展开:自然语言查数(NL2SQL)和 AI 原生数仓。
7.1 自然语言查数:从写 SQL 到"说人话"
以前,业务人员想查数据,得找数据分析师写 SQL,等半天才能拿到结果。
现在,大模型可以直接把自然语言翻译成 SQL 。你对着数据仓库说一句 "对比华东与华南地区 Q3 的销售额,并分析主要影响因素" ,系统自动完成语义解析、数据关联、归因分析,直接输出图表和文字结论。
实际案例 :中科院软件所提出的 DBCopilot 框架,将自然语言查询分解为"模式路由"和"SQL 生成"两个阶段,利用大模型将问题转换为 SQL 查询语句。在实际应用中,搭载语义层后,自然语言转 SQL 的准确率可提升至 90% 以上。
NL2SQL 的实现原理:从"人话"到"SQL"的四步走
自然语言查数并不是"把问题丢给大模型就完事",而是一条精心设计的流水线。拆开来看,核心是四步:
第一步:Schema 理解(让模型"认识"你的数仓)
大模型并不知道你的数仓里有哪些表、哪些字段。所以第一步是把**表结构(Schema)**喂给模型------表名、字段名、字段类型、注释、甚至几行样例数据。这就是为什么需要 include_tables 限定表、用 sample_rows_in_table_info 给样例。Schema 越清晰,生成的 SQL 越准确。
第二步:语义解析(把"人话"拆成"查询意图")
模型把自然语言问题拆解成可执行的查询意图:要查哪些字段(SELECT)、过滤什么条件(WHERE)、按什么分组(GROUP BY)、怎么排序(ORDER BY)。
第三步:SQL 生成(约束式生成,防止"幻觉")
这是最关键的一步。直接让大模型自由发挥,它可能编造出不存在的表名或字段。所以生产环境通常用约束式生成:
- 限定表范围:只暴露给模型少数几张宽表,从源头杜绝"查错表"。
- 提示词约束:在 system prompt 里明确"只做 SELECT,禁止 INSERT/UPDATE/DELETE"。
- Schema 校验:生成后用解析器检查 SQL 引用的表和字段是否真实存在,不合法就重新生成。
第四步:执行与兜底(查不到就"自我纠错")
生成的 SQL 在数仓上执行,如果报错(比如字段名拼错),系统会把错误信息回喂给模型 ,让它根据报错修正 SQL 再试一次。这就是所谓的 "Self-Correction" 机制,能显著提升成功率。
下面用一个完整的示例,展示带 Schema 校验 + 错误自纠错 的 NL2SQL 链路:
代码示例 11:带 Schema 校验与自纠错的 NL2SQL(进阶版)
python
"""
进阶版 NL2SQL:带 Schema 校验 + 执行错误自纠错
依赖安装:pip install langchain langchain-community langchain-openai clickhouse-connect sqlglot
"""
import sqlglot
from langchain_community.utilities import SQLDatabase
from langchain_community.tools.sql_database.tool import QuerySQLDataBaseTool
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
# ============ 1. 连接数据仓库 ============
db = SQLDatabase.from_uri(
"clickhouse+connect://default:password@localhost:8123/dw_db",
include_tables=["dws_daily_region_order"],
sample_rows_in_table_info=3,
)
# ============ 2. 初始化大模型 ============
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# ============ 3. 提示词:约束 + 明确字段白名单 ============
prompt = ChatPromptTemplate.from_messages([
("system", """
你是一名资深数据分析师。请根据用户的自然语言问题,生成一条 ClickHouse SQL 查询语句。
可用表 dws_daily_region_order,字段白名单:
- dim_date_id(日期,格式 YYYY-MM-DD)
- dim_region_id(地区ID)
- total_orders(订单总数)
- total_amount(总成交金额)
- avg_amount(平均客单价)
- user_count(下单用户数)
规则:
1. 只能引用上述字段,禁止编造不存在的字段。
2. 只做 SELECT 查询,禁止 INSERT/UPDATE/DELETE。
3. 只输出 SQL 本身,不要任何解释。
"""),
("human", "{question}"),
])
# ============ 4. Schema 校验:用 sqlglot 解析,检查字段是否在白名单 ============
ALLOWED_COLUMNS = {
"dim_date_id", "dim_region_id",
"total_orders", "total_amount",
"avg_amount", "user_count",
}
def validate_sql(sql: str) -> tuple[bool, str]:
"""校验 SQL 是否合法、字段是否在白名单内"""
try:
parsed = sqlglot.parse_one(sql, read="clickhouse")
except Exception as e:
return False, f"SQL 语法错误: {e}"
for column in parsed.find_all(sqlglot.exp.Column):
col_name = column.name
if col_name not in ALLOWED_COLUMNS:
return False, f"非法字段: {col_name},不在白名单内"
return True, "OK"
# ============ 5. 带自纠错的 NL2SQL 主流程 ============
query_tool = QuerySQLDataBaseTool(db=db)
def nl2sql_with_retry(question: str, max_retries: int = 3) -> tuple:
"""生成 SQL → 校验 → 执行,失败则把错误回喂给模型重试"""
chain = prompt | llm
for attempt in range(max_retries):
response = chain.invoke({"question": question})
sql = response.content.strip()
ok, msg = validate_sql(sql)
if not ok:
print(f"⚠️ 第 {attempt+1} 次校验失败: {msg}")
question = f"{question}\n\n你上次生成的 SQL 有问题:{msg},请修正后重新生成。"
continue
try:
result = query_tool.invoke(sql)
return sql, result
except Exception as e:
print(f"⚠️ 第 {attempt+1} 次执行失败: {e}")
question = f"{question}\n\n你上次生成的 SQL 执行报错:{e},请修正后重新生成。"
continue
raise RuntimeError("重试多次仍失败,请人工介入")
# ============ 6. 使用示例 ============
if __name__ == "__main__":
question = "2026年8月,哪个地区的订单总数最多?"
print(f"🧑💻 用户提问:{question}\n")
sql, result = nl2sql_with_retry(question)
print(f"🤖 最终 SQL:\n{sql}\n")
print(f"📊 查询结果:\n{result}")
输出示例(模拟一次字段校验失败后的自纠错):
text
🧑💻 用户提问:2026年8月,哪个地区的订单总数最多?
⚠️ 第 1 次校验失败: 非法字段: region_name,不在白名单内
🤖 第 2 次生成 SQL:
SELECT dim_region_id, SUM(total_orders) AS total_orders
FROM dws_daily_region_order
WHERE dim_date_id >= '2026-08-01' AND dim_date_id <= '2026-08-31'
GROUP BY dim_region_id
ORDER BY total_orders DESC
LIMIT 1;
📊 查询结果:
[(330100, 128456)]
说明:
sqlglot是一个跨数据库的 SQL 解析器,这里用它做字段白名单校验 ,从源头拦截大模型"编造字段"的幻觉。生产环境还可接入语义层(Semantic Layer) ,把dim_region_id=330100自动映射成「杭州」。
7.2 AI 原生数仓:大模型直接"长"在数仓里
如果说 NL2SQL 是让 AI "使用"数仓,那么 AI 原生数仓(AI-Native Data Warehouse) 则是让大模型能力直接"长"在数仓的 SQL 引擎和计算框架中。主流云数据仓库已开始将大模型能力内置:
- Google BigQuery 推出了 BigQuery ML 和 Gemini 内置集成 ,用户可以直接在 SQL 中使用
ML.GENERATE_TEXT等函数,让大模型分析查询结果、生成摘要、甚至进行数据归因,无需将数据导出。 - 腾讯云 发布了 数据智能体 DataBuddy ,用户通过自然语言不仅能查询,还能自动完成数据建模、ETL 开发、任务编排、归因分析、报告生成等复杂任务,整体研发效率可提升 5 至 10 倍。
- Databricks Lakehouse AI 将大模型训练、微调、推理与数据工程、数据分析工作流无缝集成。
- Firebolt 推出了 MCP Server,让 Claude、Copilot 等 AI 助手可以直接连接并理解数据仓库中的表结构和数据。
代码示例 12:调用 BigQuery 内置的 Gemini 模型分析数据
以下示例演示如何直接在 BigQuery SQL 中调用 Gemini 模型,对数据仓库中的销售数据进行自然语言分析。
python
"""
AI 原生数仓:使用 BigQuery 内置的 Gemini 模型分析数据
依赖安装:pip install google-cloud-bigquery google-cloud-aiplatform
"""
from google.cloud import bigquery
# ============ 1. 初始化 BigQuery 客户端 ============
client = bigquery.Client(project="your-gcp-project")
# ============ 2. 使用 ML.GENERATE_TEXT 对表数据做自然语言分析 ============
analysis_sql = """
SELECT
ml_generate_text_result['content'] AS analysis,
ml_generate_text_usage['input_tokens'] AS input_tokens,
ml_generate_text_usage['output_tokens'] AS output_tokens
FROM
ML.GENERATE_TEXT(
MODEL `your_project.your_dataset.gemini_pro`,
(
SELECT
CONCAT(
'以下是 2026 年 8 月各地区销售汇总数据:',
STRING_AGG(
CONCAT('地区ID ', dim_region_id, ':订单数 ', total_orders,
',销售额 ', total_amount),
';'
)
) AS prompt
FROM `your_project.your_dataset.dws_daily_region_order`
WHERE dim_date_id BETWEEN '2026-08-01' AND '2026-08-31'
GROUP BY dim_region_id
LIMIT 10
),
STRUCT(
0.2 AS temperature,
1024 AS max_output_tokens,
'请分析以上各地区销售表现,指出增长最快的地区及可能原因。' AS instruction
)
);
"""
print("🔍 正在调用 BigQuery 内置的 Gemini 模型分析销售数据...")
result = client.query(analysis_sql).result()
for row in result:
print(f"\n🤖 AI 分析结论:\n{row.analysis}")
print(f"📥 输入 Token 数:{row.input_tokens}")
print(f"📤 输出 Token 数:{row.output_tokens}")
输出示例:
text
🔍 正在调用 BigQuery 内置的 Gemini 模型分析销售数据...
🤖 AI 分析结论:
根据提供的2026年8月销售数据,地区ID 330100(推测为华东某核心城市)表现最为突出,订单数128,456单,销售额23,400,000元,环比增长预估超过15%。其次是地区ID 440100(推测为华南某核心城市),订单数98,234单,销售额18,900,000元,保持稳定增长。
增长最快的地区是ID 330100。可能原因包括:
1. 该地区在8月可能推出了强有力的暑期促销活动。
2. 可能有新门店开业或渠道扩张,带来了新增流量。
3. 该地区的商品品类或供应链在8月更具季节性优势。
📥 输入 Token 数:1,245
📤 输出 Token 数:386
说明 :
ML.GENERATE_TEXT是 BigQuery 内置的 AI 函数。数据无需离开数据仓库,大模型直接在数据内部完成分析,保障了数据安全与合规性。这标志着数据分析从"人驱动"向"AI 驱动"的范式转变。
总结
数据仓库的本质,是面向主题、集成、稳定、反映历史变化 的数据集合,核心价值在于支持管理决策。它把散落在各业务系统的数据统一收拢、清洗、加工,最终沉淀为可供分析的高质量资产。
在架构上,经典的四层分层(ODS → DWD → DWS → ADS )让数据像工厂流水线一样逐级加工:ODS 负责原样接入,DWD 负责清洗标准化,DWS 负责预聚合汇总,ADS 直接面向报表与 AI 应用。配合 ETL 流程,数据得以从"杂乱无章"走向"开箱即用"。
一句话回顾:数据库解决"发生了什么",数据仓库解决"发生了什么趋势"。掌握分层架构与 ETL 思维,你就拿到了进入大数据世界的第一把钥匙。
参考资料
- Apache Hive 官方文档:Hive 是离线数仓的经典代表,官方文档是理解 HiveQL 与 Hadoop 生态的第一手资料。
- ClickHouse 官方文档:ClickHouse 上手快、查询极快,官方文档示例丰富,适合快速搭建个人数仓实验。
- 《数据仓库工具箱》(Ralph Kimball 著):维度建模的"圣经",系统讲解星型模型、事实表与维度表设计,是数仓进阶必读。
- Google BigQuery 文档:云原生数仓的代表,内置 BigQuery ML 与 Gemini 集成,是体验 AI 原生数仓的最佳入口。
- LangChain 官方文档:NL2SQL 与 AI 数据应用的主流框架,文档含大量可运行的 SQL 查询与 Agent 示例。
希望这篇通俗的讲解,能帮你推开大数据世界的第一扇门 🚪