DDIA第三章读书笔记 数据存储与检索(olap相关摘取)

索引

索引可以加快读取速度。那为啥不对所有数据建索引呢

  • 建索引会带来额外的空间开销
  • 写数据的时候,需要更新索引。任何类型的索引都会降低写速度

哈希索引

bitcask存储引擎

内存中维护hashmap,kv分别对应key和其在数据文件的字节偏移量。
所以在文件中追加新的kv对的时候,还需要更新hashmap来反映刚写入数据的偏移量。

比较适合key数量较少(足够全部放在内存中),但是value数量频繁更新,数量庞大的情况。value数据量可以超过内存大小(靠一次磁盘io加载数据)

如果都只追加到一个文件,如何避免磁盘空间耗尽情况

好的方案是把日志分解成一定大小的段。当其中一个小段到达一定大小后就关闭该文件,写入到另外一个小文件。并且在其基础上执行压缩。

怎么压缩呢?

压缩意味着在日志中丢弃重复的key,并且只保留每个key最近的更新。

当然你也可以这样:

执行压缩时候,把多个段合并在一起,对于这些冻结段的合并和压缩可以在后台线程中运行,并且运行时候可以用旧的段文件继续正常读取和写请求。合并完成后,可以把读取请求切换到新的合并段上,旧的合并段这时候安全删除。

实现细节

  • 文件格式
    csv不是日志最佳格式。更快更简单方法是二进制格式,以字节为单位记录字符串长度。

  • 删除记录
    如果要删除key与其关联的value,可以在数据文件追加一个特殊的删除记录。合并日志段的时候,如果发现这个标记,就会丢弃这个删除key的所有value

  • 崩溃恢复
    bitcask还会把每个段的hash map快照存储在磁盘上,崩溃恢复的时候可以更快加载到内存中

  • 校验
    数据库可能随时会崩溃,因此把记录追加到日志过程中,bitcask文件会包括校验值,那么发现损坏部分就可以丢弃

  • 并发控制
    日志是追加写,那么实现可以为一个写线程,但是读线程有多个

缺点局限

  • 哈希表必须要全放入内存,如果key数量很大,需要放在磁盘&大量的随机访问io
  • 区间查询效率不高

SSTables 和 lsm-tree

  • SSTables
    排序字符串表。意味着段文件中的key-value顺序按key排序

好处是:
1.合并多个段的时候,就可以用mergesort来进行全局排序
2.使得稀疏索引的实现成为可能,不需要维护所有key的偏移。那么在分段文件中可以使用binary search来快速定位所要寻找的key
3.由于读请求往往需要扫描请求范围内的多个key-value对,因此可以考虑把这些记录保存到一个块中,并且在写磁盘前将其压缩。然后稀疏内存索引的每个条目都会指向压缩块的开头,除了节省磁盘空间,还会减少io带宽占用。

  • lsm-tree
    存储引擎基本工作流程:

1.写入时候,将其添加到内存中的平衡树数据结构,内存中的树有时叫做内存表。

2.当内存表大于某个阈值的时候,会将其作为sstable文件写入磁盘。新的sstable会成为数据库的最新部分,而在sstable写磁盘的同时,写入可以继续添加到一个新的内存表中

3.处理读请求的时候,首先会在内存表中查找key,然后是最新的磁盘段文件,接下来是次新的磁盘段文件

4.后台进程会周期性执行段合并和压缩过程,以合并多个段文件,然后丢弃覆盖或者删除的值

如果写入内存表的数据在落盘前数据库崩溃了,怎么办?

方案是:
在磁盘上保留单独的日志,每个写入都会立刻追加到该日志。然后数据从内存表写入sstable的时候,相应磁盘上的日志就可以丢弃。

性能优化

当去查找数据库某个不存在的key的时候,lsm-tree可能会很慢。因为在确定key不存在之前,必须先检查内存表,然后一直回溯到最旧的段文件。(这可能会涉及到多次的磁盘io读取)

possible optimization:
使用额外的bloom filter来加速判断key是否存在

当然,sstables的压缩和合并具体顺序和时机也值得讨论。

最常见方式是大小分级分层压缩

leveldb和rocksdb使用的是分层压缩,hbase用的是大小分级,cassandra是支持两种方式。
clickhouse采用的应该是按照大小来分级。

在大小分级的压缩中,较新和较小的sstables会合并到较旧并且较大的sstables中

在分层压缩中,key的范围会分裂成多个更小的sstables,旧数据会移动到单独的层级,这样压缩就可以逐步进行并且节省磁盘空间。

lsm同时也是支持非常高的写入吞吐量,因为写磁盘是顺序写入的。

并且由于数据按排序存储,执行区间查询效率也是非常高的。

根据经验,lsm树写入可能会更快,而btree读取速度会更快,lsm读取会比较慢,因为要在不同的压缩阶段检查多个不同数据结构和sstable

lsm树的优点:

btree至少要写两次数据,一次是预写日志,一次是写入树的page本身
即使page中只有少数几个字节的更改,也需要承受整个页的开销。

由于反复压缩和sstable合并,lsm-tree也会重写数据多次。
这种影响也叫做写放大,即数据库内由于一次数据库写入请求导致的多次磁盘写
对于ssd而言,只能承受有限次的擦除覆盖,因此会比较关注写放大的指标。

对于大量写密集的程序,性能瓶颈很可能在于数据库写入磁盘的速率。这种情况下写放大有直接的性能成本,存储引擎写入磁盘的次数越多,可用磁盘带宽中per second可以处理的写入就会越少。

lsmtree通常能够承受比btree更高的写入吞吐量。因为它们有时候具有较低的写放大,另外一部分原因是它们以顺序写方式写入紧凑的sstable中,而不必重写树中的多个页,要知道磁盘的顺序写远比随机写快。

