☰
从关系库到TDengine:时序数据建库建表与写入查询实践
2026/11/4 23:48:30 网站建设 项目流程

1. 从关系库迁到 TDengine:我为什么做这个决定

TDengine 时序数据库这个名字,我第一次接触是在一个设备数据采集项目里。当时整套系统跑在 MySQL 上,单表每天新增两千万行左右的设备读数,三个月后查询开始肉眼可见地变慢,磁盘也快撑不住了。那段时间我几乎每周都要去翻慢查询日志,加索引、改分表、删历史数据,折腾到最后发现,关系型数据库处理这种"写多读少、按时间聚合"的数据,本质上就是在用螺丝刀砸钉子——能干,但特别费劲。后来我把这套采集链路整体换到了 TDengine 上,同样的数据量,写入吞吐上去了,磁盘占用下来了,按时间窗口做聚合查询的响应时间也从十几秒降到几百毫秒。

这篇东西写给三类人看:一类是手里正攒着海量设备数据、还没想好往哪存的人;一类是已经用了时序数据库但只是当个"能写进去的库"在用、没真正发挥它能力的人;还有一类是刚接手别人搭好的 TDengine 环境、需要快速搞清楚建库建表逻辑和常见故障排查的人。我不会讲太多底层存储引擎的论文式内容,主要讲我实际怎么建库、怎么设计表结构、怎么写数据、怎么查数据,以及中间踩过的那些坑。文中涉及的命令和参数都以 3.x 版本为准,不同版本之间参数名和语法有差异,你自己动手前最好先确认一下手头的版本号。

1.1 时序数据把关系库压垮的那三个月

先说说我遇到的具体场景,这样后续讲设计取舍时你能对上号。这套系统的数据来源是几百台设备,每台设备每隔几秒上报一条记录,字段包括时间戳、电流、电压、功率、温度、运行状态等。听起来不多,但设备数量乘上采集频率,一天下来就是千万级。这些数据的访问特征非常鲜明:几乎不更新、不删除单条,永远是按时间追加;查询基本都带时间范围,而且大概率还要做聚合,比如"最近一小时每台设备的平均电流""昨天全天每小时的最大功率"。

关系库在这种场景下有三个绕不过去的坎。第一是写入放大的问题,每张表都要维护索引,每插入一行 B+ 树都要调整,数据量一大写入延迟就上来了。第二是单表容量,几亿行之后即使有索引,范围扫描的回表开销也不小。第三是存储成本,关系库的行式存储加上索引,同样一份数据占的空间通常是列式时序库的好几倍。我当时算过一笔账,一年数据在 MySQL 上要占掉将近两个 T,迁到 TDengine 上压缩后只用了不到三百个 G,这个差距主要来自列式存储和针对时间戳的压缩算法。

另一个容易被忽略的点是数据生命周期。设备数据一般只关心最近几个月,更早的要么归档要么直接删掉。关系库删历史数据是个很痛的操作,要么按时间分区然后 DROP PARTITION,要么写个批量删除脚本慢慢跑,前者要求你提前设计好分区,后者会带来大量碎片。时序库一般把这件事做成了内建能力,建库时指定保留时长,过期数据自动清理,这对运维来说省心太多了。

1.2 选型时我比较过的三条路

当时摆在面前的选择其实不止一个,我大概比较了三类方案,最后才落到 TDengine 上。把这些对比写出来,是因为我见过太多人一拍脑袋选了个库,用半年发现不合适再迁移,成本比一开始多花两周调研高得多。

方案优势主要顾虑我的判断
关系库继续分表团队熟悉,生态成熟写入延迟随数据量线性变差,存储成本高只能续命,半年后还得再迁
通用分布式 KV写入吞吐高,扩展方便聚合查询要自己实现,开发量大适合做底层,不适合直接做业务查询
专用时序数据库写入、压缩、时间聚合都是原生能力生态相对小,SQL 方言有差异场景高度匹配,值得投入学习成本

在专用时序库里,我重点看了三件事:一是写入模型,支不支持批量写入、支不支持无模式写入,这决定了采集端改造的工作量;二是查询语言,是不是类 SQL,如果是一门全新的查询语言,团队的学习曲线会很陡;三是部署运维,单机能不能跑起来,集群扩容是不是平滑,有没有配套的导入导出和压测工具。TDengine 在这三点上给出的答案是批量 SQL 和行协议都支持、查询基本是标准 SQL 加时间窗口函数、单机一个进程就能起、集群按节点水平扩展、自带数据导出和压测工具。对一个不想在基础设施上投入太多人力的团队来说,这套组合是比较务实的。

