为什么你的子表名是一串看不懂的字符?建库语句里每个数字到底在管什么?哪些字段该当 TAG、哪些该当列?本文用真实可运行的 TDengine 建模代码,把这三个决策点一次讲透,读完你就能独立设计一张不踩坑的超级表。
从一串乱码子表名说起
第 1 篇里我们用 INSERT ... USING telemetry TAGS ... 隐式建了一张子表 d_smoke,当时你大概没细想:为什么子表名是 d_smoke 而不是 smoke-001?
如果你直接拿设备 ID 当表名,比如 CREATE TABLE smoke-001 ...,TDengine 会立刻报错,因为 - 是非法标识符字符。更糟的情况:设备 ID 里混入 ; 或引号,可能直接构成 SQL 注入。
子表命名不是拍脑袋,而是一套严谨的算法。这篇从建库语句开始,把 TDengine 建模的三个核心决策点逐个拆开。

建库语句:每个数字都在管什么
先看 001_schema.sql 里这段完整的建库语句:
sql
CREATE DATABASE IF NOT EXISTS iot
BUFFER 256
CACHEMODEL 'none'
CACHESIZE 1
COMP 2
DURATION 10d
KEEP 3650d
MAXROWS 4096
MINROWS 100
PRECISION 'ms'
REPLICA 1
VGROUPS 4
WAL_LEVEL 1;
BUFFER 256:每个 vnode 的写入缓冲大小,单位 MB,直接决定峰值写入吞吐。工业场景每秒上千条采样,这个值不能太小。
CACHEMODEL 'none' + CACHESIZE 1:缓存模型设为 none,不缓存数据行。这个项目的查询走预聚合,不依赖行缓存,省下的内存给更需要的环节。
COMP 2:压缩等级,2 代表高压缩。时序数据的典型特征是"同列相邻值变化小",列存加高压缩能把磁盘占用压到原始数据的十分之一甚至更低。
DURATION 10d:每个数据文件的时间跨度,10 天一个文件,是新旧数据分区的基础。注意 KEEP 必须是 DURATION 的整数倍,否则建库直接报错。
KEEP 3650d:数据保留 10 年,超期自动删除。工业场景设备寿命往往 5 到 10 年,这个值要覆盖设备全生命周期。
MAXROWS 4096 / MINROWS 100:每个数据文件块内最大、最小行数。块越大压缩率越高,但查询粒度变粗;块太小文件碎片多。4096 是官方推荐的均衡值。
PRECISION 'ms':时间戳精度毫秒。采样频率到微秒级的话这里要改成 'us',但存储开销会涨。
REPLICA 1:副本数,单机部署 1 就够了,第 10 篇上集群时改成 3。
VGROUPS 4:vnode 组数量,决定并发写入通道数。4 个 vnode 意味着最多 4 路并行写入,后续扩容可以动态增加。
WAL_LEVEL 1:写 WAL 但不同步,故障时可能丢最近少量数据,但写入吞吐最高。对遥测数据,丢几秒可接受;对计费数据,建议改 2。

三张超级表:一眼看穿结构
建模层一共三张超级表,各自服务不同场景:
telemetry(设备遥测):12 列 + 6 TAG。列是温度、湿度、电压、电流、功率、压力、流量、转速、振动等采样值,TAG 是 device_id、product_key、factory_id、workshop_id、device_type、region。
vehicle_track(车辆轨迹):9 列 + 4 TAG。列是经纬度、速度、方向、海拔、里程、报警码,TAG 是 vehicle_id、fleet_id、model、province。
device_event(设备事件):7 列 + 3 TAG。列是事件码、严重级别、指标名、指标值、消息内容、确认状态,TAG 是 device_id、factory_id、device_type。
规律已经浮现:TAG 描述"谁"和"在哪",列描述"发生了什么"。但具体怎么判断,看下一节。

TAG vs 列:三个问题帮你决策
这是建模最核心的决策点。判断一个字段该当 TAG 还是列,问自己三个问题。
问题一:这个值的基数有多高?
基数就是不同取值的数量。device_id 的基数是设备数,几百到几万;factory_id 的基数是工厂数,个位数到几十。这些值适中,适合当 TAG。
反过来,temperature 每个采样点都变,基数是采样次数,天文数字。这种字段必须当列。
问题二:查询时会不会固定按它过滤?
WHERE device_id = ? 是最高频的查询模式。TAG 的底层实现是"每个 TAG 组合对应一张子表",按 TAG 过滤等于直接定位子表,走索引,极快。
如果某个字段你从不当过滤条件,它大概率是列。
问题三:它的语义是静态属性还是动态采样?
设备型号、所属工厂、安装区域是静态属性,生命周期长,当 TAG。温度、电压、消息内容随时间变化,当列。
判断顺序就两句话:先问"哪些字段用于过滤、关联设备",再问"哪些字段随时间变化"。前者入 TAG,后者入列。

