TDengine 快速上手:用 SQL 完成时序数据查询、聚合与多类时间窗口分析
2026/9/13 7:09:43 网站建设 项目流程

TDengine 快速上手:用 SQL 完成时序数据查询、聚合与多类时间窗口分析

【免费下载链接】TDengineHigh-performance, scalable time-series database designed for Industrial IoT (IIoT) scenarios项目地址: https://gitcode.com/GitHub_Trending/tde/TDengine

导读

TDengine 从首个版本起就支持标准 SQL 查询,这大幅降低了时序数据查询与分析的学习成本。本文以智能电表数据模型为例,基于taosBenchmark -y生成的test数据库与meters超级表,在taosshell 中系统演示按条件过滤、排序与限行、按标签/子表聚合,以及时间窗口、滑动窗口、状态窗口、会话窗口、事件窗口、计数窗口和外部窗口等典型查询写法。读完本文,你将掌握一套可直接复制的时序 SQL 查询模板,并能理解这些查询背后的数据模型与实现原理,为后续深入使用 TDengine 的高级查询特性打好基础。

查询能力一览

TDengine 在标准 SQL 之上,针对时序与物联网场景扩展了标签过滤、按设备分片、多种时间窗口、插值与关联查询等能力,形成了一张相对完整的能力全景:

  • 基础检索SELECT/WHERE/ORDER BY/LIMIT,时间范围过滤,正则与CASE等。详见基础查询。
  • 运算符与表达式:算术、比较、逻辑、位运算、JSON 与集合运算等。详见运算符。
  • 聚合与函数COUNT/AVG/MAX等统计聚合,以及选择、数学、时间、时序专用等内置函数。详见内置函数。
  • 标签与分片:按标签过滤;GROUP BY/PARTITION BY/tbname/SLIMIT按设备或标签聚合与限流。详见基础查询。
  • 特色查询INTERVAL/SLIDING时间窗口,以及状态、会话、事件、计数、外部窗口;FILL/INTERP补值与插值。详见特色查询与基础查询(FILL/INTERP)。
  • 关联查询:普通 Join,以及面向时序的 ASOF Join、Window Join。详见关联查询。
  • 窗口函数OVER窗口函数。详见窗口函数。
  • 自定义函数与缓存:用户自定义函数(UDF);最新行读缓存加速。详见自定义函数与读缓存。
  • 执行计划EXPLAIN/EXPLAIN ANALYZE查看执行计划。详见执行计划。

相对通用数据库,查询时序数据时尤其常用:一次查询超级表覆盖多设备用标签缩小设备范围按时间或状态切窗口做降采样与聚合。下文从最常用的过滤、聚合与时间窗口开始上手。

前提条件

请先确认已经完成以下准备:

  1. TDengine 服务已经启动,可以通过taosshell 连接。
  2. 已经在"下载与安装"章节的快速体验中执行过taosBenchmark -y,生成了test数据库和meters超级表。若尚未生成,可先在终端执行taosBenchmark -y

该命令默认写入约 1 亿条记录:10,000 张子表(d0d9999),每张 10,000 条,时间戳范围为2017-07-14 10:40:00.0002017-07-14 10:40:09.999(约 10 秒,采集间隔 1 ms)。下文窗口示例因此使用秒级窗口,而不是分钟级窗口。

数据模型与默认参数(源码佐证)

快速体验生成的数据模型并非随机拼凑,而是由taosBenchmark工具在代码中显式定义的,了解它能帮助你预判每条查询的返回形态:

  • 默认数据库名为test,超级表名为meters,子表前缀为d,子表数量 10,000、每表 10,000 行,相关默认值定义在 bench.h(DEFAULT_DATABASEDEFAULT_TB_PREFIXDEFAULT_CHILDTABLESDEFAULT_INSERT_ROWS)。
  • 超级表结构在 benchCommandOpt.c 的initStable()中定义:三列普通列为current(FLOAT,按正弦函数生成)、voltage(INT,按方波函数生成)、phase(FLOAT,按锯齿函数生成),时间戳主键为ts,采集步长timestamp_step = 1(即 1 ms)。
  • 两个标签列为groupid(INT,取值 1–10)与location(BINARY,长度 24)。
  • location标签的实际取值来自 benchData.c 中的locations[]数组,共 10 个加州城市名,如California.SanFranciscoCalifornia.LosAnglesCalifornia.SanDiego等——这也是本文"按标签过滤"示例中能够命中California.SanFrancisco的原因。

