第七章:SQL在ETL中的应用:复杂查询、窗口函数、CTE、临时表、存储过程、ETL中的SQL优化技巧

各位同学,欢迎来到第七章。这一章,我们聊聊ETL工程师吃饭的家伙——SQL。

说实话,我见过太多人把ETL当成“搬砖”工具。写个SELECT * INTO,再加个WHERE条件,就完事了。但真正高效的ETL,SQL功底必须扎实。你想想看,数据量一上来,一个烂SQL能把整个集群拖垮,而一个好SQL,可能几秒钟就跑完了。

这一章,我会把我在实战中积累的SQL技巧,尤其是那些能让你少加班的“骚操作”,都掏出来给你。

7.1 复杂查询:不只是SELECT

ETL里的复杂查询,说白了就是多表关联、子查询、聚合计算的组合拳。我见过很多新人,一遇到多表关联就写一堆嵌套子查询,性能差得离谱。

举个例子,我们要从订单表和用户表中,找出每个用户最近一笔订单的金额。很多人会这么写:

SELECT u.user_id, u.user_name,
    (SELECT MAX(order_amount) FROM orders o WHERE o.user_id = u.user_id) AS last_amount
FROM users u;

嗯,这个写法没错,但性能堪忧。尤其是当用户表有几十万行时,每行都要执行一次子查询,数据库会疯掉的。

我建议用JOIN + 聚合函数替代:

SELECT u.user_id, u.user_name, MAX(o.order_amount) AS last_amount
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.user_name;

这样数据库只需要扫描一次订单表,效率高得多。

我的经验: 在ETL中,能用JOIN解决的,尽量别用子查询。JOIN是关系型数据库的强项,优化器会帮你做很多工作。

7.2 窗口函数:ETL的瑞士军刀

窗口函数,是我在ETL中最常用的工具之一。它能在不改变行数的情况下,对数据进行分组排序、累计计算、移动平均等操作。

我记得有一次,需要计算每个用户的历史累计消费金额。如果用自连接,代码会非常复杂。但用窗口函数,一行搞定:

SELECT user_id, order_date, order_amount,
    SUM(order_amount) OVER (PARTITION BY user_id ORDER BY order_date) AS cumulative_amount
FROM orders;

你看,PARTITION BY 相当于分组,ORDER BY 指定排序,SUM() 做累计。这就是窗口函数的魅力。

常用的窗口函数还有:

  • ROW_NUMBER():给每行一个唯一序号,常用于去重
  • RANK() / DENSE_RANK():排名,常用于Top-N分析
  • LAG() / LEAD():取前一行/后一行的值,常用于环比、同比
  • FIRST_VALUE() / LAST_VALUE():取分组内第一个/最后一个值
避坑指南: 我曾经在窗口函数里忘了加 PARTITION BY,结果整个表的数据被当成一个组计算,跑出来的结果完全不对。所以,写窗口函数时,一定要先确认分组逻辑。

7.3 CTE:让SQL更清晰

CTE(公用表表达式),说白了就是给一段查询结果起个名字,方便后面复用。我特别喜欢用CTE来组织复杂的ETL逻辑。

比如,我们要先找出“高价值用户”(累计消费超过1万的),然后再统计他们的订单明细。用CTE写出来是这样的:

WITH high_value_users AS (
    SELECT user_id, SUM(order_amount) AS total_amount
    FROM orders
    GROUP BY user_id
    HAVING SUM(order_amount) > 10000
)
SELECT o.*, h.total_amount
FROM orders o
JOIN high_value_users h ON o.user_id = h.user_id;

你看,逻辑非常清晰。先定义“高价值用户”这个临时结果集,然后再用它去关联订单表。比嵌套子查询好读多了。

CTE还有一个好处:可以递归。比如处理树形结构(组织架构、分类层级)时,递归CTE是神器。

我的习惯: 在ETL脚本中,我会把每个步骤拆成一个CTE,最后再组合。这样既方便调试,也方便别人阅读。

7.4 临时表:ETL的中间站

临时表,顾名思义,就是临时存放数据的表。它在ETL中非常有用,尤其是处理大数据量时。

我举个例子。有一次,我需要从源系统抽取几千万行数据,然后做复杂的清洗和转换。如果直接写一个超长的SQL,不仅难调试,而且一旦出错,整个任务都要重跑。

我的做法是:

  1. 先把原始数据插入临时表
  2. 对临时表做清洗(去重、类型转换、空值处理)
  3. 再做业务逻辑转换
  4. 最后插入目标表

这样每一步都可以单独调试,出错了也只需要重跑那一步。

