CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • 使用Go从零开发一个数据库 说明
  • 1.文件与数据库
  • 2.索引
  • 3.B树:原理
  • 4.B树:实践(第一部分)
  • 5.B树:实践(第二部分)
  • 6.持久化到磁盘
  • 7.空闲列表:重用页面
  • 8.行与列
  • 9.范围查询
  • 10.二级索引
  • 11.原子事务
  • 12.并发读写
  • 13.查询语言:解析器
  • 14.查询语言:执行
    • 14.1 引言
    • 14.2 表达式计算
    • 14.3 获取行
      • 14.3.1 初始化迭代器
      • 14.3.2 迭代行
    • 14.4 执行语句
    • 14.5 后续步骤
目录

14.查询语言:执行

# 14. 查询语言:执行

# 14.1 引言

为了执行一条语句,需要将该语句转换为对现有数据库接口的函数调用。例如,CREATE TABLE 语句被转换为一个简单的函数调用:

func qlCreateTable(req *QLCreateTable, tx *DBTX) error {
    return tx.TableNew(&req.Def)
}
1
2
3

其他语句没这么简单,但它们本质上仍是粘合代码,本身并没有太多功能。执行 SELECT 语句则更为复杂:

func qlSelect(req *QLSelect, tx *DBReader, out []Record) ([]Record, error) {
    // 记录
    records, err := qlScan(&req.QLScan, tx, out)
    if err != nil {
       return nil, err
    }

    // 输出
    for _, irec := range records {
       orec := Record{Cols: req.Names}
       for _, node := range req.Output {
          ctx := QLEvalContex{env: irec}
          qlEval(&ctx, node)
          if ctx.err != nil {
             return nil, ctx.err
          }
          orec.Vals = append(orec.Vals, ctx.out)
       }
       out = append(out, orec)
    }
    return out, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

该函数做了两件事:

  1. 根据某些条件获取行(qlScan)。
  2. 通过计算表达式将行转换为输出(qlEval)。

这两件事也是其他一些语句的组成部分。我们先来看表达式计算。

# 14.2 表达式计算

有3处地方需要根据某一行来计算表达式:SELECT 语句的表达式列表、FILTER 子句中的条件,以及 UPDATE 语句中的值。

qlEval 函数用于完成这些任务。为使代码简洁,计算结果(标量值或错误)和当前行被存放在 QLEvalContex 结构体中。

// 用于计算表达式
type QLEvalContex struct {
    env Record // 可选的行值
    out Value
    err error
}

// 递归计算表达式
func qlEval(ctx *QLEvalContex, node QLNode)
1
2
3
4
5
6
7
8
9

qlEval 函数递归计算子表达式,然后进行运算符的计算。处理列名和字面值(整数和字符串)很直观。

func qlEval(ctx *QLEvalContex, node QLNode) {
    if ctx.err != nil {
       return
    }

    switch node.Type {
    // 引用列
    case QL_SYM:
       if v := ctx.env.Get(string(node.Str)); v != nil {
          ctx.out = *v
       } else {
          qlErr(ctx, "unknown column: %s", node.Str)
       }
    // 字面值
    case QL_I64, QL_STR:
       ctx.out = node.Value
    // 更多情况;省略...
    default:
       panic("not implemented")
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

处理运算符也很容易。在计算表达式时,我们还会进行类型检查。以 QL_NEG 运算符为例:

// 一元运算符
case QL_NEG:
    qlEval(ctx, node.Kids[0])
    if ctx.out.Type == TYPE_INT64 {
       ctx.out.I64 = -ctx.out.I64
    } else {
       qlErr(ctx, "QL_NEG type error")
    }
1
2
3
4
5
6
7
8

qlErr 函数将错误保存到 QLEvalContex 中。

# 14.3 获取行

获取行是查询需要做的另一项工作。

// 执行查询
func qlScan(req *QLScan, tx *DBReader, out []Record) ([]Record, error) {
    sc := Scanner{}
    err := qlScanInit(req, &sc)
    if err != nil {
       return nil, err
    }

    err = tx.Scan(req.Table, &sc)
    if err != nil {
       return nil, err
    }

    return qlScanRun(req, &sc, out)
}

// 根据INDEX BY子句创建Scanner
func qlScanInit(req *QLScan, sc *Scanner) error
// 从Scanner中获取所有行
func qlScanRun(req *QLScan, sc *Scanner, out []Record) ([]Record, error)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 14.3.1 初始化迭代器

首先,我们需要将 INDEX BY 子句转换为 Record 类型,供 Scanner 迭代器使用。INDEX BY 子句有以下几种情况:

  1. 点查询。
  2. 开放式范围(单个比较)。
  3. 封闭式范围(两个不同方向的比较)。

相应示例如下:

select expr... from table_name index by a = 1;
select expr... from table_name index by a > 1;
select expr... from table_name index by a > 1 and a < 5;
1
2
3

该子句对应 QLScan 的 Key1 和 Key2 字段。

type QLScan struct {
    Table  string
    Key1   QLNode // 第一个范围或点查询
    Key2   QLNode // 第二个范围
    Filter QLNode
    Offset int64
    Limit  int64
}
1
2
3
4
5
6
7
8

这种转换只是一些类型检查和类型转换操作。

// 根据INDEX BY子句创建Scanner
func qlScanInit(req *QLScan, sc *Scanner) error {
    if req.Key1.Type == 0 {
       // 没有INDEX BY;按主键扫描
       sc.Cmp1, sc.Cmp2 = CMP_GE, CMP_LE
       return nil
    }

    var err error
    sc.Key1, sc.Cmp1, err = qlEvalScanKey(req.Key1)
    if err != nil {
       return err
    }

    if req.Key2.Type != 0 {
       sc.Key2, sc.Cmp2, err = qlEvalScanKey(req.Key1)
       if err != nil {
          return err
       }
    }

    if req.Key1.Type == QL_CMP_EQ && req.Key2.Type != 0 {
       return errors.New("bad INDEX BY")
    }
    if req.Key1.Type == QL_CMP_EQ {
       sc.Key2 = sc.Key1
       sc.Cmp1, sc.Cmp2 = CMP_GE, CMP_LE
    }
    return nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

qlEvalScanKey 函数用于将比较运算符转换为 Record 类型和 CMP_?? 枚举值。这里也没什么复杂的内容。

func qlEvalScanKey(node QLNode) (Record, int, error)
1

# 14.3.2 迭代行

在迭代行时,我们需要处理 LIMIT 和 FILTER 子句。

// 从Scanner中获取所有行
func qlScanRun(req *QLScan, sc *Scanner, out []Record) ([]Record, error) {
    for i := int64(0); sc.Valid(); i++ {
       // LIMIT
       ok := req.Offset <= i && i < req.Limit

       rec := Record{}
       if ok {
          sc.Deref(&rec)
       }

       // FILTER
       if ok && req.Filter.Type != 0 {
          ctx := QLEvalContex{env: rec}
          qlEval(&ctx, req.Filter)
          if ctx.err != nil {
             return nil, ctx.err
          }
          if ctx.out.Type != TYPE_INT64 {
             return nil, errors.New("filter is not of boolean type")
          }
          ok = (ctx.out.I64 != 0)
       }

       if ok {
          out = append(out, rec)
       }
       sc.Next()
    }

    return out, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 14.4 执行语句

SELECT 语句的代码已经列出。我们再来看 DELETE 语句,它和 SELECT 语句差别不大。

func qlDelete(req *QLDelete, tx *DBTX) (uint64, error) {
    records, err := qlScan(&req.QLScan, &tx.DBReader, nil)
    if err != nil {
       return 0, err
    }

    tdef := getTableDef(&tx.DBReader, req.Table)
    pk := tdef.Cols[:tdef.PKeys]
    for _, rec := range records {
       key := Record{Cols: pk}
       for _, col := range pk {
          key.Vals = append(key.Vals, *rec.Get(col))
       }
       deleted, err := tx.Delete(req.Table, key)
       assert(err == nil && deleted) // 删除已存在的行
    }
    return uint64(len(records)), nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

此时,其他语句应该也很容易理解了。

# 14.5 后续步骤

如你所见,本章并没有添加太多新内容。查询语言的实现大多是对之前已实现功能的粘合代码。在学习到这里之前,你可能一直不明白数据库是如何将SQL语句转换为行数据的。现在你对数据库有了更深入的理解,或许还想探索其他方面的内容。

你可以尝试为数据库添加更多功能,比如连接(joins)、分组(group bys)和聚合(aggregations),这些在分析型查询中很常见。在当前阶段,实现这些功能应该并不困难。

你还可以为数据库构建客户端API和服务器。为了管理并发访问,服务器进程是必不可少的。为此,你需要学习网络编程。虽然在Go语言中进行网络编程相当简单且抽象层次较高,但如果你愿意深入学习,也可以选择从头开始学习相关知识。

上次更新: 2025/04/16, 02:04:00
13.查询语言:解析器

← 13.查询语言:解析器

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
C++语言面试问题集锦 目录与说明
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式