TDengine C++ 系列(4):时序查询——窗口聚合、插值、连接与时序函数

核心目标:掌握 TDengine SQL 的时序扩展,能写出正确、高效的时间窗口聚合、插值、对齐连接查询,并解释查询执行代价。

前置知识:Part 2(建模)、Part 3(DML);会用 taos CLI;基础 SQL SELECT。

验证环境 :TDengine 3.4.x(3.4.1.6);taosc;C++17;Docker(服务端);数据来自 seed.cpp


0. 本篇问题场景

EMS 的业务查询几乎都是"时间"问题:

  • 负荷曲线:某主变过去 24 小时的有功曲线,按 5 分钟一个点;
  • 电量报表:某线路今天的电量(有功对时间积分),按小时分段;
  • 越限统计:过去一周每天电压越上限的次数、最值;
  • 开关状态:某断路器过去 24 小时"合"了多久、"分"了多久、动作了几次;
  • 设备对比:两台主变同一时段的电流曲线叠在一起看;
  • 数据补齐:温度 30s 一条,报表要 10s 一个点,缺的怎么补?

在 MySQL 里这些查询要么靠应用层反复取数拼装,要么写很重的存储过程。TDengine 把时间窗口、插值、对齐、状态统计做进了 SQL。本篇的目标是让你写出正确且高效的这类查询,并理解每条查询"扫了多少数据、为什么快/慢"。


1. 心智模型:普通 SELECT + 四类时序扩展

TDengine SQL = 标准 SELECT 骨架 + 四类时序扩展:

复制代码
SELECT 列/聚合 FROM 表 WHERE 时间范围 [GROUP 切片]
  ① 时间窗口:INTERVAL / SLIDING / SESSION / STATE_WINDOW / EVENT_WINDOW / COUNT_WINDOW
  ② 标签切片:PARTITION BY tbname / 标签列
  ③ 时序函数:聚合/选择/计算三大类
  ④ 插值与对齐:INTERP、ASOF JOIN / WINDOW JOIN

核心心智:时序查询永远先说"哪段时间"(时间范围),再说"怎么切"(窗口/分组),最后说"算什么"(函数)。时间范围不仅是业务语义,更是性能前提(第 3 节验证)。


2. 最小可运行示例

假设 seed.cpp 已造好数据(3 台设备 1 小时遥测)。在 taos CLI 里逐条执行:

2.1 基础查询与时间裁剪

sql 复制代码
USE ems;
-- 最近 10 分钟 d_0001 的电压(时间范围裁剪 + 排序)
SELECT ts, ua, ub, uc FROM telemetry
  WHERE tbname = 'd_0001'
    AND ts >= NOW - 10m
  ORDER BY ts DESC LIMIT 10;

2.2 时间窗口聚合(负荷曲线)

sql 复制代码
-- 5 分钟一个窗口,求有功均值与最大电流(SLIDING 与 INTERVAL 相同则固定窗口)
SELECT _wstart, avg(power) AS avg_power, max(ia) AS max_ia
FROM telemetry
WHERE ts >= 2026-08-01 00:00:00.000 AND ts < 2026-08-01 01:00:00.000
INTERVAL(5m);

_wstart / _wend / _wduration 是窗口伪列,分别表示窗口起点、终点、时长。

2.3 标签切片(按设备/站点分别统计)

sql 复制代码
-- 每个设备 5 分钟均值(PARTITION BY tbname 按子表切)
SELECT tbname, _wstart, avg(power) FROM telemetry
WHERE ts >= '2026-08-01 00:00:00.000' AND ts < '2026-08-01 01:00:00.000'
PARTITION BY tbname INTERVAL(5m);

-- 按站点聚合(标签列)
SELECT station, avg(power) FROM telemetry
WHERE ts >= '2026-08-01 00:00:00.000' AND ts < '2026-08-01 01:00:00.000'
PARTITION BY station INTERVAL(5m);

2.4 滑动窗口 + FILL 补值

sql 复制代码
-- 10 分钟窗口、5 分钟滑动;缺数据的窗口用前值填充
SELECT _wstart, avg(power) FROM telemetry
WHERE ts >= '2026-08-01 00:00:00.000' AND ts < '2026-08-01 01:00:00.000'
INTERVAL(10m) SLIDING(5m) FILL(PREV);

2.5 状态窗口(遥信:开关状态持续时长)

sql 复制代码
-- 按 state 值变化切窗口:统计每个状态持续了多久
SELECT _wstart, _wend, state, _wduration / 1000 AS seconds
FROM status
WHERE ts >= '2026-08-01 00:00:00.000'
STATE_WINDOW(state);

