2025年9月25日: PostgreSQL 18 发布!
支持版本: 当前 (18) / 17 / 16 / 15 / 14 / 13
开发版本: devel
不支持版本: 12 / 11

65.1. B-Tree 索引 #

65.1.1. 简介 #

PostgreSQL 包含标准btree(多路平衡树) 索引数据结构的实现。任何可以排序成明确定义的线性顺序的数据类型都可以通过 btree 索引进行索引。唯一的限制是索引项不能超过大约一页的三分之一(在应用 TOAST 压缩后,如果适用)。

由于每个 btree 操作符类都对其数据类型施加了一个排序顺序,因此 btree 操作符类(或者更确切地说,操作符族)已成为 PostgreSQL 表示和理解排序语义的通用表示。因此,它们获得了一些超出仅支持 btree 索引所需的功能,并且系统中一些远离 btreeAM的部分会使用它们。

65.1.2. B-Tree 操作符类的行为 #

表 36.3 所示,一个 btree 操作符类必须提供五个比较操作符:<<==>=>。有人可能会期望 <> 也应该作为操作符类的一部分,但事实并非如此,因为在索引搜索中使用 <> WHERE 子句几乎没有用。(出于某些目的,规划器将 <> 与 btree 操作符类相关联;但它通过 = 操作符的否定链接来查找该操作符,而不是从 pg_amop 中查找。)

当多个数据类型共享几乎相同的排序语义时,它们的操作符类可以被分组到一个操作符族中。这样做的好处是它允许规划器推断跨类型比较。族中的每个操作符类应该包含其输入数据类型的单类型操作符(以及相关的支持函数),而跨类型比较操作符和支持函数则“松散”地存在于族中。建议在族中包含一套完整的跨类型操作符,从而确保规划器能够表示它从传递性推导出的任何比较条件。

