【从零开始编写数据库系统:架构设计与实现】第3章 SQL解析:词法与语法分析

🔥 手写数据库第三弹!用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) 实现一个可解析SELECTINSERTCREATE 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包含:类型 (如SELECTID)、 (如'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:被查询的表名 users
    • columns:投影列列表(三个 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供后续模块消费。


五、扩展与总结

✅ 本章交付物

  1. 完整的词法分析器(Lexer),识别SQL Token。
  2. 完整的语法分析器(Parser),支持SELECT/INSERT/CREATE TABLE
  3. 抽象语法树(AST)定义与生成。
  4. 单元测试套件。

🧠 面试考点速记

  • Q:SQL解析分为哪两步?

  • A:词法分析(字符串→Token流)和语法分析(Token流→AST)。

  • Q :PLY中的t_ignorep_error作用?

  • At_ignore定义忽略字符(如空格),p_error处理语法错误。

🚀 下一章预告

第4章:元数据管理与系统目录 ------我们将用刚刚实现的存储引擎和解析器,构建数据库的"数据字典",让ToyDB能够真正CREATE TABLE并记住表结构!


📢 专栏福利

订阅本专栏,你将获得:

  • 简历上多一个"手写数据库内核"的硬核项目
  • 对数据库底层认识会更加深刻
  • 对数据库教学更有帮助
  • 后续完整的可运行代码

点击关注 ,第一时间获取更新!
点赞+收藏+转发,让更多同行看到这份硬核教程!

相关推荐
2401_835956812 小时前
SQL中如何查找特定的空值行:WHERE IS NULL深度解析
jvm·数据库·python
geBR OTTE2 小时前
开源的Text-to-SQL工具WrenAI
数据库·sql·开源
梦梦代码精2 小时前
LikeShop 深度测评:开源电商的务实之选
java·前端·数据库·后端·云原生·小程序·php
m0_588758482 小时前
MySQL如何通过MVCC提升并发读性能_理解undo log版本链
jvm·数据库·python
2401_835956812 小时前
Python Web应用负载均衡方案_结合Nginx权重设置实现高可用
jvm·数据库·python
yfhmmm2 小时前
【无标题】
数据库·postgresql
2401_871696522 小时前
CSS如何制作点击展开时的手风琴动画_平滑过渡max-height高度
jvm·数据库·python
Greyson12 小时前
C#怎么使用属性Property C#自动属性和完整属性的区别get set怎么用【基础】
jvm·数据库·python
Deitymoon3 小时前
嵌入式数据库——API创建
数据库·sql