Go语言从零构建SQL数据库(6)-*号的处理

番外:处理SQL通配符查询

在SQL中,SELECT * FROM table是最基础的查询之一,星号(*)是一个通配符,表示"选择所有列"。虽然通配符查询看起来简单,但在解析器中需要特殊处理。下面详细介绍我们如何实现这一常用功能。

1. 星号查询的挑战

星号与普通列名有本质区别:

  • 普通列名是标识符(如 idname
  • 星号是一个特殊符号,表示"全部"
  • 在解析时需要区别对待,不能简单视为标识符
flowchart LR SQL["SELECT * FROM users"] --> Lexer["词法分析器"] Lexer --> TokenStream["Token流"] TokenStream -->|"Token: *"| Parser["语法分析器"] Parser -->|"特殊处理"| AST["AST: AsteriskExpression"] style SQL fill:#f5f5f5,stroke:#333,stroke-width:2px style Lexer fill:#d4e6f1,stroke:#333,stroke-width:2px style TokenStream fill:#d5e8d4,stroke:#333,stroke-width:2px style Parser fill:#ffe6cc,stroke:#333,stroke-width:2px style AST fill:#fff2cc,stroke:#333,stroke-width:2px

普通列名vs星号的处理差异

特性 普通列名 星号
语法标记 标识符(IDENTIFIER) 特殊字符(ASTERISK)
AST节点 Identifier AsteriskExpression
解析方法 parseIdentifier() parseAsterisk()
语义验证 需要验证列存在性 不需要验证(表示所有列)
执行时处理 读取单个列 读取所有列

2. 解析器中的实现

为了支持星号查询,我们需要修改解析器的几个关键部分:

步骤1:定义AST节点

首先,创建一个专用的AST节点类型表示星号:

go 复制代码
// AsteriskExpression 表示SQL中的星号(*),用于表示选择所有列
type AsteriskExpression struct{}

func (a *AsteriskExpression) expressionNode() {}
func (a *AsteriskExpression) TokenLiteral() string { return "*" }
func (a *AsteriskExpression) String() string { return "*" }

这个简单的结构体实现了 Expression接口,可以作为SELECT语句的列表达式。

classDiagram class Expression { <> +expressionNode() +TokenLiteral() string +String() string } class Identifier { +Value string +expressionNode() +TokenLiteral() string +String() string } class AsteriskExpression { +expressionNode() +TokenLiteral() string +String() string } class LiteralExpression { +Value string +Type TokenType +expressionNode() +TokenLiteral() string +String() string } Expression <|-- Identifier Expression <|-- AsteriskExpression Expression <|-- LiteralExpression

步骤2:注册前缀解析函数

在解析器初始化时,为星号符号注册专门的解析函数:

go 复制代码
// 初始化解析器
func NewParser(l *lexer.Lexer) *Parser {
    p := &Parser{
        lexer:  l,
        errors: []string{},
    }

    // 注册前缀解析函数
    p.prefixParseFns = make(map[lexer.TokenType]prefixParseFn)
    // ... 其他注册
    p.registerPrefix(lexer.ASTERISK, p.parseAsterisk) // 添加对*的解析支持
  
    // ... 其他初始化
  
    return p
}

步骤3:实现星号解析函数

go 复制代码
// parseAsterisk 解析SELECT语句中的星号(*),表示选择所有列
func (p *Parser) parseAsterisk() (ast.Expression, error) {
    return &ast.AsteriskExpression{}, nil
}

这个函数非常简单,只需创建并返回一个 AsteriskExpression实例。

星号解析的处理流程

flowchart TD A["SQL: SELECT * FROM users"] --> B["词法分析: [SELECT, *, FROM, users]"] B --> C["遇到 * Token"] C --> D{"有注册的parseAsterisk函数?"} D -->|"是"| E["调用parseAsterisk()"] D -->|"否"| F["解析错误"] E --> G["创建AsteriskExpression"] G --> H["将AsteriskExpression添加到SelectStatement的Columns列表"] H --> I["继续解析FROM子句"] style A fill:#f5f5f5,stroke:#333,stroke-width:2px style E fill:#d5e8d4,stroke:#333,stroke-width:2px style G fill:#d5e8d4,stroke:#333,stroke-width:2px style F fill:#f8cecc,stroke:#333,stroke-width:2px

3. 执行时的处理

当查询执行器遇到 AsteriskExpression时,需要:

  1. 获取表的元数据信息,找出所有列
  2. 按顺序返回所有列的数据
  3. 保持列的原始顺序
go 复制代码
// 伪代码:执行器如何处理星号
func executeSelect(stmt *ast.SelectStatement, db *Database) *ResultSet {
    // ...
  
    // 处理列选择
    var columns []Column
    for _, colExpr := range stmt.Columns {
        switch expr := colExpr.(type) {
        case *ast.AsteriskExpression:
            // 星号表达式:获取表的所有列
            allColumns := db.GetAllColumns(stmt.TableName)
            columns = append(columns, allColumns...)
        case *ast.Identifier:
            // 普通列名:获取单个列
            column := db.GetColumn(stmt.TableName, expr.Value)
            columns = append(columns, column)
        // ... 其他表达式类型
        }
    }
  
    // ... 继续执行查询
}

星号查询的执行流程

flowchart TD A["SQL: SELECT * FROM users WHERE age > 18"] --> B["解析为AST"] B --> C["executeSelect()执行"] C --> D{"处理Columns列表"} D --> E{"是否为AsteriskExpression?"} E -->|"是"| F["获取表的所有列元数据"] E -->|"否"| G["处理单个列"] F --> H["添加所有列到结果集"] G --> H H --> I["应用WHERE过滤条件"] I --> J["返回结果集"] style A fill:#f5f5f5,stroke:#333,stroke-width:2px style B fill:#dae8fc,stroke:#333,stroke-width:2px style F fill:#d5e8d4,stroke:#333,stroke-width:2px style J fill:#ffe6cc,stroke:#333,stroke-width:2px

4. 星号查询的AST表示

对于 SELECT * FROM users WHERE age > 18;,完整的AST树结构如下:

graph TD A["SelectStatement"] --> B["Columns"] A --> C["TableName: 'users'"] A --> D["Where"] B --> E["AsteriskExpression"] D --> F["BinaryExpression"] F --> G["Left: Identifier{Value: 'age'}"] F --> H["Operator: GREATER"] F --> I["Right: LiteralExpression{Value: '18', Type: NUMBER}"] style A fill:#f5f5f5,stroke:#333,stroke-width:2px style B fill:#dae8fc,stroke:#333,stroke-width:2px style C fill:#dae8fc,stroke:#333,stroke-width:2px style D fill:#dae8fc,stroke:#333,stroke-width:2px style E fill:#d5e8d4,stroke:#333,stroke-width:2px style F fill:#ffe6cc,stroke:#333,stroke-width:2px

5. 高级应用场景

5.1 表格别名下的星号

表格别名与星号结合使用时,如 SELECT u.* FROM users u,需要特殊处理:

flowchart LR A["SELECT u.* FROM users u"] --> B["词法分析"] B --> C["Token流: [SELECT, u, ., *, FROM, users, u]"] C --> D["解析u.*"] D --> E["创建QualifiedAsteriskExpression"] E --> F["TablePrefix: 'u', Value: '*'"] style A fill:#f5f5f5,stroke:#333,stroke-width:2px style D fill:#d5e8d4,stroke:#333,stroke-width:2px style E fill:#d5e8d4,stroke:#333,stroke-width:2px

在这种情况下,我们需要一个特殊的AST节点 QualifiedAsteriskExpression 来表示带表格别名的星号:

go 复制代码
// QualifiedAsteriskExpression 表示带表格别名的星号,如 t.*
type QualifiedAsteriskExpression struct {
    TablePrefix string // 表前缀,如 t
}

func (q *QualifiedAsteriskExpression) expressionNode() {}
func (q *QualifiedAsteriskExpression) TokenLiteral() string { return q.TablePrefix + ".*" }
func (q *QualifiedAsteriskExpression) String() string { return q.TablePrefix + ".*" }

5.2 多表连接中的星号处理

在多表连接中,星号会引入列名冲突问题:

flowchart TD A["SELECT * FROM users u JOIN orders o ON u.id = o.user_id"] --> B["解析为AST"] B --> C["执行计划生成"] C --> D["检测到多表的*查询"] D --> E["获取所有表的列元数据"] E --> F["检查列名冲突"] F --> G{"存在冲突列名?"} G -->|"是"| H["生成完全限定列名(表名.列名)"] G -->|"否"| I["保留原列名"] H --> J["构建结果集"] I --> J style A fill:#f5f5f5,stroke:#333,stroke-width:2px style D fill:#ffe6cc,stroke:#333,stroke-width:2px style F fill:#ffe6cc,stroke:#333,stroke-width:2px style H fill:#d5e8d4,stroke:#333,stroke-width:2px

在多表连接的例子中,当使用星号时:

  • users 表可能有 id, name, email
  • orders 表可能有 id, user_id, product_id
  • 两个表都有 id 列,会导致名称冲突
  • 执行器需要生成如 u.id, o.id 的完全限定名

5.3 星号与列选择的混合使用

SQL还允许星号与特定列的混合使用,如 SELECT *, extra_column FROM table

flowchart LR A["SELECT *, created_at FROM users"] --> B["解析"] B --> C["AST: [AsteriskExpression, Identifier{Value: 'created_at'}]"] C --> D["执行"] D --> E["获取所有列 + 再次获取created_at"] E --> F["去重处理"] F --> G["返回结果"] style A fill:#f5f5f5,stroke:#333,stroke-width:2px style C fill:#d5e8d4,stroke:#333,stroke-width:2px style E fill:#ffe6cc,stroke:#333,stroke-width:2px style F fill:#ffe6cc,stroke:#333,stroke-width:2px

这种情况下,执行器需要:

  1. 先获取所有列
  2. 再处理单独指定的列
  3. 做重复列的去重处理
  4. 可能需要调整列的顺序

6. 性能优化与最佳实践

星号查询虽然方便,但存在一些性能和维护方面的注意事项:

6.1 性能影响

flowchart TD A["查询性能考虑"] --> B["SELECT * 查询"] B --> C["读取整行数据"] B --> D["增加I/O和内存使用"] B --> E["可能影响索引使用"] A --> F["SELECT 特定列查询"] F --> G["只读取需要的列"] F --> H["减少I/O和内存开销"] F --> I["更有效地利用覆盖索引"] style A fill:#f5f5f5,stroke:#333,stroke-width:2px style B fill:#ffe6cc,stroke:#333,stroke-width:2px style F fill:#d5e8d4,stroke:#333,stroke-width:2px style D fill:#f8cecc,stroke:#333,stroke-width:1px style H fill:#d5e8d4,stroke:#333,stroke-width:1px

6.2 代码维护性

使用星号的情况 使用具体列名的情况
代码简洁 代码明确表达了需要的数据
表结构变更时自动获取新列 不会因表结构变更意外获取新列
可能获取不需要的数据 只获取必要数据
列顺序依赖表定义 列顺序由查询指定
列重命名可能导致代码错误 列重命名会导致明确的错误

6.3 最佳实践建议

graph TD A["星号查询最佳实践"] --> B["适用场景"] A --> C["避免场景"] A --> D["折中方案"] B --> B1["探索性查询/调试"] B --> B2["需要获取行的完整信息"] B --> B3["ORM自动映射实体"] C --> C1["生产环境的高性能查询"] C --> C2["只需少量列的查询"] C --> C3["多表连接查询"] D --> D1["使用表别名限定: t.*"] D --> D2["视图中封装常用列组合"] D --> D3["ORM中配置列映射"] style A fill:#f5f5f5,stroke:#333,stroke-width:2px style B fill:#d5e8d4,stroke:#333,stroke-width:1px style C fill:#f8cecc,stroke:#333,stroke-width:1px style D fill:#dae8fc,stroke:#333,stroke-width:1px

7. 实际应用示例

7.1 探索性查询

在数据探索阶段,星号查询非常实用:

sql 复制代码
-- 快速了解表结构
SELECT * FROM users LIMIT 10;

-- 调试连接查询
SELECT * FROM orders o JOIN users u ON o.user_id = u.id LIMIT 5;

7.2 与聚合函数结合

星号有时与聚合函数结合使用:

sql 复制代码
-- 计算总行数
SELECT COUNT(*) FROM users WHERE status = 'active';

-- 注意:这里的*是特殊语法,不同于列选择中的*

这种情况下,COUNT(*)是一种特殊语法,表示"计算行数",而不是"计算所有列"。在解析器中需要特殊处理这种情况。

总结

星号通配符是SQL中最基础也是最常用的功能之一。尽管语法简单,但在实现上需要特殊处理,从词法分析、语法解析到查询执行的各个环节都有其独特之处。

通过本文介绍的实现方式,我们的SQL解析器现在完全支持通配符查询,不仅处理了基本的 SELECT * FROM table 形式,还能正确解析表别名限定的星号和多表连接中的星号用法。这使我们的解析器功能更加完整和实用,为下一步开发查询执行引擎奠定了基础。

在实际使用中,应根据具体场景权衡是否使用星号查询,以在便利性和性能之间取得平衡。

相关推荐
LucianaiB8 小时前
参加高德 AI 发布会的一点感受:地图,正在变成 AI 的行动入口
后端
属于自己的天空8 小时前
一个文件让 Claude Code 理解你的项目:CLAUDE.md 从入门到精通
后端
jiangbo_dev8 小时前
还在手搓分布式事务?我把 Saga + Outbox 模板化后,新服务接入从 5 天压到 1 天
后端
BING_Algorithm8 小时前
深入理解JVM垃圾回收
jvm·后端·面试
RainCity8 小时前
Java Swing 自定义组件库分享(六)
java·笔记·后端
techdashen8 小时前
深入 Rust enum 的内存世界
开发语言·后端·rust
龙码精神9 小时前
TimescaleDB 物联网设备属性历史数据表设计及常用SQL文档
后端
小小小小宇9 小时前
Go 后端锁机制详解
后端
挖坑的张师傅9 小时前
你的仓库 Agent Ready 了吗?
后端
客场消音器9 小时前
如何使用codex进行UI重构,让AI开发的前端页面不再千篇一律
前端·后端·微信小程序