2.6 插值(低频量升采样)

sql 复制代码
-- 把低频列(如 30s 一条的油温列,本例用 telemetry 已有列 freq 演示)插值到 10s 一个点
-- 注:telemetry 建表列含 freq;若业务有低频列(油温/油位),INTERP 用法相同
SELECT _wstart, interp(freq) FROM telemetry
WHERE ts >= '2026-08-01 00:00:00.000' AND ts < '2026-08-01 00:10:00.000'
RANGE('2026-08-01 00:00:00.000', '2026-08-01 00:10:00.000', EVERY(10s)) FILL(LINEAR);

2.7 对齐连接(两台主变电流对比)

sql 复制代码
-- ASOF JOIN:每行取对方最近时间戳的值对齐
SELECT a.ts, a.ia AS ia_1, b.ia AS ia_2
FROM d_0001 a ASOF JOIN d_0002 b ON (a.ts = b.ts);

3. 机制拆解:查询代价从哪来

3.1 执行计划:EXPLAIN 看真相

sql 复制代码
EXPLAIN SELECT avg(power) FROM telemetry
  WHERE ts >= '2026-08-01 00:00:00.000' AND ts < '2026-08-01 01:00:00.000' INTERVAL(5m);

输出展示计划:qnode 分发到各 vnode → 时间范围裁剪 → 列裁剪 → 预聚合 → 汇总。调查询先 EXPLAIN,这是系列反复强调的取证习惯。

3.2 时间裁剪是性能的第一道闸

  • 带时间范围:vnode 只扫命中时间分区的数据块;
  • 不带时间范围:全量扫描 ,且数据可能横跨多个 days 分区,扫描量指数级上升。

验证方法(写进验收):

sql 复制代码
-- 同一条聚合,分别带/不带时间范围,对比耗时与 EXPLAIN 的扫描范围
SELECT avg(power) FROM telemetry WHERE ts >= '...' AND ts < '...' INTERVAL(5m);
SELECT avg(power) FROM telemetry INTERVAL(5m);   -- 全量,明显更慢

3.3 窗口语义:边界、时区与 FILL

概念 说明
INTERVAL(n) 固定长度窗口,从时间范围起点对齐切割
SLIDING(n) 窗口滑动步长,< INTERVAL 时为滑动窗口(有重叠)
FILL(...) 空窗口填充:NONE(默认不输出)/ NULL / VALUE(v) / PREV / NEXT / NEAR / LINEAR
时区 窗口边界按客户端时区 对齐;跨时区部署时,'按天'窗口落在哪一刻取决于 TSDB_OPTION_TIMEZONE
_wstart/_wend/_wduration 窗口起止与时长伪列,报表必用

FILL 的坑 :首尾窗口没有"前值/后值"时 PREV/NEXT 无值可填(填 NULL 或 NONE);LINEAR 要求窗口内有可插值的两端点,边界窗口可能失败。每个模式都要用边界数据实测(5.2 有实验)。

3.4 函数三分类

分类 函数 EMS 用途
聚合 AVG / SUM / MIN / MAX / COUNT / STDDEV / PERCENTILE / APERCENTILE 统计、分布
选择 FIRST / LAST / LAST_ROW 窗口首末值、最新值
计算 SPREAD(极差)、TWA(时间加权平均)、DIFF、DERIVATIVE、IRATE、ELAPSED、CSUM、MAVG、LEASTSQUARES、TAIL、STATECOUNT、STATEDURATION、UNIQUE 电量、变化率、状态时长

重点函数

sql 复制代码
-- TWA:时间加权平均(电量 = 功率对时间的积分基础)
-- energy_kWh = twa(power)[kW] × 窗口时长[小时] = twa × _wduration[ms] / 3600000
SELECT _wstart, twa(power) * (_wduration / 3600000.0) AS energy_kwh FROM telemetry
  WHERE ts >= '...' AND ts < '...' INTERVAL(1h);

-- STATECOUNT / STATEDURATION:配合状态窗口统计动作次数与时长
SELECT state, statecount(state), stateduration(state) FROM status
  WHERE ts >= '...' STATE_WINDOW(state);

量纲警示 :TWA 积分、DERIVATIVE 等结果的单位取决于时间戳精度与列单位,报表里必须显式换算并附验证(例:1 小时窗口内恒定 100kW,twa(power) 的数值与 kWh 的换算用人工算一遍核对)。

3.5 连接与对齐

