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 形式,还能正确解析表别名限定的星号和多表连接中的星号用法。这使我们的解析器功能更加完整和实用,为下一步开发查询执行引擎奠定了基础。

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

相关推荐
追逐时光者30 分钟前
6种流行的 API 架构风格,你知道几种?
后端
小麦果汁吨吨吨1 小时前
Flask快速入门
后端·python·flask
kinlon.liu1 小时前
SpringBoot整合Redis限流
spring boot·redis·后端
小p3 小时前
迈向全栈:服务器上的软件安装
前端·后端
Bohemian3 小时前
浅谈Golang逃逸分析
后端·面试·go
用户1529436849593 小时前
谷歌云代理商:如何配置谷歌云服务器的端口转发?
后端
努力的搬砖人.3 小时前
Spring Boot集成MinIO的详细步骤
java·spring boot·后端
货拉拉技术4 小时前
订单哨兵OrderSentinel平台介绍
javascript·后端·程序员
精神内耗中的钙奶饼干4 小时前
Springboot整合kafka记录
后端·kafka
JavaGuide4 小时前
IntelliJ IDEA 2025.1 发布!Java 24 支持、AI 重大更新!!
后端·intellij idea