🔥 手写数据库第三弹!用200行Python撸一个SQL解析器,看懂SELECT背后的词法语法分析
导语
你每天写
SELECT * FROM users WHERE id=1,有没有想过数据库是怎么"读懂"这行字符串的?本章我们将用PLY 库从零实现ToyDB的SQL解析器,涵盖词法分析 和语法分析 ,把SQL字符串变成结构化的抽象语法树(AST)。读完你不仅能答出面试题"SQL解析流程",还能自己动手写一个迷你SQL Parser!
一、SQL解析:数据库的"阅读理解"
数据库处理一条SQL的完整流程如下:
查询前端
SQL字符串
词法分析器 Lexer
Token流
语法分析器 Parser
抽象语法树 AST
语义分析
查询优化
执行引擎
词法分析 负责将字符流切分成Token(如SELECT、*、FROM), 语法分析则根据SQL语法规则,将Token序列组装成一棵AST树。
本章我们聚焦前端部分,用Python的 PLY(Python Lex-Yacc) 实现一个可解析SELECT、INSERT、CREATE TABLE的解析器。
项目结构如下所示:
shell
toydb_chapter_03/
├── src/
│ └── query/
│ ├── __init__.py
│ ├── ast.py # AST节点定义
│ ├── lexer.py # 词法分析器
│ └── parser.py # 语法分析器
├── tests/
│ └── test_parser.py # 单元测试
├── main.py # 主程序入口
└── README.md # 本文件