连接 语义 场景
普通 JOIN 严格等值时间戳对齐 同采样节奏的数据
ASOF JOIN 左表每行取右表最近 <= 该时间戳的值 不同采样节奏对齐(1s vs 30s)
WINDOW JOIN 时间窗口内对齐 窗口聚合结果对比

tbname 是一个特殊伪列/变量:在超级表查询中代表子表名,配合 PARTITION BY tbname 实现"按设备切分"。


4. ems-lab 工程实战:核心查询集

把 EMS 的核心查询固化为 scripts/ 或文档附录的一组 SQL(均可从 CLI 执行验证):

sql 复制代码
-- Q1 负荷曲线:d_0001 最近 24h 5 分钟有功均值
SELECT _wstart, avg(power) FROM telemetry
  WHERE tbname = 'd_0001' AND ts >= NOW - 24h
  INTERVAL(5m) FILL(PREV);

-- Q2 电量报表:按小时 TWA 积分(energy_kWh = twa × 窗口时长小时;量纲以业务确认为准)
SELECT _wstart, twa(power) * (_wduration / 3600000.0) AS energy_kwh
FROM telemetry
  WHERE ts >= '2026-08-01 00:00:00.000' AND ts < '2026-08-01 06:00:00.000'
  INTERVAL(1h);

-- Q3 越限统计:电压 ua 超 115 的次数与最大越限值,按天
SELECT _wstart, count(*), max(ua) FROM telemetry
  WHERE ua > 115 AND ts >= '2026-08-01 00:00:00.000'
  INTERVAL(1d);

-- Q4 开关状态:state 状态窗口持续时长
SELECT _wstart, _wend, state, stateduration(state) / 1000.0 AS sec
FROM status
  WHERE ts >= '2026-08-01 00:00:00.000'
  STATE_WINDOW(state);

-- Q5 主变对比:d_0001 与 d_0002 电流 ASOF 对齐
SELECT a.ts, a.ia, b.ia FROM d_0001 a ASOF JOIN d_0002 b ON (a.ts = b.ts);

C++ 集成 :这些查询在 C++ 里就是 TdConn::query(sql) + 遍历 taos_fetch_row(Part 5 会给出完整的类型化结果集封装)。先 CLI 验证 SQL,再进 C++ 代码------这条工作流能省下大量排错时间。


5. 失败实验与根因

5.1 不带时间范围的查询

现象:SELECT avg(power) FROM telemetry INTERVAL(5m) 在数据量上来后明显变慢,EXPLAIN 显示全量扫描。

根因:没有时间裁剪,vnode 扫全部时间分区。教训:报表/接口层必须强制时间范围参数,宁可报"缺参"也不允许全量扫。

5.2 FILL 边界行为

sql 复制代码
-- 首尾窗口没有前/后值
SELECT _wstart, avg(power) FROM telemetry
  WHERE ts >= '2026-08-01 00:00:00.000' AND ts < '2026-08-01 00:01:00.000'
  INTERVAL(10s) FILL(PREV);   -- 第一个窗口没有 PREV,填 NULL(默认)

现象:FILL(PREV) 后首窗口仍为 NULL(或 NONE 不输出)。根因:PREV 需要"前一个窗口有值",边界窗口没有。修法 :应用层对边界 NULL 做兜底,或 FILL(VALUE(0)) 显式指定。

5.3 窗口跨天/时区错位

sql 复制代码
-- 客户端时区与服务器不一致时,"按天"窗口的边界落在奇怪时刻
SELECT _wstart, count(*) FROM telemetry
  WHERE ts >= '2026-08-01 00:00:00.000' INTERVAL(1d);

现象:_wstart 是 UTC 零点而非本地零点(如 +08:00 时区)。根因:窗口按客户端时区对齐,未设置时区时用默认。修法 :客户端 taos_options(TSDB_OPTION_TIMEZONE, "GMT-8"),并验证 _wstart 是否符合业务日界。

5.4 ASOF JOIN 的"错位"

现象:ASOF JOIN 结果里某些行的 b.iaa.ts 早很多(右表最近值太旧)。

根因:ASOF 取"<= 时间戳的最近值",如果右表采样稀疏,会取到较旧的值,不会自动限距修法:业务上确认对齐容忍度;需要限距时用 WINDOW JOIN 或在应用层过滤时差。


6. 版本与环境差异

维度 说明
STATE_WINDOW 3.4.2 起支持多状态键;单状态键 3.x 一直可用
EXTERNAL_WINDOW 子查询定义窗口边界,3.4.x 引入,跨版本确认
函数全集 部分函数(如 EWMA、窗口函数)随版本演进,以官方 Functions 页为准
时区 3.x 窗口按客户端时区对齐;TSDB_OPTION_TIMEZONE 客户端设置
_wstart 伪列 3.x 中可用;2.x 差异见官方变更文档

