数据库 - MySQL、PG、MongoDB 索引

1,273 阅读3分钟

常见索引数据结构

现在的数据库索引结构主要使用如下三个数据结构

  1. B-Tree
  2. B+Tree
  3. LSM(Log-Structured Merge-Trees)

而针对以上三种数据结构。我们对应相对熟知的数据库有以下几个:

  1. MySQL:在使用 InnoDB 引擎的情况下,是使用 B+树作为索引的数据结构
  2. PostgresSQL:号称世界最先进的数据库,支持多种数据结构索引,其中我们最常用的类型是 B 树索引。
CREATE INDEX ON rel_8192_$idx.user_live_rights using btree (user_id, right_id, right_type, scope_id) where status != 'deleted';
  1. MongoDB:先进的 NoSQL 数据库。在官方文档中声明自己是用的是 B-Tree 索引。其中官方文档的原文内容如下MongoDB indexes use a B-tree data structure但是 MongoDB 在「Wired Tiger 」引擎当中实现了一个基于 LSM 的索引结构。相关的性能对比可以查看 Btree vs LSM
  2. HBase:HBase 底层的文件存储采用的是 LSM 树的实现。

数据结构回顾

B-Tree

image

B-树是一种自平衡的搜索树,其特点如下:

    1. 多路,非二叉树
    2. 每个节点既保存索引,又保存数据
    3. 搜索时相当于二分查找

B+Tree

imageB+树是 B-树的变种,其特点如下:

    1. 多路非二叉
    2. 只有叶子节点保存数据
    3. 搜索时相当于二分查找
    4. 增加了相邻接点的指向指针

由此可以看出 B-树和 B+树的区别

    1. B+树查询时间复杂度固定是 logn,B-树查询复杂度最好是 O(1)。
    2. B+树相邻接点的指针可以大大增加区间访问性,可使用在范围查询等,而 B-树每个节点 key 和 data 在一起,则无法区间查找。
    3. B+树更适合外部存储,也就是磁盘存储。由于内节点无 data 域,每个节点能索引的范围更大更精确

如下图也可以看到树的演进过程

image

LSM Tree

基本思想

LSM-tree(Log Struct Merge tree) 其中的 Log 思想来源于 Log Structured FileSystem,而不是数据库的 Write Ahead Log,而 LSM-tree 中的 Tree 理论上可以被任何有序的结构替代;因此,LSM-tree 的内核是 Merge 的思想,它充分利用了多层的存储结构,将 write 进行多层的缓冲,因此有了比较好的写性能。

image

针对上图所示,其主要流程如下:

    1. 对于一个写操作,先写入到 memtable 中
    2. 当 memtable 达到一定的限制后,这部分转成 immutable memtable(不可写)
    3. 当 immutable memtable 达到一定限制,将 flush 到磁盘中,即 sstable
    4. sstable 再进行 compaction 操作

主要数据结构

对于 LSM 树主要有三个重要的结构

    1. MemTable:MemTable 是在内存中的数据结构,用于保存最近更新的数据,会按照 Key 有序地组织这些数据,LSM 树对于具体如何组织有序地组织数据并没有明确的数据结构定义,例如 Hbase 使跳跃表来保证内存中 key 的有序。因为数据暂时保存在内存中,内存并不是可靠存储,如果断电会丢失数据,因此通常会通过 WAL(Write-ahead logging,预写式日志) 的方式来保证数据的可靠性。
    2. **Immutable MemTable:**当 MemTable 达到一定大小后,会转化成 Immutable MemTable。Immutable MemTable 是将转 MemTable 变为 SSTable 的一种中间状态。写操作由新的 MemTable 处理,在转存过程中不阻塞数据更新操作。
    3. SSTable(Sorted Sequence Table):有序键值对集合,是 LSM 树组在*磁盘中的数据结构。为了加快 SSTable 的读取,可以通过建立 key 的索引以及布隆过滤器来加快 key 的查找。

image

