【Flink SQL】Flink SQL 语法篇(四):Group 聚合、Over 聚合

本文详细介绍了ApacheFlinkSQL中的Group聚合和Over聚合功能,包括它们的基础概念、区别、SQL语义以及在批处理和实时流处理中的应用。特别强调了窗口聚合的时间语义和Groupingsets、Rollup、Cube等高级特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Flink SQL 语法篇》系列,共包含以下 10 篇文章:

😊 如果您觉得这篇文章有用 ✔️ 的话,请给博主一个一键三连 🚀🚀🚀 吧 (点赞 🧡、关注 💛、收藏 💚)!!!您的支持 💖💖💖 将激励 🔥 博主输出更多优质内容!!!

1.Group 聚合

1.1 基础概念

Group 聚合定义(支持 Batch / Streaming 任务):Flink 也支持 Group 聚合。Group 聚合和上面介绍到的窗口聚合的不同之处,就在于 Group 聚合是按照数据的类别进行分组,比如年龄、性别,是横向的;而窗口聚合是在时间粒度上对数据进行分组,是纵向的。如下图所示,就展示出了其区别。其中 按颜色分 key(横向)就是 Group 聚合按窗口划分(纵向)就是 窗口聚合

在这里插入图片描述

1.2 窗口聚合和 Group 聚合

应用场景:一般用于对数据进行分组,然后后续使用聚合函数进行 countsum 等聚合操作。

那么这时候,小伙伴萌就会问到,我其实可以把窗口聚合的写法也转换为 Group 聚合,只需要把 Group 聚合的 Group By key 换成时间就行,那这两个聚合的区别到底在哪?

首先来举一个例子看看怎么将 窗口聚合 转换为 Group 聚合。假如一个窗口聚合是按照 1 1 1 分钟的粒度进行聚合,如下 滚动窗口 SQL:

-- 数据源表
CREATE TABLE source_table (
    -- 维度数据
    dim STRING,
    -- 用户 id
    user_id BIGINT,
    -- 用户
    price BIGINT,
    -- 事件时间戳
    row_time AS cast(CURRENT_TIMESTAMP as timestamp(3)),
    -- watermark 设置
    WATERMARK FOR row_time AS row_time - INTERVAL '5' SECOND
) WITH (
  'connector' = 'datagen',
  'rows-per-second' = '10',
  'fields.dim.length' = '1',
  'fields.user_id.min' = '1',
  'fields.user_id.max' = '100000',
  'fields.price.min' = '1',
  'fields.price.max' = '100000'
)

-- 数据汇表
CREATE TABLE sink_table (
    dim STRING,
    pv BIGINT,
    sum_price BIGINT,
    max_price BIGINT,
    min_price BIGINT,
    uv BIGINT,
    window_start bigint
) WITH (
  'connector' = 'print'
)

-- 数据处理逻辑
insert into sink_table
select dim,
    count(*) as pv,
    sum(price) as sum_price,
    max(price) as max_price,
    min(price) as min_price,
    -- 计算 uv 数
    count(distinct user_id) as uv,
    UNIX_TIMESTAMP(CAST(tumble_start(row_time, interval '1' minute) AS STRING)) * 1000  as window_start
from source_table
group by
    dim,
    -- 按照 Flink SQL tumble 窗口写法划分窗口
    tumble(row_time, interval '1' minute)

转换为 Group 聚合 的写法如下:

-- 数据源表
CREATE TABLE source_table (
    -- 维度数据
    dim STRING,
    -- 用户 id
    user_id BIGINT,
    -- 用户
    price BIGINT,
    -- 事件时间戳
    row_time AS cast(CURRENT_TIMESTAMP as timestamp(3)),
    -- watermark 设置
    WATERMARK FOR row_time AS row_time - INTERVAL '5' SECOND
) WITH (
  'connector' = 'datagen',
  'rows-per-second' = '10',
  'fields.dim.length' = '1',
  'fields.user_id.min' = '1',
  'fields.user_id.max' = '100000',
  'fields.price.min' = '1',
  'fields.price.max' = '100000'
);

-- 数据汇表
CREATE TABLE sink_table (
    dim STRING,
    pv BIGINT,
    sum_price BIGINT,
    max_price BIGINT,
    min_price BIGINT,
    uv BIGINT,
    window_start bigint
) WITH (
  'connector' = 'print'
);

-- 数据处理逻辑
insert into sink_table
select dim,
    count(*) as pv,
    sum(price) as sum_price,
    max(price) as max_price,
    min(price) as min_price,
    -- 计算 uv 数
    count(distinct user_id) as uv,
    cast((UNIX_TIMESTAMP(CAST(row_time AS STRING))) / 60 as bigint) as window_start
