2. SQL标准与扩展:ANSI SQL标准、窗口函数、用户自定义函数(UDF)、表值函数(TVF)

聊到流批一体查询,绕不开一个基础问题:SQL 到底能干什么,不能干什么?

很多同学觉得 SQL 就是写写 SELECT、WHERE、GROUP BY,查个报表完事。但真到了实时流计算或者复杂批处理场景,你会发现标准 SQL 有时候挺「笨」的。比如你想算个移动平均,或者把一行拆成多行,标准 SQL 根本搞不定。

所以这一节,咱们把 SQL 的「家底」翻一翻。搞清楚哪些是 ANSI 标准规定的,哪些是各大引擎自己加的私货。我个人习惯是:能用标准就用标准,标准不够再上扩展。这样你的 SQL 换引擎时不用重写。

核心观点: 标准 SQL 是通用语言,扩展 SQL 是方言。流批一体查询中,窗口函数和 UDF/TVF 是打通流与批的关键桥梁。

2.1 ANSI SQL 标准:地基打牢了,楼才稳

ANSI SQL 标准定义了 SQL 的核心语法和语义。说白了,就是告诉所有数据库厂商:「你们得按这个规矩来,不然用户换数据库就得重学。」

标准从 SQL-86 一路演进到 SQL:2023。每次更新都会加入新特性。比如 SQL:1999 加入了递归查询、触发器等;SQL:2003 加入了窗口函数、MERGE 语句;SQL:2016 加入了 JSON 支持、行模式匹配等。

我记得有一次做项目,客户要求从 Oracle 迁移到 PostgreSQL。因为 SQL 写得比较标准,迁移时只改了少量方言差异,省了大把时间。反过来,如果当初用了大量 Oracle 专有语法,那迁移成本就高了。

标准版本 发布时间 重要新增特性
SQL-86 1986 基础查询、数据定义
SQL-92 1992 连接、集合操作、完整性约束
SQL:1999 1999 递归查询、触发器、对象类型
SQL:2003 2003 窗口函数、MERGE、序列
SQL:2016 2016 JSON、行模式匹配、多态表函数
SQL:2023 2023 属性图查询、增强的 JSON
避坑指南: 我曾经见过一个团队,所有 SQL 都写 MySQL 方言。后来业务增长要换分布式数据库,结果发现大量语法不兼容,重构了三个月。所以,核心业务逻辑尽量用标准 SQL 写,方言只用在性能优化或特殊场景。

2.2 窗口函数:流批一体的「瑞士军刀」

窗口函数是 SQL:2003 引入的,但直到最近几年才被广泛使用。为什么?因为流计算场景太需要它了。

你想想看,批处理里算个累计求和,用 GROUP BY 加自连接也能做,但性能差、代码丑。流处理里算个滑动平均,没有窗口函数几乎没法写。窗口函数正好解决了这类「在分组内排序计算」的问题。

窗口函数的核心语法就三部分:

  • PARTITION BY:定义窗口的分组逻辑,类似 GROUP BY 但不合并行
  • ORDER BY:定义窗口内的排序顺序
  • ROWS/RANGE:定义窗口的帧范围,控制当前行能看到哪些数据