进入 shell 后,切换到test数据库:

USE test;

基本查询

执行下面的 SQL,从超级表meters中查询电压大于 250V 的数据,并按时间倒序返回前 5 行:

SELECT tbname, ts, current, voltage FROM meters WHERE voltage > 250 and tbname = 'd1' ORDER BY ts DESC LIMIT 5;

其中:

  • WHERE voltage > 250用于过滤电压大于 250V 的记录。
  • ORDER BY ts DESC按时间戳倒序返回结果。
  • LIMIT 5只返回前 5 行。

tbname是伪列,表示数据来自哪张子表。返回结果类似如下,具体子表名和数值可能因taosBenchmark版本或随机数据不同而略有差异:

tbname | ts | current | voltage | ======================================================== d1 | 2017-07-14 10:40:09.998 | 11.7984 | 253 | d1 | 2017-07-14 10:40:09.998 | 11.7984 | 253 | d1 | 2017-07-14 10:40:09.998 | 11.7984 | 253 | d1 | 2017-07-14 10:40:09.998 | 11.7984 | 253 | d1 | 2017-07-14 10:40:09.998 | 11.7984 | 253 | Query OK, 5 row(s) in set

WHERE voltage > 250之所以能返回与 d1 相同行,是因为示例数据由正弦、方波等函数叠加随机数生成(见上文initStable()funType配置),电压值会周期性地超过 250V。

按标签过滤

标签适合用来描述设备的静态属性,例如位置和分组。下面的 SQL 查询位于California.SanFrancisco的电表数据:

SELECT tbname, ts, current, voltage, phase FROM meters WHERE location = "California.SanFrancisco" ORDER BY ts DESC LIMIT 5;

也可以同时使用标签条件和普通列条件:

SELECT tbname, ts, current, voltage, phase FROM meters WHERE location = "California.SanFrancisco" AND voltage > 250 ORDER BY ts DESC LIMIT 5;

返回结果类似如下:

tbname | ts | current | voltage | phase | =============================================================== d3737 | 2017-07-14 10:40:09.998 | 11.7984 | 253 | 147 | d8742 | 2017-07-14 10:40:09.998 | 11.7984 | 253 | 147 | d8745 | 2017-07-14 10:40:09.998 | 11.7984 | 253 | 147 | d6259 | 2017-07-14 10:40:09.998 | 11.7984 | 253 | 147 | d6252 | 2017-07-14 10:40:09.998 | 11.7984 | 253 | 147 | Query OK, 5 row(s) in set

结合源码可知,location标签取自locations[]数组的 10 个城市名,因此"按标签过滤"在实际业务中对应的是"按设备地理位置/分组圈定设备集合"这类高频场景。TDengine 会在元数据层面根据标签条件快速定位目标子表,避免全表扫描,这也是超级表一次查询覆盖多设备、同时用标签缩小设备范围这一组合能力的底层支撑。

聚合查询

聚合函数可以帮助你快速计算统计值。下面的 SQL 统计所有电表的平均电压、最大电压和总记录数:

SELECT AVG(voltage), MAX(voltage), COUNT(*) FROM meters;

返回结果中只有一行,表示全量数据的汇总结果:

avg(voltage) | max(voltage) | count(*) | =========================================== 243.9314 | 258 | 100000000 | Query OK, 1 row(s) in set

如果需要按分组统计,可以使用GROUP BY。快速体验数据中的分组标签列为groupId

SELECT groupId, AVG(voltage), COUNT(*) FROM meters GROUP BY groupId ORDER BY groupId;

返回结果会按groupId分组,每个分组一行:

