Flink SQL 中的 SELECT DISTINCT批流一体下的去重与状态管理

1. SELECT DISTINCT 是干嘛的?

SELECT DISTINCT 是 SQL 里最经典的"去重"工具:

  • 语义:对结果集按选出的列做去重;
  • 行为:对于重复的行,只保留一行。

示例:

sql 复制代码
SELECT DISTINCT id
FROM Orders;

如果 Orders 中有:

id price
1 10.0
1 12.0
2 5.0

SELECT DISTINCT id 的结果是:

id
1
2

注意:去重是按 SELECT 出来的列来算的,比如:

sql 复制代码
SELECT DISTINCT id, price
FROM Orders;

(1, 10.0)(1, 12.0) 就不算重复,会保留两行。

2. 在批处理里的表现:一次性扫描,一次性去重

批处理(Batch) 场景中,SELECT DISTINCT 的行为跟传统数据库基本一样:

  1. 输入数据是有界的(有开始有结束);
  2. Flink 会把所有行都看一遍,维护一个"已经见过的 key 集合";
  3. 每遇到一行新数据,就判断这个 key 是否已经见过;
  4. 没见过 → 输出并记住;
    见过 → 丢弃。

背后通常是某种基于哈希的去重或排序去重:

  • 小规模数据:可能是 hash-based;
  • 大规模数据:可能拆成分区 + 外排序等。

总之,对你作为 SQL 使用者来说:它就像普通数据库的 DISTINCT 一样用就行

3. 在流处理里的问题:状态可能"长到飞起"

重点来了:

流式查询(Streaming) 中,SELECT DISTINCT 就不再只是一个简单的去重操作了,而是一个持续维护"已出现集合"的长期任务

想象一下这样一条流式 SQL:

sql 复制代码
SELECT DISTINCT user_id
FROM UserActions;

UserActions 是一个 Kafka 流,用户操作永远在来:

  • 每来一行,就要判断 user_id 是否出现过;
  • 要做到"真正的 DISTINCT",你就要把所有出现过的 user_id 都记住;
  • 于是,这部分状态会不断增长,理论上是无限的

Flink 文档里那句话其实就是这个意思:

对于流式查询,为了计算 SELECT DISTINCT,需要维护的状态可能会无限增长,状态大小取决于不同去重值的数量。

翻译成人话:

流式 DISTINCT = 维护一个"无上限的 Set",除非你做了时间限制或 TTL。

4. 用 State TTL 控制状态大小(以及要付出的代价)

为了防止状态爆炸,Flink 提供了状态 TTL(Time-To-Live) 配置,你可以为查询设置一个"状态最长存活时间":

  • 超过这个时间没被访问的 key 会被清理;
  • 状态占用内存/RocksDB 空间可以控制在一个比较健康的范围。

文档的提醒也很直接:

你可以配置合适的 state TTL 来防止状态过大,但这可能影响结果正确性。

为什么会影响正确性?

举个简单例子:

  • 你有一条流:SELECT DISTINCT user_id FROM UserActions

  • 假设 table.exec.state.ttl = '7d'

  • 用户 A 在第一天产生一条日志 → user_id= A 被记录在状态中 → 输出一次 A;

  • 如果用户 A 在第 15 天又产生一条日志:

    • 由于超过了 7 天,之前的 A 已经过期被清理;
    • 再次出现时,算子会认为是**"一个新的 A"**;
    • 又输出一次 A。

也就是说:TTL 越短,去重"记忆"越差,结果越不靠谱

所以要明确一点:

设置 TTL 是在 "正确性" 和 "资源占用" 之间做权衡。

5. 几种常见的去重需求与实现方式

在实际业务中,"去重"并不都是同一个意思。我们可以分几类来看:

5.1 "全局唯一去重":看到一次就算数(典型 SELECT DISTINCT)

需求例子:

  • "统计所有曾经访问过系统的 user_id 列表";
  • "把所有出现过的设备 ID 打一份清单"。

如果这是一个 离线任务,用批模式跑一次即可:

sql 复制代码
SELECT DISTINCT user_id
FROM UserActions;

如果你非要搞成一个实时流式任务

  • 需要承受状态越来越大的现实;
  • 至少要设置一个足够长的 TTL(比如几个月甚至更长);
  • 还要预估:
    "我系统里最多可能有多少不同 user_id?"

这个在大规模用户体系下通常不是一个好主意,更适合做离线导出

5.2 "一段时间内去重":只关心最近 N 天 / N 小时

场景例子:

  • "统计最近 7 天内活跃的用户数(去重)";
  • "最近 1 小时内有多少不同 IP 访问了系统"。

这里其实要的是时间范围内的去重,而不是"历史以来的终身去重"。

你可以用两种思路:

思路 1:窗口 + 去重

sql 复制代码
SELECT
  window_start,
  window_end,
  COUNT(DISTINCT user_id) AS uv