from source_table
group by
    dim,
    -- 将秒级别时间戳 / 60 转化为 1min
    cast((UNIX_TIMESTAMP(CAST(row_time AS STRING))) / 60 as bigint)

确实没错,上面这个转换是一点问题都没有的。

但是窗口聚合和 Group by 聚合的差异在于:

  • 本质区别窗口聚合是具有时间语义的,其本质是想实现窗口结束输出结果之后,后续有迟到的数据也不会对原有的结果发生更改了,即输出结果值是定值(不考虑 allowLateness)。而 Group by 聚合是没有时间语义的,不管数据迟到多长时间,只要数据来了,就把上一次的输出的结果数据撤回,然后把计算好的新的结果数据发出。
  • 运行层面:窗口聚合是和 时间 绑定的,窗口聚合其中窗口的计算结果触发都是由 时间(Watermark)推动的。Group by 聚合完全由 数据 推动触发计算,新来一条数据去根据这条数据进行计算出结果发出;由此可见两者的实现方式也大为不同。

1.3 SQL 语义

SQL 语义这里也拿离线和实时做对比,Order 为 Kafka,target_table 为 Kafka,这个 SQL 生成的实时任务,在执行时,会生成三个算子。

  • 数据源算子From Order):数据源算子一直运行,实时的从 Order Kafka 中一条一条的读取数据,然后一条一条发送给下游的 Group 聚合算子,向下游发送数据的 shuffle 策略是根据 group by 中的 key 进行发送,相同的 key 发到同一个 SubTask(并发) 中。
  • Group 聚合算子group by key + sum / count / max / min):接收到上游算子发的一条一条的数据,去状态 state 中找这个 key 之前的 sum / count / max / min 结果。如果有结果 oldResult,拿出来和当前的数据进行 sum / count / max / min 计算出这个 key 的新结果 newResult,并将新结果 [key, newResult] 更新到 state 中,在向下游发送新计算的结果之前,先发一条撤回上次结果的消息 -[key, oldResult],然后再将新结果发往下游 +[key, newResult];如果 state 中没有当前 key 的结果,则直接使用当前这条数据计算 sum / max / min 结果 newResult,并将新结果 [key, newResult] 更新到 state 中,当前是第一次往下游发,则不需要先发回撤消息,直接发送 +[key, newResult]
  • 数据汇算子INSERT INTO target_table):接收到上游发的一条一条的数据,写入到 target_table Kafka 中这个实时任务也是 24 24 24 小时一直在运行的,所有的算子在同一时刻都是处于 running 状态的。

1.4 Group 聚合支持 Grouping sets、Rollup、Cube

Group 聚合也支持 Grouping setsRollupCube。举一个 Grouping sets 的案例:

SELECT 
    supplier_id
    , rating
    , product_id
    , COUNT(*)
FROM (VALUES
    ('supplier1', 'product1', 4),
    ('supplier1', 'product2', 3),
    ('supplier2', 'product3', 3),
    ('supplier2', 'product4', 4))
AS Products(supplier_id, product_id, rating)
GROUP BY GROUPING SET (
    ( supplier_id, product_id, rating ),
    ( supplier_id, product_id         ),
    ( supplier_id,             rating ),
    ( supplier_id                     ),
    (              product_id, rating ),
    (              product_id         ),
    (                          rating ),
    (                                 )
)

2.Over 聚合

Over 聚合定义(支持 Batch / Streaming):可以理解为是一种特殊的滑动窗口聚合函数。

那这里我们拿 Over 聚合窗口聚合 做一个对比,其之间的最大不同之处在于:

  • 窗口聚合:不在 group by 中的字段,不能直接在 select 中拿到。
  • Over 聚合:能够保留原始字段。

注意:其实在生产环境中,Over 聚合的使用场景还是比较少的。在 Hive 中也有相同的聚合,但是小伙伴萌可以想想你在离线数仓经常使用嘛?

  • 应用场景:计算最近一段滑动窗口的聚合结果数据。
  • 实际案例:查询每个产品最近一小时订单的金额总和。
SELECT order_id, order_time, amount,
  SUM(amount) OVER (
    PARTITION BY product
    ORDER BY order_time
    RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW
  ) AS one_hour_prod_amount_sum
FROM Orders
  • Over 聚合的语法总结如下:
SELECT
  agg_func(agg_col) OVER (
    [PARTITION BY col1[, col2, ...]]
    ORDER BY time_col
    range_definition),
  ...