注意:选型时不要只看官方 benchmark 的峰值数字,那些数字往往是在最优批次、最优硬件、最简表结构下跑出来的。真正该看的是你的数据模型下、你的批次大小下、你的查询模式下它的表现。有条件的话,拿自己业务的一份真实数据做一次小规模压测,比看十篇评测文章都管用。

1.3 动手前先把数据模型想清楚

我见过不少团队上来就建库建表,跑了两周发现标签设计不合理,想改就得重建整个超级表,数据还得重新灌一遍。所以在动手之前,我花了大概两天时间把数据模型捋了一遍。核心问题是三个:哪些字段是采集值,哪些字段是设备的静态属性;设备的层级关系怎么表达;未来会不会有新的设备类型接入。

采集值是随时间变化的量,比如电流、电压、温度,它们构成表的列。静态属性是设备一旦注册就基本不变的信息,比如设备所属的场站、设备型号、安装位置、分组编号,这些在 TDengine 里就是标签。这个划分非常关键,因为标签的取值组合决定了子表的数量,而子表的划分方式直接影响到查询时能不能高效地按设备维度过滤。我当时犯过一个错,把"设备型号"这种只有五六种取值的字段做成了列,结果每行都存一遍,白白浪费空间,查询时还得 GROUP BY 去重,改成标签之后存储和查询都清爽了。

层级关系这块,TDengine 用的是"超级表 + 标签"的扁平模型,没有真正的嵌套结构。如果你的设备是"场站—机组—传感器"三层,那就在标签里把这三层的标识都放进去,查询时用 WHERE 组合过滤。这种扁平化设计看起来简陋,但在实际使用中反而更灵活,因为你可以按任意标签组合去聚合,不受层级约束。

2. 部署与建库建表:地基打不牢后面全是坑

2.1 单机还是集群,先看数据量和并发写入

部署形态这件事,我的建议是先单机跑通业务,再考虑集群。听起来像废话,但确实有很多人一上来就要搭三节点集群,结果连单机的写入参数都没调明白,集群一出问题就抓瞎。单机版安装非常简单,下载对应平台的安装包,解开之后执行安装脚本,服务端进程就会以系统服务的方式跑起来,默认监听 6030 端口给客户端连接,6041 端口提供 REST 接口和行协议写入。装完之后用命令行客户端连上去,能看到版本号和服务器状态就说明通了。

什么时候该上集群?我给自己的判断标准是两条同时满足:单机写入已经接近磁盘 IO 瓶颈,或者单节点的数据量超过了单盘容量规划。如果只是查询慢,很多时候调索引、调缓存、优化 SQL 就能解决,不必急着上集群。集群部署的复杂度主要在网络配置和数据副本上,节点之间要能互相通信,副本数设置要考虑一致性和可用性的平衡,这些在单机阶段完全不用操心。

需要提醒的是磁盘类型的选择。时序数据写入是顺序追加为主,但合并、查询、排序都会产生随机读,所以我一般会用 SSD 或者 NVMe 做数据盘,机械盘虽然便宜但合并阶段的延迟会明显上升。另外数据目录和日志目录最好分开,避免日志刷盘影响数据写入。

2.2 建库语句里每个参数都在花钱

建库是整个系统里最重要的一个动作,因为库级参数一旦定下来,后续调整虽然可以,但有些参数改了之后只对新数据生效,老数据不会重算。我用的建库语句大概是这个样子:

CREATE DATABASE sensor_data KEEP 365 DURATION 10 PRECISION 'ms' BUFFER 256 CACHEMODEL 'last_row' WAL_LEVEL 1 MINROWS 100 MAXROWS 4096;

逐条说下我为什么这么设。KEEP 365是数据保留天数,超过一年的数据自动删除。这个值要结合业务来定,有些合规场景要求保留三年,那就写 1095,但要注意存储成本会翻三倍。DURATION 10是数据文件的分组时长,单位是天,意思是每 10 天的数据放在一组文件里。这个参数直接影响到按时间范围查询的效率——如果你的查询经常跨月,DURATION 设得太小会导致要扫描很多文件组;如果查询基本只看最近几天,DURATION 小一点反而利于过期清理。我一般按"最常用的查询时间跨度"来设,比如业务上主要看最近一周,就设 7 或者 10。

