写在前面昨天还在纸上谈兵今天直接上代码。如果你还没看过第一篇概念篇建议先翻一翻理解超级表、Tag、时间线这些概念今天的实操会更顺畅。一、先唠叨两句第一天 vs 第二天的感受昨天看完 TDengine 的架构设计脑子里只有一个感觉这设计真巧妙。超级表、子表、Tag 这套组合拳把物联网场景下海量同类设备的管理问题解得明明白白。但说实话第一天看完文档心里还是有点虚——概念都懂但真用起来顺不顺Docker 一键部署会不会有坑那个 INTERVAL 时间窗口查询真有那么好用所以今天一早我就给自己定了个目标不看完不罢休不跑通不吃饭。结果真香。整个流程比我想象的顺畅得多从拉镜像到跑完所有查询不到 30 分钟。下面把完整过程分享给你。二、环境准备Docker永远的神如果你还没装 Docker先去官网下载一个过程就不赘述了。TDengine 的官方镜像做得非常友好完全不需要自己编译源码。打开终端确认 Docker 状态docker --version看到版本号就可以开始下一步了。三、TDengine 一键部署官方提供了完整的 Docker 镜像包含服务端和客户端。一条命令搞定docker run -d --name tdengine \ -p 6030:6030 \ -p 6041:6041 \ tdengine/tdengine:latest参数简单解释一下-d后台运行--name tdengine给容器起个名字方便后续操作-p 6030:6030taosd 服务端端口-p 6041:6041RESTful 接口端口等几秒确认容器启动成功docker ps | grep tdengine看到Up状态就说明服务跑起来了。接下来进入容器内部打开 TDengine 的命令行客户端docker exec -it tdengine taos看到taos提示符恭喜你正式进入 TDengine 的世界小坑记录我第一次启动时本地 6030 端口被占用了Docker 报错bind: address already in use。查了一下原来是之前装的某个监控工具占了这个端口。解决方法是换个端口映射比如-p 16030:6030或者把占用端口的进程杀掉。如果你也遇到这个问题别慌换个端口就行。四、创建数据库TDengine 里数据库是数据的顶层容器。我们先建一个专门用来学习的数据库CREATE DATABASE IF NOT EXISTS power; USE power;IF NOT EXISTS是个好习惯防止重复执行时报错。USE power切换到刚建的数据库后续操作都在这个库里进行。五、创建超级表以智能电表为例还记得昨天讲的超级表STable吗今天直接来真的。假设我们在管理一个智能电表系统每块电表定时上报电流current、电压voltage、相位phase。同时每块电表有一些静态属性安装位置location、所属分组groupId。在 TDengine 里采集的数据用普通字段静态属性用 Tag。创建超级表的 SQL 如下CREATE STABLE IF NOT EXISTS meters ( ts TIMESTAMP, current FLOAT, voltage INT, phase FLOAT ) TAGS ( location BINARY(64), groupId INT );拆解一下ts TIMESTAMP时间戳字段必须存在而且是第一张表的第一个字段current、voltage、phase电表的采集数据TAGS关键字后面跟着的是标签字段location和groupId执行完这条 SQL超级表就建好了。可以用下面命令确认SHOW STABLES;看到meters这一行说明创建成功。六、创建子表并写入数据超级表本身不存数据数据都存在子表里。我们为 3 块电表创建子表分别位于不同位置-- 1号电表北京朝阳区A组 CREATE TABLE IF NOT EXISTS d1001 USING meters TAGS (Beijing.Chaoyang, 1); -- 2号电表北京海淀区A组 CREATE TABLE IF NOT EXISTS d1002 USING meters TAGS (Beijing.Haidian, 1); -- 3号电表上海浦东B组 CREATE TABLE IF NOT EXISTS d1003 USING meters TAGS (Shanghai.Pudong, 2);注意语法CREATE TABLE ... USING ... TAGS (...)。USING meters表示这个子表继承超级表meters的结构。接下来写入一些模拟数据。为了演示效果我构造了不同时间点的数据-- 往 d1001 写入数据 INSERT INTO d1001 VALUES (2024-01-01 10:00:00, 10.2, 220, 0.3), (2024-01-01 10:01:00, 10.5, 221, 0.31), (2024-01-01 10:02:00, 10.1, 219, 0.29), (2024-01-01 10:03:00, 10.8, 222, 0.32), (2024-01-01 10:04:00, 10.4, 220, 0.30); -- 往 d1002 写入数据 INSERT INTO d1002 VALUES (2024-01-01 10:00:00, 8.5, 220, 0.25), (2024-01-01 10:01:00, 8.8, 221, 0.26), (2024-01-01 10:02:00, 8.3, 219, 0.24), (2024-01-01 10:03:00, 9.0, 222, 0.27), (2024-01-01 10:04:00, 8.6, 220, 0.25); -- 往 d1003 写入数据 INSERT INTO d1003 VALUES (2024-01-01 10:00:00, 12.0, 223, 0.35), (2024-01-01 10:01:00, 12.3, 224, 0.36), (2024-01-01 10:02:00, 11.8, 222, 0.34), (2024-01-01 10:03:00, 12.5, 225, 0.37), (2024-01-01 10:04:00, 12.1, 223, 0.35);数据写入成功的话会看到类似Affected Rows: 5的提示。小坑记录我第一次写 INSERT 时把时间写成了2024-01-01 10:00:00结果报错invalid timestamp format。TDengine 3.x 支持单引号和双引号包裹时间字符串。如果遇到格式报错优先检查是否为YYYY-MM-DD HH:mm:ss标准格式并确保引号成对出现。七、基础查询SELECT、WHERE、聚合数据有了开始查询。TDengine 的查询语法和 MySQL 非常像上手几乎零成本。1. 查看所有数据SELECT * FROM meters;这条语句会返回所有子表的数据同时自动带上每行数据对应的子表名和 Tag 值。输出大概长这样ts | current | voltage | phase | location | groupid | 2024-01-01 10:00:00.000 | 10.20000 | 220 | 0.30000 | Beijing.Chaoyang | 1 | 2024-01-01 10:01:00.000 | 10.50000 | 221 | 0.31000 | Beijing.Haidian | 1 | ...2. 条件过滤查电压超过 220 的数据SELECT * FROM meters WHERE voltage 220;查某个时间段的数据SELECT * FROM meters WHERE ts 2024-01-01 10:01:00 AND ts 2024-01-01 10:03:00;3. 聚合查询统计所有电表的平均电流、最大电压、最小相位SELECT AVG(current) AS avg_current, MAX(voltage) AS max_voltage, MIN(phase) AS min_phase FROM meters;按位置分组统计SELECT location, AVG(current) AS avg_current FROM meters GROUP BY location;这些查询执行速度都很快——当然现在数据量只有 15 条感受不明显。但 TDengine 的优化就是针对海量数据设计的等数据量上去后优势会非常明显。八、时间窗口查询INTERVAL 大法好这是时序数据库的招牌功能也是我今天最期待的部分。假设我们想看每 2 分钟的平均电流传统数据库里你得写一堆复杂的 GROUP BY但在 TDengine 里一条INTERVAL搞定SELECT _irowts, AVG(current) AS avg_current FROM meters INTERVAL(2m);解释一下INTERVAL(2m)按 2 分钟为一个窗口聚合数据_irowts每个时间窗口的起始时间戳输出结果_irowts | avg_current | 2024-01-01 10:00:00.000 | 10.23333 | 2024-01-01 10:02:00.000 | 10.36667 | 2024-01-01 10:04:00.000 | 10.36667 |如果你想按子表分别统计加上PARTITION BY TBNAMESELECT _irowts, TBNAME, AVG(current) AS avg_current FROM meters PARTITION BY TBNAME INTERVAL(2m);这个查询在实际场景中非常有用比如统计每台设备每 5 分钟的平均 CPU 使用率计算每个传感器每小时的最大温度生成分钟级的流量趋势图小坑记录我第一次写 INTERVAL 查询时忘了加聚合函数直接写了SELECT * FROM meters INTERVAL(2m)结果报错。原来 INTERVAL 必须配合聚合函数使用比如AVG、MAX、COUNT等。这个设计很合理因为时间窗口的本质就是把一段时间内的数据汇总成一个值。九、Tag 过滤精准定位拒绝全表扫描还记得昨天说的 Tag 的作用吗今天来验证一下。查所有属于 A 组groupId 1的电表数据SELECT * FROM meters WHERE groupId 1;查北京地区的电表平均电流SELECT AVG(current) FROM meters WHERE location LIKE Beijing.%;查具体某块电表的数据SELECT * FROM d1001;这里有个很有意思的点通过 Tag 过滤时TDengine 会直接定位到相关的子表而不是扫描所有数据。这在海量设备场景下性能差距是巨大的。想象一下你有 10 万块电表只想查北京市的。如果没有 Tag 机制数据库可能要扫描全部 10 万条时间线有了 Tag它直接跳过不相关的子表查询效率提升 N 倍。十、第一天 vs 第二天学习感受对比维度第一天概念篇第二天实践篇心情设计挺巧妙但真的好用吗真香比想象中顺手难度需要理解新概念超级表、TagSQL 语法接近 MySQL几乎零门槛耗时2 小时看文档30 分钟跑完所有示例惊喜点超级表的架构设计INTERVAL 时间窗口查询太方便了踩坑数0纯看文档2 个端口占用、时间格式最大的感受是TDengine 对 SQL 的支持真的很友好。如果你会 MySQL基本上可以直接上手不需要学习什么新语法。相比之下InfluxDB 的 InfluxQL 和 Flux 确实要多花一些时间适应。十一、遇到的问题和解决方法真实记录问题 1Docker 端口冲突现象docker run报错bind: address already in use原因本地 6030 端口被其他程序占用解决换端口映射或者lsof -i :6030找到占用进程并结束问题 2时间戳格式报错现象INSERT 时提示invalid timestamp format原因时间字符串格式不对或者引号使用有误解决使用YYYY-MM-DD HH:MM:SS标准格式确保用双引号包裹问题 3INTERVAL 忘记加聚合函数现象SELECT * FROM meters INTERVAL(2m)报错原因INTERVAL 必须与聚合函数配合使用解决改成SELECT AVG(current) FROM meters INTERVAL(2m)这三个问题都不难但新手容易踩。记录下来希望能帮你少走弯路。十二、下一步学习计划今天的实践让我对 TDengine 的信心大增但这也只是冰山一角。接下来计划继续深入第三天用 Python 写个程序模拟 1000 块电表并发写入测试写入性能第四天试试数据订阅功能看看怎么把 TDengine 和 Kafka 结合起来第五天研究一下集群部署毕竟生产环境不可能单机跑周末和 InfluxDB 做个简单的对比测试用数据说话十三、写在最后从第一天的纸上谈兵到今天的真刀真枪TDengine 给我的印象越来越好。它不是一个为了国产而国产的产品而是在时序数据库这个赛道上真的做出了自己的特色和优势。超级表的设计、对 SQL 的支持、内置的数据订阅……这些都不是简单的跟随者能做出来的。当然两天的学习还远远不够。数据库这种工具只有在大数据量、高并发的真实场景下才能真正检验它的价值。我打算继续这个系列把学习过程完整记录下来也欢迎你一起交流。