FROM ...
  • ORDER BY:必须是时间戳列(事件时间、处理时间)。
  • PARTITION BY:标识了聚合窗口的聚合粒度,如上述案例是按照 product 进行聚合。
  • range_definition:这个标识聚合窗口的聚合数据范围,在 Flink 中有两种指定数据范围的方式。第一种为 按照行数聚合,第二种为 按照时间区间聚合。如下案例所示。

2.1 时间区间聚合

按照时间区间聚合就是时间区间的一个滑动窗口,比如下面案例 1 1 1 小时的区间,最新输出的一条数据的 sum 聚合结果就是最近一小时数据的 amount 之和。

CREATE TABLE source_table (
    order_id BIGINT,
    product BIGINT,
    amount BIGINT,
    order_time as cast(CURRENT_TIMESTAMP as TIMESTAMP(3)),
    WATERMARK FOR order_time AS order_time - INTERVAL '0.001' SECOND
) WITH (
  'connector' = 'datagen',
  'rows-per-second' = '1',
  'fields.order_id.min' = '1',
  'fields.order_id.max' = '2',
  'fields.amount.min' = '1',
  'fields.amount.max' = '10',
  'fields.product.min' = '1',
  'fields.product.max' = '2'
);

CREATE TABLE sink_table (
    product BIGINT,
    order_time TIMESTAMP(3),
    amount BIGINT,
    one_hour_prod_amount_sum BIGINT
) WITH (
  'connector' = 'print'
);

INSERT INTO sink_table
SELECT product, order_time, amount,
  SUM(amount) OVER (
    PARTITION BY product
    ORDER BY order_time
    -- 标识统计范围是一个 product 的最近 1 小时的数据
    RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW
  ) AS one_hour_prod_amount_sum
FROM source_table

2.2 行数聚合

按照行数聚合就是数据行数的一个滑动窗口,比如下面案例,最新输出的一条数据的 sum 聚合结果就是最近 5 5 5 行数据的 amount 之和。

CREATE TABLE source_table (
    order_id BIGINT,
    product BIGINT,
    amount BIGINT,
    order_time as cast(CURRENT_TIMESTAMP as TIMESTAMP(3)),
    WATERMARK FOR order_time AS order_time - INTERVAL '0.001' SECOND
) WITH (
  'connector' = 'datagen',
  'rows-per-second' = '1',
  'fields.order_id.min' = '1',
  'fields.order_id.max' = '2',
  'fields.amount.min' = '1',
  'fields.amount.max' = '2',
  'fields.product.min' = '1',
  'fields.product.max' = '2'
);

CREATE TABLE sink_table (
    product BIGINT,
    order_time TIMESTAMP(3),
    amount BIGINT,
    one_hour_prod_amount_sum BIGINT
) WITH (
  'connector' = 'print'
);

INSERT INTO sink_table
SELECT product, order_time, amount,
  SUM(amount) OVER (
    PARTITION BY product
    ORDER BY order_time
    -- 标识统计范围是一个 product 的最近 5 行数据
    ROWS BETWEEN 5 PRECEDING AND CURRENT ROW
  ) AS one_hour_prod_amount_sum
FROM source_table

预跑结果如下:

+I[2, 2021-12-24T22:18:19.147, 1, 9]
+I[1, 2021-12-24T22:18:20.147, 2, 11]
+I[1, 2021-12-24T22:18:21.147, 2, 12]
+I[1, 2021-12-24T22:18:22.147, 2, 12]
+I[1, 2021-12-24T22:18:23.148, 2, 12]
+I[1, 2021-12-24T22:18:24.147, 1, 11]
+I[1, 2021-12-24T22:18:25.146, 1, 10]
+I[1, 2021-12-24T22:18:26.147, 1, 9]
+I[2, 2021-12-24T22:18:27.145, 2, 11]
+I[2, 2021-12-24T22:18:28.148, 1, 10]
+I[2, 2021-12-24T22:18:29.145, 2, 10]

当然,如果你在一个 SELECT 中有多个聚合窗口的聚合方式,Flink SQL 支持了一种简化写法,如下案例:

SELECT order_id, order_time, amount,
  SUM(amount) OVER w AS sum_amount,
  AVG(amount) OVER w AS avg_amount
FROM Orders
-- 使用下面子句,定义 Over Window
WINDOW w AS (
  PARTITION BY product
  ORDER BY order_time
  RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW)