LSM 数的数据更新是日志式的,当一条数据更新是直接 append 一条更新记录完成的。这样设计的目的就是为了顺序写,不断地将 Immutable MemTable flush 到持久化存储即可,而不用去修改之前的 SSTable 中的 key,保证了顺序写。

因此当 MemTable 达到一定大小 flush 到持久化存储变成 SSTable 后,在不同的 SSTable 中,可能存在相同 Key 的记录,当然最新的那条记录才是准确的。这样设计的虽然大大提高了写性能,但同时也会带来一些问题:

    1. 冗余存储,对于某个 key,实际上除了最新的那条记录外,其他的记录都是冗余无用的,但是仍然占用了存储空间。因此需要进行 Compact 操作 (合并多个 SSTable) 来清除冗余的记录。
    2. 读取时需要从最新的倒着查询,直到找到某个 key 的记录。最坏情况需要查询完所有的 SSTable,这里可以通过前面提到的索引/布隆过滤器来优化查找速度。

布隆过滤器的内部依赖于哈希算法,当检测某一条数据是否见过时,有一定概率出现假阳性(False Positive),但一定不会出现假阴性(False Negative)。也就是说,当布隆过滤器认为一条数据出现过,那么该条数据很可能出现过;但如果布隆过滤器认为一条数据没出现过,那么该条数据一定没出现过。这种特性刚好与此处的需求相契合,即检验某条数据是否缺失。

特点

  1. 写数据时,首先将数据缓存到内存中的一个有序树结构中(称为 memtable)。同时触发相关结构的更新,例如布隆过滤器、稀疏索引。

  2. 当 memtable 积累到足够大时,会一次性写入磁盘中,生成一个内部有序的 segment 文件。该过程为连续写,因此效率极高。

    1. 进行查询时,首先检查布隆过滤器。如果布隆过滤器报告数据不存在,则直接返回不存在。否则,按照从新到老的顺序依次查询每个 segment。
  3. 在查询每个 segment 时,首先使用二分搜索检索对应的稀疏索引,找到数据所在的 offset 范围。然后读取磁盘上该范围内的数据,再次进行二分查找并获得结果。

  4. 对于大量的 segment 文件,定期在后台执行 compaction 操作,将多个文件合并为更大的文件,以保证查询效率不衰减。

From B-Tree To LSM

主流的关系型数据库均以 B/B+ tree 作为其构建索引的数据结构,这是因为 B tree 提供了理论上最高的查询效率 - image 。但对查询性能的追求也造成了 B tree 的相应缺点,即每次插入或删除一条数据时,均需要更新索引,从而造成一次磁盘 IO。这种特性决定了 B tree 只适用于频繁读、较少写的场景。如果在频繁写的场景下,将造成大量的磁盘 IO,从而导致性能骤降。这种应用场景在传统的关系型数据库中比较常见。

而 LSM tree 则避免了频繁写场景下的磁盘 IO 开销,尽管其查询效率无法达到理想的 image ,但依然非常快,可以接受。所以从本质上来说,LSM tree 相当于牺牲了一部分查询性能,换取了可观的写入性能。这对于 key-value 型或日志型数据库是非常重要的。

B+Tree 索引一定好于传统的 B-Tree 索引么

MongoDB

【原创】为什么 Mongodb 索引用 B 树,而 Mysql 用 B+树? 这篇文章中着重描述了 MongoDB 因为是文档型数据库,并且作为聚合型数据库,因此 B-Tree 的数据结构就更加适合这样的场景。

但是实际上的 MongoDB 在实现上面也是采用的最上面所描述的叶子节点存储数据的 B-Tree 索引么?

在 MongoDB 的官方文档 WiredTiger Storage Engine 中已经明确指出。在 MongoDB 3.2 版本之后,WiredTiger 已经成为了 MongoDB 的默认引擎。

在 WiredTiger 的官方文档 Tuning page size and compression 这一节中,其中有对索引实现的明确描述:

WiredTiger maintains a table's data in memory using a data structure called a B-Tree ( B+ Tree to be specific), referring to the nodes of a B-Tree as pages. Internal pages carry only keys. The leaf pages store both keys and values.