lsm-tree可以更好支持压缩, 因此通常磁盘上的文件会比btree小很多,由于碎片的存在,btree的磁盘利用率会比不上lsmtree。由于lsm-tree不是面向页的,并且会定期重写sstables来消除碎片化,所以它们通常会具有较低的存储开销,特别是在使用分层压缩的时候。

在许多ssd上,内部会使用日志结构化算法把随机写入转换为底层存储芯片上的顺序写入。

总结起来就是:
1.更低的写放大
2.更少碎片
3.更紧凑的数据组织方式
4.同样的io带宽内可以支持更多的读写请求

lsm-tree缺点

日志存储缺点是压缩过程可能会干扰正在进行的读写操作。即使存储引擎尝试增量执行压缩,也并不会影响并发访问,但由于磁盘的并发资源有限,所以当磁盘执行昂贵的压缩操作时候,很容易会发生读写请求等待的情况。

对于高写入吞吐量,压缩的另外一个问题是: 磁盘的有限写入带宽需要在初始写入(记录并且刷新内存表到磁盘)和后台运行的压缩线程之间所共享。

写入空数据库的时候,全部的磁盘带宽可用于初始写入,但数据库数据量越大,压缩所需要的磁盘带宽也会越多。

如果写入吞吐量很高,而且没有仔细配置压缩性能,那么可能会发生压缩速率无法匹配新数据写入速率的情况。

这种情况下:磁盘上未合并段的数量会不断增加,一直到磁盘空间不足,由于它们需要检查更多的段文件,因此读取速度也会降低。

列存储

bitmap index
DDIA第三章读书笔记 数据存储与检索(olap相关摘取)_第1张图片
对于同一列行数很多但是distinct值很少的情况来说,可以为每一个值维护一个bit vector
那么查询如:
WHERE product_sk IN(30,68,69)
就可以直接加载30,68,69这三个value的bit vector,并且计算bit的位或结果。
同时上图最下方就是游程编码

内存带宽与向量化处理

对于需要扫描数百万行的数据仓库查询来说,一个巨大的瓶颈是从磁盘获取数据到内存的带宽。但是,这不是唯一的瓶颈。分析数据库的开发人员也担心有效利用主存储器带宽到CPU缓存中的带宽,避免CPU指令处理流水线中的分支错误预测和泡沫,以及在现代中使用单指令多数据(SIMD)指令CPU。

除了减少需要从磁盘加载的数据量以外,面向列的存储布局也可以有效利用CPU周期。例如,查询引擎可以将大量压缩的列数据放在CPU的L1缓存中,然后在紧密的循环中循环(即没有函数调用)。一个CPU可以执行这样一个循环比代码要快得多,这个代码需要处理每个记录的大量函数调用和条件。列压缩允许列中的更多行适合相同数量的L1缓存。前面描述的按位“与”和“或”运算符可以被设计为直接在这样的压缩列数据块上操作。这种技术被称为矢量化处理。

写入列存储

这些优化在数据仓库中是有意义的,因为大多数负载由分析人员运行的大型只读查询组成。面向列的存储,压缩和排序都有助于更快地读取这些查询。然而,他们有写更加困难的缺点。
使用B树的更新就地方法对于压缩的列是不可能的。如果你想在排序表的中间插入一行,你很可能不得不重写所有的列文件。由于行由列中的位置标识,因此插入必须始终更新所有列。
幸运的是,本章前面已经看到了一个很好的解决方案:LSM树。所有的写操作首先进入一个内存中的存储,在这里它们被添加到一个已排序的结构中,并准备写入磁盘。内存中的存储是面向行还是列的,这并不重要。当已经积累了足够的写入数据时,它们将与磁盘上的列文件合并,并批量写入新文件。这基本上是Vertica所做的【62】。

查询需要检查磁盘上的列数据和最近在内存中的写入,并将两者结合起来。但是,查询优化器隐藏了用户的这个区别。从分析师的角度来看,通过插入,更新或删除操作进行修改的数据会立即反映在后续查询中。

什么是物化视图

数据仓库查询通常涉及一个聚合函数,如SQL中的COUNT,SUM,AVG,MIN或MAX。如果相同的聚合被许多不同的查询使用,那么每次都可以通过原始数据来处理。为什么不缓存一些查询使用最频繁的计数或总和?
创建这种缓存的一种方式是物化视图。在关系数据模型中,它通常被定义为一个标准(虚拟)视图:一个类似于表的对象,其内容是一些查询的结果。不同的是,物化视图是查询结果的实际副本,写入磁盘,而虚拟视图只是写入查询的捷径。从虚拟视图读取时,SQL引擎会将其展开到视图的底层查询中,然后处理展开的查询。

物化视图一个特例就是olap立方

说白了就是一些常用高频的聚合数据先存起来,如果之后的语句用到就直接取就行,就不需要再做全表扫描了对吧
DDIA第三章读书笔记 数据存储与检索(olap相关摘取)_第2张图片
在OLTP方面,我们看到了来自两大主流学派的存储引擎:

日志结构学派
只允许附加到文件和删除过时的文件,但不会更新已经写入的文件。 Bitcask,SSTables,LSM树,LevelDB,Cassandra,HBase,Lucene等都属于这个组。关键思想是把磁盘上的随机写入转化为顺序写入,加上ssd的性能特性,能实现更高的写入吞吐

就地更新学派
将磁盘视为一组可以覆盖的固定大小的页面。 B树是这种哲学的最大的例子,被用在所有主要的关系数据库中,还有许多非关系数据库。

你可能感兴趣的:(数据库)