1. 前言:所有复杂查询都从 SELECT 开始
无论你在 Flink 上跑的是离线批任务,还是实时流式任务,一切都从 SELECT 开始:
- 从 Kafka / 文件 / 数据库等源读取数据;
- 选出你关心的字段;
- 加上一些计算逻辑;
- 再用
WHERE过滤掉不需要的数据。
很多人一上来就研究窗口、Join、UDF,其实最常用、最"耐用"的语句,就是这俩:SELECT 和 WHERE。
理解了 Flink SQL 里的 SELECT / WHERE,也就打好了写复杂实时分析 SQL 的地基。
下面我们就从最基础的语法入手,一点点展开。
2. SELECT 的基础语法与 table_expression
Flink SQL 中,SELECT 的一般形式是:
sql
SELECT select_list
FROM table_expression
[ WHERE boolean_expression ]
这里面有三个核心元素:
select_list:选哪些列、做哪些计算;table_expression:数据从哪里来;WHERE boolean_expression:在结果出来前怎么过滤。
2.1 table_expression:不只是"表"
文档里提到,table_expression 可以是"任何数据来源",具体包括:
- Catalog 中的物理表(例如 Kafka、JDBC、Filesystem 等 Connector 定义的表);
- 视图 (
CREATE VIEW/CREATE TEMPORARY VIEW定义的逻辑表); VALUES/VALUE子句创建的一小段内联数据;- 多表
JOIN后的结果; - 子查询(
FROM (SELECT ...) AS t)。
比如,最简单的从一张表读数据:
sql
SELECT *
FROM Orders;
也可以从 join 后的结果读:
sql
SELECT o.order_id, c.name
FROM Orders AS o
JOIN Customers AS c
ON o.customer_id = c.id;
还可以从子查询结果中再查:
sql
SELECT user_id, total_amount
FROM (
SELECT user_id, SUM(price) AS total_amount
FROM Orders
GROUP BY user_id
) AS t;
在 Flink 里,无论是 batch 还是 streaming,这些写法都是统一的 SQL 语义,区别只在于:
- 底层的数据源是有界(batch)还是无界(stream);
- 规划器在执行计划上选择的是批执行算子还是流执行算子。
3. select_list:为什么不建议用 *?
很多人最开始写 SQL 都习惯:
sql
SELECT *
FROM Orders;
在调试、临时查询时,这非常方便------"全部拉出来先看看再说"。
但在生产 SQL 里,文档明确建议:不要用 *。
3.1 使用 * 的问题
-
对 schema 变更敏感
- 如果下游代码假设存在某些字段,而上游表 schema 改了(字段改名/删除),
*可能会导致隐性错误; - 比如多取了你不需要的字段,或者字段顺序变了,某些工具/解析逻辑可能出问题。
- 如果下游代码假设存在某些字段,而上游表 schema 改了(字段改名/删除),
-
不利于性能调优
- 尤其在大宽表场景,
SELECT *会把所有字段都读出来,即使你只用到了其中 2~3 个; - 这会增加网络传输、序列化/反序列化成本。
- 尤其在大宽表场景,
-
可读性差
- 当别人看到一个复杂查询中的
SELECT *,很难一眼看出"这条 SQL 到底想要什么"。
- 当别人看到一个复杂查询中的
3.2 推荐写法:明确列 + 计算表达式
文档中的建议写法是这样的:
sql
SELECT order_id, price + tax
FROM Orders;
你可以做几件事情:
- 直接选择字段:
order_id; - 做表达式计算:
price + tax; - 起别名:
price + tax AS total_price; - 调用函数:
UPPER(user_name) AS user_name_upper。
示例:
sql
SELECT
order_id,
price,
tax,
price + tax AS total_price
FROM Orders;
这样做的好处:
- 一眼就能知道"这个查询要的是什么数据";
- 上游 schema 调整时,更有感知、可控;
- 对下游接口、报表、实时指标都更友好。
4. 使用 VALUES 子句构造内联数据
在很多场景里,你可能想:
- 快速构造一些测试数据;
- 做一个小"维表"来参与 JOIN;
- 不想单独建表,只是临时用一下。
这时候就可以使用 VALUES 子句:
sql
SELECT order_id, price
FROM (VALUES (1, 2.0), (2, 3.1)) AS t (order_id, price);
拆解一下:
-
(VALUES (1, 2.0), (2, 3.1)):构造出两行数据:- 第 1 行:
(1, 2.0) - 第 2 行:
(2, 3.1)
- 第 1 行:
-
AS t (order_id, price):- 给这个内联表起名
t; - 并给两列命名为
order_id和price。
- 给这个内联表起名
结果等价于你创建了一张两行的小表。
典型用途:
-
调试 SQL :
在没有真实源表时,快速验证函数逻辑、表达式计算:
sqlSELECT price, tax, price + tax AS total FROM (VALUES (100, 10), (200, 30)) AS v(price, tax); -
小范围"编码表" :
比如定义状态码映射(真的很少条时):
sqlWITH status_dim AS ( SELECT * FROM (VALUES (0, 'INIT'), (1, 'PAID'), (2, 'CANCELLED') ) AS t(status_code, status_name) ) SELECT o.order_id, d.status_name FROM Orders AS o LEFT JOIN status_dim AS d ON o.status = d.status_code;
在 Flink SQL 中,这种写法在 batch / streaming 下都能用,尤其适合 SQL-CLI 的快速调试。
5. WHERE 子句:在数据进入下游前先"瘦身"
WHERE 是最常用的过滤手段,语法非常直观:
sql
SELECT price + tax
FROM Orders
WHERE id = 10;
这里 WHERE id = 10 就是一个布尔表达式 boolean_expression。
你可以在里面做各种组合:
- 比较:
=、<>、>、>=、<、<=; - 逻辑运算:
AND、OR、NOT; - 区间:
BETWEEN ... AND ...; - 集合:
IN (...); - 模糊匹配:
LIKE、NOT LIKE等; - 处理空值:
IS NULL、IS NOT NULL。
示例:
sql
SELECT order_id, price
FROM Orders
WHERE price > 100
AND status = 'PAID'
AND country = 'CN';
在流式场景下,WHERE 还有一个很重要的作用------减小下游压力:
- 越早过滤不需要的记录,下游 Join / 聚合算子压力越小;
- 对于维表 Lookup Join、Window Aggregation 等算子尤为重要。
6. 在 SELECT 中使用内置函数和 UDF
Flink SQL 支持在 SELECT 里调用各种函数:
-
内置标量函数(Scalar Function),如:
- 字符串函数:
UPPER,LOWER,SUBSTRING,TRIM... - 日期时间函数:
CURRENT_TIMESTAMP,DATE_FORMAT... - 数学函数:
ABS,ROUND,CEIL...
- 字符串函数:
-
用户自定义标量函数(UDF)。
文档中的例子是:
sql
SELECT PRETTY_PRINT(order_id)
FROM Orders;
这里 PRETTY_PRINT 就是一个函数,可以是内置的,也可以是你注册的 UDF。
6.1 使用 UDF 的步骤(概念层面)
-
实现 UDF(在 Java/Scala 中)
例如定义一个格式化订单号的函数;
-
将 UDF 注册到 catalog 中
在 Flink SQL 环境中,使用类似:
sqlCREATE FUNCTION PRETTY_PRINT AS 'com.example.PrettyPrintFunction'; -
在 SQL 中正常调用:
sqlSELECT PRETTY_PRINT(order_id) AS pretty_id FROM Orders;
这种模式非常适合把复杂的业务逻辑从 SQL 中抽出去,封装成可复用的函数。
7. 综合示例:从 Kafka 流中过滤并格式化订单数据
下面给一个偏实战一点的小例子,假设我们有一张 Kafka 源表:
sql
CREATE TABLE Orders (
order_id STRING,
user_id STRING,
price DOUBLE,
tax DOUBLE,
country STRING,
status STRING,
order_time TIMESTAMP(3),
WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND
) WITH (
'connector' = 'kafka',
'topic' = 'orders',
'properties.bootstrap.servers' = 'localhost:9092',
'scan.startup.mode' = 'latest-offset',
'format' = 'json'
);
我们希望:
- 只关心已支付订单
status = 'PAID'; - 只看中国用户
country = 'CN'; - 计算订单总金额
price + tax; - 顺便把订单号格式化一下(假设有 UDF
FORMAT_ORDER_ID)。
可以这样写:
sql
SELECT
FORMAT_ORDER_ID(order_id) AS pretty_order_id,
user_id,
price,
tax,
price + tax AS total_amount,
order_time
FROM Orders
WHERE status = 'PAID'
AND country = 'CN';
这里你能同时看到:
FROM Orders:数据来自 Kafka 流;SELECT里既有原始列,又有表达式和 UDF 调用;WHERE提前把不关心的记录过滤掉。
在流式 job 中,这条 SQL 会持续消费新数据,并实时输出满足条件的记录。
8. 小结
这篇文章围绕 Flink SQL 中最基础但最常用的两个部分:SELECT 与 WHERE,做了一个系统梳理:
SELECT负责从 table_expression 中取出你真正关心的字段;table_expression可以是表、视图、VALUES、join 后的结果或子查询;- 实际生产中尽量避免
SELECT *,推荐明确列名 + 计算表达式; VALUES子句在调试和构造小型维表时非常有用;WHERE是第一道过滤关,既简化结果,也减轻下游压力;- 内置函数和 UDF 则让
SELECT不只是"取字段",而是"做计算 + 显示逻辑"; - 所有这些,在 Flink 的 批处理与流处理 场景中语法是一致的。