分类 :4.查询引擎 | 篇章:02 Parser 与 Tokenizer

适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-06-08
SQL 解析是查询引擎的第一道工序。TDengine 使用 Lemon(与 SQLite 同源的 LALR(1) 生成器)构建解析器,将 SQL 文本逐字符切分为 Token,再按文法规则归约为抽象语法树(AST),为后续语义分析提供结构化输入。
核心概念速查表
| 概念 | 说明 |
|---|---|
| Tokenizer | 词法分析器,把字符流切成 Token |
| Token | 词法单元,如关键字、标识符、字面量、运算符 |
| Lemon | LALR(1) 解析器生成器,从 sql.y 生成解析代码 |
| AST | 抽象语法树,结构化的 SQL 内部表达 |
| Reserved Word | 保留字(如 SELECT、FROM、WHERE) |
| Identifier | 标识符(表名、列名、别名) |
详细解析
1. Tokenizer 工作原理
词法分析:字符 → Token 序列
输入 SQL:
SELECT ts, avg(current) FROM meters WHERE location='Beijing'
Tokenizer 输出:
┌─────────────┬───────────┬──────────────┐
│ Token Type │ Lexeme │ 备注 │
├─────────────┼───────────┼──────────────┤
│ KEYWORD │ SELECT │ 保留字 │
│ IDENTIFIER │ ts │ 列名 │
│ COMMA │ , │ 分隔符 │
│ IDENTIFIER │ avg │ 函数名 │
│ LPAREN │ ( │ │
│ IDENTIFIER │ current │ 列名 │
│ RPAREN │ ) │ │
│ KEYWORD │ FROM │ │
│ IDENTIFIER │ meters │ 表名 │
│ KEYWORD │ WHERE │ │
│ IDENTIFIER │ location │ │
│ EQ │ = │ │
│ STRING │ 'Beijing' │ 字面量 │
│ EOF │ <eof> │ 结束符 │
└─────────────┴───────────┴──────────────┘
2. Token 分类
| 类别 | 示例 | 说明 |
|---|---|---|
| 关键字 | SELECT, FROM, WHERE, GROUP | 大小写不敏感 |
| 标识符 | meters, ts, current | 表/列/别名 |
| 整数字面量 | 100, -42 | 含正负号 |
| 浮点字面量 | 3.14, 1.2e-3 | 科学计数法支持 |
| 字符串字面量 | 'Beijing', "value" | 单/双引号 |
| 时间字面量 | '2024-01-01 12:00:00', now-1h | 含时间常量 |
| 运算符 | +, -, *, /, =, <, >, AND, OR | |
| 分隔符 | , ;, (, ), , | |
| 注释 | -- comment, /* ... */ | 词法阶段被丢弃 |
3. 时间字面量的特殊处理
时序数据库的特色:时间常量
支持格式:
① 字符串时间:'2024-01-01 12:00:00.123'
→ 解析为 TIMESTAMP 类型
② 当前时间:now
→ 解析时绑定为当前时刻
③ 相对时间:now - 1h、now + 30m
→ 算术表达式,单位 b/u/a/s/m/h/d/w/n/y
④ 数值时间:1700000000000
→ 按精度(ms/us/ns)解释为时间戳
时间单位:
| 单位 | 含义 |
| b | 纳秒 |
| u | 微秒 |
| a | 毫秒 |
| s | 秒 |
| m | 分钟 |
| h | 小时 |
| d | 天 |
| w | 周 |
| n | 月 |
| y | 年 |
4. Lemon 语法分析
Lemon 工作流程:
① 文法定义文件(sql.y):
定义产生式规则,如:
select_stmt ::= SELECT select_list FROM table_ref where_opt
group_opt order_opt limit_opt
② Lemon 生成 LALR(1) 状态机:
编译期生成 sql.c(解析器代码)
③ 运行时执行:
- Tokenizer 产生 Token
- 解析器按文法规则归约(reduce)
- 每次归约调用对应的语义动作(构造 AST 节点)
5. AST 节点示例
AST 树形结构示例:
SQL: SELECT avg(c1) FROM t WHERE ts > '2024-01-01' GROUP BY t1
AST:
SelectStmt
├── selectList
│ └── FunctionExpr (name=avg)
│ └── ColumnRefExpr (name=c1)
├── from
│ └── TableRef (name=t)
├── where
│ └── BinaryOp (op=>)
│ ├── ColumnRefExpr (name=ts)
│ └── ValueExpr (timestamp)
└── groupBy
└── ColumnRefExpr (name=t1)
6. 错误处理与定位
词法/语法错误的报告:
错误 SQL: SELECT * FROM meters WHRE ts > 0
^^^^
Tokenizer 输出 WHRE 为 IDENTIFIER(不是关键字)
Parser 在期待 FROM 后跟 WHERE/GROUP/...的位置遇到 IDENTIFIER
报错示例:
syntax error near "WHRE ts > 0"
Error position: column 27
Expected: WHERE | GROUP | ORDER | LIMIT | ;
常见错误:
① 拼写错误(WHRE / SELCT / FORM)
② 引号不匹配
③ 括号不平衡
④ 关键字作为标识符(需要反引号转义)
7. 标识符的转义与命名规则
sql
-- 普通标识符:字母开头,含字母数字下划线
CREATE TABLE meters (ts TIMESTAMP, current FLOAT);
-- 含特殊字符 → 反引号转义
CREATE TABLE `my-table` (`column-1` INT);
-- 保留字作为列名 → 必须转义
CREATE TABLE t (`select` INT); -- 否则报语法错误
-- 大小写:
-- - 关键字大小写不敏感(SELECT = select)
-- - 标识符默认大小写不敏感(被自动转小写)
-- - 反引号包裹时保留原始大小写
8. 多语句与批量执行
分号分隔的多语句:
SQL: CREATE TABLE t (...); INSERT INTO t VALUES (...); SELECT * FROM t;
Parser 处理:
- 遇到 ; → 完成当前语句的 AST
- 继续解析下一语句
- 返回 AST 列表
执行:
- 按顺序逐条执行
- 任一失败 → 后续语句不再执行
- DDL 不在事务内(无回滚)
代码示例
触发解析错误
sql
-- 错误 1:缺少 FROM 子句
SELECT *;
-- syntax error near ";"
-- 错误 2:保留字未转义
SELECT key FROM t;
-- syntax error near "key"
-- 错误 3:字符串未闭合
SELECT * FROM t WHERE name = 'abc;
-- unterminated string literal
-- 正确写法
SELECT `key` FROM t;
SELECT * FROM t WHERE name = 'abc';
解析时间字面量
sql
-- 多种时间写法都合法
SELECT * FROM meters WHERE ts > '2024-01-01';
SELECT * FROM meters WHERE ts > '2024-01-01 12:00:00.123';
SELECT * FROM meters WHERE ts > now;
SELECT * FROM meters WHERE ts > now - 1h;
SELECT * FROM meters WHERE ts > 1700000000000; -- 毫秒时间戳
SELECT * FROM meters WHERE ts BETWEEN '2024-01-01' AND now;
性能考量
Parser 耗时
| 因素 | 影响 |
|---|---|
| SQL 长度 | 大 IN 列表(10万元素)可能耗时 100ms+ |
| AST 节点数 | 复杂 WHERE 嵌套增加节点数 |
| Parser 是否缓存 | 客户端可缓存 PreparedStatement 避免重复解析 |
Parser 优化建议
| 场景 | 建议 |
|---|---|
| 高频相同结构查询 | 使用 STMT(参数化)避免重复解析 |
| 超长 IN 列表 | 改写为 JOIN 或 Tag 索引查询 |
| 多语句批处理 | 一次 RPC 提交多条 SQL,减少网络往返 |
FAQ
Q1: SQL 大小写敏感吗?
- 关键字:不敏感(
SELECT=select) - 表名/列名:默认不敏感(内部转小写)
- 反引号包裹的标识符:敏感(
MyTable与mytable不同) - 字符串值:敏感(
'A'≠'a')
Q2: 为什么 NOW 写在不同位置结果可能不同?
NOW 在 Parser 阶段绑定为当时的时刻。如果 SQL 解析延迟较长(如客户端排队),NOW 的时刻会有偏差。但同一条 SQL 内多次出现的 NOW 是同一个值(解析时一次绑定)。
Q3: 注释会被发送到服务端吗?
不会。Tokenizer 阶段就会丢弃 -- 和 /* */ 注释。注释只在客户端起作用。
Q4: 如何让自定义关键字不冲突?
TDengine 持续扩展 SQL 语法,新版本可能将之前的标识符变为保留字。建议生产代码:
- 业务命名避免使用 SQL 标准关键字
- 对可能成为关键字的名字(如
count、order)一律用反引号
参考
系统构架篇
- 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 提供实时分析、可视化、事件管理与报警等功能。