groupId | avg(voltage) | count(*) | ==================================== 1 | 243.9314 | 9800000 | 2 | 243.9314 | 9940000 | 3 | 243.9314 | 9800000 | 4 | 243.9314 | 10040000 | 5 | 243.9314 | 10310000 | ... Query OK, 10 row(s) in set

GROUP BY的结果在未排序时不保证固定顺序。如需按统计值排序,可以继续使用ORDER BY

SELECT groupId, AVG(voltage) AS avg_voltage FROM meters GROUP BY groupId ORDER BY avg_voltage DESC;

这里使用了AS为聚合列起别名avg_voltage,从而可以在ORDER BY中直接引用,属于聚合查询中非常实用的写法。

按子表聚合

如果需要分别统计每个电表,可以使用PARTITION BY tbname。下面的 SQL 按子表统计平均电压,并用SLIMIT只取前几个分片,避免一次返回 10,000 行:

SELECT tbname, AVG(voltage), COUNT(*) FROM meters PARTITION BY tbname SLIMIT 3;

返回结果会按子表切分。分片出现顺序可能因环境略有不同,示意如下:

tbname | avg(voltage) | count(*) | =================================== d0 | 243.9314 | 10000 | d1 | 243.9314 | 10000 | d2 | 243.9314 | 10000 | Query OK, 3 row(s) in set

PARTITION BY会先把超级表数据按指定维度切分,再在每个分片中执行计算。它常用于"每台设备分别统计"的场景。与之配套的SLIMIT用于限制返回的分片数量,和限制行数的LIMIT语义不同,在分片数庞大的场景下(例如本例 10,000 张子表)可以避免结果集过大。

窗口查询

窗口查询用于把时序数据按时间、状态、事件或行数切分,再在每个窗口内计算。快速上手阶段可以先理解下面几类窗口:

  • 时间窗口:按固定时间间隔切分,使用INTERVAL
  • 滑动窗口:在时间窗口基础上设置滑动步长,使用SLIDING
  • 状态窗口:按状态值变化切分,使用STATE_WINDOW
  • 会话窗口:按相邻记录时间间隔切分,使用SESSION
  • 事件窗口:按开始条件和结束条件切分,使用EVENT_WINDOW
  • 计数窗口:按固定行数切分,使用COUNT_WINDOW
  • 外部窗口:由子查询显式给出窗口范围,使用EXTERNAL_WINDOW

窗口机制是 TDengine 区别于通用数据库的核心能力之一:它不是把窗口计算下推到应用层,而是在 SQL 引擎内部完成窗口切分与聚合(从解析器与执行器的模块划分可以看出,窗口相关语法与执行逻辑是查询引擎的一等公民),因此客户端拿到手的就是"按窗口聚合后的结果行",非常利于直接对接监控面板与报表。

下面先体验最常用的几种窗口。示例时间范围覆盖test.meters的全部数据区间。

时间窗口

下面的 SQL 按 1 秒窗口计算每个电表的平均电压:

SELECT tbname, _wstart, _wend, AVG(voltage) FROM meters WHERE ts >= "2017-07-14 10:40:00" AND ts < "2017-07-14 10:40:10" PARTITION BY tbname INTERVAL(1s) SLIMIT 2;

其中:

  • INTERVAL(1s)表示按 1 秒切分时间窗口。
  • _wstart_wend是窗口开始时间和结束时间。
  • PARTITION BY tbname表示每个子表独立做窗口聚合。
  • SLIMIT 2只返回前 2 个分片,避免结果过长。

返回结果中每一行对应一个时间窗口,节选如下:

tbname | _wstart | _wend | avg(voltage) | ========================================================================== d0 | 2017-07-14 10:40:00.000 | 2017-07-14 10:40:01.000 | 244.003 | d0 | 2017-07-14 10:40:01.000 | 2017-07-14 10:40:02.000 | 243.872 | d0 | 2017-07-14 10:40:02.000 | 2017-07-14 10:40:03.000 | 244.261 | d1 | 2017-07-14 10:40:00.000 | 2017-07-14 10:40:01.000 | 244.003 | d1 | 2017-07-14 10:40:01.000 | 2017-07-14 10:40:02.000 | 243.872 | ...