高基数陷阱:子表是怎么爆炸的
理解了 TAG 的机制,就明白高基数为什么危险。
每个 TAG 组合都会创建一张子表。假设你有 1000 台设备,每台设备有 10 种事件类型,把事件类型也当 TAG,子表数就是 1000 × 10 = 10000,看起来还好。
但如果你把 message 全文当 TAG(每条消息内容都不同),子表数就等于消息条数。每秒 100 条消息,一天就是 864 万张子表。TDengine 官方文档说明,一个 vnode 设计上可容纳的表数上限是 100 万,实践中一个 vgroup 建议只放 1 万到 10 万张子表------这直接爆掉。
子表爆炸的后果:元数据内存超限、写入变慢、查询退化。
怎么发现?监控 SHOW TABLES 的数量,如果子表数增速远超设备数增速,大概率是 TAG 设计出了问题。

子表命名算法:为什么不能直接用设备 ID
回到开头的乱码。看 common/models.py 里这段真实代码:
python
SAFE_IDENTIFIER = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]{0,191}$")
def table_name(self) -> str:
readable = re.sub(r"[^a-zA-Z0-9_]", "_", self.device_id).lower()
digest = hashlib.sha1(self.device_id.encode(), usedforsecurity=False).hexdigest()[:8]
candidate = f"d_{readable[:48]}_{digest}"
if not SAFE_IDENTIFIER.fullmatch(candidate):
raise ValueError("generated table name is invalid")
return candidate
输入是任意字符串的 device_id,可能含 .、-、空格、中文;输出是 d_{可读前缀[:48]}_{sha1[:8]}。三段各司其职:
可读前缀 :把 device_id 里的非法字符替换成 _,截断 48 字符,方便人眼排查。看到 d_workshop01_sensor_abc12345,你能立刻认出这是 workshop01 的传感器。
SHA-1 摘要 :取前 8 位十六进制,32 bit,碰撞概率约 2^-32,项目里设备数万级完全够用。摘要保证唯一性,同时天然规避 SQL 注入,因为十六进制字符只有 0-9a-f。
白名单正则兜底 :SAFE_IDENTIFIER 只允许字母开头、后续字母数字下划线、总长不超过 192。生成结果不合法直接抛异常,而不是写出非法 SQL 让数据库报错。
为什么不能直接用 device_id?三个原因:一是 SQL 标识符注入(device_id 含 ; 或引号);二是长度限制;三是大小写和转义歧义。设备 ID 是业务数据,子表名是数据库标识符,两者必须隔离。
车辆轨迹的命名更极端:v_{sha1[:10]},纯摘要 40 bit,没有可读前缀。没有排查需求,直接给最高熵,把碰撞概率压到 2^-40。

隐式建表 vs CREATE TABLE
002_examples.sql 里演示了显式建表:
sql
CREATE TABLE IF NOT EXISTS d_demo_001 USING telemetry TAGS (
'demo-001', 'temperature-sensor', 'factory-a', 'workshop-01', 'sensor', 'east'
);
INSERT INTO d_demo_001 VALUES (...);
但项目代码实际走的是隐式建表:INSERT INTO iot.d_smoke USING iot.telemetry TAGS (...) VALUES (...),写入时按超级表模板自动建子表。
为什么?设备上线时你根本不知道它什么时候来第一条数据。隐式建表让"设备首次上报"和"建表"原子完成,少一次往返,也不用手动管理设备生命周期。
显式建表只用于冒烟测试和特殊场景,比如预分配子表。
小结与预告
回顾三个决策点:
- 库参数:BUFFER 管写入吞吐,DURATION/KEEP 管数据生命周期,VGROUPS 管并发通道,COMP 管压缩率,每个数字都有明确职责。
- TAG vs 列:先问"哪些字段用于过滤",再问"哪些字段随时间变化"。前者入 TAG,后者入列。
- 子表命名:可读前缀加 SHA-1 摘要加白名单正则,同时解决可排查性、唯一性和安全性。
建模层是 TDengine 的地基。建模决策直接影响查询性能------第 3 篇进入查询层,用窗口聚合把原始采样变成可用的统计指标,到时候你会看到今天这些决策的实际效果。
你现在的项目里,有没有哪个字段当初设计成了 TAG,后来发现基数爆炸的?欢迎在评论区聊聊你的踩坑经历。