FROM TABLE(
  TUMBLE(TABLE UserActions, DESCRIPTOR(event_time), INTERVAL '1' DAY)
)
GROUP BY window_start, window_end;

这里是 window 聚合里的 COUNT(DISTINCT ...),算子会只在一个窗口内维护去重状态,窗口结束后可以释放状态,不会无限增长

思路 2:TTL + 明确"过期即忘记"

还是用:

sql 复制代码
SELECT DISTINCT user_id
FROM UserActions;

但全局配置:table.exec.state.ttl = '7d' 之类,接受这种语义:

"只要用户在 7 天以上不出现,就把他的存在感忘记掉;如果他再来一次,就当成又'新增'了一个 user_id。"

这种要求比较"宽松"的业务可以接受,比如:

  • 某些"活跃用户集合"的近似统计;
  • 某些去重报警场景。
5.3 "按主键去重保留最新":其实更适合用窗口 + ROW_NUMBER

还有一种常见需求其实不是 SELECT DISTINCT 本身,而是:

对某个 key 去重,只保留最新的一条记录(或者第一条记录)。

比如:

  • "对订单流按 order_id 去重,只保留最新状态的一条";
  • "按设备号去重,只保留最近上报的一条心跳"。

这种需求用 SELECT DISTINCT 并不好表达,更推荐:

sql 复制代码
SELECT *
FROM (
  SELECT *,
         ROW_NUMBER() OVER (
           PARTITION BY order_id
           ORDER BY event_time DESC
         ) AS rn
  FROM Orders
) t
WHERE rn = 1;

这里 DISTINCT 就退居二线了,核心是窗口函数 + 排序。

6. 对 SELECT DISTINCT 的使用建议(批 / 流分别看)

6.1 在批任务(Batch)中
  • 可以大胆用 SELECT DISTINCT 做脱重:

    • 清洗去重;
    • 统计 global distinct 值;
    • 构建维表唯一 key;
  • 就按普通数据库使用即可,不用太担心状态问题(任务会结束)。

6.2 在流任务(Streaming)中

使用前先问自己三个问题:

  1. 我是要"终身唯一"还是"某个时间范围内唯一"?
  2. 业务能不能接受"过期后再次出现时被当成新值"?
  3. distinct key 的基数大概有多大,内存放得下吗?

一般建议:

  • 真·终身去重 → 谨慎使用,尽量转为离线计算或用外部存储 + 稀疏结构(如 Bloom Filter)等方案;
  • 时间范围去重 → 优先用窗口聚合 COUNT(DISTINCT ...) 或 TTL 明确边界;
  • 实时明细去重(只要遇到就过滤) → 可以用 DISTINCT + 相对较短的 TTL,把业务语义写清楚。

7. 小结

这一小节的核心点可以压缩成三句话:

  1. SELECT DISTINCT = 对结果集按选出的列去重,批模式下基本等同于传统数据库的行为;
  2. 在流模式下,DISTINCT 需要持久维护"已见过的值"的状态,理论上会无限增长;
  3. 你可以用 State TTL 控制状态大小,但那是用"正确性"换"资源",在用之前一定想清楚业务语义。

如果你在写 Flink SQL 的"基础语法"系列,SELECT / WHERE / DISTINCT 可以自然串成一块:

  • SELECT:选字段 + 做计算;
  • WHERE:提前过滤;
  • DISTINCT:按需要去重(批很爽,流要谨慎)。

后面再接上 GROUP BY、窗口、JOIN,整套 SQL 文档就顺起来了。

相关推荐
数据知道13 分钟前
PostgreSQL 核心原理:如何利用多核 CPU 加速大数据量扫描(并行查询)
数据库·postgresql
麦聪聊数据1 小时前
Web 原生架构如何重塑企业级数据库协作流?
数据库·sql·低代码·架构
未来之窗软件服务1 小时前
数据库优化提速(四)新加坡房产系统开发数据库表结构—仙盟创梦IDE
数据库·数据库优化·计算机软考
Goat恶霸詹姆斯3 小时前
mysql常用语句
数据库·mysql·oracle
Hello.Reader3 小时前
Flink 使用 Amazon S3 读写、Checkpoint、插件选择与性能优化
大数据·flink
大模型玩家七七3 小时前
梯度累积真的省显存吗?它换走的是什么成本
java·javascript·数据库·人工智能·深度学习
曾经的三心草3 小时前
redis-9-哨兵
数据库·redis·bootstrap
明哥说编程3 小时前
Dataverse自定义表查询优化:D365集成大数据量提速实战【索引配置】
数据库·查询优化·dataverse·dataverse自定义表·索引配置·d365集成·大数据量提速
xiaowu0804 小时前
C# 拆解 “显式接口实现 + 子类强类型扩展” 的设计思想
数据库·oracle
讯方洋哥4 小时前
HarmonyOS App开发——关系型数据库应用App开发
数据库·harmonyos