段可见性映射
段可见性映射
我们会在*段*级别维护一个动态可见性映射,显示哪些段的所有行都具有 100% 的可见性。这被称为段可见性映射 (SVM)。
我们为表中的每个段分配至少两个位,而不是每个块分配两个位。这两个位允许我们表示四种状态
- 读写
- 只读
- 只读待处理(稍后解释)
- 只读冻结
一些其他已讨论的状态是
- 显式标记为只读
- 聚簇
- 压缩
- 脱机(表示访问成本高)
我们应该允许每个段 1 字节(8 位)。
即使对于相当大的表,这可能足够小,可以直接存储在 pg_class 中。但是,我们希望在 SVM 发生更改时避免膨胀,此外我们可能需要处理一些非常大的表。因此,我们将 SVM 存储为单个列,存储在具有存储属性 MAIN 的表 pg_svm 中,因此它是内联的,可压缩的。
段是块的范围,其大小可以根据需要调整。最初的建议是段的大小为 1GB,与物理数据段相同。我们可能允许用户设置这个大小,或者我们可能自动调整它以最小化开销并最大化其效用。
可以动态更改 SVM 段大小,相应地重新计算位位置。如果为了优化开销并从这种方法中获益,这是可取的。
访问 SVM
由于映射非常小,并且表的更改很少,因此我们可以轻松地将其缓存在每个后端。
如果映射发生更改,我们可以执行 relcache 失效,以使每个人重新读取映射。(那里也需要一些改进,但这里不相关)。
不需要动态共享内存缓存,因为任何对表的并发更改都会被扫描忽略,因此在扫描期间是否发生 INSERT、UPDATE 或 DELETE 并不重要。任何开始的新扫描都将尝试锁定表,然后在继续之前执行 rel 缓存检查。因此,至少对于*该*扫描,可见性将被正确设置。
在大多数情况下,可见性映射可以总结为单个布尔值,以显示是否存在*任何* 100% 可见段。这使得在常见的未设置情况下访问映射非常便宜。这将是 Relation 结构上的另一个布尔值条目。
检查 rel 缓存是否已更改本质上是免费的,因为我们已经在获取锁时进行了此操作。由于 INSERT、UPDATE 和 DELETE 只是破坏了缓存,因此它们不需要拥有完全最新的图片。(该方面可能会改变)。
如果我们希望运行非实用程序命令的后端看到 SVM,那么我们需要在每次访问表时检查 rel 缓存,而不仅仅是在每次锁定表时检查。我们不需要在所有情况下都这样做,只需要那些将从了解 SVM 信息中获益的那些情况。
取消设置段可见性映射
如果在标记为只读或只读待处理的段内发生 INSERT、UPDATE 或 DELETE,则 SVM 将被“取消设置”,即设置为读写。此目录更改可以使用非事务性覆盖来处理 - 该位始终已存在,因此覆盖的数据始终具有相同的大小。这是悲观的,因为它即使在 UPDATE/DELETE 终止时也会重置状态,但它比保持行锁定到事务完成更好,这可能会阻止其他事情发生。或者,也许我们可以在事务提交时做到这一点。
DML 命令将首先检查布尔值 rel.has_read_only_segments 以查看是否存在任何非读写段。如果是,那么我们将计算 DML 操作写入的段,然后相应地设置状态。
将段设置为只读待处理引起的 relcache 失效会刷新每个后端中与该关系关联的 rd_targBlock。其他 INSERT 不可能,因为所有当前代码路径要么使用扩展要么使用 FSM 以及表非空,而两者都不会导致问题。扩展只影响最后一个段,该段永远不会是只读待处理的。FSM 不包含任何与只读待处理段相关的条目。
SHARE 锁当前写入数据块,但由于它们表示瞬态状态,因此我们不需要取消设置分区或可见性映射。
设置段可见性映射
设置可见性映射很棘手,因为它必须在对表进行并发更改时起作用。
我们通过使用一个中间状态来做到这一点,类似于 CREATE INDEX CONCURRENTLY 的工作方式。
首先,VACUUM 将扫描一个段,如果发现所有元组都可见,则将该段的状态设置为只读待处理。我们还会记录 VACUUM 的 xid,这样我们就知道它何时被设置。
如果下一个 VACUUM 仍然看到只读待处理状态,那么我们再次执行 VACUUM。如果所有行都可见,则在第二次 VACUUM 结束时,如果仍然设置了只读待处理状态,则我们设置只读状态。(如果对 SVM 的更改是立即的,我们不必等待;如果我们只在事务结束时进行 SVM 更改,那么我们必须等待所有对表的并发锁定的完成,然后才能设置只读状态)。
我们为什么要这样做?第一次 VACUUM 可能遗漏了一些并发更改。但是,一旦设置了只读待处理状态,任何进一步的更改都会清除它。因此,在第二次 VACUUM 结束时,我们保证已经看到了第一次 VACUUM 开始和第二次 VACUUM 结束之间的任何更改。
如果我们发现一个段完全冻结,那么我们会设置只读冻结,而不仅仅是只读。
如果我们执行 VACUUM FREEZE,那么我们会重新扫描只读段,希望将它们设置为只读冻结。
VACUUM 将跳过只读和只读冻结段。
VACUUM FREEZE 将跳过只读冻结段。
VACUUM FULL 将始终扫描所有段,以便我们在怀疑出现问题时始终有一个安全选项可以回退。
我们永远不会将最高编号的段标记为只读待处理,因为它在发生 INSERT 时很可能会扩展。这也是将较小的表排除在与此功能相关的开销之外的一个有用方法。
VACUUM 优势
在仅插入的表中,这意味着只有最后 1 或 2 个段是读写的,因此 VACUUM 将始终在有限的时间内运行,无论表有多大。
通常表中存在大量被删除的区域,因此这将有助于将 VACUUM 引导到更改区域。
我们认识到只读段会改变自动真空计算 - 我们只是完全将它们排除在外。这意味着 VACUUM 的运行频率会比现在高,但它们运行时会更快。(请注意,当前,在一个不断增长的表上,VACUUM 会随着表的大小而变得越来越远,假设写入速率恒定)。
以后的 VACUUM 仍然需要冻结行,不过这可以在我们达到冻结限制时发生,或者如果我们运行显式 VACUUM FREEZE,则可以更早地发生。我们仍然只为每个表存储一个最旧的 xid。不需要为每个段存储特定 xid。
与 FSM 交互
这完全背离了可见性映射和空闲空间映射以某种方式相关的想法。但是,FSM 需要进行更改以确保提议的技术稳定且有用:如果我们扫描一个段,并且它 100% 可见,如果该段中的一个块中有空闲空间,那么我们很快就会发现我们的可见性位将很快被设置为关闭。
如果该段中总的空闲空间超过 5%,那么我们不会设置只读待处理,也不会向 FSM 报告段中的块。这样,少量空闲空间不会破坏将段设置为只读的优势。
VACUUM 当前会覆盖 FSM 信息,但如果情况并非如此,那么我们将不得不主动将其从新设置为只读的段中删除。也许该限制将为 (100 - fillfactor) %,这通常根据用户对表上可能更新数量的预期来设置。
实用程序
可以拥有一个段感知的 CLUSTER。这将要求我们在可见性映射中使用另一个标志来指示聚簇/非聚簇。这可能有助于我们改进 REINDEX 时间,因为只读聚簇段的行不需要排序。