- 数据库
- 时序数据库
- 物联网
- 大数据
- 实时分析
- 云原生
【免费下载链接】tdengine
TDengine is an open source, high-performance, cloud native time-series database optimized for Internet of Things (IoT), Connected Cars, Industrial IoT and DevOps.
导读
本文聚焦 TDengine(v3.4.2.0 起)对 SQL 标准OVER子句与窗口函数的完整支持。窗口函数与 TDengine 传统的时间窗口(INTERVAL、STATE_WINDOW、SESSION等)不同,它保留每一行原始数据、只追加一列计算结果,天然适合移动平均、累计求和、分组排名、相邻行比较等分析场景,也是报表与 BI 工具自动生成 SQL 中非常常见的语法。读完本文,你将掌握窗口函数调用语法、ROWS/RANGE窗口帧规则、默认窗口帧行为、WINDOW命名窗口复用,以及lag/lead、排名类、分布类、取值类等全部窗口函数的用法与限制,并能直接在 TDengine 中运行文中的所有示例。
窗口函数与时间窗口的区别
在 TDengine 中,INTERVAL、STATE_WINDOW、SESSION等时间窗口属于时序数据扩展功能:它们把窗口内的多行聚合坍缩成一行输出。而窗口函数完全不同:
- 它为结果集中的每一行计算一个值;
- 计算时可以看到当前行以及同一窗口内的其他行;
- 它不会把多行合并为一行,原始行全部保留,只在旁边追加一列计算结果。
这个“逐行计算、逐行输出”的特性,决定了窗口函数非常契合移动平均、运行累计、分区排名、与相邻行比较等分析场景。在 TDengine 中,窗口函数调用由两部分组成:
function_name ( [ arguments ] ) OVER ( window_spec | window_name )- window_spec:直接写在
OVER (...)括号内的内联窗口定义; - window_name:通过
WINDOW子句定义的命名窗口的引用。
窗口规范(window_spec)描述“对于当前行,参与计算的行集合如何确定”,包含三个可选部分:
window_spec: [ PARTITION BY expr [, ...] ] [ ORDER BY expr [ ASC | DESC ] [ NULLS FIRST | NULLS LAST ] [, ...] ] [ frame_clause ]- PARTITION BY:按一个或多个表达式把输入结果集拆分为相互独立的分区,每个分区独立计算;省略时整个结果集视为一个分区;
- ORDER BY:在一个分区内按一个或多个表达式排序,支持
ASC/DESC(默认ASC)以及NULLS FIRST/NULLS LAST。排序决定了行号、相邻值、累计区间等对顺序敏感的语义; - frame_clause:窗口帧,在已排序的分区内进一步限定参与当前行计算的行范围,详见下文窗口帧。
窗口函数只能出现在查询块的SELECT列表和ORDER BY中,不允许出现在WHERE、GROUP BY、HAVING、PARTITION BY或帧边界表达式中。若想对窗口计算结果过滤或继续聚合,应把窗口查询写成子查询,再在外层查询中引用其输出列。
窗口帧(Window Frame)
窗口帧在已排序分区内为当前行圈定一个更小的行集合(例如“最近 3 行”“当前行前后各 1 行”“当前行之前的所有行”)。一个帧由帧单元(frame unit)和上下边界组成:
frame_clause: { ROWS | RANGE } frame_extent frame_extent: frame_bound | BETWEEN frame_bound AND frame_bound frame_bound: UNBOUNDED PRECEDING | expr PRECEDING | CURRENT ROW | expr FOLLOWING | UNBOUNDED FOLLOWING- 帧单元(Frame unit):
ROWS:按物理行数界定范围,expr是非负整数行数;RANGE:按ORDER BY取值之间的距离界定范围,CURRENT ROW会包含与当前行排序值相等的所有行(即对等行 peer rows)。
- 省略
BETWEEN的简写形式只指定起始边界,结束边界默认为CURRENT ROW。例如ROWS 10 PRECEDING等价于ROWS BETWEEN 10 PRECEDING AND CURRENT ROW。 - 五种边界:
UNBOUNDED PRECEDING(分区起点)、expr PRECEDING(当前行之前)、CURRENT ROW(当前行)、expr FOLLOWING(当前行之后)、UNBOUNDED FOLLOWING(分区终点)。 - 当帧边界超出分区范围时,只使用分区中实际存在的行。
默认窗口帧
未显式指定帧时,默认帧按如下规则确定:
| 场景 | 默认帧 |
|---|---|
无ORDER BY | 整个分区(ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) |
有ORDER BY,聚合类窗口函数 | RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(从分区起点到当前行及其对等行) |
有ORDER BY,排名/分布/取值类窗口函数 | 这些函数不依赖帧,基于整个分区的有序结果计算 |
RANGE 帧约束
RANGE帧的取值规则比ROWS帧更严格,使用不当会直接报错:
- 带数值或时间偏移(
expr PRECEDING/expr FOLLOWING)的RANGE帧只允许一个ORDER BY表达式,多个排序表达式会报错; - 偏移类型必须与排序列类型匹配:
- 排序列为时间戳类型时,偏移必须使用 TDengine 时长写法,如
10s PRECEDING、1m PRECEDING; - 排序列为数值类型(整型或浮点,不含
UNSIGNED BIGINT)时,偏移必须是非负整数;
- 排序列为时间戳类型时,偏移必须使用 TDengine 时长写法,如
- 不带偏移的
RANGE帧(只有CURRENT ROW、UNBOUNDED PRECEDING/FOLLOWING)按对等语义计算,允许多个ORDER BY表达式,按所有排序键共同确定对等行; - 当排序列是字符串、布尔等无法解释“范围距离”的类型时,带偏移的
RANGE帧会报错。
从源码实现看,帧边界计算在窗口函数算子中完成:winCalcRangeFrameForInt64、winCalcRangeFrameForDouble分别处理整型与浮点排序列的RANGE偏移计算,winFuncFindPeerRange负责按对等语义寻找对等行区间,帧的计算结果由 windowfunc.h 中的SSqlWindowFrameRange(含start/end)承载。
命名窗口(WINDOW 子句)
当一条查询中有多个窗口函数共享同一窗口定义时,可以用WINDOW子句给该定义命名,再通过OVER window_name引用,避免重复书写:
SELECT avg(voltage) OVER win AS ma, max(voltage) OVER win AS mx FROM meters WINDOW win AS (PARTITION BY tbname ORDER BY ts ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) ORDER BY ts;WINDOW子句放在查询语句末尾,可逗号分隔定义多个命名窗口。命名窗口遵循以下规则:
- 命名窗口只在定义它的查询块内有效,不会泄漏到外层或内层查询块;
OVER window_name必须整体引用命名窗口的定义,不能在引用处追加或覆盖PARTITION BY、ORDER BY或帧;- 不支持从一个命名窗口继承另一个命名窗口;
- 引用未定义的窗口名,或同一查询块中重复定义同名窗口,都会返回明确错误。
窗口函数清单
窗口函数分为两类:既有聚合/选择函数加OVER子句即可当窗口函数使用;以及该功能新增的专用窗口函数。在规划层,这两类窗口函数由planner 的逻辑计划统一识别并生成QUERY_NODE_LOGIC_PLAN_WINDOW_FUNC节点(见其中pFunc->pOver的判断逻辑);在执行层,由窗口函数算子负责把输入行按分区物化(winFuncBuildInput中的winInputStoreAppendBlock,必要时落盘到页缓存)、逐行计算帧并产出结果。
聚合与选择类窗口函数
以下既有函数加OVER子句后即可作为窗口聚合使用,作用于当前行的窗口帧。空值处理、数值精度与类型推断遵循其常规聚合语义。
| 函数 | 说明 |
|---|---|
count(expr) | 窗口帧内非空行数 |
sum(expr) | 窗口帧内求和 |
min(expr) | 窗口帧内最小值 |
max(expr) | 窗口帧内最大值 |
avg(expr) | 窗口帧内平均值 |
percentile(expr, p) | 窗口帧内百分位 |
first(expr) | 窗口帧内第一个非空值 |
last(expr) | 窗口帧内最后一个非空值 |
last_row(expr) | 窗口帧内最后一行取值,不忽略空值 |
这些函数都不强制要求ORDER BY:省略时窗口覆盖整个分区;给了ORDER BY但未指定帧时,窗口默认覆盖从分区起点到当前行(含对等行)。
排名类窗口函数
排名类函数依赖排序结果,必须指定ORDER BY,且忽略窗口帧。
| 函数 | 返回类型 | 说明 |
|---|---|---|
row_number() | BIGINT | 当前行在分区内的行号,从 1 开始;对等行也严格递增 |
rank() | BIGINT | 当前行的排名;对等行共享同一排名,后续排名按并列数量跳号 |
dense_rank() | BIGINT | 当前行的排名;对等行共享同一排名,后续排名不跳号 |
分布类窗口函数
分布类函数同样依赖排序结果,必须指定ORDER BY,且忽略窗口帧。
| 函数 | 返回类型 | 说明 |
|---|---|---|
percent_rank() | DOUBLE | 相对排名,(rank - 1) / (分区行数 - 1);分区只有一行时返回 0 |
cume_dist() | DOUBLE | 累积分布,排序值 <= 当前行的行数 / 分区行数 |
取值类窗口函数
取值类函数依赖排序结果,必须指定ORDER BY。
| 函数 | 返回类型 | 说明 |
|---|---|---|
lag(expr [, offset [, default]]) | 与expr相同 | 当前行之前offset行的expr值 |
lead(expr [, offset [, default]]) | 与expr相同 | 当前行之后offset行的expr值 |
first_value(expr) | 与expr相同 | 当前窗口帧内第一行的expr值 |
last_value(expr) | 与expr相同 | 当前窗口帧内最后一行的expr值 |
nth_value(expr, n) | 与expr相同 | 当前窗口帧内第n行的expr值,n从 1 开始 |
lag/lead参数说明:
offset:行偏移,省略时默认为 1。作为窗口函数使用时必须>= 0(offset为 0 表示当前行本身);default:目标行不存在时返回的值,必须与expr类型兼容;省略时返回NULL;- 对
nth_value,n必须>= 1;第n行不存在时返回NULL。
:::notelag/lead也可以不带OVER子句使用,此时按输入结果集的行顺序计算,详见顺序分析函数。两者的参数规则略有差异:不带OVER时offset必须是大于 0 的整数;带OVER时offset可以为 0。 :::
函数注册表(builtins.c)中可以看到这些专用窗口函数的定义细节:例如lag/lead的参数声明为 1~3 个,第二个参数限定整型且取值范围[0, INT64_MAX](对应offset),第三个参数类型与第一个参数对齐(对应default),并带有FUNC_MGT_SQL_WINDOW_FUNC | FUNC_MGT_SQL_WINDOW_ORDER_FUNC分类标记(表明其依赖ORDER BY);row_number/rank/dense_rank声明为 0 参数并输出BIGINT;first_value、percent_rank等也都在同一注册表中定义。执行层则在winFuncCalcDedicated中按FUNCTION_TYPE_ROW_NUMBER、FUNCTION_TYPE_RANK、FUNCTION_TYPE_LAG、FUNCTION_TYPE_FIRST_VALUE等类型分别计算:例如lag的实现是target = row - offset,若目标行落在分区范围内则拷贝该行值,否则返回default或NULL;nth_value在nth超过帧内行数时直接置空。
使用限制
- 窗口函数只能出现在查询块的
SELECT列表和ORDER BY中;出现在WHERE、GROUP BY、HAVING、PARTITION BY、帧边界表达式,或作为标量函数参数、另一个窗口函数定义的一部分时都会报错; - 窗口函数不能嵌套,其参数或窗口定义中不能包含另一个窗口函数;
- 对顺序敏感的窗口函数(排名、分布、取值类)未指定
ORDER BY时直接报错; - 当前版本仅支持批量查询,流式计算中不支持窗口函数;
- 排序时
NULL默认按最小值处理。对等行的输出顺序由ORDER BY决定;需要稳定逐行顺序时请补充显式排序键。
用 OFFSET 跳过“预热期”
计算固定长度窗口的移动指标时,开头几行往往历史数据不足。可以在窗口计算完成后用OFFSET N跳过前 N 行结果。OFFSET在窗口值计算完成之后才生效,不会改变已经算出的窗口值。
从 v3.4.2.0 起,OFFSET N可以不配合LIMIT单独使用:
SELECT v, avg(v) OVER (ORDER BY ts ROWS BETWEEN 9 PRECEDING AND CURRENT ROW) AS ma FROM meters ORDER BY ts OFFSET 9;实战示例
以下示例均基于 TDengine 文档通篇使用的智能电表数据模型:超级表meters,列ts、current、voltage、phase,标签location、groupid。建议先建表并写入少量数据(参考快速上手或对应建表文档)再执行下述 SQL 对照验证。
移动平均:计算每块电表最近 10 个样本的平均电压。
SELECT tbname, ts, voltage, avg(voltage) OVER (PARTITION BY tbname ORDER BY ts ROWS BETWEEN 9 PRECEDING AND CURRENT ROW) AS ma FROM meters ORDER BY tbname, ts;运行累计:计算每块电表的电流累计值。
SELECT tbname, ts, current, sum(current) OVER (PARTITION BY tbname ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total FROM meters ORDER BY tbname, ts;分区排名:在每个分组内按电压降序排名。
SELECT groupid, tbname, voltage, row_number() OVER (PARTITION BY groupid ORDER BY voltage DESC) AS rn, rank() OVER (PARTITION BY groupid ORDER BY voltage DESC) AS rk, dense_rank() OVER (PARTITION BY groupid ORDER BY voltage DESC) AS drk FROM meters;相邻值比较:计算每块电表相邻样本间的电压差。
SELECT tbname, ts, voltage, lag(voltage) OVER (PARTITION BY tbname ORDER BY ts) AS prev_v, voltage - lag(voltage) OVER (PARTITION BY tbname ORDER BY ts) AS delta FROM meters ORDER BY tbname, ts;时间范围窗口:按时间值统计每行之前 10 秒内的电压和。
SELECT tbname, ts, voltage, sum(voltage) OVER (PARTITION BY tbname ORDER BY ts RANGE BETWEEN 10s PRECEDING AND CURRENT ROW) AS sum_10s FROM meters ORDER BY tbname, ts;命名窗口 + 子查询过滤:在子查询中计算移动平均,再在外层过滤出高于移动平均的行。
SELECT tbname, ts, voltage, ma FROM ( SELECT tbname, ts, voltage, avg(voltage) OVER win AS ma FROM meters WINDOW win AS (PARTITION BY tbname ORDER BY ts ROWS BETWEEN 9 PRECEDING AND CURRENT ROW) ) t WHERE voltage > ma ORDER BY tbname, ts;这些语法的正确性与边界行为都有规划层测试覆盖,可参考 planWindowTest.cpp,其中验证了内联OVER (...)、命名窗口OVER win、多个不同窗口函数并存、窗口聚合表达式中带标量运算(如avg(c1 + 1))等场景的解析与规划结果。
延伸阅读
- 时序数据扩展(时间窗口与插值):了解
INTERVAL、STATE_WINDOW、SESSION等与窗口函数互补的窗口聚合能力 - 顺序分析函数 lag/lead:
lag/lead不带OVER子句时的行序语义 - SQL 手册总览:TDengine 全部 SQL 语句与函数的完整索引
- 数据库
- 时序数据库
- 物联网
- 大数据
- 实时分析
- 云原生
【免费下载链接】tdengine
TDengine is an open source, high-performance, cloud native time-series database optimized for Internet of Things (IoT), Connected Cars, Industrial IoT and DevOps.
相关推荐
TDengine 窗口函数完全指南:OVER 子句、窗口帧与 SQL 标准窗口函数实践
TDengine 窗口函数完全指南:OVER 子句、窗口帧与 SQL 标准窗口函数实践 自 v3.4.2.0 起,TDengine 支持 SQL 标准的 OVE
数据库时序数据库大数据物联网云原生TDengine SQL 窗口函数(Window Functions)完全指南:OVER 子句、窗口框架与实战
TDengine SQL 窗口函数(Window Functions)完全指南:OVER 子句、窗口框架与实战 导读 :本文围绕 TDengine(从 v3.4
数据库时序数据库大数据物联网云原生Daft SQL 窗口函数完全指南:语法、窗口帧、实战案例与源码解析
Daft SQL 窗口函数完全指南:语法、窗口帧、实战案例与源码解析 窗口函数(Window Functions)是 Daft SQL 中在"与当前行相关的一组
大数据数据分析数据工程AI 应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考