Skip to content

Commit

Permalink
feat: ddia
Browse files Browse the repository at this point in the history
  • Loading branch information
raftale committed Dec 19, 2024
1 parent f7f943c commit 265fa8c
Show file tree
Hide file tree
Showing 13 changed files with 322 additions and 0 deletions.
322 changes: 322 additions & 0 deletions _posts/database/ddia/2023-07-20-ddia-chap3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
---
title: "DDIA-chap3-存储与检索"
layout: post
author: "raftale"
header-style: text
hidden: false
published: true
tags:
- database
- ddia
---

# 存储与检索
数据库最基础的需求:
1. 写入数据;
2. 查询数据

存储引擎有两大类:

1. 日志结构 log-structured
2. 面向页面 page-oriented: B+树

## 驱动数据库的数据结构

最简单数据库的写入(set)和查询(get)的实现:
1. set:在文件末尾追加(key, value)对
2. get:逐行匹配key,返回最后一次匹配成功所对应的value



性能分析:
1. 对于写来说效率是非常高的,因为追加文件是顺序写
1. 这样的存储结构我们通常称之为日志log,本质上是append-only的数据文件
2. 对于读来说效率则非常差,读任意键,我们都需要遍历整个文件并逐行匹配
3. 如何提高读的效率?

## 索引
索引是一种从原始数据中构建的用于辅助查询的数据结构;通过存储额外的信息帮助执行器更快的找到需要返回的数据;本质上也是空间换时间的思想

索引并不影响查询结果,只影响查询性能

维护额外的数据结构会引入额外的开销;尤其会导致写操作变慢。

这也是在数据存储中的一组重要权衡,读和写的性能是彼此制衡的。



### Hash Indexes

哈希索引是最简单的KV索引之一;同时也被用于构建许多更为复杂的索引。

对于我们前面的kv存储,我们可以建立内存中的hashmap作为查询加速查询

+ 内存中的哈希表的key和查询key保持一致,value则存对应记录在文件中的最新offset
+ ![](/img/ddia/in-memeory-hashmap.png)

事实上,Bitcask就是基于这个简单的思想实现的;在内存中还加了一层对查询结果的缓存。

+ 在所有key都可以存到内存中时性能非常好
- 对于写操作来说,追加写性能非常好
- 对于读操作来说,内存查询到offset后可以直接对磁盘进行寻址而不需要遍历



如果一直append,如果避免磁盘不够用呢?

+ 首先对文件进行分片成「段」(segment),文件大小达到一定尺寸后,我们就会将文件变为只读,然后开始写入新的文件;
+ 随后,我们就可以对多个分片进行压缩(compation)操作,即对于重复的键,仅保留最新的一条记录,写入新的文件中,最后,删除老文件即可减少占用的磁盘存储空间。
+ 对于所有的分片中只有一个是活跃可追加的,其余都是不可变的。因此压缩操作和读取操作并不是互斥的;通常压缩是由一个独立的后台进程完成的

![img.png](/img/ddia/img.png)
将这个想法应用到生产环境下还需要处理许多问题:

1. 文件格式;CSV并不是一个高效的存储格式
2. 记录删除:由于是append-only的存储结构,无法在文件中删除记录,一般采用标记删除实现,追加特殊的tombstone标记某个键值对已经被删除即可
3. 宕机恢复:内存中的哈希表在机器重启后会丢失;相比于全表扫描重建哈希表,一种更快的方式是在每个文件中持久化对应的索引记录,重启后可以更快的恢复。
4. 记录不完整:写入过程进行到一半的时候如果机器出现故障,会导致数据不完整
1. 解决方法:基于日志
5. 并发控制:比如greenpump,建了一个ao表,并发写。



哈希索引 + 日志存储的优势:

1. 顺序写入性能很好
2. 由于大部分文件是不可变的,因此crash recovery和concurrency更简单
3. 压缩操作的特性使得数据文件不容易碎片化

哈希索引的限制:

1. 对key对数量限制很高,因为hash table需要放在内存中,on-disk hash map性能比较差
2. 不支持range queries



## SSTables

Log-structure storage segment 是完全按照写入顺序存储的,我们可以通过设计一种存储格式让每个段中的键有序排列;我们称为Sorted String Table(SSTable)

> 实现group by的两种方法:
>
> 第一种方法:先排序,再统计
>
> 第二种方法:hash


SSTables的好处有两个:合并时更高效、内存中不需要加载所有key只需加载稀疏索引。

