1. 先回答一个几乎所有人都会问的问题:数据仓库到底是不是一种“更大的数据库”
如果有人问我数据仓库和数据库的区别,我通常不会直接甩定义,而是先反问一句:你最近一次用数据库,是查一条订单,还是统计一万个订单?
这个问题很关键。因为绝大多数人对数据仓库的误解,就是从“它大概是数据库的升级版”开始的。实际上,数据仓库不是更庞大的数据库,也不是数据库的替代品,它和数据库是两种在不同的年代、为解决不同问题而诞生的东西。数据库解决的是“业务能不能跑得动”,数据仓库解决的是“数据能不能用得好”。
我第一次接触数据仓库时也犯过这个错误。当时公司要做一个报表平台,业务方张口就要“把订单表、用户表、商品表都同步过去,然后随便查”。我就想,这不就是一个只读数据库吗?后来才发现,如果只是把数据复制一份,玩不出什么花。真正的数据仓库,从数据模型、存储方式、查询引擎到调度策略,都和联机事务处理(OLTP)数据库有本质差异。
1.1 为什么会有数据仓库这个东西
要理解数据仓库,得先理解它的诞生背景。
上世纪80年代末、90年代初,企业已经用数据库跑业务很多年了。财务系统、库存系统、CRM系统都在生产环境稳定运行。但问题来了:领导想看“本月各区域销售对比”,DBA要从订单库里写一条SQL,关联七八张表,跑上半小时,然后数据库CPU直接被打满,前台业务卡死。业务部门抱怨报表慢,DBA抱怨查询影响生产,IT部门每天救火。
于是大家意识到:面向事务处理的数据库,它的表结构设计、索引机制、锁策略,都是为了让“快速写入单条数据、修改单条数据、查询单条数据”更高效,而不是为了让“几百亿条记录做聚合分析”更高效。两者对系统的要求完全相反。数据仓库就是在这个背景下被提出的:把分析型负载从生产库中剥离出来,专门构建一套面向分析的数据存储和处理体系。
这套体系的设计原则,用一句话概括就是:面向主题、集成、非易失、随时间变化。简单解释一下:
- 面向主题:数据按业务主题组织,比如“销售”“客户”“库存”,而不是按业务系统的功能模块。
- 集成:来自不同源系统的数据要统一编码、统一单位、统一口径,比如A系统叫customer_id,B系统叫cust_no,到了数仓里必须变成一个字段。
- 非易失:数仓里的数据基本只追加、不修改,记录的是历史状态,不是当前业务账。
- 随时间变化:数据仓库天然带着时间维度,你能回答“上季度和这季度比改变了什么”,而不是只看当前值。
这四个特性,决定了数据仓库的建模方式、存储结构、ETL流程都和传统数据库不同。
1.2 数据仓库和数据库,从“设计初衷”就不一样
我从一开始就强调一个观点:不要把数据仓库当成“数据库的进阶”,它恰恰是数据库在另一个方向上的演进。
传统的OLTP数据库设计目标,是保证ACID(原子性、一致性、隔离性、持久性),核心关注点是事务处理。你去银行转账,账户扣款和收款方入账必须是一个原子操作,不能扣了钱对方没收到。这种场景要求数据库有很强的约束、索引、行级锁,大量使用范式化设计来避免数据冗余和更新异常。
数据仓库则完全反过来。它的核心关注点是分析查询的吞吐量,是“一条复杂SQL能不能在几秒内扫描完几十亿行数据”。它不关心单条记录的实时更新,更关心批量装载、列式存储、并行计算、分区裁剪。
所以你会看到两个典型的差异:
- 数据库里一张订单表可能拆成订单头表、订单明细表、客户表、商品表,通过外键关联,这叫范式化。数仓里则更喜欢把核心维度打平成大宽表,牺牲存储换速度,这叫反范式化。
- 数据库的索引是B+树为主,适合精确查找。数仓的索引更多是分区、桶、位图索引,配合列式存储,适合范围扫描和聚合。
我在和很多开发同学聊天时发现,最容易踩的坑就是用写业务系统的思路去建设数仓。比如一上来就建三范式模型,把所有维度拆得干干净净,结果分析报表要关联十几张表,性能惨不忍睹。反过来说,如果把生产库设计成大宽表,业务写入的更新异常会让你痛不欲生。这两种体系,各自有各自的土壤。
2. 从一张订单表看OLTP和OLAP在行为上的天壤之别
要真正搞懂数据仓库,不能只停留在概念上。我习惯用一张订单表来举例,因为它几乎是所有业务系统里最核心、最常见的一张表,也是OLTP和OLAP差异最直观的载体。
假设你的业务系统有一张订单表:
| 字段 | 说明 |
|---|---|
| order_id | 订单ID,主键 |
| user_id | 用户ID |
| product_id | 商品ID |
| order_amount | 订单金额 |
| order_time | 下单时间 |
| status | 订单状态 |
| province | 收货省份 |
2.1 数据库是给业务系统“跑交易”的
这套系统在线上跑的时候,用户每下一单,程序就执行一条INSERT INTO orders ...,然后给用户返回“下单成功”。有时候同时有上千人下单,数据库要保证互不干扰,每条订单都能快速写入。后台管理中,客服要根据订单号查订单详情,用SELECT * FROM orders WHERE order_id = 123456,主键命中,毫秒级返回。
这种场景还有一个特点:数据量虽然不断增长,但单次操作的数据量极小,重要的是并发能力和响应速度。数据库每次操作只需要访问几行数据,索引能非常高效地工作。这就是典型的OLTP(联机事务处理)负载。
如果用数据库去做“某个月份某省份订单总金额”这种统计,也不是不能跑,但代价很高。因为订单表可能已经有几千万行,而索引对聚合查询的帮助很有限。哪怕你建了(order_time, province)联合索引,数据库也还是要扫描一段时间窗口内的所有行,然后逐行累加。这个过程会占用大量I/O和CPU,而且统计的时间段越宽,性能越差。更麻烦的是,这种慢查询会和其他事务抢资源,直接影响线上体验。
2.2 数据仓库是给分析场景“算总账”的
现在看数据仓库怎么处理同样的表。
数仓会先把这张订单表按天分区、按省份分桶,用列式存储落到分布式节点上。同样一个统计需求“今年上半年每个省的订单总量和总金额”,在数仓里执行时会触发分区裁剪,只读1月到6月的数据;然后走列式存储,只读取province和order_amount两列,其他字段碰都不碰;再配合MPP(大规模并行处理)引擎,把数据拆到多个节点并行累加。结果就是,哪怕原始数据量已经达到几十亿行,这个统计也往往能在几秒内完成。
这就是OLAP(联机分析处理)的核心特征:单次查询涉及的数据量很大,但查询频率远低于OLTP,而且几乎不涉及单条记录级别的修改。它要的是吞吐量,不是事务性。
2.3 用一张对比表梳理核心差异
我把数据仓库和数据库的关键差异整理成一个表格,方便你对照着理解:
| 对比维度 | 数据库(OLTP) | 数据仓库(OLAP) |
|---|---|---|
| 核心目的 | 支撑业务事务处理 | 支撑分析决策 |
| 典型操作 | 增删改查单条记录 | 批量读取、聚合分析 |
| 数据量级 | 一般从几万到几千万行,部分可达亿级 | 从几千万到几十亿、上百亿行起步 |
| 数据模型 | 范式化设计为主 | 维度建模、宽表、反范式化 |
| 存储方式 | 行式存储为主 | 列式存储为主 |
| 索引策略 | B+树、唯一约束 | 分区、分桶、位图索引、排序键 |
| 实时性要求 | 毫秒级读写 | 允许秒级到分钟级延迟,批量更新 |
| 并发特征 | 高并发小查询 | 低并发大查询 |
| 数据变化 | 频繁更新、删除 | 以追加为主,历史不可变 |
| 典型产品 | MySQL、Oracle、PostgreSQL、达梦、人大金仓 | Hive、ClickHouse、Doris、Greenplum、Snowflake |
表格不是让你背的,而是帮你建立两个画面。画面一:数据库就像超市收银台,每笔交易要快速结算,排队的人很多;画面二:数据仓库就像后台财务室,定期把一天的账本拉出来,算利润、做同比、拆细项。你能要求收银台同时兼做财务分析室吗?理论上有这种小超市,但业务规模一大,必然要分开。
3. 数据仓库内部到底长什么样:分层架构是它的灵魂
理解完区别,接下来要进入实战了。数据仓库之所以叫“仓库”,是因为它内部有一套非常明确的组织逻辑。这套逻辑的第一体现就是分层。
很多刚接触数仓的人最常问的就是:数据仓库分层4层叫啥?标准答案是ODS、DWD、DWS、ADS,有时还会加一层DIM(维度层)。我在真实项目里见过五层、六层的设计,但万变不离其宗,核心就是这四层。
3.1 经典四层:ODS、DWD、DWS、ADS
把这四层拆开讲:
ODS层(操作数据存储层)
这一层是最贴近源系统的,简单说就是“原样落地”。你把业务库里的数据通过同步工具(比如DataX、Kettle、Flink CDC)抽到数仓,几乎不做清洗转换,只是在源数据基础上加上一个日期分区,按天存储。ODS的目的很简单:先把数据拿过来,别影响源系统;同时也是后续所有加工的数据原材料。
DWD层(明细数据层)
这一层是数据清洗和标准化的关键环节。你需要统一字段命名、统一枚举值、统一时间格式,比如把sex=1/0转成male/female,把created_at和create_time统一成create_time。同时会做一定的维度退化,把一些常用维度字段直接冗余到明细表,方便下游使用。DWD层保存的是经过清洗后的、最细粒度的业务事实,它是一张大明细宽表。
DWS层(汇总数据层)
这一层也叫服务层,核心是“按主题做汇总”。比如你按“当天”“省份”“商品类目”统计订单金额和订单量,把结果预聚合到一张表里。为什么需要这一层?因为直接查DWD层的十几亿行明细做报表,查询压力还是很大。DWS层相当于把一些高频率使用的统计口径提前算好,下游只要按照汇总粒度查表,秒级出结果。
ADS层(应用数据层)
ADS层就是给具体应用用的,面向BI报表、大屏、数据产品。它的数据通常是高度定制化的,一张表可能就对应一个页面。比如“经营驾驶舱”里展示的月度销售趋势、品类排行,都是ADS层直接提供的。这一层的数据量不大,但查询频率高,对稳定性要求很高。
3.2 为什么必须分层,直接一层不行吗
我在小公司见过“一张表打天下”的猛人:业务库同步过来后,写一个超级复杂的SQL,直接算所有指标给报表。前期确实爽,数据量一上来就崩了。原因不难理解:
第一,分层是“缓存思想”的体现。ODS只要同步,DWD只要清洗,DWS预计算,ADS只输出,每一层都在为上一层减负。好比做饭,买菜(ODS)、洗菜切菜(DWD)、配菜调味(DWS)、上桌装盘(ADS),如果从买菜到上桌只有一步,那厨房就全乱了。
第二,分层能实现口径统一。没有分层时,A报表统计“活跃用户”是一种SQL写法,B报表统计“活跃用户”又是另一种写法,两个数字对不上,业务部门天天扯皮。有了DWS层预先定义的指标口径,所有下游都从同一张汇总表取数,数字自然一致。
第三,分层便于权限控制和故障隔离。ODS层只允许ETL同学访问,DWS层可以开放给数据分析师,ADS层可以提供给产品经理看板。某一层挂了,不至于影响上层全部应用,至少能保证历史数据查询正常。
有些朋友问:那我不建数仓,直接用ClickHouse把明细表拉来分析行不行?可以,但你要清楚,ClickHouse本身不是完整的数据仓库体系,它更接近OLAP引擎。分层建设不是数仓的唯一答案,而是目前在大规模数据分析场景下,最成熟、最容易维护的工程模式。
3.3 我见过的最容易搞混的“维度建模”和“范式建模”
说到分层,就必须提建模。很多人在这一块很混乱,动不动就“反范式”“星型模型”一阵背。
数据库建模最常用的是三范式,目的是消除冗余、避免更新异常。但数仓里更常用的是维度建模,核心是事实表和维度表。
- 事实表:记录业务发生的过程,每行是一个测量事件,比如一笔订单、一次点击、一次登录。事实表里一般只有外键和度量值(金额、数量、时长)。
- 维度表:描述业务环境的属性,比如用户维度、商品维度、门店维度。维度表提供上下文,回答“谁、什么、在哪、何时”这些问题。
用订单分析举例:产生一笔订单后,订单事实表记录user_id、product_id、order_time、amount。而用户维度表记录这个用户的性别、年龄、城市;商品维度表记录商品名称、类目、价格。分析时要看“一线城市的女性用户最爱买什么”,只需要把事实表和这两张维度表关联起来,按维度分组聚合。
这就是最经典的星型模型:中间一张事实表,周围若干张维度表,就像星星一样。如果维度表之间还继续拆分,比如把地址维度拆成省、市、区等多张表,就形成雪花模型。雪花模型更规范,但查询关联更复杂;星型模型简单直观,在数仓里是主流选择。
4. 数据仓库落地过程中的选型与实战经验
理论说了一堆,但真正让数据仓库从“PPT架构”变成“可运行系统”的,是选型、建模、调优这三件事。我在这部分会讲一些比较实战的东西,这些东西都是我踩过坑以后总结出来的。
4.1 到底选MPP数据库还是Hadoop体系
很多人一上来就问:数仓到底用什么工具?是Oracle、MySQL,还是Hive、ClickHouse、Doris?这个问题没有标准答案,因为不同团队的数据量、预算、技术栈都不一样。
先厘清一个概念:数据仓库和数据库引擎不是一回事。数据仓库是一套数据组织方法,它可以构建在多种底层引擎上。你可以在Hive上建数仓,也可以在ClickHouse上建数仓,甚至可以在Greenplum、Doris、StarRocks这些分布式数据库上建数仓。
我们做选型时,一般会画一条分界线:
- 数据量在TB级别以内、实时性要求高、团队规模小:优先考虑MPP数据库,比如Doris、StarRocks、ClickHouse、Greenplum。它们部署简单、查询性能强,支持标准SQL,从MySQL/Oracle迁过来成本低。
- 数据量在几十TB甚至PB级、有复杂ETL和大量离线批处理需求:偏向Hadoop生态(Hive/Spark),加上配合调度系统(比如Airflow、DolphinScheduler)。Hadoop生态的扩展性强,生态丰富,但运维复杂,SQL延迟也更高。
我个人的建议是:不要为了技术新鲜感去选型。很多中小公司业务量撑不起Hadoop的运维成本,硬上Hive只会让数仓变成“查询慢、任务每天挂、DBA天天加班”的灾难。用Doris或者ClickHouse做一个轻量级数仓,往往幸福指数高很多。
4.2 数仓建模从哪下手:维度表、事实表、缓慢变化维
选好引擎之后,下一步是建模。我建议新手按这个顺序去实践:
第一,梳理业务流程和指标。先搞明白业务方关心的指标有哪些,是订单金额、下单用户数,还是复购率。明确指标之后,再反推需要哪些事实表和维度表。
第二,确定事实表的粒度。粒度就是“一行代表什么”。订单事实表每一行代表一个订单,还是代表订单明细行?这个必须一开始就定死,否则后续统计会混淆。我见过很多案例,因为粒度没定义清楚,导致同一张表既统计订单数又统计商品件数,结果口径全错。
第三,设计维度表和缓慢变化维。维度表相对好设计,重点是处理维度属性变化。比如用户的“会员等级”从普通用户变成金牌用户,这个变化对历史订单分析有没有影响?如果我们要分析“下单时的用户等级”,就不能直接覆盖原来的等级字段,而应该使用缓慢变化维(SCD)策略。
最简单的SCD策略是直接覆盖(Type 1,不保留历史),适合对历史不敏感的字段。更常用的是新增一行并带上开始时间和结束时间(Type 2),这样既能还原当时的情况,也能追踪历史变化。实际项目中,不要每条维度都做Type 2,因为会大大增加存储和ETL复杂度。只有那些真正影响业务分析的字段,比如用户等级、门店状态,才值得做。
第四,写ETL脚本,实现数据从ODS到DWD、DWS、ADS的自动加工。ETL脚本尽量用SQL实现,因为可读性强、调试方便。复杂逻辑可以用Spark/Flink写,但要做好数据血缘和任务日志。
4.3 数仓性能调优的几个常见坑
这里列几个我实际碰到过的坑,比较典型。
第一个坑是列式存储却不指定排序键。ClickHouse和Doris这类引擎,排序键决定了存储顺序和数据压缩比。如果不设计排序键,查询过滤效果会很差。比如订单表经常按order_time和province过滤,就应该把这俩字段作为排序键,让相邻数据在磁盘上尽量连续,查询时能跳过大量分区。
第二个坑是使用SELECT *到处查。列式存储的强项是按列读取,只查询需要的列能极大减少I/O。很多人写SQL懒,直接SELECT *,导致每查一次都要读全列,性能下降好几倍。这一点在数仓里比在OLTP数据库里影响更明显。
第三个坑是分区字段选错类型。有些同学把时间字段定义为字符串,再用LIKE '2024-01%'来做过滤,分区裁剪直接失效,全表扫描。正确的做法是用DATE或DATETIME类型,用>=<范围条件,才能触发分区裁剪。
第四个坑是关联查询时小表不在前面。虽然在MPP引擎里优化器会自己做谓词下推和join重排,但如果你用Hive或者Spark,关联顺序对性能影响还是很大。建议手动把维度表等小表放前面,用MapJoin或Broadcast Join减少shuffle。
这些坑看起来很细,但在数仓性能问题里占比非常高。我调优过不少慢查询,最终发现80%的问题都不是引擎不够强,而是SQL写法或者表设计不符合引擎特性。
5. 数据仓库和数据库的边界正在模糊,但这不等于没区别
近些年出了很多新概念:湖仓一体、HTAP(混合事务/分析处理)、实时数据仓库、向量数据库。有些人开始说“数据仓库要过时了”“数据库和数仓的界限已经不存在了”。我的观点是:边界确实模糊了,但底层逻辑没变,不能因为工具变强就否定架构设计的意义。
5.1 湖仓一体、HTAP到底在解决什么问题
先说HTAP。传统架构里,OLTP数据库和OLAP数据仓库是分开的:业务系统写MySQL,通过ETL同步到数仓,数仓跑分析。这套架构最大的痛点是时序性:数据从生产到分析,有T+1延迟,甚至更久。
HTAP数据库想解决的就是这个痛点,让一个系统同时提供事务处理和分析处理能力,典型产品有TiDB、OceanBase等。它能在你执行事务性写入的同时,对同一份数据做复杂的分析查询,理论上不用再分OLTP和OLAP两套系统。
那HTAP能不能完全替代数据仓库?我的判断是:能替代一部分,但替代不了复杂数仓场景。原因很简单:HTAP的定位是“实时业务分析”,能处理的数据规模和复杂程度是有限的。当你需要做跨数月的超大规模历史数据回溯、需要整合十几个异构数据源、需要做复杂的多层ETL时,仍然需要数据仓库这种体系化的分层架构。
再说湖仓一体。数据湖以低成本存储海量原始数据,包括结构化、半结构化、非结构化数据,典型如Iceberg、Hudi、Delta Lake。数据仓库擅长处理结构化数据,但面对日志文件、图片元数据、音频文件就力不从心。湖仓一体是把数据湖的灵活性和数据仓库的管理能力结合起来,把数仓的“健壮的表结构约束”下沉到数据湖的文件上。
所以你看,HTAP和湖仓一体真正解决的问题是“实时性”和“多样性”,没有推翻“事务和分析分离”的事实。数据仓库的本质仍然是面向分析、面向历史的,这个定位不会边缘化。
5.2 什么时候你该考虑上数据仓库
很多同学问,我公司才几百张表、几十G数据,有必要搞数仓吗?我的答案是:不要盲目跟风。
你现在可以拿这几个问题自测:
- 你是不是经常要跑超过10分钟的分析查询,而且已经影响到业务库性能?
- 你是不是有多个业务系统,同一个“用户ID”在A系统叫
user_id,在B系统叫uid,每次取数都要人工对齐? - 业务方是不是总抱怨“报表数据不准,两个部门出来的订单数不一样”?
- 你是不是需要看历史变化趋势,而源系统只保留最近三个月的数据?
如果以上答案全部是否,那你可能连数仓都不需要,老老实实用好数据库就行。如果中了2条以上,说明你已经有分析负载和口径统一的诉求了,可以考虑引入数仓。
我建议的最小落地路径是:先建一个ODS层,把所有源系统数据按天同步到仓库里;再建一张核心业务宽表(相当于DWD层),把常用的维度字段关联进去;最后基于宽表做报表。等这一套跑顺了,再逐步增加DWS汇总层、完善指标口径管理。不要一上来就照着大厂的“ODS-DWD-DWS-ADS”照搬,规模不同,复杂度对应也不同。
5.3 给新人的一个最低成本入门路线
如果你是个完全没接触过数仓的新人,又想快速建立体感,我建议按这个路线来操作:
第一步,找一台电脑装一下MySQL或者PostgreSQL,把一张十万行的订单表导入进去,试着自己写聚合SQL,比如按省份、按月统计订单金额。感受一下分析SQL在OLTP数据库上的性能瓶颈。
第二步,装一个开源的ClickHouse或者Doris(用官方docker部署很省事),把同一份数据导进去,按天分区、按省份排序,再跑同样的SQL,对比一下性能差异。你会发现可能从几百毫秒变成几十毫秒,这一下就理解了列式存储和OLAP引擎的威力。
第三步,手动模仿四层架构。建一个ODS表存原始数据,再建一张DWD表清洗脏数据,再建一张DWS表按天汇总,最后从DWS查数据做Excel图表。不用任何工具,用SQL就能完成全流程。做完这一步,你对数仓的理解会超过很多只背概念的人。
这个过程中,你还会接触到“ETL”“调度”“元数据”“数据血缘”这些概念。先不用焦虑,一个个在实际操作中去理解它们解决什么问题,比堆术语有效得多。
动手永远是学习数仓最好的方式。我在早期学习时就吃了只看书的亏,看了半年理论,遇到真实项目还是不知道怎么建表。后来自己搭了一套最小环境,从同步数据到报表输出完整跑通之后,很多疑虑一下子就通了。希望这篇文章能帮你减少一些摸索时间,让你少走几步弯路。