如果你把东西整理得井井有条,下次就不用再找了。
——德国谚语
从最基本的层面看,数据库只需要做两件事情:向它插入数据时,它就保存数据;之后查询时,它应该返回那些数据
许多数据库都使用日志(log),日志是一个仅支持追加式更新的数据文件;实现原理大致为:每行包含一个key-value对,用逗号分割,每次调用插入时,追加新内容到文件末尾,因此多次更新某个键时,旧版本的值不会被覆盖,而是需要查看文件中最后一次出新的键来查找最新的值
索引是基于原始数据派生而来的额外数据结构。很多数据库允许单独添加和删除索引,而不影响数据库的内容,它只会影响查询性能。维护额外结构势必会引入开销,特别是在心数据写入时,。对于写入,它很难超过简单地最佳文件方式的性能,因为那已经是最简单的写操作了。由于每次写数据,需要更新索引,因此任何类型的索引通常都会降低写的速度
key-value类型并不是唯一可以索引的数据,但随处可见,而且是其他更复杂索引的基础构造模块
那么最简单的索引策略是:保存内存中的hash map,将每个键一一映射到数据文件中的特定字节偏移量,这样就可以找到每个值的位置;每当在文件中追加新的数据时,还要更新hash map来反应刚刚写入数据的偏移量(包括插入和更新)。当查找某个值时,使用hash map来找到文件中的偏移量,即存储位置,然后读取
把数据追加到文件中,怎么避免用尽磁盘空间?可以将日志分解为一定大小的段,当文件达到一定大小时就关闭它,并将后续写入新的段文件总,然后在这些段中执行压缩。压缩意味着在日志总丢弃重复的键,只保留每个键最近的更新;此外压缩往往使得段更小,也可以在压缩的同时将多个段合并在一起
实现过程中需要考虑的问题
为什么不原地更新文件:
哈希表索引的局限性:
SSTable:与哈希索引相比,要求key-value对的顺序按键排序,要求每个键在每个合并的段文件中只能出现一次(在压缩的过程中已经确保了)
SSTable相较于哈希索引的优点
如何让数据按键排序?
在磁盘上维护排序结构式可行的(B-trees),不过将其保存在内存中更容易。内存排序有很多广为认知的树状数据结构,例如红黑树和AVL树。使用这些数据结构,可以按人一顺序插入键并以排序后的顺序读取
存储引擎的基本工作流程如下:
当数据库崩溃时,最近写入(在内存表中,未写入磁盘)将会丢失。为避免该问题,可以在磁盘中保留单独的日志,每个写入都会立即追加到该日志,该日志文件不需要按键排序,每当内存表写入SSTable时,相应的日志可以被丢弃
以上算法本质上正是LevelDB和RocksDB所使用还被用于,类似的存储引擎Cassandra和HBase
这种索引结构也并命名为Log-Structured Merge-Tree(LSM-Tree)是基于合并压缩排序文件原理的
Lucene是Elasticsearch和Solr等全文搜索系统所使用的索引引擎,它采用类似的方法来保存其词典:给定搜索查询中的某个单词,找到提及该单词的所有文档(网页、产品描述等)。它主要采用key-value结构实现,其中键是单词(词条),值是所有包含该单词的文档ID的列表(倒排表)。在Lucene中,从词条到Posting list的映射关系保存在类SSTable的排序文件总,这些文件可以根据需要在后台合并
在查找不存在key时,LSM-Tree可能很慢:在确定不存在之前,必须先检查内存表,然后将段一直回溯到最旧的的段文件(可能必须从磁盘总多次读取);为优化这种访问,存储引擎通常使用额外的布隆过滤器
不同的策略会影响甚至觉得SSTable压缩和合并时的具体顺序和时机,最常见的时大小分级和分层压缩。LevelDB和RocksDB使用分层压缩,HBase使用大小分级。Cassandra则同时支持者两种
即使有许多细微差异,但LSM-Tree的基本思想却足够简单有效,即使数据集远远大于可用内存,它仍然能够正常工作,由于数据按排序存储,因此 可以有效地执行区间查询(从最小值到最大值扫描所有键),并且由于磁盘是顺序写入的,所以LSM-tree可以支持非常高的写入吞吐量
B-Tree将数据库分解成固定大小的块或者页,传统上为4kb。页是内部读/写的最小单元。这种设计更接近底层硬件,因为磁盘也是以固定大小的块排列的
每个页面都可以使用地址或者位置进行标识,这样可以让一个页面引用另一个页面,类似指针 ,不过是指向磁盘地址,而不是内存。可以使用这些页面的引用来构造一个树状的页面
某一页被指定为B-tree的根时,每当查找索引中的一个键时,总会从这里开始,向下寻找,直到找到值的页的引用。页面包含若干个键和对子页面的引用,每个孩子都负责一个连续范围内的键,相邻引用之间的键可以指示这些范围之间的边界
B-tree中一个页包含的子页引用数量称为分支因子,分支因素取决于存储页面引用和范围边界所需的空间总量,通常为几百个
如果要更新B-tree中现有键的值,首先要搜索包含该键的叶子页,更改该页的值,并将页写回到磁盘中。如果要添加新键,则需要找到其范围包含新键的页,并将其添加到该页。如果页中没有足够的可用空间来容纳新键,则会将其分裂为两个半满的页,并且父页也需要更新以包含分裂之后的新的键范围
该算法确保树保持平衡:具有n个键的B-tree总是具有O(log n)的深度
B-tree的基本写操作是原地更新页,这和LSM-Tree不同。原地更新页在磁性硬盘驱动器上意味着磁头首先移动到正确的位置,然后旋转盘面,最后用新的数据覆盖响应的扇区。对于SSD,由于SSD必须一次擦除并重写非常大的存储芯片块,情况更负责。另外,某些操作需要覆盖多个不同的页,比如,插入导致页溢出,需要分类也,那么需要写两个分裂的页,并覆盖其父页对两个子页的引用。这是个比较危险的操作,因为如果数据库在完成部分页写入后发生崩溃,最终会导致索引破坏(可能有一个孤儿页,没有被任何其他页所指向)
为了应对数据库崩溃,B-tree的实现需要支持磁盘上额外的数据结构:预写日志(write-ahead log,WAL),也称为重做日志。这是一个仅支持追加修改的文件,每个B-tree的修改必须先修改WAL然后再修改树本身的也,当数据库在崩溃后需要恢复时,该日志用于将B-tree恢复到最近一致的状态
原地更新页的另一个复杂因素时,如果多个线程同时访问B-tree,则需要注意并发控制,否则线程可能会看到树处于不一致状态。通常使用锁存器保护树的数据结构来完成
二级索引:在关系数据库中,可以使用CREATE INDEX命令在同一个表上创建多个耳机索引,并且他们通过对于高效的执行联结操作只管重要
二级索引可以根据key-value索引来构建;可以使用索引中的每个值作为匹配行标识的列表,或者追加一些行标识来是每个键唯一
索引中的键是查询搜索的对象,而值可以是实际行,也可以是对其他地方存储行的引用。后一种情况下,存储行的具体位置被称为堆文件
当更新值而不修改键时,堆文件会非常高效:只要新值得字节数不大于旧值,记录就可以直接覆盖。如果新值较大,它可能需要易懂数据以得到一个足够大空间的新位置,这种情况下,所有索引都需要更新以指向记录的新的堆位置,或者在旧堆位置保留一个间接指针
在某些情况下,从 索引到堆文件的额外跳转对于读取来说意味着太多性能损失,因此可能希望将索引行直接存储在索引中。这类被称为聚集索引
聚集索引(在索引中直接保存行数据)和非聚集索引(仅存储索引中的数据的引用)之间有一种折中设计被称为覆盖索引或包含列的索引,它在索引中保存一些表的列值。它可以支持只通过索引即可回答某些简单查询
最常见的多列索引类型称为级联索引,它通过将一列追加到另一列,将几个字段简单地组合成一个键(索引的定义指定字段连接顺序)
多维索引是更普遍的一次查询多列的方法,这对地理空间数据尤为重要;PostGIS使用PostgreSQL的广义搜索树索引实现了地理空间索引作为R树
全文搜索引擎通常支持对一个单词的所有同义词进行查询,并忽略单词语法上的变体,在同一文档中搜索彼此接近的单词的出现,并支持多种依赖语言分析的其他高级功能。为了处理文档或查询中的拼写错误,Lucene能够在某个编辑距离内搜索文本
内存数据库的性能优势并不是因为它们不需要从磁盘读取。如果有足够的内存,即使是基于磁盘的存储引擎,也可能永远不需要从磁盘读取,因为操作系统将最近使用的磁盘块缓存在内存中。相反,内存数据库可以更快,是因为他们避免使用写磁盘的格式对内存数据结构变慢的开销
内存数据库另一个有意思的地方是,它提供了基于磁盘索引难以实现的某些数据模型,例如redis为各种数据结构(优先队列和集合)都提供了类似数据库的访问接口
最近研究表明,内存数据库架构可以扩展到支持远大于可用内存的数据集,而不会导致以磁盘为中心架构的开销。所谓反缓存方法,当没有足够的内存时,通过将最近最少使用的数据从内存写到磁盘,并在将来再次访问时将其加载到内存。这与操作系统对虚拟内存和交换文件的操作类似,但数据库可以在记录级别而不是整个内存页的粒度工作,因而比操作系统更有效的管理内存,不过这种方法仍然需要索引完全放入内存
事务不一定具有ACID(原子性,一致性,隔离性和持久性)属性。事务处理只是意味着允许客户端进行低延迟读取和写入,相比于只能周期性地运行的批处理作业
OLTP(online transaction processing,OLTP):在线事务处理,主要应用于系统基本的,日常的事务处理(银行交易等)
OLAP(online analtic processing,OLAP):在线分析处理,支持复杂的分析操作,侧重决策支持,并且提供直观易懂的查询结果
属性 | 事务处理系统(OLTP) | 分析系统(OLAP) |
---|---|---|
主要读特征 | 基于键,每次查询返回少量的记录 | 对大量记录进行汇总 |
主要写特征 | 随机访问,低延迟写入用户的输入 | 批量导入(ETL)或事件流 |
典型使用场景 | 终端用户,通过网络应用程序 | 内部分析师,为决策提供支持 |
数据表征 | 最新的数据状态(当前时间点) | 随着时间而变化的所有事件历史 |
数据规模 | GB到TB | TB到PB |
在实际应用中,OLTP系统对于业务运行只管重要,所以往往期望它们高度可用,处理事务时延迟足够低,并且数据库管理员要密切关注OLTP数据库运行状态。数据库管理员通常不愿意让业务分析人员在OLTP数据库上直接运行临时分析查询,这些查询通常代价很高,需要扫描大量数据姐,这可能会所害并发执行事务的性能
数据仓库是单独的数据库,分析人员可以在不影响OLTP操作的情况下进行地使用。数据仓库包含公司所有各种OLTP系统的只读副本。从OLTP(使用周期性数据转储或连续更新流)中提取数据,转换为分析友好的模式,执行必要的清理,然后加载到数据仓库中。将数据导入数据仓库的过程称为提取-转换-加载(Extract-transform-Load,ETL)
使用单独的数据仓库而不是直接查询OLTP系统进行分析,很大的优势在于数据仓库可以针对分析访问模式进行优化。而前面提到的索引算法适合OLTP,但不适合分析查询
数据仓库和关系型OLTP数据库看起来相似,因为它们都具有SQL查询接口。然而,系统内部实则差异很大,它们针对迥然不同的查询模式进行了各自优化。许多数据库供应商现在专注于支持事务处理或分析工作负载,但不能同时支持两者
许多数据仓库都相当公式化的使用了星型模式,也称为维度建模
模式的中心一个所谓的事实表,事实表的每一行表示在特定时间发生的事件,事实表的列表示属性。其他列可能会引用其他表的外键,称为维度表。由于事实表中的每一行都代表一个事件,维度通常代表时间的who,what,where,when,how,why
名称"星型模式"来源于当表关系可视化时,事实表位于中间,被一系列维度表包围。这些表的连接就像星星的光芒
该模版的一个变体称为雪花模式,其中维度进一步细分为子空间
在典型的数据仓库中,表通常非常宽:事实表通常超过100列,有时候几百列。维度表可能也非常宽,可能包括与分析相关的所有元数据
面向列存储的想法很简单:不要将一行中的所有值存储在一起,而是将没咧中的所有值存储在一起。如果每个列存储在一个单独的文件中,查询只需要读取和解析在该查询中使用的那些列,面向列的存储布局依赖一组列文件,每个文件以相同顺序保存着顺序行。因此,如果需要重新组装整行,可以从每个单独列文件中获取第23个条目,名将它们放在一起构成表的第23行
列存储在关系数据模型中最容易理解,但它同样适用于非关系数据,例如Parquet是基于Google的Dremel的一种支持文档数据模型的列存储格式
取决于列中具体数据模式,可以采用不同的压缩技术。在数据仓库中特别有效的是一种技术是位图编码
通常,列中的不同值得数量小于行数。现在可以使用n个不同的值得列,并将其转换为n个单独的位图:一个位图对应每个不同的值,一个位图对应不同的值,一个位对应一个行,如果行具有该值,该位为1,否则为0;
Cassandra和HBase有一个列族的概念,它们继承自Google Bigtable。但是,将它们称为面向列则非常令人误解:在每个列族中,它们将一行中的所有列与行主键一起保存,并且不是用列压缩。因此,Bigtable模型仍然主要是面向行
除了减少需要从磁盘加载的数据量之外,面向列的存储布局也有利于高效利用CPU周期。例如,查询引擎可以将一大块压缩列数据放入CPU的L1缓存中,并以紧凑循环(没有函数调用)进行迭代。对于每个被处理的记录,CPU能够比基于很多函数调用和用条件判断的代码更快地执行这种循环。列压缩使得列中更多的行可以加载到L1缓存
即使数据是按列存储的,它也需要一次排序整行。数据库管理员可以基于常见的查询的知识来选择要排序表的列。这样查询优化器可以减少扫描的范围
另一个,排序可以帮助进一步压缩列。如果主排序列上没有很多不同的值,那么在排序之后,它将出现一个非常长的序列,其中相同的值在一行中重复多次。一个简单的游程编码,即使该表可能拥有数十亿行,也可以将其压缩到几千字节
面向列的存储、压缩和排序都非常有助于加速读取查询,但写入更困难;类似于B-tree那样原地更新的方式并不适合。合适的方式是使用LSM-tree。所有的写入首先进入内存存储区,将其添加到已排序的结构中,接着再准备写入磁盘
为避免每次聚合查询时都需要处理原数据,可以选择将最常使用的一些计数或总和缓存起来
创建这种缓存的一种方式是物化视图:物化视图是查询结果的实际副本,并被写入到磁盘中
当底层数据发生变化时,物化视图需要随之更新,因为它是数据的非规范化副本。数据库可以自动执行,但这种更新方式会影响数据写入性能,这就是为什么在OLTP数据库中不经常使用物化视图。面对大量读密集的数据仓库,物化视图则更有意义
物化视图常见的一种特殊情况被称为数据立方体或OLAP立方体。它是哟不同维度分组的聚合网格
物化数据立方体的优点是某些查询会非常快,主要是它们已被预先计算出来;缺点是,数据立方体缺乏想查询原始数据那样的灵活性
存储类型分为两大类:针对事务处理(OLAP)优化的架构,以及针对分析型(OLAP)的优化架构。它们典型的访问模式存在很大的差异:
在OLTP方面,主要由两个流派的存储引擎
日志结构的存储引擎是一个相对较新的方案,其关键思想是系统地将磁盘上随机访问写入转为顺序写入,由于磁盘驱动器和SSD的性能特性,可以实现更高的写入吞吐量