核心目标:掌握 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.ia 比 a.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 的第二大性能分水岭(第一是建模)。本篇的核心收获:
- 先时间后切片再计算 :
WHERE 时间范围→PARTITION BY / INTERVAL→ 函数; - 窗口是时序 SQL 的灵魂 :
INTERVAL/SLIDING/FILL做负荷曲线与补值,STATE_WINDOW做状态统计,SESSION/EVENT/COUNT_WINDOW覆盖更多场景; - 函数三分类 :聚合(AVG/PERCENTILE)、选择(FIRST/LAST/LAST_ROW)、计算(TWA/DIFF/STATECOUNT...),用
_wstart等伪列出报表; - 插值(INTERP)与对齐(ASOF/WINDOW JOIN) 解决"频率不齐"与"设备对比";
- EXPLAIN 与 CLI 取证是每个查询结论的验收标准。
现在你能用 SQL 回答 EMS 的大部分业务问题了。下一步进入 C++ 开发的核心:连接器 API 的完整面貌------连接生命周期、结果集遍历、错误处理。