1. 从一条SELECT说起:为什么Oracle、MySQL解决不了"无限数据"
我第一次意识到批处理和流处理是两回事,是在做一个实时大屏项目的时候。业务方提了一个看起来很简单的需求:查一下当前时刻全网的订单总额。当时的架构还是经典的"业务库MySQL + 定时任务 + 汇总表",每隔五分钟跑一次SUM。上线之后产品经理说数据不对,因为五分钟内有一笔大额退款,大屏上那个数字还在往上涨。那一刻我意识到,传统数据库的SQL模型天然假设数据是静止的:你发一条SELECT,数据库给你一张结果集,查询结束,连接释放,一次交互完成。但业务现场的数据永远不会等你查完再变,它一直在流。
这就是流处理技术的核心场景:数据不是躺在表里等你查,而是像河水一样不断流过。你要做的不是"查一次",而是"持续算"。数据库领域的SQL流处理技术,解决的就是怎么用大家熟悉的SQL语言,去表达这种持续计算逻辑。这件事在过去十年里从学术界走向工业界,从Flink SQL到Kafka Streams再到ksqlDB,SQL已经成为流处理领域事实上的标准接口。这篇文章不打算从理论定义讲起,我想从实际选型和落地的角度,聊聊流式SQL到底是什么、它和传统SQL的区别在哪里、你手头的业务该不该上、以及我踩过的那些坑。
为了说清楚这个事,得先打破一个思维定式:你熟悉的SELECT、JOIN、GROUP BY,在流处理世界里语义全变了。传统数据库里,SELECT是"对当前快照做一次查询";在流处理里,SELECT是"对每一条到达的数据持续执行这个查询"。前者是"一次性的函数调用",后者是"常驻的管道"。理解了这一点,后面所有技术细节都会顺理成章。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 流式SQL到底改了什么:时间窗口、持续查询与状态
很多人第一次接触流式SQL会有一个错觉:不就是把SQL跑在流数据上吗?语法不都一样?真上手就发现,哪里是语法不一样,是整个心智模型都不一样。
2.1 持续查询:SQL从"请求-响应"变成"常驻管道"
先看最基础的一条SQL:
sql复制SELECT user_id, SUM(amount)
FROM orders
GROUP BY user_id;
在MySQL里跑这条语句,结果集是执行瞬间的订单汇总。订单表今天有100万行,返回100万行汇总。明天有120万行,你再跑一次,结果变了。但在流式SQL里,这条语句的行为完全不同:它的语义是"从今往后,每来一条订单,就更新对应user_id的累计值"。你不需要反复执行,查询本身是持续运行的。Flink SQL、ksqlDB都是这样,查询一旦提交,就一直运行,直到你手动取消。
这就带来一个关键的架构变化:数据库是"存储主导",数据先落盘、再查询;流处理是"计算主导",数据边流过边算,算完的结果可选地落盘。这也解释了为什么流处理引擎普遍对存储不太上心——它的假设是数据是瞬时的、流动的,计算结果才是你要持久化的东西。
2.2 时间窗口:流式SQL里最反直觉的概念
传统SQL里如果你要统计"最近五分钟的订单金额",得用WHERE order_time >= NOW() - INTERVAL 5 MINUTE,这是单次执行的过滤条件。但流处理里的"最近五分钟"是一个窗口,是持续滑动的。Flink SQL里是这样写的:
sql复制SELECT
TUMBLE_START(order_time, INTERVAL '5' MINUTE) AS window_start,
SUM(amount) AS total_amount
FROM orders
GROUP BY TUMBLE(order_time, INTERVAL '5' MINUTE);
这个TUMBLE函数叫"滚动窗口",每五分钟一个区间,数据按事件时间归属到某个窗口里。窗口一关,结果就发出去。你不能用传统SQL的思维去理解"窗口关闭"——它不是查询结束,而是这一批数据的计算结果对外可见了,下一个五分钟的窗口马上开始计算。
这里最容易踩的坑是"事件时间"和"处理时间"。处理时间是数据到达引擎的时刻,事件时间是业务发生的时刻。如果你的订单数据从业务库同步到Kafka有延迟,按处理时间开窗会把本该属于上午十点的订单算进十点零五分那个窗口。生产环境里几乎必须用事件时间,你的上游数据必须带上业务时间戳,并且要配合水位线(watermark)机制来处理迟到数据。具体水位线怎么配,我后面在踩坑章节细说。
2.3 状态:流式SQL里最贵的资源
GROUP BY user_id要一直累计每个用户的金额,这个"累计值"就是状态。流式SQL的JOIN也要状态——为了关联两条流,你得把一边的数据先暂存起来。状态存在哪里?Flink里默认存在内存和RocksDB,ksqlDB存在Kafka的changelog主题里。状态的大小直接决定你的内存成本和运维复杂度。
我见过一个典型的翻车案例:有人把用户维表数据当作普通流全部加载进状态,一个JOIN查询下去,状态几个GB,内存直接打爆。正确做法是用维表JOIN(lookup join)去查外部
