Hotdry.
systems-engineering

从零实现最小SQL数据库:解析器、执行器与简单存储

本文从头构建一个简易SQL数据库原型,涵盖SQL解析、查询执行和基本数据存储,适用于原型验证查询处理与数据管理。

构建一个最小 SQL 数据库的原型有助于开发者深入理解数据库的核心机制,包括查询解析、执行优化和数据持久化。这种从零开始的实现方式,能揭示 SQL 引擎的内部工作原理,避免将数据库视为黑盒子。在实际工程中,这样的原型可用于快速验证查询逻辑或教育目的,而非生产环境。

SQL 数据库的基本架构通常分为前端(Parser)和后端(Executor 与 Storage)。Parser 负责将用户输入的 SQL 字符串转换为抽象语法树(AST),Executor 则根据 AST 生成执行计划并操作数据,Storage 提供数据读写接口。借鉴经典教程如 SQLite 从零实现,我们可以简化这些组件,支持基本的 CREATE TABLE、INSERT、SELECT 语句。

首先,聚焦 Parser 的实现。Parser 是 SQL 引擎的入口,需要处理词法分析(Lexing)和语法分析(Parsing)。词法分析将 SQL 字符串拆分为 Token 序列,例如关键字(如 SELECT)、标识符(如表名)、操作符(如 =)和字面量(如 'value')。一个简单的 Lexer 可以使用状态机遍历字符串,识别这些 Token。

例如,在 Python 中实现 Lexer:

class Token:
    def __init__(self, type, value):
        self.type = type
        self.value = value

def lexer(sql):
    tokens = []
    i = 0
    while i < len(sql):
        if sql[i].isspace():
            i += 1
            continue
        if sql[i].isalpha():
            word = ''
            while i < len(sql) and sql[i].isalnum():
                word += sql[i]
                i += 1
            tokens.append(Token('KEYWORD' if word.upper() in ['SELECT', 'INSERT', 'CREATE', 'TABLE'] else 'ID', word))
            continue
        # 处理操作符、字符串等...
        if sql[i] == "'":
            i += 1
            val = ''
            while i < len(sql) and sql[i] != "'":
                val += sql[i]
                i += 1
            i += 1  # 跳过结束引号
            tokens.append(Token('STRING', val))
            continue
        # 类似处理数字、逗号等
    return tokens

这个 Lexer 支持基本 Token 识别。接下来是语法分析,使用递归下降解析器构建 AST。对于 CREATE TABLE 语句,AST 可能表示为树状结构:CreateTableNode (table_name, columns=[ColumnNode (name, type)])。

对于 SELECT 语句,AST 包括 SelectNode (columns, table, where_conditions)。一个简单的 Parser 函数可以消费 Token 列表,匹配语法规则:

def parse_select(tokens):
    if tokens[0].type == 'KEYWORD' and tokens[0].value.upper() == 'SELECT':
        # 解析列、FROM、WHERE
        pass
    # 返回AST节点

这种实现仅支持子集 SQL,避免复杂如 JOIN 或子查询。实际参数:Token 类型限制为 10 种以内,AST 节点类不超过 5 种,确保解析时间 O (n)。

证据显示,这种 Parser 设计在教程中已被验证有效。例如,在 cstack 的 SQLite 教程中,Lexer 使用有限状态机处理 SQL,Parser 生成字节码前体,支持高效执行。测试时,可用单元测试验证如 "SELECT * FROM users" 解析为正确 AST。

其次,Executor 负责执行 AST。它将 AST 转换为逻辑执行计划,然后物理执行。简单 Executor 可直接遍历 AST:对于 SELECT,扫描 Storage 中的表,应用 WHERE 过滤,投影所需列。

实现一个内存 - based Executor:

class Executor:
    def execute(self, ast, storage):
        if isinstance(ast, SelectNode):
            table = storage.get_table(ast.table)
            result = []
            for row in table.rows:
                if self.eval_where(ast.where, row):
                    projected = {col: row.get(col, None) for col in ast.columns}
                    result.append(projected)
            return result
    def eval_where(self, condition, row):
        # 简单条件评估,如 col = value
        return True  # 占位

这里,eval_where 处理基本谓词。优化点:对于常量过滤,先推下到 Storage 扫描。但在最小实现中,无需查询优化器,仅顺序扫描。参数建议:行缓冲区大小为 1000 行,WHERE 条件支持 =、>、< 三种操作符,回滚策略为简单事务(内存快照)。

从 key-value 存储启发,Executor 可集成简单索引加速扫描,但 prototype 中用全表扫描。风险:无索引时,O (n) 扫描对大表慢;限制造成:忽略 ACID,仅内存一致性。

最后,Storage 层管理数据持久化。最小设计使用内存字典存储表:{table_name: {columns: dict, rows: list of dicts}}。为持久化,序列化到 JSON 文件或二进制。

示例 Storage:

class Storage:
    def __init__(self, file_path):
        self.tables = {}
        self.file_path = file_path
        self.load()

    def create_table(self, name, columns):
        self.tables[name] = {'columns': columns, 'rows': []}
        self.persist()

    def insert(self, table, row):
        self.tables[table]['rows'].append(row)
        self.persist()

    def get_table(self, name):
        return self.tables.get(name)

    def persist(self):
        with open(self.file_path, 'w') as f:
            json.dump(self.tables, f)

    def load(self):
        try:
            with open(self.file_path, 'r') as f:
                self.tables = json.load(f)
        except FileNotFoundError:
            self.tables = {}

这个 Storage 支持基本 CRUD,文件路径如 'db.json'。参数:每 100 次写操作 flush 一次,减少 IO;支持 TEXT 和 INT 类型,长度限 20 字符。借鉴 nan.fyi 的 append-only 文件,可扩展为段文件,但 prototype 用 JSON 简单。

整合组件:REPL 循环读取 SQL,Parser 生成 AST,Executor 执行返回结果,Storage 持久数据。完整 prototype 约 500 行代码,用 Python 快速迭代。

可落地清单:

  1. 环境:Python 3.8+,无外部依赖。
  2. 测试用例:CREATE TABLE users (id INT, name TEXT); INSERT INTO users VALUES (1, 'Alice'); SELECT * FROM users;
  3. 监控点:解析时间 < 1ms,执行延迟 < 10ms for 1000 行。
  4. 回滚策略:内存事务,用 try-except 包装执行。
  5. 扩展:添加 B-tree 索引,支持 UPDATE/DELETE。

这种最小 SQL 数据库原型揭示了引擎本质:Parser 桥接用户与内部,Executor 桥接逻辑与物理,Storage 桥接内存与盘。虽简陋,却为深入如 LSM 树或优化器铺路。在工程中,用作教学工具或查询模拟器。

资料来源:

查看归档