由于快速体验数据的采集间隔为 1 ms、每表 10,000 条,恰好 10 秒,因此 1 秒窗口在每张子表上会切出 10 个窗口,每个窗口聚合 1,000 条记录——这正是"按时间切窗口做降采样"的标准姿势:把高密度的原始采样点压缩成固定粒度的统计值。

滑动窗口

如果希望窗口按更短步长滑动,可以加上SLIDING。下面的 SQL 使用 1 秒窗口,并每 500 毫秒滑动一次:

SELECT tbname, _wstart, AVG(voltage) FROM meters WHERE ts >= "2017-07-14 10:40:00" AND ts < "2017-07-14 10:40:10" PARTITION BY tbname INTERVAL(1s) SLIDING(500a) SLIMIT 1;

返回结果中,_wstart每次前进 500 毫秒,说明 1 秒窗口正在按 500 毫秒步长滑动:

tbname | _wstart | avg(voltage) | ================================================== d0 | 2017-07-14 10:39:59.500 | 243.808 | d0 | 2017-07-14 10:40:00.000 | 244.003 | d0 | 2017-07-14 10:40:00.500 | 244.089 | d0 | 2017-07-14 10:40:01.000 | 243.872 | d0 | 2017-07-14 10:40:01.500 | 244.019 | ...

注意此处时间单位写的是500aa表示毫秒),与INTERVAL(1s)中的s是同一套时间单位体系,常见的还有m(分钟)、h(小时)、d(天)等。滑动窗口适合"需要更细粒度观察趋势,但窗口本身又要足够大以保证统计稳定"的场景,比如 1 小时窗口每 5 分钟滑动一次用于滚动监控。滑动步长必须小于等于窗口宽度。

填充缺失窗口

窗口中没有数据时,可以使用FILL指定填充方式。下面的 SQL 使用前一个非空值填充缺失窗口:

SELECT _wstart, _wend, AVG(voltage) FROM d0 WHERE ts >= "2017-07-14 10:40:00" AND ts < "2017-07-14 10:40:10" INTERVAL(1s) FILL(prev);

本章示例数据较连续,下面结果主要展示FILL查询的输出结构。若某个窗口没有数据,FILL(prev)会使用前一个非空窗口的结果填充:

_wstart | _wend | avg(voltage) | ================================================================ 2017-07-14 10:40:00.000 | 2017-07-14 10:40:01.000 | 244.003 | 2017-07-14 10:40:01.000 | 2017-07-14 10:40:02.000 | 243.872 | 2017-07-14 10:40:02.000 | 2017-07-14 10:40:03.000 | 244.261 | 2017-07-14 10:40:03.000 | 2017-07-14 10:40:04.000 | 243.479 | 2017-07-14 10:40:04.000 | 2017-07-14 10:40:05.000 | 243.972 | ...

FILL的取值除了prev(前一个非空值)外,还包括none(不填充)、null(填 NULL)、value(填充常量)、linear(线性插值)、next(后一个非空值)等,实际业务中常用于补全采样缺失导致的窗口空洞,保证下游趋势图连续。

状态窗口

状态窗口适合按状态变化切分数据。下面的 SQL 根据电压是否处于 240V 到 250V 的范围划分窗口:

SELECT _wstart, _wend, COUNT(*), CASE WHEN voltage >= 240 AND voltage <= 250 THEN 1 ELSE 0 END AS status FROM d0 WHERE ts >= "2017-07-14 10:40:00" AND ts < "2017-07-14 10:40:03" STATE_WINDOW( CASE WHEN voltage >= 240 AND voltage <= 250 THEN 1 ELSE 0 END ) LIMIT 4;

返回结果中,相邻窗口的status不同,表示状态发生了变化:

_wstart | _wend | count(*) | status | ===================================================================== 2017-07-14 10:40:00.000 | 2017-07-14 10:40:00.001 | 2 | 0 | 2017-07-14 10:40:00.002 | 2017-07-14 10:40:00.002 | 1 | 1 | 2017-07-14 10:40:00.003 | 2017-07-14 10:40:00.006 | 4 | 0 | 2017-07-14 10:40:00.007 | 2017-07-14 10:40:00.014 | 8 | 1 | Query OK, 4 row(s) in set

状态窗口的典型价值在于:你可以在一条 SQL 里同时输出"状态区间"和"该状态下持续了多久、有多少条记录",直接用于计算设备处于正常/异常状态的时长占比,而无需在应用层自行做游标式扫描。

会话窗口

会话窗口适合按相邻记录的时间间隔切分数据。下面的 SQL 将间隔不超过 30 秒的数据归为同一个会话。由于d0中相邻点间隔为 1 ms,整段数据会落在同一个会话窗口中:

SELECT _wstart, _wend, COUNT(*) FROM d0 WHERE ts >= "2017-07-14 10:40:00" AND ts < "2017-07-14 10:40:10" SESSION(ts, 30s);

返回结果如下:

_wstart | _wend | count(*) | ============================================================= 2017-07-14 10:40:00.000 | 2017-07-14 10:40:09.999 | 10000 | Query OK, 1 row(s) in set

会话窗口的语义是:只要相邻两条记录的时间间隔不超过阈值(此处 30s),就认为它们属于同一次"会话";一旦出现超过阈值的间隔,则开启新的会话窗口。它非常适合分析用户在线会话、设备连续运行段等场景。如果想看到多个会话切分的效果,可以构造间隔较大的数据再验证。

事件窗口

事件窗口适合"满足开始条件后开窗,满足结束条件后关窗"的场景。例如电压升高到某个阈值后开始观察,降回另一个阈值后结束观察:

SELECT _wstart, _wend, COUNT(*) FROM d0 WHERE ts >= "2017-07-14 10:40:00" AND ts < "2017-07-14 10:40:10" EVENT_WINDOW START WITH voltage >= 250 END WITH voltage < 245 LIMIT 4;

返回结果中,每一行表示一次从开窗条件到关窗条件之间的事件区间:

_wstart | _wend | count(*) | ============================================================= 2017-07-14 10:40:00.000 | 2017-07-14 10:40:00.001 | 2 | 2017-07-14 10:40:00.004 | 2017-07-14 10:40:00.005 | 2 | 2017-07-14 10:40:00.006 | 2017-07-14 10:40:00.011 | 6 | 2017-07-14 10:40:00.016 | 2017-07-14 10:40:00.017 | 2 | Query OK, 4 row(s) in set

事件窗口与状态窗口的区别在于:状态窗口切分的依据是"状态值的连续相同段",而事件窗口的窗口边界由显式的开始/结束条件独立驱动——例如"电压冲高越过 250V 开始记录,跌破 245V 结束记录",天然贴合故障波形分析、设备启停过程跟踪等场景。

计数窗口

计数窗口适合按固定行数分组。下面的 SQL 每 100 行切分一个窗口:

SELECT _wstart, _wend, COUNT(*) FROM d0 WHERE ts >= "2017-07-14 10:40:00" AND ts < "2017-07-14 10:40:10" COUNT_WINDOW(100) LIMIT 5;

返回结果中,每个窗口最多包含 100 行数据:

_wstart | _wend | count(*) | ============================================================= 2017-07-14 10:40:00.000 | 2017-07-14 10:40:00.099 | 100 | 2017-07-14 10:40:00.100 | 2017-07-14 10:40:00.199 | 100 | 2017-07-14 10:40:00.200 | 2017-07-14 10:40:00.299 | 100 | 2017-07-14 10:40:00.300 | 2017-07-14 10:40:00.399 | 100 | 2017-07-14 10:40:00.400 | 2017-07-14 10:40:00.499 | 100 | Query OK, 5 row(s) in set