<think>嗯,我现在需要学习如何在Flink SQL中使用窗口。我之前对Flink有一些了解,但窗口部分还不太清楚。让我先想想,窗口是用来做什么的呢?应该是用来处理无界流数据的一种方式,把数据分成不同的块,然后对这些块进行计算,比如求和、计数之类的。 首先,Flink SQL支持哪些类型的窗口呢?我记得可能有滚动窗口、滑动窗口和会话窗口这三种常见类型。滚动窗口是按固定时间或数量划分,没有重叠。滑动窗口则是可以重叠的,比如每5分钟统计一次过去10分钟的数据。会话窗口是根据活动的间隔来分组的,如果一段时间没有数据就关闭窗口。 那在Flink SQL中,具体怎么定义这些窗口呢?可能需要用到TUMBLE、HOP和SESSION这些函数吧。例如,TUMBLE用于滚动窗口,HOP用于滑动窗口,SESSION用于会话窗口。然后,这些函数通常会和GROUP BY一起使用,对吧? 让我看看具体的语法结构。比如,滚动窗口可能像这样:TUMBLE(事件时间字段, INTERVAL '时间' 单位)。然后作为GROUP BY的一部分,比如GROUP BY TUMBLE(rowtime, INTERVAL '1' MINUTE)。这样每个窗口会按照1分钟划分,然后对每个窗口内的数据进行聚合。 那时间属性字段需要是事件时间还是处理时间呢?Flink SQL中处理时间的话,可能需要用PROCTIME()函数来声明一个处理时间字段。而事件时间则需要用TIMESTAMP类型,并且可能需要配合水印来正确处理乱序事件。所以在创建表的时候,可能需要定义水印,比如WATERMARK FOR rowtime AS rowtime - INTERVAL '5' SECOND。 举个例子,假设我有一个表叫user_actions,里面有user_id、action_type、rowtime作为事件时间字段。如果我要统计每1分钟的点击次数,应该怎么写SQL呢?应该是这样的: SELECT user_id, TUMBLE_START(rowtime, INTERVAL '1' MINUTE) as window_start, TUMBLE_END(rowtime, INTERVAL '1' MINUTE) as window_end, COUNT(*) as action_count FROM user_actions GROUP BY TUMBLE(rowtime, INTERVAL '1' MINUTE), user_id 这样就能按每个用户和1分钟的滚动窗口来统计了。这里用了TUMBLE_START和TUMBLE_END来获取窗口的开始和结束时间。 那滑动窗口的话,HOP函数需要指定滑动步长和窗口大小。比如每30秒滑动一次,窗口大小1分钟。那么GROUP BY应该是HOP(rowtime, INTERVAL '30' SECOND, INTERVAL '1' MINUTE)。这样每个窗口会覆盖1分钟的数据,每30秒滑动一次,产生重叠的窗口。 会话窗口可能比较复杂,因为它是根据不活动的间隙来划分的。SESSION函数需要指定一个间隙时间,比如5分钟。当超过5分钟没有数据,窗口就关闭。那么GROUP BY应该是SESSION(rowtime, INTERVAL '5' MINUTE)。不过会话窗口可能需要更多的配置,或者依赖于数据本身的活动情况。 另外,需要注意的是,窗口函数必须作为GROUP BY的一部分,而且时间字段必须已经定义成事件时间或处理时间属性。如果使用处理时间的话,可能无法处理乱序事件,但实现起来更简单,不需要水印。 还有,窗口的开始和结束时间可以通过TUMBLE_START、TUMBLE_END等函数获取,这些是元数据,可以像普通字段一样在SELECT语句中使用。此外,窗口的大小和滑动步长需要合理设置,避免资源消耗过大,特别是滑动窗口,如果滑动步长很小,窗口数量会很多。 可能还有一些注意事项,比如水印的设置会影响窗口的触发时机。如果水印延迟设置较长,窗口的计算会延迟,但能处理更多的迟到数据。如果设置较短,可能会更及时输出结果,但可能遗漏一些数据。 再想想,有没有可能用OVER窗口?比如每来一条数据,计算最近N条数据的统计值。不过OVER窗口和分组窗口可能有所不同,这里的问题主要是关于分组窗口的,也就是TUMBLE、HOP、SESSION这些。 总之,在Flink SQL中使用窗口的关键步骤应该是:定义时间属性,选择合适的窗口函数,在GROUP BY子句中使用窗口函数,并在SELECT中使用窗口的元数据函数来获取窗口时间。同时,根据需求选择滚动、滑动或会话窗口,并合理设置窗口参数和水印策略。</think>在 Flink SQL 中实现窗口操作,主要通过内置的窗口函数(TUMBLE、HOP、SESSION)结合 `GROUP BY` 实现。以下是详细步骤和示例: --- ### **1. 前置条件:定义时间属性和水印** 窗口操作依赖时间语义(事件时间或处理时间),需在表定义中明确时间属性和水印(事件时间必选)。 ```sql CREATE TABLE user_actions ( user_id INT, action_type STRING, -- 定义事件时间字段(TIMESTAMP类型) rowtime TIMESTAMP(3), -- 定义水印,允许5秒的乱序 WATERMARK FOR rowtime AS rowtime - INTERVAL '5' SECOND ) WITH ( ... ); ``` --- ### **2. 窗口类型及语法** #### **(1) 滚动窗口(TUMBLE)** - **特点**:固定大小、无重叠。 - **函数**:`TUMBLE(时间字段, 窗口大小)` - **示例**:统计每分钟每个用户的点击次数 ```sql SELECT user_id, TUMBLE_START(rowtime, INTERVAL '1' MINUTE) AS window_start, TUMBLE_END(rowtime, INTERVAL '1' MINUTE) AS window_end, COUNT(*) AS action_count FROM user_actions GROUP BY TUMBLE(rowtime, INTERVAL '1' MINUTE), -- 1分钟滚动窗口 user_id; ``` #### **(2) 滑动窗口(HOP)** - **特点**:窗口可重叠,需指定步长(滑动间隔)。 - **函数**:`HOP(时间字段, 滑动步长, 窗口大小)` - **示例**:每30秒统计一次过去1分钟的点击量 ```sql SELECT HOP_START(rowtime, INTERVAL '30' SECOND, INTERVAL '1' MINUTE) AS window_start, HOP_END(rowtime, INTERVAL '30' SECOND, INTERVAL '1' MINUTE) AS window_end, COUNT(*) AS action_count FROM user_actions GROUP BY HOP(rowtime, INTERVAL '30' SECOND, INTERVAL '1' MINUTE); -- 步长30秒,窗口1分钟 ``` #### **(3) 会话窗口(SESSION)** - **特点**:基于数据活跃性划分,超过空闲间隙则关闭窗口。 - **函数**:`SESSION(时间字段, 空闲间隙)` - **示例**:用户5分钟无活动则关闭会话 ```sql SELECT user_id, SESSION_START(rowtime, INTERVAL '5' MINUTE) AS window_start, SESSION_END(rowtime, INTERVAL '5' MINUTE) AS window_end, COUNT(*) AS action_count FROM user_actions GROUP BY SESSION(rowtime, INTERVAL '5' MINUTE), -- 5分钟空闲间隙 user_id; ``` --- ### **3. 关键函数说明** - **窗口元数据**: - `窗口函数_START(时间字段, 参数)`: 窗口开始时间。 - `窗口函数_END(时间字段, 参数)`: 窗口结束时间(不包含)。 - 如 `TUMBLE_START(rowtime, INTERVAL '1' MINUTE)`。 - **时间参数**: - 使用 `INTERVAL` 语法,如 `INTERVAL '10' SECOND`。 --- ### **4. 处理时间(Processing Time)** 若使用处理时间,需定义处理时间字段: ```sql CREATE TABLE user_actions ( user_id INT, action_type STRING, -- 定义处理时间字段 proc_time AS PROCTIME() -- 虚拟列,自动生成 ) WITH (...); ``` 在窗口函数中使用 `proc_time`: ```sql GROUP BY TUMBLE(proc_time, INTERVAL '1' HOUR) ``` --- ### **5. 注意事项** - **水印策略**:事件时间需合理设置水印(如 `WATERMARK FOR rowtime AS rowtime - INTERVAL '5' SECOND`),控制允许的乱序时间。 - **窗口触发时机**:窗口计算结果会在水印越过窗口结束时间时触发。 - **数据迟到处理**:可通过 `ALLOW_LATENESS` 配置(动态表参数)处理迟到数据。 - **资源消耗**:滑动窗口的步长越小,窗口数量越多,计算开销可能增大。 --- ### **完整示例** ```sql -- 1. 定义表结构(事件时间+水印) CREATE TABLE orders ( order_id STRING, amount DOUBLE, order_time TIMESTAMP(3), WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND ) WITH (...); -- 2. 每10分钟统计一次销售额(滚动窗口) SELECT TUMBLE_START(order_time, INTERVAL '10' MINUTE) AS window_start, TUMBLE_END(order_time, INTERVAL '10' MINUTE) AS window_end, SUM(amount) AS total_sales FROM orders GROUP BY TUMBLE(order_time, INTERVAL '10' MINUTE); ``` 通过以上步骤,可以灵活地在 Flink SQL 中实现各类窗口计算。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

G皮T

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值