1. 更高效的合并操作
1. 由于每个文件分片内都是有序排列的,为了过滤掉重复的键,我们只需要采用归并排序算法即可
1. ![](https://cdn.nlark.com/yuque/0/2023/png/32473878/1690119720397-aac477cf-4c8b-448e-9cbd-61d6f751ec1b.png)
2. ![img.png](/img/ddia/img_0.png)
2. 因为数据在磁盘中是有序的,所以查找的时候不需要在内存中存储一个包含所有key的偏移索引了,可以用一个稀疏索引来替代(相当于是对磁盘的一个采样)。![img_1.png](/img/ddia/img_1.png)
1. 相似的数据可以进行压缩,降低空间开销和IO

### 构建和维护 SSTables

+ 如何进行写操作?
- 先在内存中维护有序存储结构,如AVL树,我们称之为内存表memtable
* 设置一个阈值,达到阈值后就将有序的数据结构落盘
* ![img_2.png](/img/ddia/img_2.png)
* 问题:
* memtable是否导致数据可能因为重启丢失
+ 已经被提交的状态在一段时间内可能仅被持有在内存中,如果断电,数据将丢失
+ 可以采用预写日志WAL
- 当占用的内存达到一个阈值后,将其写入到一个分片文件中,我们称之为SSTable file



## LSM-Trees

上面讲的数据结构被称为日志结构合并树(LSM tree)。这种基于合并和压缩排序文件原理的存储引擎通常被称为LSM存储引擎。



### 基于SSTables实现LSM-Tree

SSTable的问题有:

1. 随着SSTables数量的增加,查找key值的时间会越来越长。
2. 随着SSTables数量的增加,随着key的更新和删除标记tombstone的增加,磁盘空间占用会增加

为了解决这些问题,在后台运行一个周期性的合并和压缩过程来合并SSTables并丢弃过时或删除的值。


![img_3.png](/img/ddia/img_3.png)


### 性能优化:

当SSTables被合并时,它们被组织为层级level,就像一颗树。

有两种策略来确定何时以及如何合并和压缩SSTable:

1. size-tiered compaction: HBase\consandra
1. 较新和较小的sstables被合并到较旧的和较大的SSTable中
2. leveled compaction: levelDB and RocksDB \consandra
1. SSTable被拆分的较小,较旧的数据被移动到单独的层级,使得压缩能够增量的进行,并且使用较小的硬盘空间。

###
当查找数据库中不存在的key时,查询要扫描完所有的数据,全表扫描的性能太差,可以用布隆过滤器来优化。

> 布隆过滤器能很快的判断出这个key是否是真的存在于某个集合中


压缩和合并使SSTable的数量保持可控;

1. 每个级别都会层指数级增长
2. 压缩会消耗大量 IO



## BTree

BTree于1970年就被提出,仍是在目前在关系型数据库中最为流行的数据索引实现。

和LSM-Tree一样支持高效的点查和范围查询

其数据组织形式和LSM-Tree不同

+ 以固定大小的页(page)或块(block,4k大小)来组织数据,每次读取或者写入一个页
+ 页可以通过地址标识,彼此之间可以通过类似指针的方式互相引用
+ B树的根是其中一个页面,每次查询都会从根页面开始进行。

![img_4.png](/img/ddia/img_4.png)


B树的一个页面对子页面的引用(ref)的数量称为分支因子(branching factor),上图分支因子为6。实际中,分支因子的大小取决于存储页面引用和范围边界所需的空间,这个值通常是几百。

ref指向另一个页,上图是一个具体的查询例子,我们每次读入每一页至内存中后,可以通过二分查找快速找到目标元组可能所在的叶子结点的路径。不断二分,最终我们会达到叶子结点,要么命中,要么说明数据不在这个Btree中。
![img_5.png](/img/ddia/img_5.png)

上图是一个具体的插入例子,首先我们也要先查询到目标key所在页面,找到的话在原地更新即可,否则需要在当前页面插入数据,然后将页面整体返回。

+ 每一个页面都有一个阈值,如果包含的key超过这个阈值则会将当前页面分裂为两个页面,并在上层页中继续插入新的页面的引用;这个过程可能会级联发生,小于某个阈值也会产生级联合并的操作。

每个操作的ref数被称为branching factor;通常在数百个。整体查询时间复杂度与树高度有关,由于btree很好的维护了树的平衡性,对于有n个key的b-tree来说,时间复杂度为O(logN)

> A four-level tree of 4KB pages with a branching factor of 500 can store up to 256 TB


### making Btree reliable

由于会原地修改数据,并且可能会由于分裂合并操作调整树的结构,需要引入预写日志 redo log,保证数据安全性。

redo log 是一个仅追加的文件,每个B树的修改在其能被应用到树本身的页面之前都必须先写入到该文件,当数据库在崩溃后恢复时,这个日志将用来使B树恢复到一致的状态。

并发控制也是一个非常复杂的问题,如果多个线程同时访问b树,通过使用锁存器(latches,轻量级锁)来保护树的数据结构完成。

latch又分为mutex(互斥量)和rwlock(读写锁),latch持续时间很短,无死锁检测,存在于每个数据结构的对象中。

![img_6.png](/img/ddia/img_6.png)


> **<font style="color:rgb(51, 51, 51);">SHOW ENGINE</font>**<font style="color:rgb(51, 51, 51);"> INNODB MUTEX</font>
### BTree optimization

1. copy-on-write 代替WAL
2. 非叶子结点仅仅保留路由信息,而不存储数据
3. 尽可能使得相邻叶子结点在**物理存储**上连续
4. 叶子节点间增加兄弟结点的指针,遍历查询时效率更高
5. fractal tree借鉴了一些LSM-tree的思想



### Comparing B-Trees and LSM-Trees
最主要的差异在于B-Tree读取更快,LSM-Tree写入更快。时序数据库的场景下,LSM-Tree经常是一个更好的选择。

除次之外:

+ LSM-Tree的优势:
- B-Tree每块数据都必须至少写入WAL和树页本身;
- 对于B-Tree来说写入是以页为单位的,因此即使只有一个元素变动,也需要写入整个页面
- LSM-Tree通常来说有更小的写放大,B-Tree更改数据时对页面可能有多次覆盖
- 顺序写吞吐更大
- 数据更紧凑,更适合压缩,相比于B-Tree有更小的磁盘碎片产生
+ LSM-Tree的劣势:
- 压缩进程可能会干扰当前的读写进程,导致性能不稳定
- 配置压缩是很重要的,否则可能导致压缩的速度跟不上写入的速度,从而导致磁盘耗尽和读取速度变慢
- LSM-Tree实现事务语义更为困难


### Other Indexing Structures
在关系型模型中我们除了有类似于前述针对key-value索引的主键索引,还有二级索引,即,关系表中在非主键索引属性到该表的索引。

+ 通常包括:
- 聚簇索引和非聚簇索引
* 聚簇索引,叶子结点存储数据本身;非聚簇索引,叶子节点存储主键的数据引用
* covering indx or index with included columns
- 多列索引
* 一种实现方式是将二维地址通过space-filling curve转化成一维,然后用普通索引存储
* 更常见的是用专门的数据结构比如R树进行索引,POstGIS就是一种R树的实现
+ 全文索引:
- 这里的全文索引是指full-text的精确查询
- 前面提到lucene使用了类似LSM-Tree的存储结构存储term到postlist的映射;其内存中的索引结构是FST,类似于Trie树。
- [https://github.com/wfnuser/Algorithms/tree/main/Data%20Structures/FST/c%2B%2B](https://github.com/wfnuser/Algorithms/tree/main/Data%20Structures/FST/c%2B%2B)
- [https://blog.burntsushi.net/transducers/#fst-construction](https://blog.burntsushi.net/transducers/#fst-construction)
+ 纯内存数据库
- Memcached、Redis
- 内存数据库更快的原因是省去了将内存数据结构编码为硬盘数据结构的开销;
- 内存数据库还可以提供难以用基于硬盘的索引实现的数据模型,如优先队列和集合;
- 使用NVM的存储引擎是目前研究的热点



## 事务处理还是分析
事务不一定具有ACID属性,事务处理只是意味着允许客户端进行低延迟的读取和写入,而不是定期运行的批处理作业。

在线事务处理:OLTP, Online Transaction Processing,低延迟、高可用



数据库也常用于数据分析,这类模式被称为在线分析处理(OLAP, Online Analytic Processing)。

### 数据仓库
数据仓库是独立的数据库,不会影响OLTP操作。

数仓从OTLP数据库中提取数据,转换成适合分析的模式,清理并加载到数据仓库中。这个过程被称为「抽取-转换-加载:ETL」。



### 星型和雪花型:分析的模式

许多数据仓库都以相当公式化的方式使用,被称为星型模式(也称为维度建模)

## 列式存储
![img_7.png](/img/ddia/img_7.png)
将来自每一列的所有值存储在一起,如果每个列式存储在一个单独的文件中,查询只需要读取和解析查询中使用的那些列,这可以节省大量的工作。
![img_8.png](/img/ddia/img_8.png)
### 列压缩
![img_9.png](/img/ddia/img_9.png)
一列有很多值是重复的,可以使用位图进行压缩。



```sql
where product_sk in (30, 68, 90)
```

加载product_sk = 30, 68, 90的这三个位图,然后进行OR运算即可。

```sql
WHERE product_sk = 31 AND store_sk = 3
```

加载 product_sk = 31 和 store_sk = 3 的位图,并计算按位与(AND)



### 聚合:数据立方体和物化视图
一些聚合操作可以在写入的时候进行冗余,方便查询。

## 总结
OLTP和OLAP

1. OLTP:
1. 日志结构学派:LSM
2. 就地更新学派:B Tree
Binary file added img/ddia/img.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/ddia/img_0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/ddia/img_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/ddia/img_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/ddia/img_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/ddia/img_4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/ddia/img_5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/ddia/img_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/ddia/img_7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/ddia/img_8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/ddia/img_9.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/ddia/in-memeory-hashmap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 265fa8c

Please sign in to comment.