PRECISION 'ms'是时间戳精度,可选毫秒、微秒、纳秒。采集端如果只到秒级,其实用默认的毫秒就够了,没必要上微秒,精度越高时间戳占的存储空间越大。BUFFER 256是每个虚拟节点组的写入缓存大小,单位 MB,这个值影响写入吞吐,内存充裕的话可以往上调,但要注意总内存占用是 BUFFER 乘以虚拟节点数。CACHEMODEL 'last_row'是缓存每张子表最新一行,这个对"查设备当前状态"这类查询特别有用,能从缓存直接返回而不扫盘。WAL_LEVEL 1是预写日志级别,1 表示只写 WAL 不强制落盘,写入性能最好,代价是极端情况下可能丢少量数据;如果是金融、计费这类不能丢数据的场景,就调到 2。

MINROWS和MAXROWS控制的是数据块的行数区间,这两个参数比较微妙。块太小,元数据开销大;块太大,查询时扫描的无效数据多。默认值在很多场景下够用,我一般在写入量大、查询范围窄的场景下把 MAXROWS 调小一点,让块更细粒度。

注意:KEEP和DURATION是配合使用的,DURATION 不能大于 KEEP。曾经有个同事把 DURATION 设成了 30 而 KEEP 设成了 7,建库直接报错,排查了半天才发现是参数约束问题。

2.3 超级表、子表、标签到底怎么划分

TDengine 的数据模型是三层:库、超级表、子表。超级表定义的是同一类设备的列结构,也就是所有子表共享的采集字段;子表对应具体的一台设备,通过标签区分彼此。我建表的时候是这么写的:

CREATE STABLE meters ( ts TIMESTAMP, current FLOAT, voltage INT, power FLOAT, temperature FLOAT ) TAGS ( station_id INT, device_type VARCHAR(16), location VARCHAR(32) );

建完超级表之后,不需要预先创建子表,写入时带上标签就会自动建。比如:

INSERT INTO dev_1001 USING meters TAGS (3, 'meter', 'north-hall') VALUES ('2024-05-01 10:00:00.000', 10.32, 220, 2270.4, 36.5);

这条语句的意思是:往dev_1001这张子表写一行,如果子表不存在,就用meters的结构和给定标签创建它。这个自动建表的能力非常省事,采集端不用维护一份设备清单再去建表,新设备接入只要开始上报数据就行。

关于标签怎么划分,我的经验是标签数量控制在五个以内,每个标签的取值基数不要太高。标签太多会让元数据膨胀,取值基数太高(比如把设备编号做成标签)则失去了分组聚合的意义,因为每个子表的标签组合都不一样,按标签聚合等同于按子表聚合。比较合理的标签是场站、设备类型、区域这类维度,取值数量在几十到几百之间。

还有一点容易被忽略:超级表不是必须的。如果你的数据只有一类设备、字段完全统一,直接用普通表也能跑。但一旦有多类设备,超级表的价值就出来了——一次查询可以跨所有子表做聚合,比如"统计所有场站过去一小时的总功率",不用写 UNION。所以我现在的习惯是,只要预估未来会接入第二种设备类型,一律用超级表。

3. 数据写入:三条路子和我踩过的批次坑

3.1 SQL 批量写入的正确姿势

写入方式的选择取决于采集端的形态。如果是 Java 或者 Go 写的采集服务,一般用官方连接器;如果是 Python 脚本或者临时导入,用 REST 接口或者命令行更顺手。不管用哪种,核心原则只有一个:一定要批量写,千万别一条一条插入。我做过对比测试,同样一百万行数据,单条插入耗时接近二十分钟,改成每批五百行之后降到了不到一分钟,差距全靠网络往返和事务开销。

SQL 批量写入的写法是在一条 INSERT 里跟多个 VALUES 组:

INSERT INTO dev_1001 VALUES ('2024-05-01 10:00:00.000', 10.32, 220, 2270.4, 36.5) ('2024-05-01 10:00:05.000', 10.35, 221, 2287.4, 36.6) ('2024-05-01 10:00:10.000', 10.29, 219, 2253.5, 36.4);

如果一次要写多台设备,可以把多条 INSERT 拼在一条语句里,用子表名分隔:

INSERT INTO dev_1001 VALUES ('2024-05-01 10:00:00.000', 10.32, 220, 2270.4, 36.5) dev_1002 VALUES ('2024-05-01 10:00:00.000', 8.71, 219, 1907.5, 35.8) dev_1003 VALUES ('2024-05-01 10:00:00.000', 12.05, 222, 2675.1, 37.1);