其中明确指出了 MongoDB 也是采用的 B+Tree 实现索引

对于 MongoDB 之前所采用的数据引擎「MMAPv1」也没有找到明确的文献表示原有的数据索引是采用的叶子节点存储数据的 B-Tree 实现。

关于 MongoDB 新旧数据引擎的具体对比可以看 What’s New in MongoDB 3.0 Part 3: Performance & Efficiency Gains, New Storage Architecture

image

Postgres

在使用 Pg 建立数据库索引的时候我们可以看到如下的语句

CREATE INDEX ON rel_8192_$idx.user_live_rights using btree (user_id, right_id, right_type, scope_id) where status != 'deleted';

在语句中明确表述了,在创建数据库索引的时候 using btree。但是这个 btree 是我们印象中叶子节点也存储数据的 b-tree 么?

在 PostgreSQL B-Tree 是传统树算法的一个变种(high-concurrency B-tree management algorithm),其算法详情可以参考入场的 github 描述。其主要的改进如下

  1. compared to a classic B-tree, L&Y adds a right-link pointer to each page to the page's right sibling.
  2. It also adds a "high key" to each page, which is an upper bound on the keys that are allowed on that page.

These two additions make it possible detect a concurrent page split, which allows the tree to be searched without holding any read locks (except to keep a single page from being modified while reading it).

其主要的组织形式可以查看《深入浅出 PostgreSQL B-Tree 索引结构》

image

总结

B+Tree 的优点

  1. B+ 树有更低的树高:平衡树的树高 O(h)=O(logdN),其中 d 为每个节点的出度。红黑树的出度为 2,而 B+ Tree 的出度一般都非常大,所以红黑树的树高 h 很明显比 B+ Tree 大非常多。
  2. 磁盘访问原理:操作系统一般将内存和磁盘分割成固定大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。
    • 数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点。
    • 如果数据不在同一个磁盘块上,那么通常需要移动制动手臂进行寻道,而制动手臂因为其物理结构导致了移动效率低下,从而增加磁盘数据读取时间。
    • B+ 树有更低的树高,进行寻道的次数与树高成正比,在同一个磁盘块上进行访问只需要很短的磁盘旋转时间,所以 B+ 树更适合磁盘数据的读取。
  1. 磁盘预读特性:为了减少磁盘 I/O 操作,磁盘往往不是严格按需读取,而是每次都会预读。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的磁盘旋转时间,速度会非常快。并且可以利用预读特性,相邻的节点也能够被预先载入。

参考资料参考资料

  1. MongoDB WiredTiger Internals
  2. Indexes in PostgreSQL — 4 (Btree)
  3. LSM 数增删改查步骤
  4. MONGODB MANUAL - Indexes
  5. 深入浅出 PostgreSQL B-Tree 索引结构
  6. wiredtiger manual
  7. The Difference Between B-trees and B+trees
  8. 为什么 MySQL 使用 B+ 树
  9. LSM-Trees and B-Trees: The Best of Both Worlds
  10. The advantages of an LSM vs a B-Tree
  11. B-Tree-vs-Log-Structured-Merge-Tree
  12. MongoDB · 内核特性 · wiredtiger page 逐出
  13. postgres B 树算法解析
  14. 从 MongoDB 及 Mysql 谈 B-/B+树
  15. From Btree To LSM-tree
  16. LSM 算法的原理是什么?:知乎话题讨论。里面提供了很多 LSM 的实现细节
  17. 【原创】为什么 Mongodb 索引用 B 树,而 Mysql 用 B+树?
  18. LSM 树详解 :里面详细解释了 LSM 的两种合并策略,以及每个策略的优缺点
  19. Effective MongoDB Indexing (Part 1)
  20. [mongodb] MMAP 和 wiredTiger 的比较
  21. MongoDB Engines: MMAPV1 Vs WiredTiger
  22. Read, write & space amplification - B-Tree vs LSM
  23. Indexes in PostgreSQL — 4 (Btree)
  24. [新旧存储引擎比较, WiredTiger与MMAPv1](