ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ELT二十年:概念拆解与Python+PostgreSQL实战

ELT二十年:概念拆解与Python+PostgreSQL实战 如果你最近几年才开始接触数据工程一定会频繁听到这样一句话先把数据抽过来、装进去再在仓库内部慢慢转换不要每一步都急着做 ETL。这套“先装载、后转换”的思路如今已经成为云数仓时代的默认做法。但如果把时间往前拨二十年ELT 这个简洁的理念并没有这么顺理成章——那时候数据量还没有今天夸张传统数仓的算力也很金贵大家更习惯在进入数仓之前就把数据清洗干净。回过头看ELT 概念从被少数团队尝试到成为数据管道的主流范式中间恰好走过了大约二十年。这篇文章不打算只写概念复盘。我会先拆解 ELT 的核心思路和它与 ETL 的本质区别再给出一套不依赖云服务也能跑通的 ELT 完整实战用 Python 做抽取和装载用 PostgreSQL 做转换层让“先 Load 再 Transform”真正落到代码上。最后还会补充高频问题、排查思路和工程化建议。整个流程适合数据开发初学者也适合想在本地搭一套最小 ELT 管道的后端工程师。1. ELT 是什么为什么能走过二十年1.1 一句大白话理解 ELTELT 的全称是 Extract-Load-Transform即抽取、装载、转换。它的执行顺序比名字更有代表性Extract从源系统获取数据Load把原始数据原封不动装载到目标数据库或数据仓库Transform在目标数据仓库内部完成清洗、关联、聚合等转换逻辑。与它对应的 ETLExtract-Transform-Load是传统数仓时代的经典做法数据先在外面处理完毕再装载进目标库。ELT 最大的变化是“转换位置变了”——转换不再发生在源头和终点之间的管道里而是发生在目标系统内部。这个“顺序变化”看起来很简单但它直接改变了整个数据架构的思考方式。ELT 不要求你在装载前想清楚所有业务口径而是“先让数据进仓库等需要时再按需加工”。这种思路在探索型分析、机器学习特征工程、多源数据融合场景里非常受用。1.2 ELT 20 周年的背景严格来说ELT 并没有一个精确的“诞生纪念日”。数据仓库领域很早就有人做过类似实践只是当时没有明确的命名和成熟工具链。业界一般把 ELT 概念被广泛讨论的起点回溯到 2000 年代中期那时候一批数据仓库厂商和大数据技术开始把“转换下推”作为卖点。从那个时间点算起ELT 理念在数据工程领域从“个别团队的尝试”到“主流最佳实践”确实走了接近二十年。这二十年里有几个关键推力让 ELT 从边缘走向中心云数据仓库快速发展。Snowflake、BigQuery、Redshift 这类产品把计算和存储分离做到了产品级用户在 SQL 中做复杂转换代价比传统 Oracle 数仓低很多。数据规模爆发。数据从 GB 级走向 TB、PB 级在管道中间做转换既要额外开发又容易因为数据量太大而性能失控。ELT 把压力交给目标仓库的水平扩展能力。转换工具成熟。dbt 这样的工具让“在 SQL 中管理转换逻辑”变成了工程化标准测试、文档、版本控制、血缘追踪一应俱全。所以ELT 的二十年也是数据工程师角色变化的二十年以前大家写大量 Java 或 Python 代码处理数据流转现在的核心工作变成了建模 SQL、设计分层、编排调度和维护数据质量。1.3 常见应用场景ELT 并不是万能方案但它在以下场景中优势很明显多源数据汇聚。业务库、埋点日志、第三方接口数据先统一入湖再按不同部门需求做加工。数据湖和数据仓库结合的湖仓一体架构。原始数据低成本留存后续需要时可以回溯重新加工。探索式分析。业务团队口径频繁变化ELT 允许先装载历史数据再做快速试错。数据产品与报表体系。每天批量的订单、流量、用户行为数据先入仓再通过 SQL 建模成宽表或指标表。在这些场景里ELT 让数据管道更“轻”抽取装载可以做成通用组件转换交给团队更熟悉的 SQL。理解了它解决的问题后面的实战案例就会更容易上手。2. ETL 与 ELT不是替代而是选择2.1 两种流程的核心差异从执行顺序看ETL 和 ELT 的差别很直观对比维度ETLELT完整流程抽取 - 转换 - 装载抽取 - 装载 - 转换转换位置中间管道目标数据仓库中间存储一般需要临时区或暂存服务器几乎不需要数据形态装载进数仓的大多是加工后数据原始数据先进数仓对数仓算力要求较低较高适合数据量中低规模、结构稳定大规模、多源异构业务口径变化变更成本高变更成本低典型工具Informatica、DataStage、Kettledbt、Snowflake、BigQuery这个表格里最值得关注的是“业务口径变化”。ETL 模式下如果报表口径从“下单金额”改成“支付金额”管道里的转换代码要改动、测试、重跑ELT 模式下原始数据一直都在仓库里只需要改一条 SQL 视图或模型重算一下目标表就可以。2.2 ELT 的三种主要架构形态ELT 在工程落地时可以细分成几种形态方便我们理解实战案例在整个体系中的位置批式 ELT。最常见的形态。每天或每小时用调度平台触发抽取装载任务然后执行一组 SQL 转换脚本生成业务表。流式 ELT。Kafka、Flink CDC 等组件负责实时同步数据先落入实时数仓或消息系统再通过 SQL 做流式加工。湖仓 ELT。数据先进入数据湖之后在湖上用 Spark SQL、Trino、Hive 等做转换。这是数据湖场景下 ELT 的典型做法。本文实战案例专注于批式 ELT它最容易理解也适合作为入门起点。2.3 选择建议我个人的项目经验是不要在“ETL 和 ELT 谁更好”上花太多时间争辩更多要看你的目标系统是什么。如果目标系统是传统关系型数据库计算资源有限数据质量要求极高ETL 仍然可以用来保证装载前就完成清洗。如果目标系统是云数仓或支持大规模并行计算的数据库ELT 通常是更省力的选择。如果团队里大部分人熟悉 SQL、不熟悉 Java/SparkELT 能显著降低开发门槛。数据工程领域没有银弹选型的关键是让数据管道在性能、成本和可维护性之间取得平衡。3. 环境准备与工具选型3.1 最小可用组合为了不依赖云账号也能复现 ELT 流程本文选用本地最常见的组合PostgreSQL Python SQL。PostgreSQL 扮演目标数据仓库负责存储原始数据并执行转换 SQL。Python 脚本负责 Extrcat 和 Load从 CSV 中读取数据并写入 PostgreSQL。SQL 负责 Transform在 PostgreSQL 内部完成清洗、汇总、建模。这套组合没有引入大数据组件但它完整体现了 ELT 的“转换发生在目标库内部”这一理念也方便你之后把 SQL 转换部分迁移到 dbt 或云数仓。3.2 版本与环境清单以下版本以常见稳定环境为例实际操作时请根据自己的环境调整操作系统Windows / macOS / Linux 均可Python3.9 及以上PostgreSQL13 及以上Python 依赖psycopg2-binary、pandas可选用于读取 CSV数据库客户端psql、Navicat、DBeaver 任一即可。如果你还没有安装 PostgreSQL可以按官方安装包完成安装也可以使用 Docker 快速启动一个实例docker run -d \ --name elt-postgres \ -e POSTGRES_USERpostgres \ -e POSTGRES_PASSWORDpostgres \ -e POSTGRES_DBelt_demo \ -p 5432:5432 \ postgres:15这个命令会启动一个名为 elt_demo 的数据库端口映射到本机 5432。本文后续的 SQL 和 Python 示例都以这个连接信息为例。3.3 项目结构在本地新建一个目录推荐按以下结构组织elt-demo/ ├── data/ │ └── orders.csv ├── scripts/ │ ├── load_raw.py │ └── transform.sql └── requirements.txtdata 目录存放模拟源系统的导出数据scripts 目录存放抽取装载脚本和转换 SQLrequirements.txt 记录 Python 依赖。这个结构虽然简单但已经具备了“源数据层、装载脚本、转换脚本”三个 ELT 必备模块。实际项目中它们会分别对应更庞大的数据源管理、同步任务和 dbt 模型目录。4. 完整实战案例用 Python PostgreSQL 实现 ELT 管道下面进入正题。我们的业务场景是模拟一个电商平台的订单数据周期性地导出到 CSV 文件希望把订单数据统一装载进数据仓库再通过 SQL 转换计算每日有效销售额并产出订单状态维度表。4.1 创建初始数据先创建一个模拟源数据的 CSV 文件。路径data/orders.csv。order_id,customer_id,order_date,amount,status 1001,C001,2024-01-05,299.00,completed 1002,C002,2024-01-05,159.50,pending 1003,C001,2024-01-05,89.90,completed 1004,C003,2024-01-06,599.00,cancelled 1005,C002,2024-01-06,1299.00,completed 1006,C004,2024-01-07,45.00,completed 1007,C001,2024-01-07,1580.00,pending 1008,C005,2024-01-08,320.00,completed 1009,C003,2024-01-08,670.00,completed 1010,C006,2024-01-08,99.00,cancelled字段说明order_id订单唯一编号customer_id用户编号order_date下单日期amount订单金额status订单状态completed 表示已完成pending 表示待处理cancelled 表示已取消。这份 CSV 可以理解为业务库每小时的导出快照。ELT 的思想是先不要管这些数据是否干净直接原样装入仓库的原始层。4.2 创建原始表在 PostgreSQL 中连接到 elt_demo 数据库执行建表语句。下面直接在 psql 或 DBeaver 中执行CREATE TABLE IF NOT EXISTS raw_orders ( order_id INT PRIMARY KEY, customer_id VARCHAR(20) NOT NULL, order_date DATE NOT NULL, amount NUMERIC(10,2) NOT NULL, status VARCHAR(20) NOT NULL );这里需要补充说明为什么要建一张 raw_orders 原始表而不是直接建一张汇总表。ELT 的核心理念就是“原始数据先留存”后续任何口径变化都可以基于这张表重新加工。建表时字段类型根据 CSV 约定调整order_id 是整数amount 用 NUMERIC(10,2) 保留两位小数date 用 DATE 类型。4.3 编写 Python 抽取装载脚本接下来写装载脚本scripts/load_raw.py。这个脚本的作用是读取 CSV 数据并把数据写入 raw_orders 表。# -*- coding: utf-8 -*- import psycopg2 DB_CONFIG { host: localhost, port: 5432, dbname: elt_demo, user: postgres, password: postgres, } CSV_PATH ../data/orders.csv def load_orders(): conn psycopg2.connect(**DB_CONFIG) cur conn.cursor() # 演示环境先清空原表保证脚本重复执行不会堆积重复数据。 # 生产环境建议使用更完善的幂等策略例如按业务日期分区删除。 cur.execute(TRUNCATE raw_orders;) with open(CSV_PATH, r, encodingutf-8) as f: header f.readline().strip().split(,) print(CSV 表头:, header) rows [] for line in f: line line.strip() if not line: continue fields line.split(,) order_id int(fields[0]) customer_id fields[1] order_date fields[2] amount float(fields[3]) status fields[4] rows.append((order_id, customer_id, order_date, amount, status)) insert_sql INSERT INTO raw_orders (order_id, customer_id, order_date, amount, status) VALUES (%s, %s, %s, %s, %s) cur.executemany(insert_sql, rows) conn.commit() cur.close() conn.close() print(f成功装载 {len(rows)} 条订单数据) if __name__ __main__: load_orders()这个脚本分四步完成装载建立 PostgreSQL 连接清空目标表防止重复运行导致数据重复读取 CSV 并转换为元组列表使用 executemany 批量插入最后提交事务。你可能注意到脚本里的 TRUNCATE。在演示环境里我们希望脚本能反复执行TRUNCATE 能保证每次跑完表里数据都是 CSV 的最新内容。但在生产环境数据装载通常是增量进行的不能简单清空全表后面最佳实践部分我还会继续讲。运行脚本前需要安装依赖pip install -r requirements.txtrequirements.txt内容psycopg2-binary2.9.9如果 CSV 解析逻辑需要更复杂的格式转换可以额外引入 pandas这里为了减少依赖使用 Python 标准文件读取即可。运行命令cd scripts python load_raw.py预期输出类似CSV 表头: [order_id, customer_id, order_date, amount, status] 成功装载 10 条订单数据到这里ELT 的 Extract 和 Load 就完成了。我们还没有做任何转换数据已经安静地躺在 raw_orders 表里。4.4 编写 SQL 转换脚本接下来实现 ELT 的 Transform 部分。转换不写在 Python 里而是直接写在 SQL 中这正好体现 ELT 的关键思想让目标数据库完成加工。创建scripts/transform.sql-- 1. 基础数据探查先确认装载结果 SELECT * FROM raw_orders ORDER BY order_date; -- 2. 计算每日有效销售额 -- 状态为 completed 的订单才算有效销售 CREATE OR REPLACE VIEW v_daily_sales AS SELECT order_date, COUNT(*) AS order_cnt, SUM(amount) AS total_amount, ROUND(AVG(amount), 2) AS avg_amount FROM raw_orders WHERE status completed GROUP BY order_date ORDER BY order_date; -- 3. 统计订单状态分布 SELECT status, COUNT(*) AS order_cnt, SUM(amount) AS total_amount FROM raw_orders GROUP BY status ORDER BY order_cnt DESC; -- 4. 创建分析宽表每日订单概览 CREATE TABLE IF NOT EXISTS daily_order_stats AS SELECT order_date, COUNT(*) AS total_orders, COUNT(*) FILTER (WHERE status completed) AS completed_orders, SUM(amount) FILTER (WHERE status completed) AS completed_amount, SUM(amount) AS gross_amount FROM raw_orders GROUP BY order_date ORDER BY order_date;在这个 SQL 脚本里我们做了三层事情视图v_daily_sales把过滤条件 status completed 放在 SQL 内部计算每日订单数、销售金额和平均金额。视图的好处是可以随着底层 raw_orders 数据变化自动更新适合业务口径频繁调整的场景。聚合查询直接查看订单状态分布帮助验证数据质量。分析表daily_order_stats生成一张物化的每日概览表报表可以直接查询不用每次都扫描全部原始数据。在 psql 中执行psql -h localhost -U postgres -d elt_demo -f scripts/transform.sql需要注意CREATE TABLE IF NOT EXISTS daily_order_stats AS在表已存在时不会更新数据。如果你想在转换阶段反复重跑需要先执行DROP TABLE IF EXISTS daily_order_stats;这一点后面的排错部分也会提到。4.5 运行与验证装载和转换完成后建议用几条查询验证整个 ELT 管道是否正确。查询每日有效销售额SELECT * FROM v_daily_sales;预期结果order_dateorder_cnttotal_amountavg_amount2024-01-052388.90194.452024-01-0611299.001299.002024-01-07145.0045.002024-01-082990.00495.00这个结果直接验证了 ELT 管道的价值CSV 原始数据进入 PostgreSQL 后我们不需要在 Python 里写统计逻辑只需要编写 SQL数据库就会帮我们完成所有计算。4.6 结果说明从上面的输出能看出ELT 管道的完整路径是CSV 作为源系统快照被 Python 脚本抽取数据原样装载进 raw_orders 原始层SQL 在目标数据库中完成过滤、聚合、建宽表报表直接查询视图或分析表。如果后续业务口径从“completed 才算完成”改为“pending 也计入销售额”我们只需要修改 SQL 中的 WHERE 条件然后重跑视图而不用重新抽取数据。这就是 ELT 相比 ETL 在口径变化上的巨大优势。5. ELT 链路中的常见问题与排查思路在实际落地 ELT 管道时问题往往集中出现在数据装载、SQL 转换和调度重跑几个环节。下面整理一份高频问题清单。问题现象常见原因解决思路装载后中文乱码CSV 文件编码不是 UTF-8统一使用 UTF-8 编码或指定 encodinggbk 等源编码重复执行脚本导致数据翻倍装载逻辑没有幂等设计演示环境用 TRUNCATE生产环境按业务分区删除或使用主键冲突更新executemany 装载大文件性能差单条插入网络开销过大改为 PostgreSQL COPY 命令或分批提交执行转换 SQL 速度慢没有索引或统计信息陈旧在 where、group by 字段上建索引执行 ANALYZECREATE TABLE AS 重复执行不更新IF NOT EXISTS 不会覆盖已有表重新建表前先 DROP或使用视图/物化视图源字段类型与目标表不一致CSV 未做类型映射在 Python 装载前统一类型转换或在临时表中先装载再转换增量场景下重复装载同一批数据缺少同步批次标记增加批次号、etl_time 字段通过唯一约束去重5.1 数据重复问题数据重复是 ELT 新手最容易踩的坑。比如说同一份 CSV 被调度系统重复执行了两次如果没有处理raw_orders 里会出现两批相同订单最终报表全部翻倍。解决思路通常有三个层次全量重跑直接 TRUNCATE 再插入适合小表主键冲突更新在 INSERT 语句中追加 ON CONFLICT让重复主键更新字段分区替换按业务日期删除指定分区再装载当天数据。最低成本的做法是在装载脚本里加入批次时间戳并在业务表上建立唯一索引。这样即使任务重跑也不会造成脏数据。5.2 大表装载性能问题当 CSV 文件从几十行变成几百万行时executemany 的性能会明显下降。更推荐的方式是使用 PostgreSQL 的 COPY 命令。下面给出一个使用 COPY 的装载片段逻辑与前面的脚本等价但性能更优import psycopg2 from io import StringIO DB_CONFIG { host: localhost, port: 5432, dbname: elt_demo, user: postgres, password: postgres, } CSV_PATH ../data/orders.csv conn psycopg2.connect(**DB_CONFIG) cur conn.cursor() with open(CSV_PATH, r, encodingutf-8) as f: buffer StringIO() buffer.write(f.read()) buffer.seek(0) cur.execute(TRUNCATE raw_orders;) cur.copy_expert( COPY raw_orders (order_id, customer_id, order_date, amount, status) FROM STDIN WITH CSV HEADER, buffer, ) conn.commit() cur.close() conn.close() print(COPY 装载完成)COPY 是 PostgreSQL 官方推荐的高效导入方式。在 ELT 管道中如果源数据是文件形式优先使用 COPY 而不是逐行 INSERT。6. ELT 最佳实践与工程建议6.1 分层建模不要把所有转换堆在一起ELT 的最佳实践不是把几百行 SQL 写在一个文件里而是要像数据仓库建模一样分层管理原始层Raw / Bronze保留源数据的原貌字段名和类型尽量贴近来源清洗层Core / Silver完成去重、类型标准化、字段改名、脏数据处理应用层App / Gold面向报表和业务分析产出汇总表、指标宽表、数据集。在分层建模时ELT 的优势会进一步体现每一层都只依赖下一层的数据业务口径变化时只需要改最上层的模型不需要重跑底层。6.2 脚本必须做到可重复执行生产环境中的 ELT 任务不会只跑一次。管道任务需要支持重复执行不产生重复数据失败后可以从断点重跑多次执行结果保持一致。常见的工程化手段包括建表使用 DROP TABLE IF EXISTS 或 CREATE OR REPLACE VIEW装载脚本增加幂等逻辑调度任务增加批次字段。建议在开发阶段就把“重跑安全”当成默认要求而不是上线后再补救。6.3 数据质量校验是 ELT 的生命线ELT 把转换后置意味着原始数据会直接进入数仓。如果源系统数据有问题脏数据会更快影响下游。因此在转换前后必须增加质量校验。推荐至少做以下检查行数校验装载前后对比源文件行数和目标表行数主键唯一性校验查询是否存在重复 order_id空值校验核心字段是否存在 NULL金额合理性校验是否存在金额为负或超过阈值的数据。这些校验可以写成一组 SQL 脚本或 Python 断言放在调度流程中。如果校验失败则暂停后续转换并告警。6.4 配置与连接信息不要硬编码上面的示例把数据库连接信息写在了 Python 文件中这适合本地演示但不适合工程环境。更规范的做法是使用环境变量或配置中心export PG_HOSTlocalhost export PG_PORT5432 export PG_DBNAMEelt_demo export PG_USERpostgres export PG_PASSWORDpostgresPython 中读取import os DB_CONFIG { host: os.getenv(PG_HOST, localhost), port: os.getenv(PG_PORT, 5432), dbname: os.getenv(PG_DBNAME, elt_demo), user: os.getenv(PG_USER, postgres), password: os.getenv(PG_PASSWORD, postgres), }真实项目还要注意权限管理数据库账号只授予管道运行所需的最小权限避免一个通用超管账号被多套任务复用。6.5 调度、监控与血缘当 ELT 任务越来越多手工执行 SQL 就跑不过来了。工程化方向是引入调度平台比如 Apache Airflow、Apache DolphinScheduler 或简单的 cron。调度平台负责按时间触发装载和转换任务并在任务失败时自动重试和告警。同时要记录如下元数据每个任务的运行时间和状态每个表的更新批次数据血缘即某个报表字段来自哪个原始字段。这些元数据一旦积累起来排查问题和变更口径都会高效很多。dbt 之所以流行很大一部分原因就是它把 SQL 转换、测试、文档和血缘整合到了一个工作流中。6.6 云数仓时代的 ELT如果你所在的公司已经使用 Snowflake、BigQuery、RedshiftELT 的落地会更加顺畅。你只需要把数据源同步到云数仓的原始表再通过 dbt 或 SQL 脚本完成分层建模。这个模式里传统 ETL 工具被拆分成了两部分数据同步工具负责 Extract 和 Load例如 Fivetran、Airbyte、DataX数据转换工具负责 Transform例如 dbt。这种拆分让“装载”和“转换”各自专业化这也是 ELT 二十年演进过程中最明显的变化不需要一个巨型平台统治整条管道而是用生态协作完成数据开发。7. 总结与学习路线回到“ELT 20周年”这个主题ELT 能走到今天核心并不是某个工具或某个平台而是一种理念的变化数据先留存、再按需加工让计算发生在最合适的地方。从早期少数厂商的探索到云数仓时代成为主流实践ELT 真正改变了数据工程师的工作方式。现在越来越多团队正在用 ELT 替代传统 ETL 管道尤其是面对多源异构数据和快速变化的业务需求时这种“先装载、后转换”的架构明显更灵活。如果你想进一步学习 ELT我建议按下面的路线走把本文的 Python PostgreSQL 实战跑通理解 Extract、Load、Transform 的边界尝试往 raw_orders 表增加更多字段和数据量用不同 SQL 模拟真实转换逻辑学习 dbt 的基础用法把 transform.sql 改造成 dbt 模型体验测试、文档和血缘管理接触一个调度平台让 ELT 任务每天自动运行如果业务数据上云再对比云数仓的 ELT 实现方式通常会更简单。数据工程是实践性很强的领域不要停留在概念层面。建议你找一份真实的业务数据从本地这套最小管道开始逐步增加分层、调度、质量校验慢慢就能建立对 ELT 全链路的掌控感。希望这篇文章能帮你少走一些弯路。
返回列表