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.行与列
    • 8.1 引言
    • 8.2 数据结构
    • 8.3 点查询
    • 8.4 更新操作
    • 8.5 创建新表
  • 9.范围查询
  • 10.二级索引
  • 11.原子事务
  • 12.并发读写
  • 13.查询语言:解析器
  • 14.查询语言:执行
目录

8.行与列

# 08. 行与列

# 8.1 引言

在键值对存储(KV store)之上构建关系型数据库的第一步是添加表。表由一系列的行和列组成。一部分列被定义为 “主键”;主键具有唯一性,因此可以在查询和二级索引中用来引用某一行数据。

表如何融入键值对存储呢?我们可以将一行数据拆分为两部分:

  1. 主键列存入键值对存储的 “键” 部分。
  2. 非主键列存入键值对存储的 “值” 部分。

这样我们就可以对主键进行点查询和范围查询。目前,我们只考虑对主键的查询,二级索引的使用将在后续章节介绍。

# 8.2 数据结构

以下是行和单元格的定义。目前,我们只支持两种数据类型(int64和字节切片)。

const  (
    TYPE_ERROR =  0
    TYPE_BYTES =  1
    TYPE_INT64 =  2
)

// 表单元格
type  Value struct  {
    Type uint32
    I64  int64
    Str  []byte
}

// 表行
type  Record struct  {
    Cols []string
    Vals []Value
}

func  ( rec *Record)  AddStr(key string,  val []byte)  *Record
func  ( rec *Record)  AddInt64(key string,  val int64)  *Record
func  ( rec *Record)  Get(key string)  *Value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

数据库(DB)和表的定义如下:

type  DB struct  {
    Path string
    // 内部使用
    kv     KV
    tables map[string]*TableDef // 缓存的表定义
}

