财务部门想要过去两年按折扣码统计的收入。在一家公司,这只是改一点 SQL 再重建,因为加载过的每一行订单都还留在 raw 表里。在另一家公司,夜间任务从没保留过折扣列,于是有人要修改任务,再从生产库重新抽取两年的历史数据。
区别在于转换在哪里运行。ETL(extract、transform、load)在数据到达仓库之前,用单独的引擎对数据整形。ELT(extract、load、transform)把原始数据复制进仓库,再在仓库里用 SQL 整形。这个选择决定了你的计算账单、数据的新鲜度、修改逻辑的难易程度,以及个人数据最终落在哪里。
本文依次讲解两种管道并加以对比,说明 ETL 仍然占优的场景和常见的混合做法,最后给出一张并排对比图。
ETL 管道如何工作
在 ETL 中,转换引擎位于数据源和仓库之间。一次夜间运行如下:
- **抽取(Extract)**新增和变更的行,可以用基于
updated_at列的查询,也可以来自 change data capture 流。 - **转换(Transform)**这些行:在引擎上清洗数值、去重、关联参考数据、对个人数据脱敏,并把结果整理成事实表和维度表。
- 加载(Load):用 insert 或 merge 把结果写入仓库。
引擎可以是 Informatica、SQL Server Integration Services(SSIS)、Spark 作业或 Python 脚本。原始抽取结果通常放在暂存区,任务成功运行后会被清空,所以仓库无法回答任务事先没有预料到的问题。
ELT 管道如何工作
在 ELT 中,由仓库来完成转换工作:
- 抽取并加载。 Fivetran 或 Airbyte 这样的 loader 把源表几乎原样复制到
rawschema,并添加_fivetran_synced或_airbyte_extracted_at这样的元数据列,记录每一行是何时同步的。 - 转换。 SQL 模型(通常用 dbt 管理)读取 raw 表,在其他 schema 中构建清洗并关联好的表。
- 提供服务。 BI 工具查询最终的表,这些表通常称为 mart。
ELT 随着存储与计算分离的云仓库(如 BigQuery 和 Snowflake)而流行:保留原始副本成本很低,转换能力随仓库一起扩展。因为加载过的每一行都保留着,你可以修正模型、新增列或定义新指标,然后从历史数据重建,而不必碰数据源。
ETL 与 ELT 对比
| ETL | ELT | |
|---|---|---|
| 转换在哪里运行 | 仓库之前的单独引擎 | 仓库内部,以 SQL 运行 |
| 进入仓库的内容 | 只有建好模型的表 | 先是 raw 表,再是建好模型的表 |
| 是否保留原始数据 | 很少;抽取结果会被清空 | 保留,放在 raw schema 中 |
| 源端新增列 | 在有人做映射之前会被忽略 | loader 可以把它们加到 raw 表;修改 SQL 后模型才会使用 |
| 修改业务逻辑 | 修改任务,重新抽取历史数据 | 修改 SQL,从 raw 重建 |
| 计算成本 | 引擎的服务器或集群,加上仓库 | 每次运行模型都消耗仓库计算,加上 raw 存储 |
| 个人数据 | 落地之前已脱敏 | 原样落地,除非 loader 做了脱敏 |
| 新鲜度 | 整个任务结束后才更新 | 原始数据按 loader 的计划更新;mart 在模型运行时更新 |
| 常用工具 | Informatica、SSIS、AWS Glue、Spark | Fivetran 或 Airbyte、dbt,以及 BigQuery、Snowflake、Redshift 或 Databricks |
调度计划比缩写更重要:Fivetran 默认每六小时同步一次,这比每小时运行的 ETL 任务更不新鲜。
ETL 仍然占优的场景
- 个人数据不能以原始形式落地。 如果明文邮箱地址或银行卡号绝不能进入仓库,就要在加载之前脱敏。仓库的脱敏策略不会改变存储的内容:Snowflake 的 Dynamic Data Masking(Enterprise Edition 的功能)是在查询时对明文值做脱敏。
- 目标端无法做转换。 容量固定的旧仓库、搜索索引或合作伙伴的文件投递,都要求拿到的是成品数据。
- SQL 不擅长的数据增强,例如逐行调用 API、地理编码、PDF 解析或模型打分,通常更适合用 Python 或 Spark。
- 低延迟的流处理。 Apache Flink 或 Kafka Streams 在事件发生后几秒内就完成转换;Kafka 架构图展示了它们从哪里读取数据。
- 你不想保留的数据量。 10,000 个传感器每秒一次读数,每天产生 8.64 亿行(10,000 × 86,400)。在加载之前先算出一分钟平均值,可以降到 1,440 万行(10,000 × 1,440),少了 60 倍。
常见的混合做法
实际上,许多 ELT 管道会在加载之前加入一小步转换,有时写作 EtLT。只在传输途中转换那些绝不能原样落地的内容;其余全部放在仓库里做,那里有版本管理、有测试,也方便重建。
对个人数据来说,Fivetran 的列哈希会在同步时用 SHA-256 加每个目标端独立的盐对列做哈希,所以哈希后的邮箱仍然可以关联;数据屏蔽则会直接丢弃整列。Airbyte 的 mappings 可以按 stream 对字段做哈希或加密。无论用哪种工具,都应优先选择加盐哈希:邮箱的普通 SHA-256 可以和已知地址列表比对。还有两点需要注意:哈希只覆盖之后的同步,所以历史数据需要重新同步;加盐哈希是假名化,而不是匿名化。根据 GDPR,假名化数据仍然属于个人数据(鉴于条款第 26 条)。本文不构成法律意见;请和你的隐私团队一起阅读官方文本,并维护一份数据映射,记录个人数据的流向。
用分层 SQL 模型做 ELT
dbt 的项目结构指南介绍了三层结构,适用于任何 SQL 工具:
| 层 | 作用 | 物化方式 | 读取方 |
|---|---|---|---|
| Staging | 每个源表一个模型,例如 stg_shop__orders:重命名、转换类型、把美分换算成美元;不做 join,也不做聚合 |
视图 | 其他模型 |
| Intermediate | 有特定用途的中间步骤,例如把订单明细汇总成订单 | 临时(ephemeral)模型,或单独 schema 中的视图 | 其他模型 |
| Marts | 每个业务实体一张宽表,例如订单或客户 | 表或增量模型 | BI 工具和分析师 |
Lakehouse 用的是同样的思路,只是叫青铜、白银和黄金;参见奖章架构图。
dbt 自带四种通用数据测试:unique、not_null、accepted_values 和 relationships,任何返回失败行的查询都可以变成一个测试。新鲜度检查能发现悄悄停掉的 loader。一个 source 定义就能同时声明两者:
sources:
- name: shop
schema: raw_shop
config:
loaded_at_field: _fivetran_synced
freshness:
warn_after: {count: 12, period: hour}
error_after: {count: 24, period: hour}
tables:
- name: orders
columns:
- name: id
data_tests:
- unique
- not_null
当最新的 _fivetran_synced 值超过 12 小时时,dbt source freshness(在 dbt v2 中是 dbt freshness)会发出警告,超过 24 小时则报错。
编排与数据质量
编排器按顺序运行各个步骤。在 Airflow 中,DAG(带依赖关系和调度计划的任务集合)会触发 loader 同步,等待它完成,再运行 dbt。Dagster 的 dbt 集成会把每个 dbt 模型变成一个 asset(应当存在的表),因此 mart 可以直接依赖 Fivetran 同步写入的表。
无论哪种方式,dbt build 都能把坏数据挡在仪表盘之外:它按 DAG 顺序运行模型、测试、快照和种子,某个测试失败时会跳过所有下游。再加上新鲜度检查和发给负责团队的告警,错误数据和过期数据都会在相关方发现之前暴露出来。
Reverse ETL(Airbyte 称之为 data activation)方向相反:把建好模型的数据从仓库复制到 CRM 或客服系统等工具中。截至本文撰写时(2026 年 11 月),Hightouch 将它作为独立产品提供,Fivetran(收购了 Census)和 Airbyte 则把它与数据接入捆绑在一起。
ETL 与 ELT 对比图
图表内容
- ETL: extract, transform, then load
- ELT: extract, load, then transform in the warehouse
- Warehouse
- Orders DB (Postgres)
- Transform engine (Spark, SSIS)
- fct_orders
- BI dashboards
- Loader (Fivetran, Airbyte)
- raw.orders (as loaded)
- dbt models: staging, intermediate
- marts.fct_orders
- raw rows
- cleaned, conformed rows
- SQL queries
- raw rows, PII hashed
- raw tables
- built tables
这张图的读法如下:
- 两条泳道,两端相同:同一个 Postgres 订单数据库,同样的 BI 仪表盘。只有中间部分不同。
- 琥珀色标出转换:ETL 中是仓库之前的单独引擎,ELT 中是仓库内的 dbt 模型方框。
- 仓库的框显示存储的内容。 ETL 的框里只有
fct_orders;ELT 的框里有按原样加载的raw.orders、各个模型和marts.fct_orders。 - 紫色的边代表混合做法:loader 在行落地之前对个人数据做哈希。
- Markdown 块列出了 ELT 的运行顺序、新鲜度阈值,以及 BI 读取哪个 schema。
如何清晰地绘制 ETL 和 ELT 图
- 把转换画在它运行的地方。 把 ELT 的转换画在仓库之外,会掩盖它最主要的成本和风险。
- 把仓库画成一个边界。 raw、staging 和 marts 放在同一个框里,说明它们是同一个系统中的 schema,而不是各自独立的数据库。
- 在边上标注流动的内容(原始行、哈希后的行、构建好的表),必要时再标注调度计划。
- 标出个人数据在哪里脱敏,以及哪里仍以明文形式存在。
- 展示层,而不是每个模型。 每层给一张示例表就够了;数据血缘应由 dbt 生成的文档来承载。
- 把控制流分开。 把编排器的触发画成虚线边,避免有人把它们当成数据。
数据管道架构图展示了包含 CDC、Kafka 和仓库的完整管道。
在 Archboard 中构建
- 用 Google 或 GitHub 登录并创建一个图表。
- 从基础包中添加两个框架节点,上下排列,分别用于 ETL 和 ELT 泳道;在每条泳道中再嵌套一个用于仓库的框架,水平位置保持一致。
- 在 ETL 泳道中,添加一个来自数据包的 Postgres 节点,一个基础包的方框作为转换引擎,一个来自数据包的数据仓库节点(在仓库框架内,用于
fct_orders),以及一个基础包的方框作为 BI。给边加上标签:raw rows、cleaned and conformed rows、SQL queries。 - 在 ELT 泳道中,添加一个 Postgres 节点和一个基础包的方框作为 loader。在仓库框架内,为
raw.orders和marts.fct_orders添加数据仓库节点,并在它们之间放一个基础包的方框表示 dbt 模型。最后放一个基础包的方框作为 BI,并用同样的方式给边加标签。 - 把两个转换方框设为琥珀色,把 loader 的输出边设为紫色。在需要说明的节点下方添加基础包的文本备注和一个简短的颜色图例。
- 从基础包中添加一个 Markdown 块,写上运行顺序和新鲜度阈值。
如果管道已经以 Mermaid 流程图的形式存在,可以把它粘贴到 Mermaid 转可编辑图表工具中生成初稿。这个示例共用 15 个图表节点,在 Free 套餐的限制之内。
常见问题
ETL 和 ELT 有什么区别? 区别在于转换在哪里运行。ETL 在单独的引擎上转换数据,只加载结果。ELT 先把原始数据加载进仓库,再用 SQL 在仓库内转换,所以原始副本仍然可用于重建。
ETL 过时了吗? 没有。在云仓库上,ELT 通常是默认选择,但在个人数据必须先脱敏再落地、目标端无法做转换,以及需要重度数据增强或低延迟流处理时,ETL 仍然适用。
dbt 是 ETL 工具吗? dbt 负责 ELT 中的 T。它在你的仓库内运行 SQL 模型和测试,但不抽取也不加载数据;这是 Fivetran 或 Airbyte 这样的 loader 的工作。
ELT 比 ETL 便宜吗? 这取决于数据量和使用习惯。ELT 省去了单独的引擎,但每次运行模型都按仓库计算计费,还要存储原始副本。增量模型能压低成本;对大表每小时做一次全量重建则不行。
什么是 reverse ETL? 把建好模型的数据从仓库复制到 CRM 等业务工具中。它应当从 mart 读取,按计划运行,并和其他管道一样配备测试和负责人。