临时表有两种:

  • 本地临时表(#temp):只对当前会话可见,会话结束自动删除
  • 全局临时表(##temp):对所有会话可见,但所有引用它的会话都关闭后才删除
注意: 临时表虽然好用,但别滥用。如果数据量不大,用CTE或子查询就够了。临时表会占用tempdb空间,频繁创建删除也会影响性能。

7.5 存储过程:ETL的自动化引擎

存储过程,就是把一组SQL语句打包成一个可重复调用的单元。在ETL中,我经常用存储过程来实现:

  • 数据抽取、清洗、加载的完整流程
  • 错误处理和日志记录
  • 动态SQL生成(比如根据参数动态拼接表名)

举个例子,一个简单的ETL存储过程:

CREATE PROCEDURE usp_ETL_LoadOrders
    @StartDate DATE,
    @EndDate DATE
AS
BEGIN
    SET NOCOUNT ON;
    
    -- 1. 抽取
    SELECT * INTO #temp_orders
    FROM source_orders
    WHERE order_date BETWEEN @StartDate AND @EndDate;
    
    -- 2. 清洗
    DELETE FROM #temp_orders WHERE order_amount IS NULL;
    
    -- 3. 加载
    INSERT INTO target_orders (order_id, user_id, order_date, order_amount)
    SELECT order_id, user_id, order_date, order_amount
    FROM #temp_orders;
    
    -- 4. 日志
    INSERT INTO etl_log (table_name, rows_affected, run_time)
    VALUES ('orders', @@ROWCOUNT, GETDATE());
    
    DROP TABLE #temp_orders;
END;

你看,整个ETL流程封装在一个存储过程里,调用时只需要传两个参数:开始日期和结束日期。非常方便。

我的建议: 存储过程里一定要加错误处理(TRY...CATCH)和事务控制(BEGIN TRAN...COMMIT/ROLLBACK)。否则,一旦中间步骤出错,数据可能处于不一致状态。

7.6 ETL中的SQL优化技巧

最后,我们来聊聊优化。这部分是我最想分享的,因为我在项目中踩过太多坑了。

7.6.1 索引策略

索引是SQL优化的第一道防线。在ETL中,我建议:

  • 在JOIN字段、WHERE条件字段上建索引
  • 避免在索引列上使用函数(比如 WHERE DATE(order_date) = '2024-01-01' 会导致索引失效)
  • 对于大表,考虑使用分区表,按日期分区

7.6.2 避免SELECT *

这个我强调过很多次。在ETL中,SELECT * 会带来两个问题:

  1. 读取不必要的列,增加I/O开销
  2. 如果源表结构发生变化,ETL可能报错

我习惯显式列出需要的列名,哪怕多打几个字。

7.6.3 批量操作代替逐行操作

我曾经接手过一个ETL任务,里面用了游标(CURSOR)逐行处理数据。几百万行数据,跑了整整8个小时。我改成基于集合的批量操作后,只用了15分钟。

记住:SQL是集合语言,能用一条语句解决的,千万别用循环。

7.6.4 合理使用临时表和表变量

临时表和表变量各有优劣:

特性 临时表 (#temp) 表变量 (@table)
存储位置 tempdb 内存(可能溢出到tempdb)
索引 支持 仅主键
事务 参与 不参与
统计信息 有 无

我的经验是:数据量小(几千行以内)用表变量,数据量大用临时表。

7.6.5 执行计划分析

这是优化SQL的终极武器。遇到慢查询,我会先看执行计划:

  • 有没有表扫描(Table Scan)?如果有,考虑加索引
  • 有没有哈希匹配(Hash Match)?如果有,考虑调整JOIN顺序
  • 有没有排序(Sort)?如果有,考虑加索引或优化ORDER BY
核心原则: ETL中的SQL优化,不是追求“最优雅”的写法,而是追求“最快”的执行。有时候,拆成多个简单SQL,比一个复杂SQL更快。
SQL在ETL中的应用知识体系 SQL在ETL中的应用 复杂查询 多表JOIN 子查询优化 聚合计算 窗口函数 ROW_NUMBER RANK / DENSE_RANK LAG / LEAD CTE WITH子句 递归CTE 临时表 #temp 本地 ##temp 全局 表变量 存储过程 ETL流程封装 错误处理 动态SQL SQL优化技巧 索引策略 避免SELECT * 批量操作 执行计划分析 合理使用临时表 vs 表变量

好了,这一章的内容就到这里。SQL在ETL中的应用,远不止我上面提到的这些。但掌握了这些核心技巧,你已经能应对大部分ETL场景了。

记住,SQL是ETL工程师的基石。多写、多练、多看执行计划,你的SQL能力一定会突飞猛进。

公众号:蓝海资料掘金营,微信deep3321