-- 计算每个部门员工的薪资排名,以及累计薪资
SELECT
  dept_id,
  emp_name,
  salary,
  RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rank_in_dept,
  SUM(salary) OVER (PARTITION BY dept_id ORDER BY salary
                    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
FROM employee;

这段代码里,RANK() 是窗口函数,SUM() 也是窗口函数(因为带了 OVER 子句)。注意看 ROWS BETWEEN 那一段,它定义了窗口帧的范围——从分区第一行到当前行。这就是流批一体的关键:批处理里它是累计求和,流处理里它就是滑动窗口。

注意: 窗口函数的帧定义在不同引擎中行为有差异。比如 Flink SQL 的窗口函数支持时间属性,而传统数据库不支持。写跨引擎 SQL 时,要特别注意 ROWS 和 RANGE 的区别。ROWS 基于物理行数,RANGE 基于逻辑值范围。

我个人习惯把窗口函数分成三类:

  • 排序类:ROW_NUMBER、RANK、DENSE_RANK、NTILE。用于排名和分桶。
  • 聚合类:SUM、AVG、COUNT、MAX、MIN 加 OVER。用于累计、移动平均。
  • 偏移类:LAG、LEAD、FIRST_VALUE、LAST_VALUE。用于前后行对比。

举个例子,实时交易风控里,经常要判断当前交易金额是否比前 3 笔的平均值高出 5 倍。用 LAG 加 AVG 窗口函数,一行 SQL 搞定。

-- 检测异常交易:当前金额 > 前3笔平均金额 * 5
SELECT
  trans_id,
  amount,
  AVG(amount) OVER (PARTITION BY user_id ORDER BY trans_time
                    ROWS BETWEEN 3 PRECEDING AND 1 PRECEDING) AS avg_prev_3,
  CASE
    WHEN amount > AVG(amount) OVER (PARTITION BY user_id ORDER BY trans_time
                                    ROWS BETWEEN 3 PRECEDING AND 1 PRECEDING) * 5
    THEN '异常'
    ELSE '正常'
  END AS flag
FROM transaction;

2.3 用户自定义函数(UDF):给 SQL 装上「外挂」

标准 SQL 的函数库有限。遇到复杂业务逻辑,比如加密解密、地理编码、自定义聚合,标准函数搞不定。这时候就需要 UDF。

UDF 允许你用 Java、Python、Scala 等语言写函数,然后在 SQL 里直接调用。流批一体场景下,UDF 特别适合处理那些「批处理和流处理逻辑一致,但实现方式不同」的场景。

UDF 通常分三种:

类型 输入 输出 典型场景
标量函数(UDF) 一行,多列 一个标量值 字符串处理、数学计算、类型转换
聚合函数(UDAF) 多行,多列 一个标量值 自定义聚合逻辑(如中位数、众数)
表值函数(UDTF) 一行,多列 多行,多列 一行拆多行、解析嵌套数据

嗯,这里要注意:UDF 的性能开销比内置函数大得多。我曾经在 Flink 作业里写了一个 Java UDF,每次调用都要解析 JSON,结果吞吐量直接掉了 70%。后来改成内置 JSON 函数,性能就回来了。所以,能用内置函数就别用 UDF,UDF 是最后的选择。

经验之谈: 写 UDF 时,尽量做到「无状态」。有状态的 UDF 在流处理中容易出问题,比如状态不一致、Checkpoint 恢复困难。如果必须用状态,记得用好引擎提供的状态 API,别自己搞全局变量。

2.4 表值函数(TVF):把函数当表用

表值函数是 UDF 的一种特殊形式。普通函数返回一个值,TVF 返回一张表。你可以在 FROM 子句里直接调用它,就像查一张物理表一样。

TVF 在流批一体中特别有用。比如,你想把 Kafka 消息里的 JSON 数组展开成多行,或者把一段时间的日志按小时分组。这些操作用 TVF 写起来非常自然。

-- 假设有一个 TVF:parse_json_array(json_str),返回多行
SELECT
  user_id,
  event_type,
  event_time
FROM
  parse_json_array(
    '[
      {"user_id": 1, "event_type": "click", "event_time": "2024-01-01 10:00:00"},
      {"user_id": 2, "event_type": "view", "event_time": "2024-01-01 10:01:00"}
    ]'
  );

这段代码里,parse_json_array 就是一个 TVF。它接收一个 JSON 字符串,返回一个包含三列的表。在流处理中,你可以把 Kafka 消息的 value 字段直接传给 TVF,实现实时解析。

TVF 还有一个重要用途:动态表生成。比如,你想生成一个日期序列,或者一个数字序列。标准 SQL 没有这个功能,但 TVF 可以轻松实现。

-- 生成 2024-01-01 到 2024-01-10 的日期序列
SELECT *
FROM generate_series(DATE '2024-01-01', DATE '2024-01-10', INTERVAL '1' DAY);

这个 generate_series 在 PostgreSQL 里是内置函数,在 Flink 里需要自己实现 TVF。但概念是一样的:输入参数,输出一张表。

注意: TVF 在不同引擎中的支持程度差异很大。PostgreSQL 支持得很好,MySQL 直到 8.0 才支持 CTE,但 TVF 支持有限。Flink 和 Spark 都支持自定义 TVF,但 API 不同。写跨引擎 SQL 时,TVF 往往是需要特殊处理的部分。

2.5 知识体系总览

为了让你更直观地理解这一章的知识结构,我画了一张图。它展示了 ANSI 标准、窗口函数、UDF、TVF 之间的关系,以及它们在流批一体查询中的位置。

SQL标准与扩展知识体系 SQL 标准与扩展知识体系 ANSI SQL 标准 窗口函数 用户自定义函数 (UDF) 表值函数 (TVF) PARTITION BY ORDER BY ROWS / RANGE 标量 UDF 聚合 UDAF 返回表(FROM 子句调用) 动态表生成 流批一体查询:标准 SQL 做骨架,窗口函数 + UDF/TVF 做血肉

从这张图你能看到,ANSI SQL 标准是基础,窗口函数、UDF、TVF 是三大扩展方向。窗口函数解决「分组排序计算」问题,UDF 解决「自定义逻辑」问题,TVF 解决「表级变换」问题。三者结合,基本能覆盖流批一体查询中 90% 以上的场景。

好了,这一节的内容就到这里。记住一句话:标准是底线,扩展是能力。写 SQL 时先想能不能用标准语法,不行再上扩展。这样你的代码才能既好用又长寿。