CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (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系统编程
  • Go语言特性

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

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (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系统编程
  • Go语言特性

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

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
  • 使用Go从零开发一个数据库 说明
  • 1.文件与数据库
  • 2.索引
  • 3.B树:原理
  • 4.B树:实践(第一部分)
  • 5.B树:实践(第二部分)
  • 6.持久化到磁盘
  • 7.空闲列表:重用页面
  • 8.行与列
  • 9.范围查询
    • 9.1 B树迭代器
    • 9.2 数据序列化
    • 9.3 范围查询
  • 10.二级索引
  • 11.原子事务
  • 12.并发读写
  • 13.查询语言:解析器
  • 14.查询语言:执行
目录

9.范围查询

# 09. 范围查询

我们已经在键值存储(key-value store,KV store)之上实现了表结构,并且能够通过主键检索记录。在本章中,我们将增加按排序顺序检索一系列记录的功能。

# 9.1 B树迭代器

第一步是在B树中添加范围查询功能。BIter 类型允许我们以迭代方式遍历B树。

// B树迭代器
type BIter struct {
    tree *BTree
    path []BNode // 从根节点到叶节点的路径
    pos  []uint16 // 节点中的索引
}

// 获取当前键值对
func (iter *BIter) Deref() ([]byte, []byte)
// Deref() 的前置条件
func (iter *BIter) Valid() bool
// 向后和向前移动
func (iter *BIter) Prev()
func (iter *BIter) Next()
1
2
3
4
5
6
7
8
9
10
11
12
13
14

BIter 是从根节点到叶节点中键值对的一条路径。移动迭代器只需将位置或节点移动到兄弟节点即可。

func iterPrev(iter *BIter, level int) {
    if iter.pos[level] > 0 {
       iter.pos[level]-- // 在当前节点内移动
    } else if level > 0 {
       iterPrev(iter, level - 1) // 移动到兄弟节点
    } else {
       return // 伪键
    }

    if level + 1 < len(iter.pos) {
       // 更新子节点
       node := iter.path[level]
       kid := iter.tree.get(node.getPtr(iter.pos[level]))
       iter.path[level + 1] = kid
       iter.pos[level + 1] = kid.nkeys() - 1
    }
}

func (iter *BIter) Prev() {
    iterPrev(iter, len(iter.path) - 1)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

BTree.SeekLE 是在范围查询中查找初始位置的函数。它只是一个普通的B树查找,并记录路径。

// 查找小于或等于输入键的最接近位置
func (tree *BTree) SeekLE(key []byte) *BIter {
    iter := &BIter{tree: tree}
    for ptr := tree.root; ptr != 0; {
       node := tree.get(ptr)
       idx := nodeLookupLE(node, key)
       iter.path = append(iter.path, node)
       iter.pos = append(iter.pos, idx)
       if node.btype() == BNODE_NODE {
          ptr = node.getPtr(idx)
       } else {
          ptr = 0
       }
    }
    return iter
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

nodeLookupLE 函数仅适用于范围查询中的 “小于或等于” 运算符,对于其他3个运算符(小于、大于、大于或等于),结果可能会偏差一个。我们将通过 BTree.Seek 函数来修复这个问题。

const (
    CMP_GE = +3 // >=
    CMP_GT = +2 // >
    CMP_LT = -2 // <
    CMP_LE = -3 // <=
)

// 根据比较关系查找与键最接近的位置
func (tree *BTree) Seek(key []byte, cmp int) *BIter {
    iter := tree.SeekLE(key)
    if cmp != CMP_LE && iter.Valid() {
       cur, _ := iter.Deref()
       if!cmpOK(cur, cmp, key) {
          // 偏差一个
          if cmp > 0 {
             iter.Next()
          } else {
             iter.Prev()
          }
       }
    }
    return iter
}

// 键比较参考
func cmpOK(key []byte, cmp int, ref []byte) bool {
    r := bytes.Compare(key, ref)
    switch cmp {
    case CMP_GE:
       return r >= 0
    case CMP_GT:
       return r > 0
    case CMP_LT:
       return r < 0
    case CMP_LE:
       return r <= 0
    default:
       panic("what?")
    }
}
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
33
34
35
36
37
38
39
40

# 9.2 数据序列化

为了支持范围查询,序列化的主键必须在键值存储中能够正确比较。一种方法是反序列化主键并逐列进行比较。我们将采用另一种方法,使序列化的键字节反映其字典顺序,也就是说,无需先反序列化,就可以通过 bytes.Compare 或 memcmp 正确比较键。我们将这种技术称为 “保序编码(order-preserving encoding)”,它可以在不控制底层键值存储的键比较函数的情况下使用。

对于整数,很容易看出无符号大端序(big-endian)整数是保序的 —— 大端序格式中最高有效位排在前面。以空字符结尾的字符串也是保序的。

对于有符号整数,问题在于负数的最高有效位(符号位)被设置。在以大端序编码之前,我们需要翻转符号位,使负数的值更低。

// 保序编码
func encodeValues(out []byte, vals []Value) []byte {
    for _, v := range vals {
       switch v.Type {
       case TYPE_INT64:
          var buf [8]byte
          u := uint64(v.I64) + (1 << 63)
          binary.BigEndian.PutUint64(buf[:], u)
          out = append(out, buf[:]...)
       case TYPE_BYTES:
          out = append(out, escapeString(v.Str)...)
          out = append(out, 0) // 以空字符结尾
       default:
          panic("what?")
       }
    }
    return out
}

func decodeValues(in []byte, out []Value) {
    // 省略...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

以空字符结尾的字符串的问题在于它们不能包含空字节。我们将通过 “转义” 空字节来解决这个问题。\x00 被替换为 \x01\x01,转义字节 \x01 本身被替换为 \x01\x02,这样仍然保留了排序顺序。

// 字符串被编码为以空字符结尾的字符串,
// 转义空字节,使字符串不包含空字节。
func escapeString(in []byte) []byte {
    zeros := bytes.Count(in, []byte{0})
    ones := bytes.Count(in, []byte{1})
    if zeros + ones == 0 {
       return in
    }

    out := make([]byte, len(in) + zeros + ones)
    pos := 0
    for _, ch := range in {
       if ch <= 1 {
          out[pos + 0] = 0x01
          out[pos + 1] = ch + 1
          pos += 2
       } else {
          out[pos] = ch
          pos += 1
       }
    }
    return out
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 9.3 范围查询

最后,我们将添加 Scanner 类型,它允许我们按排序顺序遍历一系列记录。

// 范围查询的迭代器
type Scanner struct {
    // 范围,从Key1到Key2
    Cmp1 int // CMP_??
    Cmp2 int
    Key1 Record
    Key2 Record
    // 内部
    tdef   *TableDef
    iter   *BIter // 底层B树迭代器
    keyEnd []byte // 编码后的Key2
}

// 是否在范围内?
func (sc *Scanner) Valid() bool
// 移动底层B树迭代器
func (sc *Scanner) Next()
// 获取当前行
func (sc *Scanner) Deref(rec *Record)

func (db *DB) Scan(table string, req *Scanner) error {
    tdef := getTableDef(db, table)
    if tdef == nil {
       return fmt.Errorf("table not found: %s", table)
    }
    return dbScan(db, tdef, req)
}
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

初始化迭代器:

func dbScan(db *DB, tdef *TableDef, req *Scanner) error {
    // 合理性检查
    switch {
    case req.Cmp1 > 0 && req.Cmp2 < 0:
    case req.Cmp2 > 0 && req.Cmp1 < 0:
    default:
       return fmt.Errorf("bad range")
    }

    values1, err := checkRecord(tdef, req.Key1, tdef.PKeys)
    if err != nil {
       return err
    }
    values2, err := checkRecord(tdef, req.Key2, tdef.PKeys)
    if err != nil {
       return err
    }

    req.tdef = tdef

    // 定位到起始键
    keyStart := encodeKey(nil, tdef.Prefix, values1[:tdef.PKeys])
    req.keyEnd = encodeKey(nil, tdef.Prefix, values2[:tdef.PKeys])
    req.iter = db.kv.tree.Seek(keyStart, req.Cmp1)
    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

移动迭代器:

// 是否在范围内?
func (sc *Scanner) Valid() bool {
    if!sc.iter.Valid() {
       return false
    }
    key, _ := sc.iter.Deref()
    return cmpOK(key, sc.Cmp2, sc.keyEnd)
}

// 移动底层B树迭代器
func (sc *Scanner) Next() {
    assert(sc.Valid())
    if sc.Cmp1 > 0 {
       sc.iter.Next()
    } else {
       sc.iter.Prev()
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

点查询(Point queries)只是范围查询的特殊情况,那么为什么不把它们去掉呢?

// 通过主键获取单行记录
func dbGet(db *DB, tdef *TableDef, rec *Record) (bool, error) {
    // 只是扫描操作的快捷方式
    sc := Scanner{
       Cmp1: CMP_GE,
       Cmp2: CMP_LE,
       Key1: *rec,
       Key2: *rec,
    }

    if err := dbScan(db, tdef, &sc); err != nil {
       return false, err
    }

    if sc.Valid() {
       sc.Deref(rec)
       return true, nil
    } else {
       return false, nil
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

我们只允许对完整主键进行范围查询,但对主键前缀进行范围查询也是合理的。我们将在下一章连同二级索引(secondary indexes)一起解决这个问题。

上次更新: 2025/04/16, 02:04:00
8.行与列
10.二级索引

← 8.行与列 10.二级索引→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
第四章 Lambda函数
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式