// 表定义
type  TableDef struct  {
    // 用户定义
    Name  string
    Types []uint32 // 列类型
    Cols  []string // 列名
    PKeys int     // 前PKeys列是主键
    // 为不同表自动分配的B树键前缀
    Prefix uint32
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

为了支持多个表,键值对存储中的键会加上一个唯一的32位数字作为前缀。

表定义需要存储在某个地方,我们将使用一个内部表来存储它们。同时,我们还会添加一个内部表来存储数据库自身使用的元数据。

// 内部表:元数据
var  TDEF_META =  &TableDef{
    Prefix:  1,
    Name:       "@meta",
    Types:     []uint32{TYPE_BYTES,  TYPE_BYTES},
    Cols:       []string{"key",  "val"},
    PKeys:     1,
}

// 内部表:表结构
var  TDEF_TABLE =  &TableDef{
    Prefix:  2,
    Name:       "@table",
    Types:     []uint32{TYPE_BYTES,  TYPE_BYTES},
    Cols:       []string{"name",  "def"},
    PKeys:     1,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 8.3 点查询

我们来实现基于主键的点查询,范围查询将在下一章添加。

// 根据主键获取单行数据
func  dbGet(db *DB,  tdef *TableDef,  rec *Record)   (bool,  error)  {
    values,  err :=  checkRecord(tdef,  * rec,  tdef.PKeys)
    if  err !=  nil {
        return  false,  err
    }

    key :=  encodeKey(nil,  tdef.Prefix,  values[:tdef.PKeys])
    val,  ok :=  db.kv.Get(key)
    if   ! ok {
        return  false,  nil
    }

    for  i :=  tdef.PKeys;  i <  len(tdef.Cols);  i++  {
        values[i].Type =  tdef.Types[i]
    }
    decodeValues(val,  values[tdef.PKeys:])

    rec.Cols =  append(rec.Cols,  tdef.Cols[tdef.PKeys:]... )
    rec.Vals =  append(rec.Vals,  values[tdef.PKeys:]... )
    return  true,  nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

具体步骤如下:

  1. 验证输入是否为完整的主键。(checkRecord)
  2. 对主键进行编码。(encodeKey)
  3. 在键值对存储中查询。(db.kv.Get)
  4. 对值进行解码。(decodeValues)
// 重新排序记录并检查是否缺少列。
// n == tdef.PKeys:记录恰好是主键
// n == len(tdef.Cols):记录包含所有列
func  checkRecord(tdef *TableDef,  rec Record,  n int)   ([]Value,  error)  {
    // 省略……
}
1
2
3
4
5
6

数据编码为字节和从字节解码的方法将在下一章解释。目前,本章中任何序列化方案都可以。

func  encodeValues(out []byte,  vals []Value)   []byte
func  decodeValues(in []byte,  out []Value)

// 用于主键
func  encodeKey(out []byte,  prefix uint32,  vals []Value)   []byte {
    var  buf [4]byte
    binary.BigEndian.PutUint32(buf[:],  prefix)
    out =  append(out,  buf[:]... )
    out =  encodeValues(out,  vals)
    return  out
}
1
2
3
4
5
6
7
8
9
10
11

要查询一个表,我们必须首先获取其定义。

// 根据主键获取单行数据
func  (db *DB)  Get(table string,  rec *Record)  (bool,  error)  {
    tdef :=  getTableDef(db,  table)
    if  tdef ==  nil {
        return  false,  fmt.Errorf("table not found: %s",  table)
    }
    return  dbGet(db,  tdef,  rec)
}
1
2
3
4
5
6
7
8

表定义以JSON格式存储在内部表TDEF_TABLE中。

// 根据表名获取表定义
func  getTableDef(db *DB,  name string)  *TableDef {
    tdef,  ok :=  db.tables[name]
    if   ! ok {
        if  db.tables ==  nil {
            db.tables =  map[string]*TableDef{}
        }
        tdef =  getTableDefDB(db,  name)
        if  tdef !=  nil {
            db.tables[name]  =  tdef
        }
    }
    return  tdef
}

func  getTableDefDB(db *DB,  name string)  *TableDef {
    rec :=   (&Record{}).AddStr("name",   []byte(name))
    ok,  err :=  dbGet(db,  TDEF_TABLE,   rec)
    assert(err ==  nil)
    if   ! ok {
        return  nil
    }

    tdef :=  &TableDef{}
    err =  json.Unmarshal(rec.Get("def").Str,  tdef)
    assert(err ==  nil)
    return  tdef
}
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

# 8.4 更新操作

更新操作可以是插入新行或替换现有行。B树接口被修改以支持不同的更新模式。

// 更新模式
const  (
    MODE_UPSERT     =  0 // 插入或替换
    MODE_UPDATE_ONLY =  1 // 仅更新现有键
    MODE_INSERT_ONLY =  2 // 仅添加新键
)

type  InsertReq struct  {
    tree *BTree
    // 输出
    Added bool // 是否添加了新键
    // 输入
    Key []byte
    Val []byte
    Mode int
}

func  (tree *BTree)  InsertEx(req *InsertReq)
func  (db *KV)  Update(key []byte,  val []byte,  mode int)   (bool,  error)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

通过主键更新记录的函数:

// 向表中添加一行数据
func  dbUpdate(db *DB,  tdef *TableDef,   rec Record,  mode int)   (bool,  error)  {
    values,  err :=  checkRecord(tdef,   rec,  len(tdef.Cols))
    if  err !=  nil {
        return  false,  err
    }

    key :=  encodeKey(nil,  tdef.Prefix,  values[:tdef.PKeys])
    val :=  encodeValues(nil,  values[tdef.PKeys:])
    return  db.kv.Update(key,  val,  mode)
}
1
2
3
4
5
6
7
8
9
10
11

不同的更新模式:

// 添加一条记录
func  (db *DB)  Set(table string,   rec Record,  mode int)   (bool,  error)  {
    tdef :=  getTableDef(db,  table)
    if  tdef ==  nil {
        return  false,  fmt.Errorf("table not found: %s",  table)
    }
    return  dbUpdate(db,  tdef,  rec,  mode)
}
func  (db *DB)  Insert(table string,   rec Record)  (bool,  error)  {
    return  db.Set(table,  rec,  MODE_INSERT_ONLY)
}
func  (db *DB)  Update(table string,   rec Record)  (bool,  error)  {
    return  db.Set(table,  rec,  MODE_UPDATE_ONLY)
}
func  (db *DB)  Upsert(table string,   rec Record)  (bool,  error)  {
    return  db.Set(table,  rec,  MODE_UPSERT)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

删除一行数据的操作类似:

// 根据主键删除一条记录
func  dbDelete(db *DB,  tdef *TableDef,   rec Record)  (bool,  error)  {
    values,  err :=  checkRecord(tdef,   rec,  tdef.PKeys)
    if  err !=  nil {
        return  false,  err
    }

    key :=  encodeKey(nil,  tdef.Prefix,  values[:tdef.PKeys])
    return  db.kv.Del(key)
}

func  (db *DB)  Delete(table string,   rec Record)  (bool,  error)  {
    tdef :=  getTableDef(db,  table)
    if  tdef ==  nil {
        return  false,  fmt.Errorf("table not found: %s",  table)
    }
    return  dbDelete(db,  tdef,  rec)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 8.5 创建新表

创建新表需要三个步骤:

  1. 检查表定义。
  2. 分配表键前缀。
  3. 存储下一个表前缀和表定义。
func  (db *DB)  TableNew(tdef *TableDef)  error {
    if  err :=  tableDefCheck(tdef);  err !=  nil {
        return  err
    }

    // 检查已存在的表
    table :=   (&Record{}).AddStr("name",   []byte(tdef.Name))
    ok,  err :=  dbGet(db,  TDEF_TABLE,  table)
    assert(err ==  nil)
    if  ok {
        return  fmt.Errorf("table exists: %s",  tdef.Name)
    }

    // 分配新的前缀
    assert(tdef.Prefix ==  0)
    tdef.Prefix =  TABLE_PREFIX_MIN
    meta :=   (&Record{}).AddStr("key",   []byte("next_prefix"))
    ok,  err =  dbGet(db,  TDEF_META,  meta)
    assert(err ==  nil)
    if  ok {
        tdef.Prefix =  binary.LittleEndian.Uint32(meta.Get("val").Str)
        assert(tdef.Prefix >  TABLE_PREFIX_MIN)
    }  else  {
        meta.AddStr("val",  make([]byte,  4))
    }

    // 更新下一个前缀
    binary.LittleEndian.PutUint32(meta.Get("val").Str,  tdef.Prefix+1)
    _,  err =  dbUpdate(db,  TDEF_META,  *meta,  0)
    if  err !=  nil {
        return  err
    }

    // 存储表定义
    val,  err :=  json.Marshal(tdef)
    assert(err ==  nil)
    table.AddStr("def",  val)
    _,  err =  dbUpdate(db,  TDEF_TABLE,  *table,  0)
    return  err
}
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

前缀数字从TDEF_META内部表的next_prefix键开始依次分配。表定义以JSON格式存储在TDEF_TABLE表中。

尽管我们添加了表结构,但结果在很大程度上仍然是一个键值对存储。还缺少一些重要的方面:

  1. 下一章的范围查询。
  2. 再下一章的二级索引。
上次更新: 2025/04/16, 02:04:00
7.空闲列表:重用页面
9.范围查询

← 7.空闲列表:重用页面 9.范围查询→

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