TDengine Schemaless 无模式写入指南:行协议、自动建表规则与多语言实战
【免费下载链接】TDengineHigh-performance, scalable time-series database designed for Industrial IoT (IIoT) scenarios项目地址: https://gitcode.com/GitHub_Trending/tde/TDengine
导读
在工业物联网(IIoT)场景中,设备采集项往往随应用逻辑升级、硬件调整而频繁变化,若每次都手工建表将极大拖慢数据接入节奏。TDengine 提供了 Schemaless(无模式)写入方式:无需预先创建超级表与子表,写入时自动完成建表、加列与类型推断,同时保持与 InfluxDB Line Protocol、OpenTSDB Telnet / JSON 协议的兼容。本文基于 TDengine 开源仓库的开发者指南文档(docs/en/10-developer-guide/04-schemaless.md)与对应源码、示例,系统讲解行协议语法、自动建表命名规则、配置项、时间精度识别、数据模式映射与变化处理,并给出 WebSocket 与 Native 两种连接的 Java/Python/Go/Rust/Node.js/C#/C 多语言可运行示例,帮助你快速上手并规避常见写入错误。
Schemaless 写入机制概述
在 IoT 应用中,为达成自动化管理、业务分析、设备监控等功能,往往需要采集大量数据项。由于应用逻辑升级、设备硬件调整等原因,采集项可能频繁变化。TDengine 的 Schemaless 写入方法正是为简化数据记录流程而设计。
其核心机制包含三点:
- 自动建表:用户无需预先创建超级表(supertable)或子表(subtable),TDengine 会根据实际写入的数据自动创建对应的存储结构。
- 自动加列:必要时自动为已存在的表补充缺失的数据列(普通列)或标签列(tag),确保用户写入的数据被正确存储。
- 与 SQL 建表等价:通过 Schemaless 写入创建的超级表及其子表,与直接通过 SQL 创建的表在功能上没有任何差异,仍可直接使用 SQL 写入数据。唯一的区别在于:由 Schemaless 自动生成的子表名基于标签值按固定映射规则生成,可读性较差、不易直接理解。
重要提示:使用 Schemaless 写入时表由系统自动创建,手动创建同名表可能导致未知错误;同理,文档也不推荐手动预建超级表,否则数据插入可能异常。
Schemaless 写入行协议
TDengine 的 Schemaless 行协议兼容三类协议:
- InfluxDB 行协议(Line Protocol)
- OpenTSDB Telnet 行协议
- OpenTSDB JSON 格式协议
其中 InfluxDB 与 OpenTSDB 的标准协议写法请参考其各自官方文档。下面重点介绍 TDengine 在 InfluxDB 行协议基础上扩展出的协议内容,它允许用户更精细地控制超级表 schema。每一行数据用一条字符串表达,多条行字符串可一次性传入写入 API 实现批量写入,格式为:
measurement,tag_set field_set timestamp各组成部分说明如下:
| 组成部分 | 格式 | 说明 |
|---|---|---|
measurement | 表名 | 与tag_set之间用逗号分隔 |
tag_set | <tag_key>=<tag_value>,<tag_key>=<tag_value> | 标签列数据,各项用逗号分隔,与field_set之间用空格分隔 |
field_set | <field_key>=<field_value>,<field_key>=<field_value> | 普通列数据,各项用逗号分隔,与timestamp之间用空格分隔 |
timestamp | 时间戳 | 该行数据的主键时间戳 |
注意:Schemaless 写入不支持向带第二复合主键列的表写入数据。
tag_set中的所有数据都会被自动转换为nchar数据类型,无需使用双引号。
数据项类型标注(field_set 类型描述)
在 Schemaless 行协议中,field_set中的每个数据项都需要描述自身的数据类型,规则如下:
| 写法 | 类型 | 示例 |
|---|---|---|
| 双引号包裹 | varchar(Binary) | "abc" |
双引号包裹且前缀L或l | nchar | L" error message " |
双引号包裹且前缀G或g | geometry | G"Point(4.343 89.342)" |
双引号包裹且前缀B或b | varbinary | B"\x98f46e"、B"hello"(双引号内可含以\x开头的十六进制,也可为普通字符串) |
转义规则
对于空格、等号(=)、逗号(,)、双引号(")、反斜杠(\),都需要使用反斜杠进行转义(全部为半角英文符号)。各字段域(domain)的转义规则如下:
| 编号 | 字段 | 需转义的字符 |
|---|---|---|
| 1 | 超级表名 | 逗号、空格 |
| 2 | 标签名 | 逗号、等号、空格 |
| 3 | 标签值 | 逗号、等号、空格 |
| 4 | 列名 | 逗号、等号、空格 |
| 5 | 列值 | 双引号、反斜杠 |
反斜杠自身的转义遵循"连续反斜杠"规则:若连续出现两个反斜杠,第一个作为转义字符;若只有一个反斜杠,则无需转义。具体映射如下:
| 编号 | 反斜杠(原始) | 转义结果 |
|---|---|---|
| 1 | \ | \ |
| 2 | \\ | \ |
| 3 | \\\ | \\ |
| 4 | \\\\ | \\ |
| 5 | \\\+\\(五个\) | \\\ |
| 6 | \\\\+\\(六个\) | \\\ |
数值类型后缀映射
数值类型通过后缀区分,映射规则如下:
| 编号 | 后缀 | 映射类型 | 字节数 |
|---|---|---|---|
| 1 | 无后缀或f64 | double | 8 |
| 2 | f32 | float | 4 |
| 3 | i8/u8 | TinyInt / UTinyInt | 1 |
| 4 | i16/u16 | SmallInt / USmallInt | 2 |
| 5 | i32/u32 | Int / UInt | 4 |
| 6 | i64/i/u64/u | BigInt / BigInt / UBigInt / UBigInt | 8 |
此外,t、T、true、True、TRUE、f、F、false、False会被直接识别为BOOL类型。
完整示例
下面这行数据表示:在名为st的超级表下,以标签t1="3"(NCHAR)、t2="4"(NCHAR)、t3="t3"(NCHAR)定位子表,写入一行数据:列c1=3(BIGINT)、c2=false(BOOL)、c3="passit"(BINARY)、c4=4(DOUBLE),主键时间戳为1626006833639000000:
st,t1=3,t2=4,t3=t3 c1=3i64,c3="passit",c2=false,c4=4f64 1626006833639000000注意:如果数据类型后缀描述有误(如大小写错误),或为数据指定的类型本身不正确,可能触发错误信息并导致写入失败。
幂等性与原子性
TDengine 为数据写入提供了幂等性:可以重复调用 API 写入之前写入失败的数据,不会产生重复副作用。但不提供多行数据的原子性:批量写入多行数据时,可能出现部分行成功、部分行失败的情况,需要应用侧根据返回错误码做补偿处理。
Schemaless 写入处理规则
Schemaless 写入按以下 12 条原则处理行数据:
子表名生成规则:先将 measurement 名称与标签的 key、value 拼接为如下字符串:
"measurement,tag_key1=tag_value1,tag_key2=tag_value2"- 注意
tag_key1、tag_key2并非用户输入时的原始顺序,而是按标签名升序排序后的结果,因此tag_key1不一定是行协议中第一个输入的标签。 - 排序后对该字符串计算 MD5 哈希值
md5_val,再将计算结果与字符串拼接生成表名:t_md5_val。t_是固定前缀,所有通过该映射自动生成的表都带此前缀。 - 如果不想使用自动生成的表名,有两种方式指定子表名(方式一优先级更高):
- 在
taos.cfg中配置smlAutoChildTableNameDelimiter(取值不能包含@ # 空格 CR LF 制表符)。例如配置smlAutoChildTableNameDelimiter=-后,写入st,t0=cpu1,t1=4 c1=3 1626006833639000000,创建的表名为cpu1-4。 - 在
taos.cfg中配置smlChildTableName。例如配置smlChildTableName=tname后,写入st,tname=cpu1,t1=4 c1=3 1626006833639000000,创建的表名为cpu1。注意:如果多行数据具有相同的tname但tag_set不同,则以首次自动建表时指定的 tag_set 为准,其余行会被忽略。
- 在
- 注意
若解析行协议得到的超级表不存在,则自动创建(不推荐手动创建超级表,否则数据插入可能异常)。
若解析得到的子表不存在,则按第 1 步确定的子表名创建子表。
若数据行中指定的标签列或普通列不存在,会将其添加到超级表中(只增不减)。
若超级表中已存在某些标签列或普通列,但某数据行未指定它们,则该行中这些列的值为NULL。
对于 BINARY 或 NCHAR 列,若数据行提供的值长度超过列类型上限,会自动扩大该列的最大字符存储上限(只增不减),确保数据完整存储。
整个处理过程中遇到的错误会中断写入流程并返回错误码。
为提高写入效率,默认假设同一超级表内
field_set的列顺序一致(首条数据包含全部字段,后续数据沿用该顺序)。若顺序不同,需配置smlDataFormat为false,否则数据会按相同顺序写入,导致库内数据错乱。自3.0.3.0版本起系统会自动检查顺序一致性,该配置已废弃。由于 SQL 建表不支持点号(
.),Schemaless 会将自动创建的表名中的点号替换为下划线(_)。若手工指定子表名且包含点号,也会被转换为下划线。taos.cfg新增smlTsDefaultName配置(值为字符串),仅作用于客户端。配置后可通过它设置 Schemaless 自动建表的时间列名,未配置时默认为_ts。Schemaless 写入中的超级表名、子表名区分大小写。
Schemaless 写入仍受 TDengine 底层数据结构限制:每行数据总长度不能超过 48KB(自3.0.5.0版本起为 64KB),标签值总长度不能超过 16KB。
配置项在源码中的注册位置
上述smlChildTableName、smlAutoChildTableNameDelimiter、smlTagName、smlTsDefaultName、smlDot2Underline等配置项均作为客户端本地(CFG_SCOPE_CLIENT / CFG_CATEGORY_LOCAL)动态配置在 source/common/src/tglobal.c 中注册,且支持运行时动态调整(CFG_DYN_CLIENT),可在taos.cfg中集中管理,也可以在连接建立前通过客户端 API 设置。
时间分辨率识别
Schemaless 写入支持三种指定模式:
| 编号 | 值 | 描述 |
|---|---|---|
| 1 | SML_LINE_PROTOCOL | InfluxDB Line Protocol |
| 2 | SML_TELNET_PROTOCOL | OpenTSDB Text Line Protocol |
| 3 | SML_JSON_PROTOCOL | JSON 格式协议 |
在SML_LINE_PROTOCOL解析模式下,用户需要显式指定输入时间戳的时间分辨率,可选值如下:
| 编号 | 时间分辨率定义 | 含义 |
|---|---|---|
| 1 | TSDB_SML_TIMESTAMP_NOT_CONFIGURED | 未定义(非法) |
| 2 | TSDB_SML_TIMESTAMP_HOURS | 小时 |
| 3 | TSDB_SML_TIMESTAMP_MINUTES | 分钟 |
| 4 | TSDB_SML_TIMESTAMP_SECONDS | 秒 |
| 5 | TSDB_SML_TIMESTAMP_MILLI_SECONDS | 毫秒 |
| 6 | TSDB_SML_TIMESTAMP_MICRO_SECONDS | 微秒 |
| 7 | TSDB_SML_TIMESTAMP_NANO_SECONDS | 纳秒 |
在SML_TELNET_PROTOCOL与SML_JSON_PROTOCOL模式下,时间精度由时间戳的长度决定(与 OpenTSDB 标准行为一致),用户指定的时间分辨率将被忽略。
数据模式映射规则
InfluxDB 行协议数据会被映射为带 schema 的表结构,映射关系为:
measurement→ 超级表名tag_set中的标签名 → schema 中的标签名field_set中的字段名 → 普通列名
以如下数据为例:
st,t1=3,t2=4,t3=t3 c1=3i64,c3="passit",c2=false,c4=4f64 1626006833639000000该行数据映射创建超级表st,包含 3 个 nchar 类型标签t1、t2、t3,以及 5 个数据列:ts(timestamp)、c1(bigint)、c3(binary)、c2(bool)、c4(bigint),等价于如下 SQL:
create stable st (_ts timestamp, c1 bigint, c2 bool, c3 binary(6), c4 bigint) tags(t1 nchar(1), t2 nchar(1), t3 nchar(2))可见:数值后缀直接决定列类型(如3i64映射为 bigint),双引号字符串按长度映射为binary(n),标签值按长度映射为nchar(n)。
数据模式变化处理
本节说明不同行数据写入场景对数据 schema 的影响。
场景一:显式类型标识变更 → 报错
使用行协议写入带明确类型标识的字段时,后续若改变该字段的类型定义,将触发明确的数据 schema 错误,写入 API 返回错误。例如:
st,t1=3,t2=4,t3=t3 c1=3i64,c3="passit",c2=false,c4=4 1626006833639000000 st,t1=3,t2=4,t3=t3 c1=3i64,c3="passit",c2=false,c4=4i 1626006833640000000第一行将c4定义为 Double,第二行却通过数值后缀将同一列声明为 BigInt,从而触发 Schemaless 解析错误。
场景二:BINARY 列长度扩展 → 自动加宽
若前面行将某数据列声明为 binary,后续行需要更长的二进制长度,将触发超级表 schema 变更(自动加宽):
st,t1=3,t2=4,t3=t3 c1=3i64,c5="pass" 1626006833639000000 st,t1=3,t2=4,t3=t3 c1=3i64,c5="passit" 1626006833640000000第一行声明列c5为binary(4);第二行写入时识别到c5仍为 binary 列但宽度为 6,此时自动将列宽扩大以容纳新字符串(binary(6))。
场景三:新增列 → 自动加列
st,t1=3,t2=4,t3=t3 c1=3i64 1626006833639000000 st,t1=3,t2=4,t3=t3 c1=3i64,c6="passit" 1626006833640000000第二行相对第一行新增了列c6,类型为binary(6),系统将自动为超级表添加c6(binary(6))列。
Schemaless 写入示例(智能电表场景)
下面以智能电表为例,介绍使用各类语言连接器通过 Schemaless 写入接口写数据的代码示例,覆盖 InfluxDB Line Protocol、OpenTSDB Telnet 与 OpenTSDB JSON 三种协议。
运行前提:
- 由于 Schemaless 自动建表规则与 SQL 示例不同,运行前请确保
meters、metric_telnet、metric_json等表不存在。- OpenTSDB Telnet 行协议与 OpenTSDB JSON 格式协议仅支持一个数据列,因此示例采用了其他数据组合。
WebSocket 连接方式
WebSocket 连接通过 REST/WebSocket 网关(默认端口 6041)写入,典型示例文件如下:
- Java:执行带
reqId的 Schemaless 写入,最后一个参数reqId可用于请求链路追踪:writer.write(lineDemo, SchemalessProtocolType.LINE, SchemalessTimestampType.NANO_SECONDS, 1L);完整代码参见 docs/examples/JDBC/JDBCDemo/src/main/java/com/taos/example/SchemalessWsTest.java。
- Python(docs/examples/python/schemaless_ws.py):先创建数据库
power,再通过conn.schemaless_insert(...)分别以 Line、Telnet、Json 三种协议写入:import taosws host = "localhost" port = 6041 lineDemo = [ "meters,groupid=2,location=California.SanFrancisco current=10.3000002f64,voltage=219i32,phase=0.31f64 1626006833639" ] telnetDemo = ["metric_telnet 1707095283260 4 host=host0 interface=eth0"] jsonDemo = [ '{"metric": "metric_json","timestamp": 1626846400,"value": 10.3, "tags": {"groupid": 2, "location": "California.SanFrancisco", "id": "d1001"}}' ] conn = taosws.connect(user="root", password="taosdata", host=host, port=port, database='power') conn.schemaless_insert( lines=lineDemo, protocol=taosws.PySchemalessProtocol.Line, precision=taosws.PySchemalessPrecision.Millisecond, ttl=1, req_id=1, ) conn.schemaless_insert( lines=telnetDemo, protocol=taosws.PySchemalessProtocol.Telnet, precision=taosws.PySchemalessPrecision.Microsecond, ttl=1, req_id=2, ) conn.schemaless_insert( lines=jsonDemo, protocol=taosws.PySchemalessProtocol.Json, precision=taosws.PySchemalessPrecision.Millisecond, ttl=1, req_id=3, )可以看到,WebSocket 版 API 还支持
ttl(数据生命周期)与req_id(链路追踪)参数。 - Go:推荐使用
ws/unifiedSchemaless 接口(自v3.8.0起,见 docs/examples/go/schemaless/unified/main.go);ws/schemaless兼容接口(docs/examples/go/schemaless/ws/main.go)自v3.8.0起标记为废弃,暂时仍可用,建议迁移。 - Rust(docs/examples/rust/restexample/examples/schemaless.rs)、Node.js(docs/examples/node/websocketexample/line_example.js)、C#(docs/examples/csharp/wssml/Program.cs)、C(docs/examples/c-ws-new/sml_insert_demo.c)均有对应示例。
- REST API:不支持Schemaless 写入。
Native 连接方式
Native 连接使用 TCP 原生协议(默认端口 6030),示例文件如下:
- Java:同样支持带
reqId的写入,参见 docs/examples/JDBC/JDBCDemo/src/main/java/com/taos/example/SchemalessJniTest.java。 - Python(docs/examples/python/schemaless_native.py):
import taos lineDemo = [ "meters,groupid=2,location=California.SanFrancisco current=10.3000002f64,voltage=219i32,phase=0.31f64 1626006833639" ] telnetDemo = ["metric_telnet 1707095283260 4 host=host0 interface=eth0"] jsonDemo = [ '{"metric": "metric_json","timestamp": 1626846400,"value": 10.3, "tags": {"groupid": 2, "location": "California.SanFrancisco", "id": "d1001"}}' ] conn = taos.connect(user="root", password="taosdata", host="localhost", port=6030) conn.execute("CREATE DATABASE IF NOT EXISTS power") conn.select_db("power") conn.schemaless_insert( lineDemo, taos.SmlProtocol.LINE_PROTOCOL, taos.SmlPrecision.MILLI_SECONDS ) conn.schemaless_insert( telnetDemo, taos.SmlProtocol.TELNET_PROTOCOL, taos.SmlPrecision.MICRO_SECONDS ) conn.schemaless_insert( jsonDemo, taos.SmlProtocol.JSON_PROTOCOL, taos.SmlPrecision.MILLI_SECONDS )Native 版通过
taos.SmlProtocol.LINE_PROTOCOL / TELNET_PROTOCOL / JSON_PROTOCOL与taos.SmlPrecision.*指定协议与时间精度。 - Go(docs/examples/go/schemaless/native/main.go)、Rust(docs/examples/rust/nativeexample/examples/schemaless.rs)、C#(docs/examples/csharp/nativesml/Program.cs)、C(docs/examples/c/schemaless.c)均有对应示例。
- Node.js 与 REST API:不支持Native Schemaless 写入。
查询写入的数据
运行上述示例后,power数据库中会自动创建相应表。可使用 TDengine CLI 或应用程序查询验证写入结果,例如:
taos> show power.stables; stable_name | ================================= meter_current | stb0_0 | meters | Query OK, 3 row(s) in set (0.002527s) taos> select * from power.meters limit 1 \G; *************************** 1.row *************************** _ts: 2021-07-11 20:33:53.639 current: 10.300000199999999 voltage: 219 phase: 0.310000000000000 groupid: 2 location: California.SanFrancisco Query OK, 1 row(s) in set (0.004501s)可以看到:meters超级表已按智能电表场景自动建表,标签groupid、location与普通列current、voltage、phase均被正确存储,时间列默认为_ts。你既可以用 CLI 交互查询,也可以在应用中通过 SQL 直接读写这些自动创建的表。
实践建议与注意事项
综合文档与源码,使用 Schemaless 写入时建议遵循以下实践:
- 不要手动预建表:让系统自动建表,避免与自动命名规则冲突引发未知错误。
- 保持同一超级表内 field_set 顺序一致:虽然 3.0.3.0 之后系统会自动校验列顺序,但保持一致的字段顺序仍是提升解析与写入效率的好习惯。
- 正确选择时间精度:Line 协议必须显式指定时间分辨率(毫秒/微秒/纳秒等);Telnet 与 JSON 协议按时间戳长度推断精度,注意与写入数据匹配。
- 善用子表名定制:若自动生成的
t_<md5>表名不利于后续 SQL 检索,可通过smlAutoChildTableNameDelimiter或smlChildTableName两个客户端配置指定可读的子表名。 - 留意底层限制:单行数据总长度不超过 64KB(3.0.5.0 起),标签值总长度不超过 16KB;BINARY/NCHAR 列超长时会自动加宽(只增不减),但列类型冲突(如 Double 变 BigInt)会直接报错。
- 充分利用幂等性:写入失败后可重复调用 API 重试;但多行批量写入不具备原子性,需按返回错误码做局部补偿。
通过本文的协议语法、自动建表规则与多语言示例,你可以将任意 IIoT 设备的动态采集项以最小改造成本接入 TDengine,并依靠 SQL 继续完成后续的查询、订阅与流式计算。
【免费下载链接】TDengineHigh-performance, scalable time-series database designed for Industrial IoT (IIoT) scenarios项目地址: https://gitcode.com/GitHub_Trending/tde/TDengine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考