TDengine SQL 解析与词法分析 — 从字符串到 AST 的转换之路

分类 :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
  • 表名/列名:默认不敏感(内部转小写)
  • 反引号包裹的标识符:敏感(MyTablemytable 不同)
  • 字符串值:敏感('A''a'

Q2: 为什么 NOW 写在不同位置结果可能不同?

NOW 在 Parser 阶段绑定为当时的时刻。如果 SQL 解析延迟较长(如客户端排队),NOW 的时刻会有偏差。但同一条 SQL 内多次出现的 NOW 是同一个值(解析时一次绑定)。

Q3: 注释会被发送到服务端吗?

不会。Tokenizer 阶段就会丢弃 --/* */ 注释。注释只在客户端起作用。

Q4: 如何让自定义关键字不冲突?

TDengine 持续扩展 SQL 语法,新版本可能将之前的标识符变为保留字。建议生产代码:

  1. 业务命名避免使用 SQL 标准关键字
  2. 对可能成为关键字的名字(如 countorder)一律用反引号

参考

系统构架篇

数据模型

存储引擎

查询引擎

关于 TDengine

TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。

相关推荐
StarRocks_labs2 小时前
StarRocks × Iceberg:联邦查询实践解析
数据库·starrocks·sql·iceberg·物化视图
IvorySQL2 小时前
PostgreSQL 技术日报 (6月7日)|峰会线上通道开放
数据库·postgresql
熊文豪2 小时前
SQL并行查询优化实践:从执行计划看并行能力的正确使用
数据库·sql·电科金仓
Ze3G90nYt2 小时前
Redis 分布式锁进阶第一百二十篇
数据库·redis·分布式
华山令狐虫2 小时前
DBAPI 接入 Milvus 向量数据库:HTTP 执行器参数映射实战
数据库·http·milvus·dbapi
Fuly10242 小时前
LangGraph学习-(1)跑通一个最小状态图
数据库·学习
段一凡-华北理工大学2 小时前
工业领域的Hadoop架构学习~系列文章19:能源行业Hadoop应用实践
大数据·人工智能·hadoop·分布式·学习·架构·高炉炼铁
计算机安禾2 小时前
【数据库系统原理】第5篇:关系的完整性约束:实体、参照与用户定义的逻辑守卫
数据库·oracle
snow@li2 小时前
数据库:Schema = 数据库的“蓝图“或“命名空间“
数据库