分类 :4.查询引擎 | 篇章:03 语义分析

适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-06-09
语义分析(Translator)是连接 Parser 与 Planner 的桥梁。它验证 SQL 在语义上是否合法(表是否存在、列类型是否匹配、聚合函数使用是否正确等),并对 AST 做规范化重写,为后续计划生成准备一棵"干净"的逻辑表达。
核心概念速查表
| 概念 | 说明 |
|---|---|
| Catalog | 客户端缓存的数据库元数据(库/表/列/Tag) |
| Type Check | 类型推导与类型一致性校验 |
| Name Resolution | 标识符绑定(列名→具体表的某列) |
| Constant Folding | 常量折叠(编译期计算表达式) |
| AST Rewrite | 重写 AST(展开 *、补默认列等) |
| Implicit Cast | 隐式类型转换 |
详细解析
1. 语义分析的整体流程
Translator 五大步骤:
AST (Parser 输出)
│
▼
① Catalog 查询:
- 查询 Mnode 获取库/表/Schema/VGroup 信息
- 缓存到客户端 Catalog Cache(避免重复请求)
│
▼
② 名字解析(Name Resolution):
- 表名 → 实际表对象
- 列名 → 具体表的列
- 处理别名(AS)
- 解决歧义(多表 JOIN 时的同名列)
│
▼
③ 类型推导与校验:
- 函数参数类型校验
- 二元运算的类型兼容性
- 必要时插入隐式 CAST
│
▼
④ 语义规则校验:
- 聚合函数使用规则
- GROUP BY 与 SELECT 列的一致性
- 时序专属语法的合法性
│
▼
⑤ AST 重写:
- 展开 *
- 常量折叠
- 子查询展开
- 视图替换
│
▼
净化后的 AST → 交给 Planner
2. Catalog 元数据获取
Catalog 缓存机制:
客户端首次查询某表:
SELECT * FROM power.meters WHERE ts > now-1h
│
▼
Catalog Cache 查找 "power.meters"
│
┌────┴─────┐
│ 未命中 │ 命中 ↓
▼ │
请求 Mnode │
│ │
▼ ▼
获取: 使用缓存
- Schema - Schema 版本号
- VGroup 分布 - 检查是否需要刷新
- Tag 元数据
│
▼
写入 Cache(带 TTL)
缓存失效场景:
- ALTER TABLE 修改 Schema(版本号变化)
- DROP TABLE
- 主动 RESET QUERY CACHE
3. 名字解析示例
列名绑定过程:
SQL: SELECT t.ts, t.current
FROM meters t
WHERE t.location='BJ'
Translator 处理:
① t → 解析为 meters 的别名
② t.ts → 绑定到 meters.ts (TIMESTAMP)
③ t.current → 绑定到 meters.current (FLOAT)
④ t.location → 绑定到 meters.location (BINARY) [Tag]
歧义示例:
SELECT id FROM t1 JOIN t2 ON t1.id=t2.id
→ 报错:column 'id' is ambiguous
→ 必须写 t1.id 或 t2.id
星号展开:
SELECT * FROM meters
→ 展开为列出所有列:ts, current, voltage, ...
→ 不包含 Tag(需要写 SELECT *, location)
4. 类型推导与隐式转换
类型推导规则:
① 二元运算的结果类型:
INT + INT → INT
INT + FLOAT → FLOAT
FLOAT * DOUBLE → DOUBLE
VARCHAR + VARCHAR → 错误(不支持字符串拼接)
② 比较运算的类型对齐:
ts > '2024-01-01'
→ 字符串自动转 TIMESTAMP
current > 5
→ 整数 5 转 FLOAT(与 current 类型对齐)
③ 函数参数类型:
AVG(VARCHAR) → 错误(AVG 只能用于数值类型)
CONCAT(INT, VARCHAR) → INT 自动转 VARCHAR
| 显式 CAST 语法 | 示例 |
|---|---|
| CAST(expr AS TYPE) | CAST(ts AS BIGINT) |
| 类型转换函数 | TO_CHAR(ts, 'yyyy-mm-dd') |
5. 聚合函数的语义校验
SELECT 列与聚合函数的关系校验:
规则:SELECT 列表中的非聚合列必须出现在 GROUP BY 中
✓ 合法:
SELECT location, AVG(current) FROM meters GROUP BY location
✗ 非法:
SELECT location, current, AVG(current) FROM meters GROUP BY location
→ location 在 GROUP BY 中 ✓
→ current 既不是聚合也不在 GROUP BY ✗
→ 报错:column must appear in GROUP BY
特殊:
SELECT TBNAME, LAST(*) FROM meters GROUP BY TBNAME
→ TBNAME 在 GROUP BY → 合法
→ LAST(*) 是聚合 → 合法
时序专属:
SELECT _wstart, _wend, COUNT(*) FROM meters
INTERVAL(1h)
→ 窗口函数下,_wstart/_wend 是隐式分组键
→ 不需要写在 GROUP BY 中
6. AST 重写示例
常见的重写规则:
① 星号展开:
SELECT * FROM meters
→ SELECT ts, current, voltage, phase FROM meters
② 常量折叠:
SELECT * FROM t WHERE ts > now - 60*60*1000
→ SELECT * FROM t WHERE ts > 1700000000000
③ 谓词重写:
WHERE NOT (a < 5)
→ WHERE a >= 5
④ 子查询展开(简单情况):
SELECT * FROM (SELECT * FROM t WHERE c>0) WHERE c<10
→ SELECT * FROM t WHERE c>0 AND c<10
⑤ 视图替换:
CREATE VIEW v AS SELECT * FROM t WHERE c>0;
SELECT * FROM v WHERE c<10
→ SELECT * FROM t WHERE c>0 AND c<10
7. 时序专属语法的语义检查
时序场景的特殊校验:
① INTERVAL 窗口校验:
SELECT COUNT(*) FROM meters INTERVAL(1h) SLIDING(2h)
→ 错误:SLIDING 必须 ≤ INTERVAL
② FILL 子句校验:
SELECT AVG(c) FROM t WHERE ts > ... FILL(LINEAR)
→ 错误:FILL 必须与 INTERVAL/EVENT_WINDOW 等窗口一起使用
③ PARTITION BY 校验:
SELECT _wstart, COUNT(*) FROM meters
PARTITION BY tbname
INTERVAL(1h)
→ 合法
④ STATE_WINDOW 列校验:
SELECT COUNT(*) FROM meters STATE_WINDOW(status)
→ status 必须是非数值/字符串类型,或离散值
→ 不能用 TIMESTAMP / FLOAT
8. Tag 过滤的特殊处理
Tag 过滤的优化路径:
SQL: SELECT * FROM meters
WHERE location='Beijing' AND ts > now-1h
Translator 识别:
- location 是 Tag
- ts 是普通列(数据列)
分离过滤条件:
Tag 过滤:location='Beijing'
Data 过滤:ts > now-1h
Tag 过滤的提前处理:
① 调用 Catalog/META 的 Tag 索引
② 返回符合条件的子表 uid 列表
③ 后续 Scan 只针对这些 uid
效果:
- 100 万子表中只有 1 万属于 'Beijing'
- 跳过 99 万子表的数据文件读取
代码示例
触发常见语义错误
sql
-- 错误 1:表不存在
SELECT * FROM nonexistent_table;
-- error: Table does not exist
-- 错误 2:列名歧义
SELECT id FROM t1, t2;
-- error: ambiguous column name 'id'
-- 错误 3:聚合规则违反
SELECT location, current FROM meters GROUP BY location;
-- error: 'current' must appear in GROUP BY
-- 错误 4:类型不兼容
SELECT current + name FROM meters;
-- error: invalid operation between FLOAT and VARCHAR
利用语义优化提速
sql
-- 推荐:常量在右侧 + 时间范围 + Tag 过滤
SELECT * FROM meters
WHERE location='Beijing'
AND ts > now - 1h
AND current > 10;
-- ✓ Tag 过滤提前
-- ✓ 时间裁剪 File Set
-- ✓ 数据过滤下推到 Scan
-- 不推荐:函数包裹列字段
SELECT * FROM meters WHERE DATE(ts) = '2024-01-01';
-- ✗ ts 被函数包裹 → 无法使用时间裁剪
Catalog Cache 控制
sql
-- 手动刷新 Catalog 缓存
RESET QUERY CACHE;
-- 查看缓存命中情况(系统表)
SELECT * FROM performance_schema.perf_apps;
性能考量
Catalog 请求开销
| 场景 | 开销 |
|---|---|
| 首次查询新表 | 1 次 Mnode RPC(~10ms) |
| 命中 Cache | 微秒级(本地哈希查找) |
| Schema 变更后首次查询 | 重新拉取,~10ms |
| 跨大量库/表的复杂查询 | 多次 Catalog 请求叠加 |
语义分析优化建议
| 场景 | 建议 |
|---|---|
| 高频查询 | 使用 STMT 避免重复语义分析 |
| 程序生成 SQL | 表/列名预校验,避免运行时失败 |
| 大量 IN 列表 | 拆分多次查询或改 Tag 查询 |
FAQ
Q1: Catalog 缓存什么时候自动失效?
服务端 Schema 变更后,下一次查询请求会通过 Mnode 检测版本号不一致 → 自动拉取新 Schema → 更新本地 Cache。客户端无需手动干预。
Q2: SELECT * 会展开 Tag 吗?
不会。SELECT * 只展开普通数据列。如需 Tag,必须显式写出:SELECT *, location, groupid FROM meters,或使用 SELECT TAGS *(部分版本支持)。
Q3: 隐式 CAST 会损失精度吗?
可能。如 BIGINT → FLOAT 在大数值时会丢失精度。FLOAT → INT 会截断小数。推荐显式 CAST 并预判精度影响。
Q4: 子查询会被优化吗?
简单子查询会被展开(合并到外层)。复杂子查询(含聚合、不同窗口、CTE)会保留并生成嵌套计划。语义分析阶段不做优化决策,由 Planner 决定是否展开。
参考
系统构架篇
- 01-《TDengine 整体架构全景》
- 02-《集群拓扑深度解析》
- 03-《MNode 内部机制深度解析》
- 04-《RPC 通信层深度解析》
- 05-《VNode 生命周期》
- 06-《RAFT 共识协议》
- 07-《端到端的消息流》
数据模型
- 01-《数据库创建与参数详解》
- 02-《超级表/子表/普通表》
- 03-《支持数据类型深度解析》
- 04-《TDengine Tag 设计哲学与 Schema 变更机制》
- 05-《TDengine 虚拟表实现原理》
存储引擎
- 01-《TDengine 存储引擎概览》
- 02-《TDengine MemTable 深度解析》
- 03-《TDengine WAL 预写日志机制》
- 04-《TDengine 数据文件格式》
- 05-《TDengine Commit 与 Flush 机制 》
- 06-《TDengine Compaction 合并策略 》
- 07-《TDengine 数据保留与 TTL》
- 08-《TDengine 压缩编码机制》
- 09-《TDengine Cache 与 Last 查询加速》
- 10-《TDengine 数据修复与迁移》
查询引擎
关于 TDengine
TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。