一些缓冲池优化方式:
- 多缓冲池(Multiple Buffer Pools):多个同时使用多个并发缓冲池而不是一个
- 缓存预取(Pre-fetching):提前将一些加载到缓冲池减少 I/O
- 扫描共享(Scan Sharing):多个查询共享一个扫描的结果
- 绕过缓冲池(Buffer Pool Bypass):对于某些查询,不通过缓冲池以防污染
我们从多缓冲池(Multiple Buffer Pools)的概念开始:从逻辑上讲 DBMS 有一种缓冲池,你可以把页从磁盘加载到内存中,但在物理上,它可以被实现为具有不同策略的多个单独的缓冲池。例如你的系统管理多个并发数据库,每一个都可以有自己的缓冲池。例如你可以针对不同的页类型有不同的缓冲池,例如表的页,索引的页,这些可以由完全独立的缓冲池处理。
它有很多优点,减少锁存器争用,并且可以每个缓冲池针对不同的需求使用不同的优化策略(例如针对查询的,数据库的,不同类型页的缓冲池)。但是也引入了一个问题:你如何判断你有一个页,你想让它存在与唯一一个缓冲池?
有两种常用的方法:
- 第一种方法是当你存储页时你存储一些与之相关的对象 id(Object Id):例如这里对象id是指页类型,它可以是一个存储元组和表的页,它可以是存储部分索引数据结构的页,可能是存储日志记录的页。举一个实例:假设 Q1 查询想得到记录 123,根据前面的课程,我们知道这个 123 可以解析出数据库中这个记录的位置信息,这里这个位置信息包括 ObjectId,PageId,SLotNum,根据 ObjectId 去对应的缓冲池寻找。
- 第二种方法是取哈希值:还是对于 Q1 查询想得到记录 123,对于这个记录取哈希值,然后对独立缓冲池的数量取余数得出该去哪个缓冲池去查询。
我们要讲的下一个重要优化是缓存预取(Pre-fetching),这种想法是 DBMS 可以根据查询计划在实际需要之前预取页。假设我们有一个查询 Q1 执行顺序查询扫描所有页,DBMS 可以执行一些数据预取,比如在开始扫描第 0 页的时候,就把第 0,1,2 页都加载到缓冲池中。之后到第 3 页的时候,第 0,1,2 页不再使用,可以被替换成 3,4,5 页,这样查询不用在扫描每一页的时候都阻塞。
我们接下来看这样一个查询,查询 val 在 100 ~ 250 之间的所有记录,这个查询是可以通过索引优化不用扫描所有页的。索引会在后面的课程详细讲。
目前对于 val 这个索引,你可以把它想象成一个平衡二叉树。我们将从这个根页(index-page0)开始,之后搜索左子树 index-page1,然后找到 index-page3,由于叶子节点互相之间是有指针连接起来相当于一个双向链表,所以我们不必重新遍历二叉树就能找到 index-page5。这样我们就找到了索引中我们所有要扫描的页。
这个例子告诉我们预取需要根据数据结构以及扫描方式做出改变,并不是一直顺序扫描的
问题:你怎么知道你应该分配多少资源来做预取?学术界有很多关于预取的研究,在商业系统中,是一个很大的卖点,更好的预取应该是可以计算出你知道用这种方式预取需要付出多少资源,如果你花费太多资源做预取,那么你就会阻碍系统进行的实际工作;而如果你什么都不做,就会出现太多 I/O 阻塞。所以这是你在两者之间必须达成一种微妙的平衡。
下一个是扫描共享(Scan Sharing)
其基本思想是查询可以重用从存储中检索的数据,这也被称为同步扫描(Synchronized scans),它不同于结果缓存。结果缓存主要是针对某个特定查询的,对于不同的查询一般不会生效。扫描共享则是不必是一样的查询,但是可以共享中间结果的。
如果一个查询从磁盘读取页,并将它们放入内存,可以让另一个需要访问相同页的查询重用它们,它允许多个查询附加到一个正在扫描表的游标上。查询不一定是一样的,但是他们需要访问相同的页。
假设有两个查询:
- Q1:SELECT SUM(val) FROM A
- Q2:SELECT AVG(val) FROM A
这两个查询,都是扫描 A 表的所有页。假设 Q1 先开始执行,读取到第 3 页,这时候 Q2 开始执行,Q2 和 Q1 要扫描的页是一样的,但是 Q2 可以直接附加在 Q1 的游标上继续扫描,等 Q1 扫描完,Q2 再扫描剩下的之前没有扫描到的。
但是,如果这里 Q2 加上 limit 100,这种限制,如果配合扫描共享,那么可能每次扫描出来的结果是不一样的,因为你也不确认它到底是从头扫描还是附加到其他查询的游标上以及当前游标的位置。所以,我们最好不要有这样的查询,对于所有带 Limit 的查询,最好都指定排序条件。