2. SQL标准与扩展:ANSI SQL标准、窗口函数、用户自定义函数(UDF)、表值函数(TVF)
聊到流批一体查询,绕不开一个基础问题:SQL 到底能干什么,不能干什么?
很多同学觉得 SQL 就是写写 SELECT、WHERE、GROUP BY,查个报表完事。但真到了实时流计算或者复杂批处理场景,你会发现标准 SQL 有时候挺「笨」的。比如你想算个移动平均,或者把一行拆成多行,标准 SQL 根本搞不定。
所以这一节,咱们把 SQL 的「家底」翻一翻。搞清楚哪些是 ANSI 标准规定的,哪些是各大引擎自己加的私货。我个人习惯是:能用标准就用标准,标准不够再上扩展。这样你的 SQL 换引擎时不用重写。
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 |
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 那一段,它定义了窗口帧的范围——从分区第一行到当前行。这就是流批一体的关键:批处理里它是累计求和,流处理里它就是滑动窗口。
我个人习惯把窗口函数分成三类:
- 排序类: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 是最后的选择。
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。但概念是一样的:输入参数,输出一张表。
2.5 知识体系总览
为了让你更直观地理解这一章的知识结构,我画了一张图。它展示了 ANSI 标准、窗口函数、UDF、TVF 之间的关系,以及它们在流批一体查询中的位置。
从这张图你能看到,ANSI SQL 标准是基础,窗口函数、UDF、TVF 是三大扩展方向。窗口函数解决「分组排序计算」问题,UDF 解决「自定义逻辑」问题,TVF 解决「表级变换」问题。三者结合,基本能覆盖流批一体查询中 90% 以上的场景。
好了,这一节的内容就到这里。记住一句话:标准是底线,扩展是能力。写 SQL 时先想能不能用标准语法,不行再上扩展。这样你的代码才能既好用又长寿。