main.py程序运行结果预览:
shell
████████████████████████████████████████████████████████████
█ ToyDB SQL解析器演示 - 第3章
████████████████████████████████████████████████████████████
============================================================
SQL语句: SELECT id, name, age FROM users WHERE age > 18;
============================================================
============================================================
【步骤1】词法分析 - Token流
============================================================
LexToken(SELECT,'SELECT',1,0)
LexToken(ID,'id',1,7)
LexToken(COMMA,',',1,9)
LexToken(ID,'name',1,11)
LexToken(COMMA,',',1,15)
LexToken(ID,'age',1,17)
LexToken(FROM,'FROM',1,21)
LexToken(ID,'users',1,26)
LexToken(WHERE,'WHERE',1,32)
LexToken(ID,'age',1,38)
LexToken(GT,'>',1,42)
LexToken(NUMBER,18,1,44)
LexToken(SEMICOLON,';',1,46)
共生成 13 个Token
============================================================
【步骤2】语法分析 - 生成AST
============================================================
✅ 解析成功!AST结构:
SelectStmt
table: users
columns:
ColumnRef: id
ColumnRef: name
ColumnRef: age
where:
BinaryOp: >
ColumnRef: age
Literal: 18
============================================================
【步骤3】AST结构摘要
============================================================
根节点类型: SelectStmt
涉及表: users
投影列: ['id', 'name', 'age']
包含WHERE条件
────────────────────────────────────────────────────────────
============================================================
SQL语句: INSERT INTO users (id, name, score) VALUES (1, 'Alice', 95.5);
============================================================
============================================================
【步骤1】词法分析 - Token流
============================================================
LexToken(INSERT,'INSERT',1,0)
LexToken(INTO,'INTO',1,7)
LexToken(ID,'users',1,12)
LexToken(LPAREN,'(',1,18)
LexToken(ID,'id',1,19)
LexToken(COMMA,',',1,21)
LexToken(ID,'name',1,23)
LexToken(COMMA,',',1,27)
LexToken(ID,'score',1,29)
LexToken(RPAREN,')',1,34)
LexToken(VALUES,'VALUES',1,36)
LexToken(LPAREN,'(',1,43)
LexToken(NUMBER,1,1,44)
LexToken(COMMA,',',1,45)
LexToken(STRING,'Alice',1,47)
LexToken(COMMA,',',1,54)
LexToken(FLOAT_NUM,95.5,1,56)
LexToken(RPAREN,')',1,60)
LexToken(SEMICOLON,';',1,61)
共生成 19 个Token
============================================================
【步骤2】语法分析 - 生成AST
============================================================
✅ 解析成功!AST结构:
InsertStmt
table: users
columns: ['id', 'name', 'score']
values:
Literal: 1
Literal: 'Alice'
Literal: 95.5
============================================================
【步骤3】AST结构摘要
============================================================
根节点类型: InsertStmt
目标表: users
插入列数: 3
值数量: 3
────────────────────────────────────────────────────────────
============================================================
SQL语句: CREATE TABLE products (id INT NOT NULL, name VARCHAR(100), price FLOAT);
============================================================
============================================================
【步骤1】词法分析 - Token流
============================================================
LexToken(CREATE,'CREATE',1,0)
LexToken(TABLE,'TABLE',1,7)
LexToken(ID,'products',1,13)
LexToken(LPAREN,'(',1,22)
LexToken(ID,'id',1,23)
LexToken(INT,'INT',1,26)
LexToken(NOT,'NOT',1,30)
LexToken(NULL,'NULL',1,34)
LexToken(COMMA,',',1,38)
LexToken(ID,'name',1,40)
LexToken(VARCHAR,'VARCHAR',1,45)
LexToken(LPAREN,'(',1,52)
LexToken(NUMBER,100,1,53)
LexToken(RPAREN,')',1,56)
LexToken(COMMA,',',1,57)
LexToken(ID,'price',1,59)
LexToken(FLOAT,'FLOAT',1,65)
LexToken(RPAREN,')',1,70)
LexToken(SEMICOLON,';',1,71)
共生成 19 个Token
============================================================
【步骤2】语法分析 - 生成AST
============================================================
✅ 解析成功!AST结构:
CreateTableStmt
table: products
columns:
ColumnDef: id INT NULL
ColumnDef: name VARCHAR(100) NOT NULL
ColumnDef: price FLOAT NOT NULL
============================================================
【步骤3】AST结构摘要
============================================================
根节点类型: CreateTableStmt
创建表: products
列数量: 3
────────────────────────────────────────────────────────────
============================================================
SQL语句: SELECT * FROM orders WHERE total >= 100.0 AND status = 'paid';
============================================================
============================================================
【步骤1】词法分析 - Token流
============================================================
LexToken(SELECT,'SELECT',1,0)
LexToken(STAR,'*',1,7)
LexToken(FROM,'FROM',1,9)
LexToken(ID,'orders',1,14)
LexToken(WHERE,'WHERE',1,21)
LexToken(ID,'total',1,27)
LexToken(GE,'>=',1,33)
LexToken(FLOAT_NUM,100.0,1,36)
LexToken(AND,'AND',1,42)
LexToken(ID,'status',1,46)
LexToken(EQ,'=',1,53)
LexToken(STRING,'paid',1,55)
LexToken(SEMICOLON,';',1,61)
共生成 13 个Token
============================================================
【步骤2】语法分析 - 生成AST
============================================================
✅ 解析成功!AST结构:
SelectStmt
table: orders
columns:
Star
where:
BinaryOp: AND
BinaryOp: >=
ColumnRef: total
Literal: 100.0
BinaryOp: =
ColumnRef: status
Literal: 'paid'
============================================================
【步骤3】AST结构摘要
============================================================
根节点类型: SelectStmt
涉及表: orders
投影列: ['*']
包含WHERE条件
────────────────────────────────────────────────────────────
✅ 所有示例执行完毕!
Process finished with exit code 0
📊 输出结果解读
以第一条SQL语句 SELECT id, name, age FROM users WHERE age > 18; 为例,程序输出分为三个步骤:
步骤1:词法分析 - Token流
LexToken(SELECT,'SELECT',1,0)
LexToken(ID,'id',1,7)
LexToken(COMMA,',',1,9)
LexToken(ID,'name',1,11)
LexToken(COMMA,',',1,15)
LexToken(ID,'age',1,17)
LexToken(FROM,'FROM',1,21)
LexToken(ID,'users',1,26)
LexToken(WHERE,'WHERE',1,32)
LexToken(ID,'age',1,38)
LexToken(GT,'>',1,42)
LexToken(NUMBER,18,1,44)
LexToken(SEMICOLON,';',1,46)
解读:
- 词法分析器将输入字符串切分为有意义的词法单元(Token)。
- 每个Token包含:类型 (如
SELECT、ID)、值 (如'id'、18)、行号 和列位置。 - 例如
LexToken(ID,'users',1,26)表示在第1行第26列识别到一个标识符users。
步骤2:语法分析 - 生成AST
✅ 解析成功!AST结构:
SelectStmt
table: users
columns:
ColumnRef: id
ColumnRef: name
ColumnRef: age
where:
BinaryOp: >
ColumnRef: age
Literal: 18
解读:
- 语法分析器根据SQL语法规则,将Token序列组装成一棵抽象语法树(AST)。
- 根节点为
SelectStmt,包含:table:被查询的表名userscolumns:投影列列表(三个ColumnRef)where:条件表达式(BinaryOp节点,操作符为>,左操作数为列引用age,右操作数为字面量18)
步骤3:AST结构摘要
根节点类型: SelectStmt
涉及表: users
投影列: ['id', 'name', 'age']
包含WHERE条件
解读:
- 程序提取AST的关键信息,便于快速理解SQL语句的意图。
- 后续章节(语义分析、查询优化)将基于此AST进行进一步处理。
二、词法分析器:把字符串切成Token
2.1 Token分类
| 类别 | 示例 | 正则描述 |
|---|---|---|
| 关键字 | SELECT, FROM, WHERE | 保留字 |
| 标识符 | users, id | [a-zA-Z_][a-zA-Z0-9_]* |
| 数字 | 123, 3.14 | \d+ 或 \d+\.\d+ |
| 字符串 | 'Alice' | '[^']*' |
| 运算符 | =, <>, >, +, - | 固定字符 |
| 分隔符 | (, ), , ; | 固定字符 |
2.2 PLY Lexer实现:构建词法分析器
python
"""
SQL词法分析器
基于PLY实现
"""
import ply.lex as lex
# 保留关键字
reserved = {
'SELECT': 'SELECT',
'FROM': 'FROM',
'WHERE': 'WHERE',
'INSERT': 'INSERT',
'INTO': 'INTO',
'VALUES': 'VALUES',
'CREATE': 'CREATE',
'TABLE': 'TABLE',
'DROP': 'DROP',
'INT': 'INT',
'VARCHAR': 'VARCHAR',
'BOOL': 'BOOL',
'FLOAT': 'FLOAT',
'AND': 'AND',
'OR': 'OR',
'NOT': 'NOT',
'NULL': 'NULL',
'TRUE': 'TRUE',
'FALSE': 'FALSE',
}
# Token列表
tokens = [
'ID', 'NUMBER', 'STRING', 'FLOAT_NUM',
'EQ', 'NE', 'LE', 'GE', 'LT', 'GT',
'PLUS', 'MINUS', 'TIMES', 'DIVIDE',
'LPAREN', 'RPAREN', 'COMMA', 'SEMICOLON', 'STAR',
] + list(reserved.values())
# 简单Token的正则规则
t_EQ = r'='
t_NE = r'<>|!='
t_LE = r'<='
t_GE = r'>='
t_LT = r'<'
t_GT = r'>'
t_PLUS = r'\+'
t_MINUS = r'-'
t_DIVIDE = r'/'
t_LPAREN = r'\('
t_RPAREN = r'\)'
t_COMMA = r','
t_SEMICOLON = r';'
# 忽略空白字符
t_ignore = ' \t\n'
def t_STAR(t):
r'\*'
# 区分乘号还是SELECT *
# 这里简化,均作为STAR处理,后续语义分析时区分
return t
def t_FLOAT_NUM(t):
r'\d+\.\d+'
t.value = float(t.value)
return t
def t_NUMBER(t):
r'\d+'
t.value = int(t.value)
return t
def t_STRING(t):
r'\'[^\']*\''
t.value = t.value[1:-1] # 去除引号
return t
def t_ID(t):
r'[a-zA-Z_][a-zA-Z0-9_]*'
# 检查是否为关键字
t.type = reserved.get(t.value.upper(), 'ID')
return t
def t_error(t):
print(f"词法错误: 非法字符 '{t.value[0]}' 在位置 {t.lexpos}")
t.lexer.skip(1)
# 构建词法分析器
lexer = lex.lex()
三、语法分析器:构建AST
3.1 AST节点设计
ASTNode
SelectStmt
+columns: list
+table: str
+where: Expression
InsertStmt
+table: str
+columns: list
+values: list
CreateTableStmt
+table: str
+columns: list
Expression
ColumnRef
+name: str
Literal
+value: any
BinaryOp
+op: str
+left: Expression
+right: Expression
3.2 语法规则(BNF)
bnf
select_stmt ::= SELECT select_list FROM ID [ WHERE expr ]
expr ::= ID | literal | expr op expr | '(' expr ')'
insert_stmt ::= INSERT INTO ID [ '(' column_list ')' ] VALUES '(' value_list ')'
create_stmt ::= CREATE TABLE ID '(' column_def_list ')'
3.3 PLY Yacc实现
python
def p_select_stmt(p):
'''select_stmt : SELECT select_list FROM ID opt_where'''
p[0] = SelectStmt(columns=p[2], table=p[4], where=p[5])
def p_expr_binary(p):
'''expr : expr EQ expr
| expr AND expr
| expr PLUS expr'''
p[0] = BinaryOp(p[2], p[1], p[3])
def p_literal(p):
'''literal : NUMBER | STRING | TRUE | FALSE | NULL'''
p[0] = Literal(p[1])
3.4 解析示例
SQL:
sql
SELECT id, name FROM users WHERE age > 18;
生成的AST结构:
SelectStmt
columns
ColumnRef: id
ColumnRef: name
table: 'users'
where: BinaryOp
op: '>'
ColumnRef: age
Literal: 18
四、测试与集成
编写单元测试验证解析正确性:
python
def test_select():
sql = "SELECT id FROM users WHERE id = 1;"
ast = parser.parse(sql)
assert ast.table == 'users'
assert isinstance(ast.where, BinaryOp)
assert ast.where.op == '='
至此,ToyDB可以解析三类基础SQL,输出结构化的AST供后续模块消费。
五、扩展与总结
✅ 本章交付物
- 完整的词法分析器(Lexer),识别SQL Token。
- 完整的语法分析器(Parser),支持
SELECT/INSERT/CREATE TABLE。 - 抽象语法树(AST)定义与生成。
- 单元测试套件。
🧠 面试考点速记
-
Q:SQL解析分为哪两步?
-
A:词法分析(字符串→Token流)和语法分析(Token流→AST)。
-
Q :PLY中的
t_ignore和p_error作用? -
A :
t_ignore定义忽略字符(如空格),p_error处理语法错误。
🚀 下一章预告
第4章:元数据管理与系统目录 ------我们将用刚刚实现的存储引擎和解析器,构建数据库的"数据字典",让ToyDB能够真正CREATE TABLE并记住表结构!
📢 专栏福利
订阅本专栏,你将获得:
- 简历上多一个"手写数据库内核"的硬核项目
- 对数据库底层认识会更加深刻
- 对数据库教学更有帮助
- 后续完整的可运行代码
点击关注 ,第一时间获取更新!
点赞+收藏+转发,让更多同行看到这份硬核教程!