用连接器的时候,更推荐参数绑定加批处理的方式。以 Python 的 REST 连接器为例,大致流程是建连接、准备语句、循环填参、攒够一批执行一次:

import taosrest conn = taosrest.connect(url="http://127.0.0.1:6041", user="root", password="your_password") cursor = conn.cursor() batch = [] for row in generate_rows(): batch.append(row) if len(batch) >= 500: sql = "INSERT INTO ? USING meters TAGS (?, ?, ?) VALUES (?, ?, ?, ?, ?)" cursor.executemany(sql, batch) batch.clear()

参数绑定的好处是省掉了 SQL 字符串拼接的开销,也避免了时间戳格式转义的问题。不过要注意,绑定模式下每条记录都要带上标签,批次内的标签组合最好一致,否则会自动拆成多条语句执行。

3.2 行协议与无模式写入

有些场景下采集端不方便拼 SQL,比如设备端的固件只能发简单的文本报文,或者你手上有一堆用其他时序库行协议写的旧代码。这时候可以用行协议接口,它的格式是这样的:

curl -u root:your_password \ -d "meters,station_id=3,device_type=meter,location=north-hall current=10.32,voltage=220,power=2270.4 1714564800000" \ http://127.0.0.1:6041/influxdb/v1/ib/write

前面是测量名和标签,中间是字段,最后是毫秒时间戳。这个接口的好处是无模式,第一次写入时会自动根据字段类型建表,后续字段增减也能自适应。但它也有代价:自动推断类型有时候会推错,比如某个字段一直是整数,某天突然来了个小数,就可能出现类型不匹配的问题;另外无模式写入的性能比预建表要低一些,因为每次都要做元数据检查。

我的做法是核心业务表预建,边缘设备用无模式兜底。主链路的数据采集用预建超级表加批量 SQL,保证性能和类型确定;一些临时接入的第三方设备或者调试阶段的数据,走行协议快速落地。这样既不牺牲主链路的性能,也不至于为了接一个新设备就改一次表结构。

注意:行协议的时间戳默认按毫秒解析,如果你的采集端给的是秒级时间戳,记得在建库时把精度设对,或者在报文里做单位换算。我曾经因为这个问题导致所有数据都落在 1970 年附近,查了半天才发现是时间戳单位搞错了。

3.3 批次大小、并发度和写入顺序的实测

批次大小不是越大越好。我实测下来,单条 SQL 的批次在 300 到 1000 行之间比较合适,太小了网络往返占比高,太大了单条语句解析和内存占用会上升,而且失败重试的成本变高。具体设多少要看单行数据的大小,字段多、字符串长的表,批次就往下压;字段少、全是数值的表,批次可以往上提。

并发度方面,我一般按"写入线程数不超过虚拟节点组数"来配。单机默认是一个虚拟节点组,起太多写入线程反而会因为锁竞争变慢。有一个简单的方法可以找到合适的并发数:从单线程开始,逐步加倍,观察每秒写入行数的变化,当加倍后吞吐不再明显提升甚至下降时,就说明到瓶颈了。

写入顺序上有一个细节值得说:同一张子表的数据尽量按时间顺序写。TDengine 对乱序数据是有处理能力的,但乱序写入会触发额外的合并操作,写入放大的倍数会上升。如果采集端本身就有乱序(比如多路数据汇聚到一个写入队列),可以在写入前做一次按时间排序的缓冲,代价不大,收益不小。

4. 查询与计算:把时序数据真正用起来

4.1 时间窗口聚合与降采样

时序数据最常见的查询就是降采样,也就是把秒级数据聚合成分钟级、小时级。TDengine 提供的INTERVAL子句就是干这个的,配合_wstart、_wend能看到窗口的起止时间:

SELECT _wstart AS window_start, AVG(current) AS avg_current, MAX(voltage) AS max_voltage, COUNT(*) AS sample_count FROM meters WHERE ts >= NOW - 1h PARTITION BY tbname INTERVAL(1m);

这条查询的意思是:取最近一小时的数据,按子表分组,每分钟一个窗口,算出平均电流、最大电压和采样点数。PARTITION BY tbname保证每台设备单独出结果,如果不需要区分设备,去掉这个子句就是全局聚合。

INTERVAL还有一个兄弟叫SLIDING,用来做滑动窗口。比如每 30 秒算一次过去 5 分钟的平均值,就写成INTERVAL(5m) SLIDING(30s)。滑动窗口在计算移动平均、检测趋势突变时特别好用,但要注意窗口重叠会带来额外的计算量,滑动步长越小、窗口越长,计算开销越大。

