Go语言从零构建SQL数据库(6) - sql解析器(番外)- *号的处理

番外:处理SQL通配符查询

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

1. 星号查询的挑战

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

  • 普通列名是标识符(如 idname
  • 星号是一个特殊符号,表示"全部"
  • 在解析时需要区别对待,不能简单视为标识符

Token: * 特殊处理 SELECT * FROM users 词法分析器 Token流 语法分析器 AST: AsteriskExpression

普通列名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语句的列表达式。
<<interface>> Expression +expressionNode() +TokenLiteral() : string +String() : string Identifier +Value string +expressionNode() +TokenLiteral() : string +String() : string AsteriskExpression +expressionNode() +TokenLiteral() : string +String() : string LiteralExpression +Value string +Type TokenType +expressionNode() +TokenLiteral() : string +String() : string

步骤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实例。

星号解析的处理流程

是 否 SQL: SELECT * FROM users 词法分析: [SELECT, *, FROM, users] 遇到 * Token 有注册的parseAsterisk函数? 调用parseAsterisk() 解析错误 创建AsteriskExpression 将AsteriskExpression添加到SelectStatement的Columns列表 继续解析FROM子句

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)
        // ... 其他表达式类型
        }
    }
  
    // ... 继续执行查询
}

星号查询的执行流程

是 否 SQL: SELECT * FROM users WHERE age > 18 解析为AST executeSelect()执行 处理Columns列表 是否为AsteriskExpression? 获取表的所有列元数据 处理单个列 添加所有列到结果集 应用WHERE过滤条件 返回结果集

4. 星号查询的AST表示

对于 SELECT * FROM users WHERE age > 18;,完整的AST树结构如下:
SelectStatement Columns TableName: 'users' Where AsteriskExpression BinaryExpression Left: Identifier{Value: 'age'} Operator: GREATER Right: LiteralExpression{Value: '18', Type: NUMBER}

5. 高级应用场景

5.1 表格别名下的星号

表格别名与星号结合使用时,如 SELECT u.* FROM users u,需要特殊处理:
SELECT u.* FROM users u 词法分析 Token流: [SELECT, u, ., *, FROM, users, u] 解析u.* 创建QualifiedAsteriskExpression TablePrefix: 'u', Value: '*'

在这种情况下,我们需要一个特殊的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 多表连接中的星号处理

在多表连接中,星号会引入列名冲突问题:
是 否 SELECT * FROM users u JOIN orders o ON u.id = o.user_id 解析为AST 执行计划生成 检测到多表的*查询 获取所有表的列元数据 检查列名冲突 存在冲突列名? 生成完全限定列名(表名.列名) 保留原列名 构建结果集

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

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

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

SQL还允许星号与特定列的混合使用,如 SELECT *, extra_column FROM table
SELECT *, created_at FROM users 解析 AST: [AsteriskExpression, Identifier{Value: 'created_at'}] 执行 获取所有列 + 再次获取created_at 去重处理 返回结果

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

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

6. 性能优化与最佳实践

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

6.1 性能影响

查询性能考虑 SELECT * 查询 读取整行数据 增加I/O和内存使用 可能影响索引使用 SELECT 特定列查询 只读取需要的列 减少I/O和内存开销 更有效地利用覆盖索引

6.2 代码维护性

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

6.3 最佳实践建议

星号查询最佳实践 适用场景 避免场景 折中方案 探索性查询/调试 需要获取行的完整信息 ORM自动映射实体 生产环境的高性能查询 只需少量列的查询 多表连接查询 使用表别名限定: t.* 视图中封装常用列组合 ORM中配置列映射

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

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

相关推荐
倔强的石头_15 小时前
kingbase备份与恢复实战(二)—— sys_dump库级逻辑备份与恢复(Windows详细步骤)
数据库
jiayou642 天前
KingbaseES 实战:深度解析数据库对象访问权限管理
数据库
李广坤3 天前
MySQL 大表字段变更实践(改名 + 改类型 + 改长度)
数据库
爱可生开源社区4 天前
2026 年,优秀的 DBA 需要具备哪些素质?
数据库·人工智能·dba
随逸1774 天前
《从零搭建NestJS项目》
数据库·typescript
花酒锄作田4 天前
Gin 框架中的规范响应格式设计与实现
golang·gin
加号35 天前
windows系统下mysql多源数据库同步部署
数据库·windows·mysql
シ風箏5 天前
MySQL【部署 04】Docker部署 MySQL8.0.32 版本(网盘镜像及启动命令分享)
数据库·mysql·docker
李慕婉学姐5 天前
Springboot智慧社区系统设计与开发6n99s526(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
百锦再5 天前
Django实现接口token检测的实现方案
数据库·python·django·sqlite·flask·fastapi·pip