btree 操作符族必须满足一些基本假设

  • 一个 = 操作符必须是等价关系;也就是说,对于数据类型的每一个非 NULL 值 ABC

    • A = A 为真(自反律

    • 如果 A = B,则 B = A对称律

    • 如果 A = BB = C,则 A = C传递律

  • 一个 < 操作符必须是强序关系;也就是说,对于每一个非 NULL 值 ABC

    • A < A 为假(非自反律

    • 如果 A < BB < C,则 A < C传递律

  • 此外,该排序是全序的;也就是说,对于每一个非 NULL 值 AB

    • 以下三者中恰好有一个为真:A < BA = BB < A三分律

    (三分律当然证明了比较支持函数的定义。)

其他三个操作符以显而易见的方式根据 =< 定义,并且必须与它们保持一致。

对于支持多种数据类型的操作符族,上述定律必须在 ABC 来自族中任何数据类型时成立。传递定律是最难确保的,因为在跨类型情况下,它们表示两个或三个不同操作符的行为是一致的。例如,将 float8numeric 放入同一个操作符族中是行不通的,至少在当前将 numeric 值转换为 float8 以便与 float8 进行比较的语义下是行不通的。由于 float8 的精度有限,这意味着存在不同的 numeric 值将与同一个 float8 值比较相等,因此传递定律会失败。

多数据类型族还有一个要求是,操作符族中包含的数据类型之间定义的任何隐式或二进制强制转换都不得改变相关的排序顺序。

对于 btree 索引来说,需要这些定律在单个数据类型内成立是相当清楚的:没有它们,就没有可以排列键的顺序。另外,使用不同数据类型的比较键进行索引搜索要求比较在两种数据类型之间表现得合理。扩展到族中的三个或更多数据类型并非 btree 索引机制本身严格必需的,但规划器依赖它们进行优化。

65.1.3. B-Tree 支持函数 #

表 36.9 所示,btree 定义了一个必需的和一个可选的五个支持函数。六个用户定义的方法是

order

对于 btree 操作符族提供比较操作符的每一种数据类型组合,它都必须提供一个比较支持函数,在 pg_amproc 中注册,支持函数编号为 1,并且 amproclefttype/amprocrighttype 等于比较的左侧和右侧数据类型(即,与在 pg_amop 中注册的匹配操作符相同的数据类型)。比较函数必须接受两个非 NULL 值 AB,并返回一个 int32 值,当 A < BA = BA > B 时,分别返回 < 0、0 或 > 0。不允许返回 NULL:数据类型的任何值都必须是可比较的。有关示例,请参见 src/backend/access/nbtree/nbtcompare.c

如果比较的值是可排序的数据类型,则相应的排序 OID 将使用标准的 PG_GET_COLLATION() 机制传递给比较支持函数。

sortsupport

可选地,btree 操作符族可以提供 排序支持 函数,在支持函数编号 2 下注册。这些函数允许以比朴素调用比较支持函数更有效的方式实现排序目的的比较。涉及的 API 定义在 src/include/utils/sortsupport.h 中。

in_range

可选地,btree 操作符族可以提供 in_range 支持函数,在支持函数编号 3 下注册。这些函数不用于 btree 索引操作;相反,它们扩展了操作符族的语义,使其能够支持包含 RANGE offset PRECEDINGRANGE offset FOLLOWING 框架界限类型的窗口子句(参见 第 4.2.8 节)。根本上,提供的额外信息是如何以与族的数据排序兼容的方式添加或减去 offset 值。

一个 in_range 函数必须具有签名

in_range(val type1, base type1, offset type2, sub bool, less bool)
returns bool

valbase 必须是同一类型,这是操作符族支持的类型之一(即,它提供排序的类型)。然而,offset 可以是不同类型,可能是族本身不支持的类型。例如,内置的 time_ops 族提供了一个 in_range 函数,其中 offset 的类型是 interval。一个族可以为它支持的任何类型以及一个或多个 offset 类型提供 in_range 函数。每个 in_range 函数都应该在 pg_amproc 中注册,其中 amproclefttype 等于 type1amprocrighttype 等于 type2

一个 in_range 函数的基本语义取决于两个布尔标志参数。它应该添加或减去 baseoffset,然后将 val 与结果进行比较,如下所示:

  • 如果 !sub!less,则返回 val >= (base + offset)

  • 如果 !subless,则返回 val <= (base + offset)

  • 如果 sub!less,则返回 val >= (base - offset)

  • 如果 subless,则返回 val <= (base - offset)

在此之前,该函数应检查 offset 的符号:如果它小于零,则引发错误 ERRCODE_INVALID_PRECEDING_OR_FOLLOWING_SIZE (22013),并附带类似 invalid preceding or following size in window function 的错误文本。(SQL 标准要求如此,尽管非标准操作符族可能选择忽略此限制,因为其语义必要性不大。)此要求委托给 in_range 函数,以便核心代码无需理解特定数据类型的“小于零”意味着什么。

此外,还期望 in_range 函数在可行的情况下,避免在 base + offsetbase - offset 会溢出时抛出错误。即使该值超出数据类型的范围,也可以确定正确的比较结果。请注意,如果数据类型包含“无穷大”或“NaN”等概念,则可能需要格外小心,以确保 in_range 的结果与操作符族的正常排序一致。

in_range 函数的结果必须与操作符族施加的排序一致。精确地说,给定任何固定的 offsetsub 值,则

  • 如果在某些 val1base 下,具有 less = true 的 in_range 为真,则对于具有相同 base 的每一个 val2 <= val1,它都必须为真。

  • 如果在某些 val1base 下,具有 less = true 的 in_range 为假,则对于具有相同 base 的每一个 val2 >= val1,它都必须为假。

  • 如果在某些 valbase1 下,具有 less = true 的 in_range 为真,则对于具有相同 val 的每一个 base2 >= base1,它都必须为真。

  • 如果在某些 valbase1 下,具有 less = true 的 in_range 为假,则对于具有相同 val 的每一个 base2 <= base1,它都必须为假。

less = false 时,具有相反条件的类似陈述也成立。

如果被排序的类型(type1)是可排序的,则相应的排序 OID 将使用标准的 PG_GET_COLLATION() 机制传递给 in_range 函数。

in_range 函数不需要处理 NULL 输入,通常会被标记为严格(strict)。

equalimage

可选地,btree 操作符族可以提供 equalimage(“相等蕴含映像相等”)支持函数,在支持函数编号 4 下注册。这些函数允许核心代码确定何时应用 btree 的去重优化是安全的。目前,equalimage 函数仅在构建或重建索引时被调用。

一个 equalimage 函数必须具有签名

equalimage(opcintype oid) returns bool

返回值是关于操作符类和排序的静态信息。返回 true 表示操作符类的 order 函数保证仅在 AB 参数也互换时才返回 0(“参数相等”),且不丢失任何语义信息。不注册 equalimage 函数或返回 false 表示不能假定此条件成立。

opcintype 参数是操作符类索引的数据类型的 pg_type.oid。这是一个方便的参数,允许跨操作符类重用相同的底层 equalimage 函数。如果 opcintype 是可排序的数据类型,则相应的排序 OID 将使用标准的 PG_GET_COLLATION() 机制传递给 equalimage 函数。

就操作符类而言,返回 true 表示去重是安全的(或者对于传递给其 equalimage 函数的排序 OID 是安全的)。但是,核心代码只有在 每个 索引列都使用注册了 equalimage 函数的操作符类,并且每个函数在被调用时实际返回 true 时,才会认为对该索引的去重是安全的。

映像相等 几乎 与简单的按位相等相同。有一个细微的区别:当索引 varlena 数据类型时,两个映像相等的 datum 的磁盘表示可能不按位相等,因为在输入时应用了不一致的TOAST压缩。形式上,当操作符类的 equalimage 函数返回 true 时,可以安全地假设 datum_image_eq() C 函数将始终与操作符类的 order 函数一致(前提是将相同的排序 OID 传递给 equalimageorder 函数)。

核心代码基本无法从同一族中其他操作符类的详细信息推断出关于多数据类型族中操作符类的“相等蕴含映像相等”状态。另外,操作符族注册跨类型 equalimage 函数是不合理的,尝试这样做会导致错误。这是因为“相等蕴含映像相等”状态不仅取决于排序/相等语义(这些语义在操作符族级别或多或少已定义)。通常,特定数据类型实现的确切语义必须单独考虑。

核心 PostgreSQL 发行版包含的操作符类遵循的约定是注册一个标准的、通用的 equalimage 函数。大多数操作符类注册 btequalimage(),这表示去重总是安全的。可排序数据类型(如 text)的操作符类注册 btvarstrequalimage(),这表示在使用确定性排序时去重是安全的。第三方扩展的最佳实践是注册自己的自定义函数以保留控制权。

options

可选地,B-Tree 操作符族可以提供 options(“操作符类特定选项”)支持函数,在支持函数编号 5 下注册。这些函数定义了一组用户可见的参数,用于控制操作符类的行为。

一个 options 支持函数必须具有签名

options(relopts local_relopts *) returns void

该函数接收一个指向 local_relopts 结构的指针,该结构需要填充一组操作符类特定的选项。可以使用 PG_HAS_OPCLASS_OPTIONS()PG_GET_OPCLASS_OPTIONS() 宏从其他支持函数访问这些选项。

目前,没有 B-Tree 操作符类具有 options 支持函数。B-tree 不像 GiST、SP-GiST、GIN 和 BRIN 那样允许灵活地表示键。因此,options 在当前的 B-tree 索引访问方法中可能应用不多。尽管如此,此支持函数为了统一性被添加到了 B-tree 中,并且可能会在 B-tree 的进一步发展中找到用途。

skipsupport

可选地,btree 操作符族可以提供 skip support 函数,在支持函数编号 6 下注册。这些函数为 B-tree 代码提供了一种迭代操作符类底层输入类型可以表示的每个可能值的方法,按键空间顺序。核心代码在应用跳跃扫描优化时使用此功能。涉及的 API 定义在 src/include/utils/skipsupport.h 中。

不提供跳跃支持函数的类仍然有资格使用跳跃扫描。核心代码仍然可以使用其回退策略,尽管这对于某些离散类型可能不是最优的。对于连续类型上的操作符类提供跳跃支持函数通常没有意义(甚至可能不可行)。

操作符族注册跨类型 skipsupport 函数是不合理的,尝试这样做会导致错误。这是因为确定下一个可索引值必须通过递增从索引元组复制的值来完成。生成的值都必须是相同的底层数据类型(“被跳过”的索引列的 opclass 输入类型)。

65.1.4. 实现 #

本节介绍可能对高级用户有用的 B-Tree 索引实现细节。有关 B-Tree 实现的更详细、侧重于内部的描述,请参阅源代码分发中的 src/backend/access/nbtree/README

65.1.4.1. B-Tree 结构 #

PostgreSQL B-Tree 索引是多级树结构,树的每个级别都可以用作页的双向链表。一个单独的元页存储在索引第一个段文件的固定位置。所有其他页要么是叶子页,要么是内部页。叶子页是树最低级别的页。所有其他级别都由内部页组成。每个叶子页包含指向表行的元组。每个内部页包含指向树下一层的元组。通常,99% 以上的页都是叶子页。内部页和叶子页都使用第 66.6 节中描述的标准页格式。

当现有的叶子页无法容纳传入元组时,B-Tree 索引会添加新的叶子页。一个 页分裂 操作通过将一部分项移动到新页来为原本属于溢出页的项腾出空间。页分裂还必须在父页中插入指向新页的新 下行链接,这可能会导致父页也分裂。页分裂以递归方式“向上级联”。当根页最终无法容纳新的下行链接时,会发生 根页分裂 操作。这通过创建一个比原始根页高一级的根页来为树结构添加一个新级别。

65.1.4.2. 自底向上索引删除 #

B-Tree 索引不直接知道在 MVCC 下可能存在同一逻辑表行的多个现有版本;对索引而言,每个元组都是一个需要自己的索引条目的独立对象。“版本交错”元组有时可能会累积并对查询延迟和吞吐量产生不利影响。这通常发生在 UPDATE 密集型工作负载中,其中大多数单个更新都无法应用 HOT 优化。 仅更改由一个索引覆盖的列的值在 UPDATE总是 需要一组新的索引元组——为表上的每一个索引创建一个。请特别注意,这包括没有被 UPDATE逻辑修改”的索引。所有索引都需要一个指向表中最新版本的后继物理索引元组。每个索引中的每个新元组通常都需要与原始的“已更新”元组短暂共存(通常直到 UPDATE 事务提交后不久)。

B-Tree 索引通过执行 自底向上索引删除 传递来增量删除版本交错索引元组。每次删除传递都是响应预期的“版本交错页分裂”而触发的。这只发生在未被 UPDATE 语句逻辑修改的索引中,否则会在特定页中集中积累过时版本。通常会避免页分裂,尽管有可能某些实现级别的启发式算法未能识别和删除任何一个垃圾索引元组(在这种情况下,页分裂或去重传递会解决新元组不适合叶子页的问题)。任何索引扫描(针对任何单个逻辑行)必须遍历的版本数是影响整体系统响应能力和吞吐量的一个重要因素。自底向上索引删除传递基于逻辑行和版本的定性区别,针对单个叶子页中可疑的垃圾元组。这与 autovacuum 工作进程执行的“自顶向下”索引清理形成对比,后者在满足某些定量表级阈值时触发(参见 第 24.1.6 节)。

注意

并非 B-Tree 索引中执行的所有删除操作都是自底向上删除操作。还有一类独立的索引元组删除:简单索引元组删除。这是一个延迟的维护操作,它删除已知可以安全删除的索引元组(其项标识符的 LP_DEAD 位已设置)。与自底向上索引删除一样,简单索引删除发生在预期的页分裂点,以避免分裂。

简单删除是机会性的,因为它只能在最近的索引扫描在通过时设置受影响项的 LP_DEAD 位时发生。在 PostgreSQL 14 之前,B-Tree 删除的唯一类别是简单删除。它与自底向上删除的主要区别在于,前者是由通过的索引扫描活动机会性地驱动,而后者专门针对不会逻辑修改索引列的 UPDATE 产生的版本交错。

自底向上索引删除负责执行特定索引在某些工作负载下的大部分垃圾索引元组清理。对于任何 B-Tree 索引,如果它受到很少或从不逻辑修改索引覆盖的列的 UPDATE 产生的大量版本交错,则预期会如此。平均和最坏情况下每个逻辑行的版本数可以通过有针对性的增量删除传递来保持较低水平。即使在持续的版本交错 UPDATE 下,某些索引的磁盘大小也可能永远不会增加一个页面/块。即便如此,VACUUM 操作(通常在 autovacuum 工作进程中运行)的一次详尽的“全面清理”最终将作为表及其每个索引集体清理的一部分而需要。

VACUUM 不同,自底向上索引删除不对最旧的垃圾索引元组的年龄提供任何强有力的保证。任何索引都不能保留在表及其所有索引 collectively 的保守截止点之前的“浮动垃圾”索引元组。这个基本的表级不变量使得回收表TID是安全的。这就是不同的逻辑行如何能够随着时间重用相同的表TID(尽管这永远不会发生在生命周期跨越同一 VACUUM 周期的两个逻辑行之间)。

65.1.4.3. 去重 #

重复项是叶子页元组(指向表行的元组),其中所有索引键列的值都与其他索引中的至少一个叶子页元组的相应列值匹配。在实践中,重复元组非常普遍。当启用一个可选技术:去重时,B-Tree 索引可以使用一种特殊的、节省空间的表示法来表示重复项。

去重通过定期将一组重复元组合并在一起,为每个组形成一个单独的发布列表元组来工作。列键值仅出现一次。然后是一个指向表行的TID的排序数组。这极大地减小了索引的存储大小,其中每个值(或每个不同的列值组合)平均出现几次。查询的延迟可以显著降低。整体查询吞吐量可能显著提高。例行索引真空的开销也可能显著降低。

注意

B-Tree 去重对于包含 NULL 值的“重复项”同样有效,即使根据任何 B-Tree 操作符类的 = 成员,NULL 值也永远不会彼此相等。就理解磁盘上 B-Tree 结构的任何部分而言,NULL 只是索引值域中的另一个值。

去重过程是惰性的,当新项被插入但无法放入现有叶子页时发生,但前提是索引元组删除未能为新项腾出足够的空间(通常会短暂考虑删除然后跳过)。与 GIN 发布列表元组不同,B-Tree 发布列表元组不需要在每次插入新重复项时扩展;它们只是叶子页原始逻辑内容的替代物理表示。此设计优先考虑混合读写工作负载的一致性能。大多数客户端应用程序至少会看到使用去重带来的适度性能提升。默认启用去重。

CREATE INDEXREINDEX 应用去重来创建发布列表元组,尽管它们使用的策略略有不同。从表中获取的排序输入中遇到的每个重复普通元组组在添加到当前待定叶子页之前会合并到一个发布列表元组中。单个发布列表元组尽可能地打包了TID。叶子页以通常的方式写入,没有单独的去重传递。此策略非常适合 CREATE INDEXREINDEX,因为它们是一次性的批量操作。

如果索引中重复值很少或没有重复值,则写密集型工作负载不会从去重中受益,但会承担一个小的、固定的性能损失(除非明确禁用去重)。deduplicate_items 存储参数可用于禁用单个索引内的去重。对于只读工作负载,永远不会有性能损失,因为读取发布列表元组至少与读取标准元组表示一样高效。禁用去重通常没有益处。

在某些情况下,唯一索引(以及唯一约束)也可以使用去重。这允许叶子页暂时“吸收”额外的版本交错重复项。唯一索引中的去重增强了自底向上索引删除,特别是在长时间运行的事务持有阻止垃圾回收的快照的情况下。目的是为自底向上索引删除策略再次生效争取时间。将页分裂延迟到单个长时间运行的事务自然消失,可以使删除传递成功,而之前的删除传递失败。

提示

会应用一种特殊的启发式方法来确定唯一索引中的去重传递是否应该发生。它通常可以跳到分裂叶子页,避免了在无用的去重传递上浪费周期的性能损失。如果您担心去重的开销,可以考虑选择性地设置 deduplicate_items = off。在唯一索引中保持启用去重几乎没有缺点。

由于实现级别的限制,去重并非在所有情况下都可用。去重安全性在运行 CREATE INDEXREINDEX 时确定。

请注意,在涉及语义上重要的相等 datum 差异的情况下,去重被视为不安全且无法使用

  • textvarcharchar 在使用非确定性排序时不能使用去重。大小写和重音差异必须在相等的 datum 中保留。

  • numeric 不能使用去重。数字显示精度必须在相等的 datum 中保留。

  • jsonb 不能使用去重,因为 jsonb B-Tree 操作符类在内部使用 numeric

  • float4float8 不能使用去重。这些类型具有 -00 的不同表示,但它们被视为相等。必须保留此差异。

还有一个实现级别的限制,在未来的 PostgreSQL 版本中可能会被解除

  • 容器类型(如复合类型、数组或范围类型)不能使用去重。

还有一个实现级别的限制,该限制适用于所有操作符类或排序

  • INCLUDE 索引永远不能使用去重。

提交更正

如果您在文档中发现任何不正确、与您对特定功能的使用经验不符或需要进一步澄清的内容,请使用 此表单 报告文档问题。