还有一个函数我经常用,就是FILL。时间窗口聚合有个天然问题:如果某个窗口内没有数据,默认就不输出这一行,导致前端画图时曲线断掉。加上FILL(PREV)可以用前一个窗口的值填充,FILL(VALUE, 0)填零,FILL(LINEAR)做线性插值。设备离线导致的数据空洞,用FILL(NULL)明确标记出来,反而比强行插值更诚实,这点在做数据质量分析时很重要。

4.2 连续查询与流式计算

如果某个降采样结果是高频使用的,比如前端大屏每分钟都要拉一次"过去 24 小时每小时的均值",每次都实时算一遍其实很浪费。TDengine 提供了流式计算的能力,可以把聚合结果持续写入另一张表:

CREATE STREAM hourly_stat INTO hourly_meters AS SELECT _wstart AS ts, tbname AS device_name, AVG(current) AS avg_current, MAX(power) AS max_power FROM meters PARTITION BY tbname INTERVAL(1h);

建好之后,新写入的数据会自动触发计算,结果落到hourly_meters表里。前端查大屏的时候直接查这张结果表,响应时间从秒级降到毫秒级。这个机制本质上是用写入时的少量计算换查询时的大量计算,对于读多写少的报表场景非常划算。

流计算的坑主要在重启和补数据上。如果流任务停了几个小时再启动,中间的数据能不能补算,取决于流任务的配置和版本行为。我的做法是在创建流的时候明确指定触发方式,并且对结果表保留一段时间的原始数据,万一流算错了还能回溯重算。另外结果表的 KEEP 设置要跟流任务的保留策略对齐,否则可能出现流还在跑但结果表数据被清掉的情况。

4.3 数据生命周期管理

数据保留这件事,在建库时通过KEEP定了大概,但运行过程中可能需要调整。比如业务上突然要求历史数据保留两年,那就得改库参数:

ALTER DATABASE sensor_data KEEP 730;

这个改动只影响后续的数据清理策略,已经删掉的数据找不回来,所以调整前最好先确认一下归档有没有做。我现在的标准做法是双保险:库上设 KEEP 做自动清理,同时定期用导出工具把冷数据备份到对象存储。导出工具支持按时间范围导出,我一般按月导,命名带上年月,需要回溯的时候再按需导入。

还有一个小技巧是分区表的手动清理。TDengine 的数据文件是按时间分组的,如果某一批数据确认是脏数据需要立刻删掉,可以用DELETE FROM按时间条件删,但要注意这个操作会重写数据文件,数据量大的时候比较慢,最好在业务低峰期做。日常的过期清理交给 KEEP 就够了,不要频繁手动删。

5. 排障实录:那些年我对着日志发呆的夜晚

5.1 连接、认证与端口问题

刚接触的时候最容易卡在连接上。第一个要分清的是端口:6030 是客户端原生连接用的,6041 是 REST 和行协议用的,两个端口都要在防火墙放行。我遇到过用 REST 连接器去连 6030 的情况,报错信息不太直观,排查了半天才反应过来是端口搞错了。

第二个是认证。默认用户名是 root,初始密码在安装时设定,如果忘了可以用命令行工具重置。REST 接口的认证信息放在请求头或者 URL 里,用 curl 测试的时候记得带上-u参数。有一个容易忽略的点是密码里包含特殊字符,拼在 URL 里会被转义,导致认证失败,这种情况建议改用请求头传认证信息。

第三个是版本兼容。客户端连接器的版本和服务端版本差太多的时候,可能出现协议不匹配。我的习惯是服务端和客户端连接器尽量用同一个大版本,升级的时候先升服务端再升客户端,中间留一段观察期。

现象可能原因排查动作
连接超时端口未放行或服务未启动检查服务进程状态,用 telnet 测端口连通性
认证失败密码错误或特殊字符转义改用请求头传认证,用命令行工具验证密码
连接被重置版本协议不匹配核对服务端与客户端连接器版本
时断时续网络质量或连接池配置不当检查连接池最大连接数和空闲回收时间

5.2 写入异常与时间戳相关的坑

写入报错里最常见的是表不存在和类型不匹配。表不存在一般发生在没有用USING子句指定超级表、又直接往一个不存在的子表写数据的时候。类型不匹配则多出现在行协议场景,自动推断的类型和实际值冲突。解决方式是预建超级表,把字段类型明确下来,行协议只作为辅助通道。

