Flink SQL 集合运算UNION / INTERSECT / EXCEPT 以及 IN / EXISTS 在流式场景下怎么用?

一、UNION / UNION ALL:合并数据流的第一选择

1.1 基本语义

  • UNION集合并集 + 自动去重
  • UNION ALL简单拼接,不去重

两张表结构兼容时(字段数、字段类型一致),就可以使用。

文档里的例子:

sql 复制代码
CREATE VIEW t1(s) AS VALUES ('c'), ('a'), ('b'), ('b'), ('c');
CREATE VIEW t2(s) AS VALUES ('d'), ('e'), ('a'), ('b'), ('b');

UNION:去重合并

sql 复制代码
(SELECT s FROM t1) UNION (SELECT s FROM t2);

+---+
| s |
+---+
| c |
| a |
| b |
| d |
| e |
+---+

UNION ALL:不去重合并

sql 复制代码
(SELECT s FROM t1) UNION ALL (SELECT s FROM t2);

+---+
| s |
+---+
| c |
| a |
| b |
| b |
| c |
| d |
| e |
| a |
| b |
| b |
+---+

可以看到:

  • UNION 结果中,a / b / c 只出现一次;
  • UNION ALL 完全保留了两张表中各自的行数。

1.2 在 Streaming 模式下的思考

  • UNION ALL 非常适合多流合并的场景,比如多 topic 合并、按业务线拆分的流汇总成主流;
  • UNION 在流上做"全局去重",需要维护所有已经输出过的 key ,这在长时间运行的作业中几乎一定会状态无限膨胀

经验:
流式场景 99% 情况用 UNION ALL,只有明确需要全局去重、且数据量可控时才考虑 UNION

二、INTERSECT / INTERSECT ALL:交集运算

2.1 基本语义

  • INTERSECT:两张表的交集,结果去重
  • INTERSECT ALL:两张表的交集,保留重复次数

还是用 t1t2 示例:

sql 复制代码
(SELECT s FROM t1) INTERSECT (SELECT s FROM t2);

+---+
| s |
+---+
| a |
| b |
+---+

说明两张表都出现过 ab

INTERSECT ALL

sql 复制代码
(SELECT s FROM t1) INTERSECT ALL (SELECT s FROM t2);

+---+
| s |
+---+
| a |
| b |
| b |
+---+

b 出现了两次,说明在"计数意义上的交集"中,b 至少低于等于两边出现次数的最小值。

2.2 Streaming 场景下的影响

交集的核心逻辑是:两边都出现过才算。在流上要支撑:

  • 需要记录:某个值在 t1 中出现了多少次、在 t2 中出现了多少次;
  • 这些信息必须长期维护,除非你引入**时间维度(窗口)**或 TTL。

经验:

  • 实时流上尽量用"窗口 + JOIN"表达交集语义,比如某个时间窗口内都出现过的用户;
  • 直接用全局 INTERSECT/INTERSECT ALL 做"无限时间交集",几乎必然是状态炸弹。

三、EXCEPT / EXCEPT ALL:差集运算

3.1 基本语义

  • EXCEPT:左表中存在,右表中不存在的行,结果去重
  • EXCEPT ALL:也是"左减右",但保留计数差

示例:

sql 复制代码
(SELECT s FROM t1) EXCEPT (SELECT s FROM t2);

+---+
| s |
+---+
| c |
+---+

t1 中的 c 在 t2 里不存在。

EXCEPT ALL

sql 复制代码
(SELECT s FROM t1) EXCEPT ALL (SELECT s FROM t2);

+---+
| s |
+---+
| c |
| c |
+---+

说明:

  • t1 中 c 出现了 2 次;
  • t2 中 c 出现了 0 次;
  • 计数差为 2,因此结果中也有两行 c

3.2 和业务场景的对应

差集非常适合用来表达:

  • "存在于 A 而不存在于 B"的数据:

    • 已下单但未支付的订单 ID;
    • 完成注册但未完成实名认证的用户;
    • 数据对账时,某个系统存在但另一个系统缺失的记录。

同样,在流式场景下:

  • 只要你不加时间限定,这个差集逻辑就意味着要记住所有历史出现过的 key,状态难以收敛;
  • 真实业务里,通常都应该用窗口 + ANTI JOIN进行限定(你前面已经看过 Window Join 的 ANTI 写法了),比如:

"最近 5 分钟内,出现在流 A 但未出现在流 B 的订单"。

四、IN:子查询 + 存在性过滤(底层会被改写成 Join)

4.1 基本语义

IN 用来判断某个表达式是否存在于子查询结果中。子查询返回的表必须:

  • 只有 1 列;
  • 且该列类型与被检测的表达式类型一致。

示例:

sql 复制代码
SELECT user, amount
FROM Orders
WHERE product IN (
  SELECT product FROM NewProducts
);

语义很直观:筛选出"下单的商品在 NewProducts 列表中的订单"。

文档里明确说:

优化器会把 IN 条件重写为 Join + Group 操作。

也就是说:

sql 复制代码
SELECT ...
FROM Orders
WHERE product IN (SELECT product FROM NewProducts)

会被重写为类似:

sql 复制代码
SELECT ...
FROM Orders o
JOIN (
  SELECT DISTINCT product FROM NewProducts
) p
ON o.product = p.product;

对流式查询的影响:

  • 这实际上就是一场 Join + 去重
  • 需要维护 NewProducts 子查询的结果(相当于维表),随着时间推移,这个集合可能越来越大;
  • 所以状态可能 无限增长

