第1章:你不知道的TDEngine窗口查询——开局就不简单
先别急着翻白眼,提到时间窗口查询,可能你脑子里立马浮现的就是那些常规套路:GROUP BY time_interval
、FIRST()
、LAST()
,再加上点AVG()
和MAX()
,一锅端。
可是,如果你用TDEngine还只是停留在这些层面,那说实话——你用的是一半的TDEngine,甚至不及格。
今天我们就不讲那些你能从官网文档抄到的东西,而是来把TDEngine时间窗口查询这块,彻底挖一挖,翻一翻,不光看它怎么用,还得琢磨出怎么用得漂亮。
1.1 时间窗口的本质:不是GROUP而是TIME ALIGN
很多人误以为TDEngine的窗口查询只是SQL里GROUP BY的变种。错了,TDEngine的窗口查询背后其实是一个时间对齐模型,它会根据你指定的窗口步长、起点,对原始数据进行自动分桶。
举个简单例子:
SELECT COUNT(*) FROM temperature_table INTERVAL(10m);
你以为只是10分钟分组?不对,其实它在做的是:从UNIX EPOCH(1970年1月1日)或你的查询起点,每隔10分钟生成一个桶,再把每个数据点丢到对应桶里。想清楚这一点,很多问题自然就通透了。
1.2 窗口查询的完整语法骨架
我们要玩得溜,就得先掌握TDEngine窗口查询的完整句式结构。
SELECT [agg_funcs] FROM [stable/table]
[WHERE condition]
INTERVAL(step_time)
[SLIDING(step)]
[FILL(null | previous | 0)]
[ALIGN(starting_point)]
[GROUP BY tags]
这几个关键词你得明白它们的作用和组合方式,不然后面进阶玩法全玩不转。
-
INTERVAL(x)
:这个是窗口的宽度。 -
SLIDING(x)
:这个是滑动步长,默认跟INTERVAL
一样,但你可以自定义成任意值,实现滑窗效果。 -
FILL
:控制窗口里没数据的桶的填充值。 -
ALIGN
:决定窗口从哪一刻开始划分。
现在我们直接从实战讲起,不空讲任何一个参数。
1.3 INTERVAL + SLIDING:滑窗玩法的关键
实例:按15分钟聚合,每5分钟滚动一次
SELECT AVG(temperature)
FROM temperature_table
WHERE ts >= now - 2h
INTERVAL(15m) SLIDING(5m);
这就是一个滑动窗口聚合查询,每个15分钟窗口会向前滑动5分钟生成一个新的结果。
也就是说,如果你的时间范围是2小时,那你将会得到:
-
00:00 ~ 00:15
-
00:05 ~ 00:20
-
00:10 ~ 00:25 ...
这种玩法特别适合用于实时数据的趋势监测,尤其是在工业设备监控和金融K线生成中非常常见。
再来一个带点刺激的:
实例:动态监控平均温度变化率
SELECT (AVG(temperature) - LAG(AVG(temperature))) / SLIDING(5m) AS rate
FROM temperature_table
WHERE ts >= now - 1h
INTERVAL(15m) SLIDING(5m);
这段SQL可不常规,它是通过窗口差值做了一个时间上的衍生指标计算,常用于预测模型特征提取。
注:目前TDEngine原生还不支持LAG这种窗口函数,这种写法需要通过客户端拆解计算实现。
第2章:别让“缺口”搞乱你的窗口——深入FILL与ALIGN的奥秘
TDEngine 的窗口查询,很多时候看起来没毛病,可结果就偏偏不对,这种情况十有八九都是 FILL
和 ALIGN
在搞事情。
我们一个个来抠细节。
2.1 FILL:给数据“打补丁”的神技能
默认情况下,如果某个窗口内没有任何数据点,那这一段时间的桶压根就不会出现在查询结果里。
但是你做可视化,或做统计模型训练时,突然断档,不仅图表变形、聚合数出错,严重点可能直接导致你的报警系统漏报或者误报。
所以,FILL这个参数你必须会用。
FILL(null)
最简单粗暴的做法——什么都不补,窗口没数据就让它空着:
SELECT AVG(temperature)
FROM temperature_table
WHERE ts >= now - 1h
INTERVAL(10m) FILL(null);
结果中,你会看到有些10分钟时间段是空的。这个策略适合数据科学用途,但不适合报表可视化。
FILL(0) 或 FILL(previous)
-
FILL(0)
:补零,这种方式简单粗暴,适合电量统计、总线监控这类场景。 -
FILL(previous)
:拿前一个非空值来填补空位,适合做趋势对比、图表平滑处理。
实战案例:气压监测不中断
SELECT MAX(pressure)
FROM station_data
WHERE ts >= now - 1d
INTERVAL(30m) FILL(previous);
用这个你就可以确保图表上的曲线不会有断点。
⚠️注意:FILL(previous)
只在时间序列有“连续性预期”的情况下使用,别滥用。
2.2 ALIGN:对齐时间才是硬道理
你是不是遇到过这种怪事:你明明查的是now - 1h
的数据,结果窗口的起点却是一些奇怪的时间,比如13:02:30
?
问题就出在ALIGN
参数没设置。
ALIGN的作用
ALIGN(ts)
指定你的窗口应该从哪个时间点开始对齐切分。
SELECT COUNT(*)
FROM login_table
WHERE ts >= now - 6h
INTERVAL(1h) ALIGN(00:00);
上面的语句表示:窗口从每天的00:00点开始切分,每1小时一个桶。
这样你在做日报时,结果就非常整齐划一。
使用ALIGN(now)实时对齐
你也可以让窗口以当前时间对齐:
SELECT SUM(energy)
FROM device_power
WHERE ts >= now - 4h
INTERVAL(1h) ALIGN(now);
这就意味着每次查询结果的窗口都会动态对齐到此刻。
第3章:超级表的窗口聚合——你以为的简单,其实暗藏玄机
讲真,TDEngine的“超级表(Super Table)”机制可以说是它最酷也最容易踩坑的一部分。尤其是当你试图用窗口聚合搞多个子表汇总时,如果你不留个心眼,结果就会比你想象的还乱——而且还是悄悄地乱。
3.1 什么是超级表的窗口聚合?
场景设定:假如你有一张超级表 meters
,它包含了成百上千个子表(比如一栋楼的不同电表):
CREATE STABLE meters (ts TIMESTAMP, voltage FLOAT, current FLOAT) TAGS (location BINARY(64), phase INT);
然后你基于这张超级表创建了几千个表:
CREATE TABLE meter001 USING meters TAGS ("A栋1单元", 1);
CREATE TABLE meter002 USING meters TAGS ("A栋2单元", 1);
...
现在你想统计这栋楼里每个相位(phase)的平均电压变化趋势,每15分钟为一个窗口,每小时滚动一次。
很多人上来就写:
SELECT AVG(voltage)
FROM meters
WHERE ts > now - 1d
INTERVAL(15m) SLIDING(1h)
GROUP BY phase;
然后你发现结果“怪怪的”:
-
有的时间段缺数据;
-
某些phase没出现在结果里;
-
数据数量不稳定。
问题在哪?是数据分布不均、窗口未对齐、子表未完整聚合,等等统统叠加。
3.2 拆分对齐:稳准狠的三步法
要想搞定超级表聚合查询,建议按以下思路来分步处理:
第一步:确认数据分布是否均衡
你得先了解每个子表是否在你关注的时间段内都有数据。
SELECT tbname, COUNT(*)
FROM meters
WHERE ts >= now - 1d
GROUP BY tbname;
如果你看到很多是0,那说明不是查询语句的问题,而是压根没数据。
第二步:强制时间对齐 + 填补空缺
这是窗口查询里必须加的参数组合:
SELECT AVG(voltage)
FROM meters
WHERE ts >= now - 1d
INTERVAL(15m) SLIDING(1h)
FILL(previous) ALIGN(00:00)
GROUP BY phase;
有了这个组合,结果才是完整、有规律的窗口段——对BI系统来说简直不要太重要。
第三步:GROUP BY顺序 VS TAG使用
如果你 GROUP BY 的字段不是 TAG,而是某个字段值,那在超级表场景下可能会出错。
错误示范:
-- voltage不是tag,这样写在超级表上毫无意义
SELECT AVG(voltage) FROM meters INTERVAL(15m) GROUP BY voltage;
正确做法要结合超级表的 TAG 字段:
SELECT AVG(voltage)
FROM meters
WHERE ts >= now - 12h
INTERVAL(15m)
GROUP BY location;
一句话:TDEngine 的 GROUP BY 只能对 TAG 字段起到分组汇总作用!
小贴士:如果你对查询结果不放心,建议输出 tbname
一列,对比一下具体是哪些子表返回了数据。
SELECT tbname, AVG(current)
FROM meters
WHERE ts >= now - 3h
INTERVAL(10m)
GROUP BY tbname;
这个技巧在调试复杂聚合时非常有用,别忽视它!
第4章:窗口滑动别只会“匀速跑”——不规则窗口 + 嵌套窗口查询解法
时间窗口查询的“滑动”功能(SLIDING
)非常强大,但很多时候我们用得还不够花——绝大多数人都在用等间距滑窗,比如每小时滑动一次、每五分钟滑动一次。
可现实数据分析可没那么规整,有时候你就得:
-
定义“跳跃式窗口”:窗口本身间隔固定,但不连续滚动;
-
在同一查询中对多个时间粒度做对比(比如小时内再聚合分钟趋势);
-
甚至在子查询中对滑动窗口进行再聚合,这个时候你需要嵌套窗口查询。
我们下面一个个来拆。
4.1 不规则滑动窗口:跳着来照样聚合
场景:每小时数据只取前15分钟做统计
你希望每小时内只分析第一个15分钟窗口数据,后面的不看。
你以为只能用 INTERVAL(1h)
?其实用 SLIDING 更灵活:
SELECT AVG(temperature)
FROM temperature_table
WHERE ts >= now - 6h
INTERVAL(15m) SLIDING(1h);
别小看这个用法,它其实实现的是“每小时一个窗口,但窗口长度只有15分钟”,跳窗分析就这么玩。
场景升级:仅分析奇数小时数据
这种需求就得靠业务逻辑去组合:
SELECT * FROM (SELECT AVG(voltage) AS v, ts FROM power_data WHERE HOUR(ts) % 2 = 1 INTERVAL(30m) SLIDING(1h)
) t;
注意这里其实是靠 SQL 中的时间函数加窗口语法联合筛选达成。
4.2 窗口 + 窗口?可以!嵌套查询了解下
TDEngine 虽然目前对窗口函数嵌套支持有限,但我们可以通过子查询的方式实现“窗口套窗口”的效果。
案例:先按10分钟滑动平均,再按小时汇总滑动平均走势
SELECT AVG(avg_temp)
FROM (SELECT AVG(temperature) AS avg_temp FROM sensors WHERE ts >= now - 2h INTERVAL(10m) SLIDING(5m)
) t
INTERVAL(1h);
这个操作在数据科学和模型训练中非常常用,尤其是在构建多级平滑曲线、分层特征提取时效果拔群。
⚠️ 注意:当前 TDEngine 在子查询中不能嵌套
GROUP BY tags
,需要按单表/结构规避。
4.3 JOIN + 窗口:动态对齐不同类型数据
有没有遇到过这种情况:你有两个超级表,一个记录设备状态(开关变化),一个记录温度,你想知道“每次设备开的时候,接下来的5分钟内温度均值”?
这时候我们可以用 JOIN + 窗口来干:
SELECT s.ts, d.status, AVG(s.temperature)
FROM temperature_table s
JOIN status_table d ON s.ts BETWEEN d.ts AND d.ts + 5m
WHERE d.status = 1
GROUP BY d.ts;
或者你用窗口套件配合写法:
SELECT AVG(temp)
FROM (SELECT s.temperature AS temp, d.ts AS anchor FROM s JOIN d ON s.ts BETWEEN d.ts AND d.ts + 5m WHERE d.status = 1
) t
INTERVAL(5m) ALIGN(anchor);
是不是已经闻到高级数据处理的味道了?
第6章:性能,才是生产级窗口查询的底线
很多人写窗口查询时从不关心性能,直到查询开始“转圈圈”,然后盯着后台日志一脸懵逼。其实,TDEngine 的窗口查询本身是非常高效的,但前提是你避开以下几个常见陷阱。
这一章,我们就来拆开这些“隐形炸弹”,再教你几招能让你的窗口查询飞起来的优化套路。
6.1 避雷第一条:INTERVAL步长别太小!
你想象一下,如果你执行的是:
SELECT AVG(temp)
FROM data
WHERE ts >= now - 1h
INTERVAL(1s);
意味着什么?意味着 TDEngine 要在一小时内切出 3600个桶!
这不是查询,这是压力测试……
建议:INTERVAL 不要低于5s,常用为 10s ~ 1min。
如果你必须这么小的粒度,那就考虑先用大粒度做聚合,做成中间表缓存再查。
6.2 SLIDING步长≠精度
很多同学以为“我把 SLIDING 改成 1s,就能实时得出结果”,这是错的。
SLIDING 控制的是窗口生成频率,不是数据采样频率。
你该关心的是:有没有数据进入这个窗口,而不是滑了多少次。
正确理解这个参数,有助于你把查询控制在“刚刚好”的复杂度上。
6.3 GROUP BY TAG = 分片执行!
TDEngine 的超级表分组其实是一种“分片执行”。
如果你写了:
SELECT AVG(v) FROM stable_name INTERVAL(1m) GROUP BY device_id;
那系统会为每个 device_id
子表执行一次完整查询,再汇总。
所以如果你有 10w 个设备,恭喜你,触发了 10w 次执行路径。
解决方案:分批查询、分页聚合、或建立预聚合表(物化视图)。
6.4 LIMIT和TOPN一定要加
执行窗口查询的时候,如果你只是临时查数据,那请加上 LIMIT,比如:
SELECT AVG(x) FROM y INTERVAL(10m) LIMIT 100;
或者你只关心某几个设备的:
SELECT TOP(10) device_id, AVG(x) FROM y GROUP BY device_id;
没有 LIMIT = 默认全表聚合 = 查询崩溃边缘试探。
6.5 提前准备的数据结构优化
你别以为 SQL 优化就够了,其实 TDEngine 在建表时就能埋下优化伏笔:
-
建表时设置合适的
COMP
压缩算法,对时序聚合性能有帮助; -
使用合理的 TAG 作为查询条件,可以极大减少 I/O 扫描成本;
-
利用
TTL
机制清理旧数据,避免“历史包袱”拖慢全局。