时间戳的问题更隐蔽。有一次我发现查出来的数据总是少一截,排查下来是采集端用的是本地时间字符串,而服务端按 UTC 解析,两边差了八小时。后来统一改成传毫秒时间戳,问题消失。还有一次是精度设置的问题,建库时用了毫秒精度,但采集端给的是微秒时间戳,结果所有数据被截断到毫秒,同一毫秒内的多条数据互相覆盖。这类问题的排查方法是随机抽几条原始报文,手工换算时间戳,跟库里查出来的值对一下,很快就能定位。

注意:时间戳是时序库的命门,写入前一定要确认三件事——单位是秒、毫秒还是微秒,时区是本地还是 UTC,精度和建库时设置的是否一致。这三件事任何一件对不上,数据就是错的,而且错得很安静,不会报错。

5.3 查询变慢与资源占用的排查

查询变慢的原因通常有三类:扫描的数据量太大、聚合计算太重、结果集太大。第一类最典型的表现是查询没带时间条件,或者时间范围太宽。我见过有人写了个不带 WHERE 的 COUNT 查询,直接把整库扫了一遍,机器差点没扛住。时序库的查询一定要带时间范围,这是铁律。

第二类是聚合计算太重。比如在千万级数据上做COUNT(DISTINCT),或者用了太多嵌套函数。优化思路是先降采样再计算,用INTERVAL把数据先聚到分钟级,再在外层做二次聚合。另外PARTITION BY的维度不要太细,按几千个子表分区计算,中间结果的内存占用会很可观。

第三类是结果集太大。有人一次查出几十万行明细往前端推,前端渲染卡死,还以为是数据库慢。这种场景应该在后端做分页或者只返回聚合结果。TDengine 支持LIMIT和OFFSET,但要注意深分页的效率问题,更好的做法是用时间游标翻页。

资源占用方面,内存是最容易出问题的。建库时的BUFFER乘以虚拟节点数就是写入缓存的基本开销,再加上查询时的中间结果内存,如果机器内存不够,就会出现频繁换页,表现为整体变慢。我一般会留出至少百分之三十的余量,并且监控写入缓存的使用率和查询的内存峰值,接近上限就提前扩容或者调整参数。

6. 几条不怎么写在文档里的经验

6.1 关于批次、并发和压测的实践建议

压测这件事,我强烈建议在上线前做一次,哪怕规模很小。用官方的压测工具造一批模拟数据,跑一遍写入和查询,你能提前知道这套配置大概能扛多少。压测的时候要注意两点:模拟数据的分布要接近真实,比如子表数量、单表行数、标签基数都要对上,否则压出来的数字没有参考价值;查询也要压,很多人只压写入不压查询,结果上线之后写入没问题,查询把 CPU 打满了。

批次和并发的调优没有万能公式,我的方法是固定其他变量,单变量扫描。先固定并发为一,批次从一百开始翻倍,记录吞吐;找到吞吐拐点之后,固定批次,并发从一翻倍,再找拐点。整个过程大概半小时,比凭感觉设参数可靠得多。

6.2 表结构演进时怎么少走弯路

业务是会变的,设备类型会增加,采集字段会增减,这些都是常态。TDengine 支持给超级表增加列,语法是ALTER STABLE ... ADD COLUMN,但已有的子表不会自动补这一列,新写入的数据会带新列,老数据该列为空。这个行为本身没问题,但如果你在查询里用了SELECT *,不同子表返回的列数可能不一致,前端解析就会出错。所以我的习惯是查询永远显式列出字段,不用星号。

标签的修改要更谨慎。标签值可以更新,但标签的结构(数量和类型)改动代价很大,基本等同于重建超级表。所以第一次设计标签的时候,宁可多留一个冗余字段,也不要在上线三个月后再加。如果确实要加,稳妥的做法是建一张新的超级表,用INSERT INTO ... SELECT把老数据迁过去,再切换写入和查询,中间留一段双写过渡期。

这套东西用下来,我最大的感受是:时序数据库的价值不在于它能存多少数据,而在于它把"按时间聚合"这件事变成了原生能力,省掉的是你在应用层反复造轮子的时间。但前提是你得把数据模型设计对、把写入方式用对,否则它就是个写入快一点的普通库,该慢的地方还是慢。我见过有人把 TDengine 当成关系库用,每个字段都做成标签,最后子表数量爆炸,查询慢得离谱,然后得出结论说这时序库不行——问题从来不在库,在用法。

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

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

立即咨询