解决思路:

  • 如果子查询是有限集(静态维表、配置表),可以直接把它做成维表 / Lookup Join;
  • 如果是实时变化的集合,考虑结合 水位线 + 窗口 + TTL 控制存活时间;
  • 或者改写成有明确时间范围的 Window Join / Interval Join

五、EXISTS:只关心"有没有",不关心具体值

5.1 基本语义

EXISTS 用来判断子查询是否至少返回一行。语法类似:

sql 复制代码
SELECT user, amount
FROM Orders
WHERE EXISTS (
  SELECT product FROM NewProducts
);

由于这个例子里子查询和外表并没有关联,这条 SQL 的语义是:

只要 NewProducts 这个子查询有任何一行,就对 Orders 的每一行返回 true ------ 实际上没啥意义。

真正有用的 EXISTS 一般都会和外表关联,比如:

sql 复制代码
SELECT user, amount
FROM Orders o
WHERE EXISTS (
  SELECT 1
  FROM NewProducts n
  WHERE o.product = n.product
);

这就和:

sql 复制代码
WHERE product IN (SELECT product FROM NewProducts)

语义非常接近了(可以视为一种 SEMI JOIN)。

5.2 底层同样会被改写成 Join

文档指出:

优化器会把 EXISTS 改写成 Join + Group 操作。

因此在流式场景下:

  • 它同样需要维护子查询(右表)的状态;

  • 状态大小依赖于:

    • 右表的去重后 key 数;
    • 左右表的 Join 复合逻辑;
  • 同样需要结合 TTL、窗口等手段做控制。

六、Streaming 模式下的"无限状态"问题与 TTL

Flink 文档反复强调一个点:

在流式查询中,许多操作的状态大小可能无限增长,包括:

  • UNION(需要去重时)
  • INTERSECT / EXCEPT
  • IN / EXISTS 改写出来的 Join
  • GROUPING SETS / ROLLUP / CUBE 等复杂聚合

解决问题的核心武器:

  • State TTL(状态超时) :通过配置 table.exec.state.ttl 等参数,为状态设置"保质期";
  • 到期后状态会被清理,避免占满内存和磁盘。

但要注意:

  • 一旦引入 TTL,就等于你接受了**"只在最近 N 时间内结果严谨,超出范围的结果可被视为过期或忽略"**;
  • 对于长期全局去重 / 全局交叉对账类需求,必须非常谨慎,往往更适合跑离线批任务。

七、在实际项目中怎么选?

把上面的内容总结成几条实践建议:

  1. 合并多路流:优先用 UNION ALL

    • 不要在 Streaming 里随手 UNION 去重;
    • 去重另开一步专门的聚合或者窗口去重,更可控。
  2. 交集 / 差集要加时间维度

    • 避免"无限历史交集/差集"的思路;

    • 更推荐:

      • "某个时间窗口内都出现过" → 用 Window Join + INNER / SEMI;
      • "某个时间窗口内只出现在一边" → 用 Window Join + ANTI。
  3. IN / EXISTS 就是"语法糖 + Join"

    • Streaming 模式下,记住它们本质上就是 Join;

    • 数据量大时,要考虑改成更明确的:

      • Lookup Join;
      • Temporal Join;
      • Window Join + TTL。
  4. 所有"可能无限长时间保留状态"的操作,都要明确 TTL 策略

    • 尤其是在"业务上只关注最近 X 时间"的场景里,TTL 是非常合理的手段;
    • 如果业务要求"绝对精确的全局结果",建议把这部分交给离线批处理完成。

小结

Flink SQL 的集合运算和 IN/EXISTS 表面看起来和传统数据库非常像,但只要一进入 Streaming 模式,它们背后就会多出一层 "状态膨胀 vs 结果精度" 的权衡。

可以这样记:

  • UNION ALL:流合并的默认方案;
  • UNION / INTERSECT / EXCEPT:更适合作离线批或"窗口 + 有界范围"的实时分析;
  • IN / EXISTS:底层其实是 Join,状态问题要特别小心;
  • 一旦进入"全局语义",就必须认真考虑 TTL、窗口、或者干脆放到离线任务做。
相关推荐
_Minato_2 小时前
数据库知识整理——数据库控制功能
数据库·经验分享·笔记·软考·计算机系统
TDengine (老段)2 小时前
TDengine 数据订阅架构设计与最佳实践
大数据·数据库·时序数据库·tdengine·涛思数据
Jtti2 小时前
MySQL磁盘不足会导致服务直接崩溃吗?
数据库·mysql
蜂蜜黄油呀土豆2 小时前
分布式基础知识:分布式事务完整解析(背景、模式、协议、优缺点)
数据库·微服务·分布式事务·架构设计·分布式系统·2pc/3pc·tcc/saga
写代码的【黑咖啡】2 小时前
MySQL 主从同步与读写分离详解
数据库·mysql
我是高手高手高高手2 小时前
TP8 增加数据时在数据回滚事务时没错误数据却没有插入(表数据插入不了)startTrans() rollback()Db::transaction
数据库
小光学长2 小时前
基于web的影视网站设计与实现14yj533o(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
java·前端·数据库
菜鸟小九2 小时前
redis基础(数据结构)
数据结构·数据库·redis
bkspiderx2 小时前
libmysqlclient:MySQL 底层客户端库的全面指南
数据库·mysql·mysqlclient·libmysqlclient·mysql 底层客户端库