可以看到,当采集频率固定(1 ms/条)时,计数窗口的_wstart/_wend恰好按 100 ms 推进,_wend取的是窗口内最后一条记录的时间戳。计数窗口适合"不以时间为准、而以数据量为准"的批处理,例如每处理 1,000 条记录做一次汇总。

外部窗口

外部窗口更适合用已有事件表、排班表或维护计划定义窗口范围。下面的 SQL 先用子查询显式给出两个窗口边界,再在d0表中分别计算每个窗口内的平均电压:

SELECT _wstart, _wend, AVG(voltage) FROM d0 EXTERNAL_WINDOW ( (SELECT CAST("2017-07-14 10:40:00" AS TIMESTAMP) AS ws, CAST("2017-07-14 10:40:01" AS TIMESTAMP) AS we UNION ALL SELECT CAST("2017-07-14 10:40:01" AS TIMESTAMP), CAST("2017-07-14 10:40:02" AS TIMESTAMP) ORDER BY ws) w );

返回结果中,窗口边界来自子查询,而不是由INTERVAL自动切分:

_wstart | _wend | avg(voltage) | ================================================================= 2017-07-14 10:40:00.000 | 2017-07-14 10:40:01.000 | 244.206793 | 2017-07-14 10:40:01.000 | 2017-07-14 10:40:02.000 | 244.367632 | Query OK, 2 row(s) in set

也可以用INTERVAL子查询生成有序窗口,效果类似:

SELECT _wstart, _wend, AVG(voltage) FROM d0 EXTERNAL_WINDOW ( (SELECT _wstart, _wend FROM d0 WHERE ts >= "2017-07-14 10:40:00" AND ts < "2017-07-14 10:40:02" INTERVAL(1s)) w );

外部窗口的价值在于窗口边界完全由业务定义:比如生产班次表(早班 08:00–16:00、中班 16:00–24:00)、设备维护窗口、电价时段等,都可以作为"外部窗口"的来源,让聚合统计与业务日历严格对齐。

常用查询模式

下面是几个快速上手阶段常用的查询模式。

查看某张子表的最新数据:

SELECT * FROM d0 ORDER BY ts DESC LIMIT 1;

查看某个位置的电表记录数:

SELECT location, COUNT(*) FROM meters GROUP BY location ORDER BY location;

查看每个电表的最大电压(用SLIMIT限制返回分片数):

SELECT tbname, MAX(voltage) FROM meters PARTITION BY tbname SLIMIT 3;

查看某段时间内的电流平均值:

SELECT AVG(current) FROM meters WHERE ts >= "2017-07-14 10:40:00" AND ts < "2017-07-14 10:40:10";

这四个模式分别对应四类最常用的时序诉求:查最新状态、按维度统计分布、按设备独立聚合、按时间段聚合——几乎可以组合出日常监控场景 80% 以上的查询。如果希望深入理解这些查询在引擎内部如何执行,可以在 SQL 前加上EXPLAIN查看执行计划(如是否命中时间过滤、是否走了标签裁剪、窗口如何下推),相关说明见执行计划。

继续阅读

本章只覆盖快速上手阶段最常用的查询方式。更多高级查询能力,请继续阅读以下文档:

  • 基础查询:SELECT语句语法、常用子句与查询示例
  • 运算符:算术、位运算、比较、逻辑等运算符
  • 内置函数:内置函数分类、语法与使用说明
  • 特色查询:时序数据特有的查询功能(多种窗口等)
  • 关联查询:关联查询(JOIN)概念、类型、语法与限制
  • 窗口函数:OVER子句与 SQL 标准窗口函数说明
  • 自定义函数:创建、管理与调用用户自定义函数(UDF)
  • 读缓存:通过CACHEMODEL缓存子表最近数据,加速LAST/LAST_ROW查询
  • 执行计划:使用EXPLAIN/EXPLAIN ANALYZE查看查询执行计划与运行期指标

【免费下载链接】TDengineHigh-performance, scalable time-series database designed for Industrial IoT (IIoT) scenarios项目地址: https://gitcode.com/GitHub_Trending/tde/TDengine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询