Flink SQL 中的 SELECT & WHERE,批流统一的查询入口

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 ]

这里面有三个核心元素:

  1. select_list:选哪些列、做哪些计算;
  2. table_expression:数据从哪里来;
  3. 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 使用 * 的问题
  1. 对 schema 变更敏感

    • 如果下游代码假设存在某些字段,而上游表 schema 改了(字段改名/删除),* 可能会导致隐性错误;
    • 比如多取了你不需要的字段,或者字段顺序变了,某些工具/解析逻辑可能出问题。
  2. 不利于性能调优

    • 尤其在大宽表场景,SELECT * 会把所有字段都读出来,即使你只用到了其中 2~3 个;
    • 这会增加网络传输、序列化/反序列化成本。
  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)
  • AS t (order_id, price):

    • 给这个内联表起名 t;
    • 并给两列命名为 order_id 和 price。

结果等价于你创建了一张两行的小表。

典型用途:

  1. 调试 SQL :

    在没有真实源表时,快速验证函数逻辑、表达式计算:

    sql 复制代码
    SELECT price, tax, price + tax AS total
    FROM (VALUES (100, 10), (200, 30)) AS v(price, tax);
  2. 小范围"编码表" :

    比如定义状态码映射(真的很少条时):

    sql 复制代码
    WITH 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 的步骤(概念层面)
  1. 实现 UDF(在 Java/Scala 中)

    例如定义一个格式化订单号的函数;

  2. 将 UDF 注册到 catalog 中

    在 Flink SQL 环境中,使用类似:

    sql 复制代码
    CREATE FUNCTION PRETTY_PRINT AS 'com.example.PrettyPrintFunction';
  3. 在 SQL 中正常调用:

    sql 复制代码
    SELECT 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,做了一个系统梳理:

  1. SELECT 负责从 table_expression 中取出你真正关心的字段;
  2. table_expression 可以是表、视图、VALUES、join 后的结果或子查询;
  3. 实际生产中尽量避免 SELECT *,推荐明确列名 + 计算表达式;
  4. VALUES 子句在调试和构造小型维表时非常有用;
  5. WHERE 是第一道过滤关,既简化结果,也减轻下游压力;
  6. 内置函数和 UDF 则让 SELECT 不只是"取字段",而是"做计算 + 显示逻辑";
  7. 所有这些,在 Flink 的 批处理与流处理 场景中语法是一致的。
相关推荐
金伟API102414 小时前
不会写复杂SQL怎么办?
数据库·sql
geovindu15 小时前
sql:Search-Optimized SQL Queries using mysql 9.0
android·数据库·sql·mysql·数据库开发·数据库架构
这个DBA有点耶18 小时前
PostgreSQL HOT更新:频繁更新表的索引膨胀根因与fillfactor调优
数据库·sql·postgresql
starzy199019 小时前
Flink 注册Slot源码深度剖析:从TaskManager注册到Slot状态机的完整链路
大数据·flink
一条小小yu20 小时前
Text-to-SQL Agent
数据库·sql
用户3610588626121 天前
Flink Time 之企业级 WaterMark 案例分析:从四大场景到调优体系与事故排查
大数据·flink
10mAh1 天前
【SQL】NOT IN 遇到 NULL 为什么返回空集?三值逻辑与反关联实战
jvm·数据库·sql
todoitbo1 天前
大数据平台如何选时序数据库:用业务 SQL 验证查询、留存和 Spark 链路
大数据·sql·时序数据库
starzy19901 天前
Flink 启动TaskManager流程设计:从TaskManagerRunner到Slot创建的完整启动链路
大数据·flink
阿华田5121 天前
Java 类加载冲突实战:解决 is not an instance of
c#·linq