发布前核对:窗口/函数语法以所装版本官方 SQL 文档为准,升级后重跑核心查询集(Q1-Q5)做回归。


7. 测试与验收

7.1 自动化断言

把 Q1-Q5 固化为集成测试断言:

  • Q1:窗口数 = 预期(24h / 5m = 288 个窗口,FILL 后 288 行);
  • Q2:恒定功率窗口的 TWA 值与手算量纲一致(人工核对一次);
  • Q3:越限窗口数与造数脚本的注入越限次数一致;
  • Q4:状态窗口数与 seed_status 的变位次数 + 1 一致;
  • Q5:ASOF 行数 = 左表行数(每行都有对齐值)。

7.2 本篇验收清单

  • 能徒手写出 Q1-Q5 五条核心查询(窗口、切片、状态窗口、TWA、ASOF);
  • 对任意查询能先说"扫哪段时间、怎么切、算什么",再写 SQL;
  • EXPLAIN 验证"带时间范围 vs 不带"的扫描差异;
  • 复现 4 个失败实验(全量扫描、FILL 边界、时区日界、ASOF 错位)并解释根因;
  • 量纲换算(TWA → kWh)完成人工核对并记录验证过程。

8. 常见误区

误区 事实
"时序查询不需要 WHERE 时间" 不带时间范围 = 全量扫描,接口必须强制时间参数
"FILL(PREV) 能填所有空窗口" 首尾窗口没有前/后值,会填 NULL 或不输出
"INTERVAL 窗口按服务器时区切" 按客户端时区对齐,跨时区必须显式设置
"ASOF JOIN 是精确对齐" 是"最近 <= "对齐,可能取到旧值,需要容忍度控制
"TWA 结果就是电量" TWA 是时间加权均值,电量要按量纲换算并人工核对
"窗口越大查询越准" 窗口粒度决定精度与扫描量,先业务定粒度再优化

9. 本篇小结

查询是 TDengine 的第二大性能分水岭(第一是建模)。本篇的核心收获:

  1. 先时间后切片再计算WHERE 时间范围PARTITION BY / INTERVAL → 函数;
  2. 窗口是时序 SQL 的灵魂INTERVAL/SLIDING/FILL 做负荷曲线与补值,STATE_WINDOW 做状态统计,SESSION/EVENT/COUNT_WINDOW 覆盖更多场景;
  3. 函数三分类 :聚合(AVG/PERCENTILE)、选择(FIRST/LAST/LAST_ROW)、计算(TWA/DIFF/STATECOUNT...),用 _wstart 等伪列出报表;
  4. 插值(INTERP)与对齐(ASOF/WINDOW JOIN) 解决"频率不齐"与"设备对比";
  5. EXPLAIN 与 CLI 取证是每个查询结论的验收标准。

现在你能用 SQL 回答 EMS 的大部分业务问题了。下一步进入 C++ 开发的核心:连接器 API 的完整面貌------连接生命周期、结果集遍历、错误处理。

10. 官方资料

相关推荐
junuo1 小时前
10_C++内存分区与动态内存管理
c++·编程语言
科技风向标go1 小时前
2026户外太阳能监控怎么选不踩坑?户外(格行AOV+黑光)、工程(海康大华)、生态(小米萤石)——三大派系技术路线全解析
大数据·人工智能·智能家居·监控·户外安防
欧特克_Glodon1 小时前
OpenCV计算机视觉开发入门与实践<二>:图像噪声与图像处理
c++·图像处理·opencv·计算机视觉
键盘会跳舞2 小时前
C++:函数对象与 std::function 源码级深度拆解——泛型算法的策略内核与可调用对象统一封装
c++·算法·仿函数
环球科讯2 小时前
金融赋能焕新居——建行广东江门分行启动家装补贴季助力消费升温
大数据·人工智能·物联网
LONGZETECH2 小时前
无人机实训高成本痛点解法:虚拟仿真实现 70% 耗材损耗下降
大数据·算法·unity·架构·无人机
2401_865261632 小时前
亦唐科技(YIKTANG):创新引领国产贴片机发展,稳居行业领军地位
大数据·人工智能·物联网
阿部多瑞 ABU2 小时前
私域道德的“立法”与公共秩序的失序——法律-道德异化悖论研究
大数据·人工智能
研华科技Advantech2 小时前
【案例分享】从单点突破到全场景复制:研华赋能药机装备迈向高效合规新范式
大数据·人工智能·边缘计算·工业设备升级