尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

TDengine时序数据库实战:从数据模型到窗口聚合SQL

发布时间:2026/9/26 7:38:16

资讯中心
01
ARTICLE

TDengine时序数据库实战:从数据模型到窗口聚合SQL

TDengine时序数据库实战:从数据模型到窗口聚合SQL
我在第一次把上千台路由器和一个工厂车间的采集器数据往同一套库里灌的时候用的还是 MySQL。等到监控面板上的查询从平均 300 毫秒慢慢变成 5 秒以上我才意识到不是 SQL 写错了而是这种“每个设备每秒钟上报一条记录”的场景本质上就不该交给通用关系型数据库来扛。后来我换用 TDengine 做时序数据分析算是正式接触了 TSDBTime Series Database这个品类。时序数据库和普通关系型数据库最大的区别在于它的 SQL 基本语法虽然接近标准 SQL但又针对时间序列数据加了大量专有写法比如时间窗口聚合、FILL 补数、连续查询这些特性用得好查询性能和对业务模型的表达能力完全是两个档次。这篇内容适合刚接触时序数据库、准备把设备数据或监控数据落到 TDengine 的读者我会从数据模型讲到高频 SQL 写法再把我踩过的坑一并整理出来。1. 为什么时序场景要单独用一套数据库1.1 MySQL 处理时序数据的真实痛点如果你维护过设备监控或 IoT 平台应该对下面这种表结构很熟悉CREATE TABLE device_metric ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_id VARCHAR(32), ts DATETIME, value FLOAT, INDEX idx_device_ts (device_id, ts) );表建得没什么问题索引也加了。但数据量到了几亿行之后问题就会一个个浮出来。首先是写入压力设备高频上报时MySQL 的 InnoDB 既要更新 B 树索引又要写 WAL 日志每秒几万条的写入能耗很高其次是查询变慢你想看过去 24 小时每个设备每 5 分钟的平均值SQL 写起来并不难难的是它需要对几亿行做范围扫描再分组聚合即使走了索引扫描的记录数也巨大第三是存储膨胀同样一份数据通用数据库往往比时序数据库占用更多空间因为通用行式存储对数值类型压缩得不够狠而时序数据往往具有明显的周期性压缩潜力极大。这就是时序数据库存在的意义。它不追求像 MySQL 那样通用的数据管理能力而是围绕时间戳 设备维度 度量值这三要素做专门优化。1.2 时序数据场景的三个核心特征我习惯把时序场景拆成三个特征来看第一个是写多读少设备 7x24 小时上报写入是源源不断的但查询往往集中在最近一段时间第二个是数据按时间天然有序每个设备的数据追加写入旧数据大概率不会被修改第三个是查询大多围绕时间段和聚合展开比如最近 5 分钟 CPU 使用率的平均值过去 30 天每天的峰值。这三个特征决定了 TSDB 的设计方向用时间分片管理数据、对设备维度建立独立的子表或分区、在 SQL 层提供原生的时间窗口聚合能力。TDengine 做的是更彻底的一步——把表结构直接和数据模型绑定用超级表统一管理同一类型的设备用子表对应每一台具体设备标签列单独存放设备静态属性。理解了这个模型后续所有 SQL 都顺理成章。2. 理解TDengine的数据模型库、超级表、子表与标签2.1 一张子表对应一台设备第一次用 TDengine 的人最容易困惑的就是它多出来的两个概念超级表STABLE和子表CHILD TABLE。我用一句话概括超级表是模板定义了同一类设备的数据结构子表是实例每个设备一张子表。以电表为例所有电表都采集电流、电压、相位这些是数据列而电表所在的站点、分组这些不变属性就是 TAGS标签。具体建表语句是这样-- 先建库后面会细说参数 CREATE DATABASE power KEEP 365 DURATION 10 BUFFER 16 VGROUPS 4; -- 使用 power 库 USE power; -- 创建超级表 meters CREATE STABLE meters ( ts TIMESTAMP, current FLOAT, voltage INT, phase FLOAT ) TAGS ( location BINARY(24), group_id INT );有了超级表之后给具体电表建子表CREATE TABLE d1001 USING meters TAGS (Beijing.China, 2); CREATE TABLE d1002 USING meters TAGS (Shanghai.China, 3);子表的名字建议直接使用设备 ID 或业务编码例如 d1001、meter_0001这样查询时很清楚不需要额外映射。你在 TSDB 里做数据模型设计时先想清楚哪些列是随时间变化的度量值哪些列是设备固有属性这比急着写 SQL 更重要。2.2 标签和数据列为什么设备ID不能当普通列很多从关系型数据库转过来的人有一个习惯就是把 device_id 作为普通列写进超级表再对 device_id 建索引。在 TDengine 里这样做是错误的方向。标签TAGS的价值在于查询时可以像普通 WHERE 条件一样过滤但它对应的是一个分区维度——TDengine 会根据标签把数据分布和索引组织好。设备 ID 作为标签相当于每个设备天然有一张物理上独立的子表时间和存储空间都被切分得更干净。如果你把它当普通列数据会混在一起超级表就退化成了一张普通的大表时间窗口聚合和按设备查询的优化就发挥不出来。所以设计规则很简单设备编号、所属站点、分组 ID、设备型号这类取值有限且不变的信息全部放 TAGS传感器读数、状态值、温度、电压这类随时间变化的信息放普通列。2.3 动态增删列和标签的注意事项TDengine 支持 ALTER TABLE 和 ALTER STABLE 来增删普通列也支持修改标签。但在实际项目里我不建议在数据量上来之后频繁修改超级表结构。原因有两个第一结构变更在分布式环境下会触发元数据同步批量写入期间执行 DDL 可能引起短暂阻塞第二时序表往往已经按列做了压缩加列容易删列和改类型代价不小。所以建表之前尽量把字段一次想清楚不要依赖先上线、后加字段的思维。3. 从建库到第一次查询基础SQL操作3.1 建库参数KEEP、DURATION、VGROUPS 的实际意义建库不只是一个 CREATE DATABASE 动作TDengine 的几个参数会直接影响存储和写入性能。常用参数如下参数作用我的建议KEEP数据保留时长单位天按业务数据生命周期设置过期自动删除DURATION数据落盘文件的时间跨度单位天10 到 30 天比较常见太小会导致小文件过多BUFFER每个虚拟节点内存缓冲大小单位 MB写入频繁可以适当调大VGROUPS数据库分片数分布式集群场景下有效单机通常 4 到 8 够用一个典型建库语句CREATE DATABASE power KEEP 365 DURATION 10 BUFFER 16 VGROUPS 4 PRECISION ms;PRECISION指定时间精度默认毫秒。这一点容易被忽略一旦库建好了精度就不能随便改所以接入前要确认上报数据的时间戳单位是秒、毫秒还是微秒。如果选错了会出现时间戳显示诡异或者查询边界对不上的问题。3.2 数据写入INSERT 的两种用法TDengine 的 INSERT 语法和标准 SQL 有区别推荐使用参数绑定或批量提交尽量不要逐条单插。最简单的写入方式INSERT INTO d1001 VALUES (NOW, 10.3, 220, 0.32); INSERT INTO d1002 VALUES (NOW - 1s, 9.8, 220, 0.28);也可以用自动建表的方式更适合设备新增后第一次上报的场景INSERT INTO d1003 USING meters TAGS (Guangzhou.China, 5) VALUES (NOW, 11.2, 220, 0.35);这条语句在 d1003 不存在时会自动创建子表并写入第一条数据省去了单独建表的环节。但我的建议是如果业务允许最好还是先由程序在初始化阶段批量建好所有子表不要完全依赖写入时的自动建表。因为在高并发下自动建表会带来额外的元数据创建开销容易在设备批量上线时造成写入抖动。写入侧还有一个常见误区现在很多语言的连接器已经支持批量参数绑定一次插多条或一次插多表性能远高于循环单插。不要图编码省事在 for 循环里每次执行一条 INSERT。3.3 基础查询时间过滤是最重要的 WHERE 条件时序数据查询第一原则查询一定要带上时间范围。TDengine 在存储和索引层面对时间主键做了大量优化不带时间范围的查询不是不能跑而是会扫描全量数据性能断崖式下跌。简单查询例子SELECT ts, current, voltage FROM meters WHERE ts 2024-06-01 00:00:00 AND ts 2024-06-02 00:00:00 AND location Beijing.China;注意时间条件用大于等于开始时间小于结束时间这样可以避免同一个时间点被相邻两个查询区间重复统计。另外 TDengine 默认对查询结果不保证顺序如果业务展示需要按时间展示必须显式加ORDER BY ts不要假设底层会返回有序结果。4. 时间窗口、降采样与聚合时序SQL的灵魂4.1 从原始数据到统计结果INTERVAL 的用法时序查询和普通查询最大的不同就在于几乎每一条分析 SQL 都离不开时间窗口。举个例子我想看每个电表每隔 5 分钟的电流平均值直接 GROUP BY 时间戳是不可能的因为每条记录的时间戳都不同。TDengine 提供了INTERVAL关键字把时间轴切成一格一格然后对每个窗口内做聚合SELECT _wstart, _wend, COUNT(*), AVG(current) FROM meters WHERE ts NOW - 1d INTERVAL(5m);_wstart和_wend是窗口开始时间和结束时间TDengine 会自动生成查询结果里代表聚合窗口的边界非常好用。INTERVAL(5m)表示 5 分钟一个窗口时间单位支持秒s、分钟m、小时h、天d。在窗口内你可以用 AVG、SUM、MIN、MAX、COUNT、FIRST、LAST 这些常用聚合函数以及其他更复杂一点的函数。4.2 滑动窗口SLIDING 参数和控制聚合粒度固定窗口有时不能满足需求。比如实时告警场景每 5 分钟统计一次过去 10 分钟的均值如果只用 INTERVAL(10m)那窗口是固定的 10 分钟块统计出来的结果会每隔整整 10 分钟才更新一次。这时候就需要滑动窗口SELECT _wstart, _wend, AVG(current) FROM meters WHERE ts NOW - 1h INTERVAL(10m) SLIDING(1m);这段 SQL 的含义是窗口长度为 10 分钟每 1 分钟滑动一次产出结果。注意滑动步长一般要小于等于窗口长度否则窗口之间会出现空隙。实际项目中这种用法很适合做“近 N 分钟的滑动平均”比在应用层自己维护滑动队列可靠得多也省资源。4.3 PARTITION BY按标签分组再做窗口聚合当你面对的不止一台设备而是成百上千台设备时聚合不能只针对整个超级表做还要拆到每个设备维度。TDengine 的PARTITION BY就是干这个的它和标准 SQL 里的 GROUP BY 有点接近但语义专门配合时序窗口SELECT location, _wstart, AVG(current) AS avg_cur FROM meters WHERE ts NOW - 1d PARTITION BY location INTERVAL(5m);这里每个 location 标签都会独立执行窗口聚合相当于按站点分组后各自出统计结果。PARTITION BY后面可以接标签列也可以接普通列但要注意如果你的查询里同时有PARTITION BY又有非聚合普通列TDengine 对列的选择有限制和标准 SQL 的 GROUP BY 规则不完全一样后面我会单独讲差异。4.4 一个完整案例设备日峰值报表把上面几个概念连起来就能写出一条很实用的报表查询。比如统计过去 7 天每个站点每天的最大电压SELECT location, _wend AS day_end, MAX(voltage) AS max_vol FROM meters WHERE ts NOW - 7d PARTITION BY location INTERVAL(1d);还有更复杂一点的需求按设备维度统计然后按电压值排序取 TOP 10SELECT device_id, _wend, MAX(voltage) AS max_vol FROM ( SELECT tbname AS device_id, ts, voltage FROM meters WHERE ts NOW - 7d ) t PARTITION BY device_id INTERVAL(1d) ORDER BY max_vol DESC LIMIT 10;这里的tbname是 TDengine 内置的伪列代表子表名称通过它可以拿到具体设备 ID。时序 SQL 写多了之后你会发现窗口聚合、按标签分组和时间过滤的组合基本能覆盖 90% 的报表需求。5. 与标准SQL的差异初学时最容易踩的语法边界5.1 排序、去重、别名的几个怪癖TDengine 的 SQL 总体上是 MySQL 风格的但有几个地方和标准 SQL 不太一样。第一查询结果默认不保证顺序必须显式ORDER BY第二GROUP BY的使用范围受限很多需要按设备分组 按时间窗口的场景正确写法是PARTITION BY搭配INTERVAL而不是标准 SQL 里随便GROUP BY时间截断表达式第三聚合查询里如果混进了非聚合列TDengine 会要求你把它写在PARTITION BY后面否则直接报错。关于表字段别名TDengine 的列别名能力比传统关系数据库弱一些。虽然SELECT AVG(current) AS avg_cur能写出别名但有些版本在外层嵌套查询里使用别名时偶尔出现限制。所以我建议在复杂嵌套 SQL 里宁可多写几遍原始表达式减少对别名的过度依赖避免到了线上发现某个版本语法不支持。5.2 时间戳的格式化与特殊时间字面量TDengine 支持使用字符串直接描述时间点比如2024-06-01 10:00:00.000也支持NOW表示当前时间支持NOW - 1h、NOW 5m这种相对时间。这在做近 N 小时查询时很方便不需要在程序里算出具体时间。但有一个坑跨天、跨月、夏令时这种边界情况下用相对时间表达虽然方便其语义是按照数据库服务本地时区计算的。如果你的设备上报时间戳用的是 UTC而 TDengine 服务端设置是中国标准时间那么用NOW - 1d来查最近一天会和你预期差 8 小时。建议在程序侧统一把时间换算好或者在 SELECT 里对时间字段做显示转换不要在多个时区之间来回混用。5.3 数据的删改能力时序数据库不是 OLTP标准 SQL 里对单行进行 UPDATE 是很常见的事。但时序数据库的本质是追加写入模型TDengine 虽然后续版本提供了一些更新能力但我不建议把业务里的改数逻辑建立在 TDengine 上。如果确实需要修正历史数据优先考虑按时间范围删除错误数据再重新写入正确数据而不是跑到线上环境去做高频 UPDATE。删除数据的 SQL 语法DELETE FROM d1001 WHERE ts 2024-06-01 00:00:00 AND ts 2024-06-01 01:00:00;注意 DELETE 一样要带时间条件全表删除这种操作在时序库里要非常谨慎。正常情况下业务数据过期由 KEEP 参数自动清理不需要人为干预删除。6. 实战中的坑与优化建议6.1 时间戳精度毫秒、微秒、纳秒三选一这个坑我印象最深。TDengine 建库时指定时间精度PRECISION ms是毫秒us是微秒ns是纳秒。库一旦建立精度不能修改。如果你的数据源上报的是秒级时间戳建库用毫秒没问题但如果是某些高频采集设备直接上报微秒甚至纳秒时间建库用毫秒会导致数据写入报错或者被自动转成整秒导致大量时间戳重复。我建议在项目启动阶段就让数据接入的同学把时间戳统一成毫秒或者微秒并且由后端统一做转换不要把不同精度的数据直接混入同一套库。6.2 子表数量爆炸和一表一设备原则TDengine 的设计是一张子表对应一台设备这个模型很清晰但如果你有几十万台设备子表数量会非常大。TDengine 能够支撑百万级表但表数量膨胀后元数据管理内存占用会增加。建议在设备数量极大的场景下评估是否需要拆库或者拆超级表比如按业务类型拆成多个超级表而不是把所有乱七八糟的设备类型都塞进一张超级表。我现在一般按业务域来分超级表比如电表、水表、环境传感器分开建而不是建一张万能表。6.3 查询性能的几条实用优化思路时序 SQL 的性能问题很多不是数据库不行而是查询习惯不好。我的三条经验是第一时间范围尽量收紧能查最近 1 小时就不要查最近 7 天TDengine 的时间过滤效率最高第二避免在 WHERE 条件里对时间列做函数处理比如WHERE DATE(ts) 2024-06-01这种写法会阻碍时间索引优化先把时间范围转换成区间条件再查第三选择性查询字段不要习惯性SELECT *尤其是采集列很多的时候列裁剪能显著减少数据扫描量。6.4 关于TDengine 太贵了的一点个人看法网上能看到一些关于 TDengine 收费的讨论实际上去搞清楚哪部分收费很重要。TDengine 的社区版是开源的提供了集群功能之外的大多数核心能力单机或者少量节点的场景完全够用商业版主要针对企业级集群、技术支持、更多高级特性。如果只是中小规模物联网平台社区版完全能撑住。真正需要算清楚成本的是大型集群场景节点数、存储量、支持级别都会影响整体支出。我的建议是先把业务跑通把 SQL 和模型设计做好性能实在不够再考虑商业版本不要一开始就盲选大集群配置。6.5 连续查询把报表计算下沉到数据库最后再说一个我用了觉得非常值得的语法——连续查询Continuous Query, CQ。它能把一批聚合 SQL 变成周期性自动执行的预计算任务结果可以写入一张新表报表查询直接查结果表速度极快。举个例子每 5 分钟自动计算一次电表平均电流并写入汇总表CREATE CONTINUOUS QUERY cq_avg_current ON power BEGIN SELECT _wstart, _wend, AVG(current) AS avg_cur INTO avg_current_by_device FROM meters INTERVAL(5m) END;创建后数据库会按照设定的窗口周期自动运行应用查询平均值时不再需要全量扫原始数据。这个功能对定时报表 实时监控大屏的组合场景特别有用算是 TDengine 自带的一个高阶但写入简单的能力。时序数据库的 SQL 上手难度其实不高难的是从关系型数据库的思维切换到以时间窗口和标签维度为主导的建模方式。我最早做的就是老老实实建库、建超级表、写基础 SELECT后来逐渐加上 INTERVAL、SLIDING、PARTITION BY 和连续查询查询代码变少性能反而提升很多。你如果刚接触 TDengine建议先从一套真实的采集数据练手把每个时间窗口的查询执行一遍感受一下 _wstart、_wend 这些伪列的输出方式。等模型和查询都顺手了再去研究集群部署和调优也不迟。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。