PostgreSQL 包含标准btree(多路平衡树) 索引数据结构的实现。任何可排序为明确线性顺序的数据类型都可由 btree 索引编制索引。唯一限制是,索引项不能超过一个页面的大约三分之一(如果适用,在 TOAST 压缩之后)。
由于每个 btree 操作符类都会对其数据类型施加排序顺序,因此 btree 操作符类(或实际上的操作符系列)被用作PostgreSQL 对排序语义的一般表示和理解方式。因此,它们具备了一些超出仅支持 btree 索引所需的功能,并且与 btreeAM相距甚远的系统部件也会使用它们。
如 表 36.3 所示,一个 btree 运算符类必须提供五个比较运算符,<
、<=
、=
、>=
和 >
。可能会认为 <>
也应是运算符类的组成部分,但它不是,因为在一个索引搜索中使用 <>
WHERE 子句几乎永远没有用。(有时,计划处理程序把 <>
当做与一个 btree 运算符类相关;但它是通过 =
运算符的 negator 链接,而不是通过 pg_amop
来查找该运算符的。)
当几种数据类型共享几乎相同的排序语义时,它们的运算符类可以分组到一个运算符族中。这样做的优点是,它允许计划处理程序对跨类型比较做出推断。族内的每个运算符类都应包含其输入数据类型的单类型运算符(和关联支持函数),而跨类型比较运算符和支持函数在族内是“松散的”。建议在族内包含一组完整的跨类型运算符,从而确保计划处理程序可以表示它从传递性推断出的任何比较条件。
有的基本假设是,一个 btree 运算符族必须满足的
一个 =
运算符必须是一个等价关系;也就是说,对于数据类型的所有非空值 A
、B
、C
A
=
A
为真(自反律)
如果 A
=
B
,则 B
=
A
(对称律)
如果 A
=
B
并且 B
=
C
,则 A
=
C
(传递律)
一个 <
运算符必须是一个严格排序关系;也就是说,对于所有非空值 A
、B
、C
A
<
A
为假 (非自反律)
如果 A
<
B
且 B
<
C
,则 A
<
C
(传递律)
此外,排序是全面的;也就是说,对于所有非空值 A
,B
恰好满足以下条件之一:A
<
B
、A
=
B
和 B
<
A
(三段论律)
(当然,三段论律也证明了比较支持函数的定义。)
其他三个运算符按显而易见的方式使用 =
和 <
定义,并且必须与它们保持一致。
对于支持多种数据类型的运算符系列,当从系列中的任何数据类型获取 A
、B
、C
时,上述定律必须成立。传递律最难确保,因为在跨类型情况下,它们表示两个或三个不同运算符的行为是一致的。例如,将 float8
和 numeric
放在同一个运算符系列中是行不通的,至少对于现有的语义而言不是这样,即在与 float8
值进行比较时,numeric
值被转换为 float8
。由于 float8
的精度有限,这意味着有不同的 numeric
值与同一个 float8
值相等,因此传递律将失败。
对多数据类型系列的另一个要求是,在运算符系列中包含的数据类型之间定义的任何隐式或二进制强制转换都不能更改关联的排序顺序。
btree 索引要求这些定律在一个数据类型内成立的原因非常清晰:如果没有这些定律,就没有排序可以按排列键。此外,使用不同数据类型进行比较键的索引搜索要求跨两种数据类型合理进行比较。btree 索引机制本身并不严格要求将数据类型扩展到三种或更多种,但是规划器出于优化目的依赖于它们。
如 表 36.9 所示,btree 定义了一个必需的支持函数和四个可选支持函数。五个用户定义方法是
order
对于 btree 运算符族提供比较运算符的所有数据类型组合,它必须提供一个比较支持函数,该函数在 pg_amproc
中注册支持函数 1 和 amproclefttype
/amprocrighttype
等于用于比较的左右数据类型(即,与匹配运算符在 pg_amop
中注册的数据类型相同)。比较函数必须采用两个非空值 A
和 B
,并返回 int32
值,该值在 A
<
B
、A
=
B
或 A
>
B
时分别为 <
0
、0
或 >
0
。禁止空结果:数据类型的所有值都必须可比较。有关示例,请参见 src/backend/access/nbtree/nbtcompare.c
。
如果比较值是可整理数据类型,则使用标准 PG_GET_COLLATION()
机制将适当的排序规则 OID 传递给比较支持函数。
sortsupport
或者,btree 运算符族可以提供在支持函数 2 下注册的 排序支持 函数。这些函数允许以比天真调用比较支持函数更有效的方式实现用于排序的比较。此过程涉及的 API 在 src/include/utils/sortsupport.h
中定义。
in_range
或者,btree 运算符族可以提供在支持函数 3 下注册的 in_range 支持函数。这些函数不会在 btree 索引操作中使用;而是扩展运算符族的语义,使其能够支持包含 RANGE
offset
PRECEDING
和 RANGE
offset
FOLLOWING
帧边界的窗口子句(请参见 4.2.8 节)。从根本上讲,提供的额外信息是添加或减去 offset
值的方式,与该族的排序方式兼容。
in_range
函数必须具有以下签名
in_range(val
type1,base
type1,offset
type2,sub
bool,less
bool) returns bool
val
和 base
必须为同一种类型,这属于运算符族(也就是说,它提供排序的类型)所支持的类型之一。但是,offset
可能是不同的类型,甚至可能是此运算符族不支持的类型。内置的 time_ops
族提供一个 in_range
函数,其中 offset
的类型为 interval
。一个运算符族可以为其任何支持的类型提供 in_range
函数以及一种或多种 offset
类型。每个 in_range
函数都应输入到 pg_amproc
中,其 amproclefttype
等于 type1
,amprocrighttype
等于 type2
。
一个 in_range
函数的基本语义取决于两个布尔标志参数。它应对 base
和 offset
进行加或减运算,然后将 val
与结果进行比较,如下所示
如果 !
sub
和 !
less
,则返回 val
>=
(base
+
offset
)
如果 !
sub
和 less
,则返回 val
<=
(base
+
offset
)
如果 sub
和 !
less
,则返回 val
>=
(base
-
offset
)
如果 sub
和 less
,则返回 val
<=
(base
-
offset
)
在执行此操作之前,此功能应检查偏移量
的符号:如果 ERRCODE_INVALID_PRECEDING_OR_FOLLOWING_SIZE
(22013) 小于零,则使用错误文本引发错误,例如 “窗口函数中的无效前缀或后缀大小”。(这是 SQL 标准的要求,虽然非标准运算符族可能会选择忽略此限制,因为这似乎没有太大的语义必要性。)此要求已委托给in_range
函数,以便核心代码不必了解 “小于零” 对特定数据类型的含义。
另一个期望是,in_range
函数在实际情况下应避免在基础
+
偏移量
或 基础
-
偏移量
发生溢出时引发错误。即使该值超出了数据类型的范围,也可确定正确的比较结果。请注意,如果数据类型包含“无穷大”或 “NaN”等概念,则可能需要格外小心,以确保in_range
的结果与运算符族的正常排序顺序一致。
in_range
函数的结果必须与运算符族施加的排序顺序一致。确切地说,对于 偏移量
和 子集
的任何固定值,
如果 in_range
和less
= true 对于某些val1
和base
为真,则对于具有相同 base
的每个 val2
<=
val1
也应为真。
如果 in_range
和less
= true 对于某些val1
和base
为假,则对于具有相同 base
的每个 val2
>=
val1
也应为假。
如果 in_range
和less
= true 对于某些val
和base1
为真,则对于具有相同 val
的每个 base2
>=
base1
也应为真。
如果 in_range
和less
= true 对于某些val
和base1
为假,则对于具有相同 val
的每个 base2
<=
base1
也应为假。
在 less
= false 时,带有反向条件的类似陈述同样成立。
如果被排序的类型(type1
)可排序,则将通过标准 PG_GET_COLLATION() 机制将适当的排序规则 OID 传递给 in_range
函数。
in_range
函数不需要处理 NULL 输入,且通常会被标记为严格的。
equalimage
还有,btree 运算符族可以提供 equalimage
(“相等暗示图像相等”) 支持函数,注册在支持函数编号 4 下。这些函数允许核心代码确定 btree 去重优化何时可以安全应用。当前,只有在构建或重建索引时才会调用 equalimage
函数。
要拥有签名,equalimage
函数必须
equalimage(opcintype
oid
) returns bool
返回值是关于运算符类和排序规则的静态信息。返回 true
指示运算符类的 order
函数保证仅当其 A
和 B
参数在没有任何语义信息丢失的情况下也可以互换时才返回 0
(“参数相等”)。未注册 equalimage
函数或返回 false
表示不能假定有这个条件。
参数 opcintype
是运算符类建立索引的数据类型的
。这提供了便利,允许在运算符类中重用相同的底层 pg_type
.oidequalimage
函数。如果 opcintype
是可排序数据类型,则将适当的排序规则 OID 传递给 equalimage
函数,并使用标准 PG_GET_COLLATION()
机制。
就运算符类而言,返回 true
表明去重是安全的(或者对传递给其 equalimage
函数的 OID 的排序规则而言是安全的)。然而,只有当一个索引的 每个 已编入索引的列都使用一个注册有 equalimage
函数的运算符类,并且当调用时每个函数实际上都返回 true
时,核心代码才会认为对于索引而言去重是安全的。
图像相等 几乎 与简单的按位相等条件相同。有一个细微的差别:在为 varlena 数据类型建立索引时,两个图像相等的数据项的磁盘表示可能由于以下情况的不一致应用而不能按位相等TOAST对输入的压缩。正式地说,当运算符类别的 equalimage
函数返回 true
时,可以安全地假设 datum_image_eq()
C 函数总是与运算符类别的 order
函数一致(前提是将相同的校验和 OID 同时传递给 equalimage
和 order
函数)。
核心代码基本上无法从同类其他运算符类别的详细信息推论出多数据类型系列中运算符类别的 “相等暗示图像相等” 状态。此外,运算符系列注册跨类型 equalimage
函数是不明智的,并且尝试这样做将导致错误。这是因为 “相等暗示图像相等” 状态不仅取决于排序/相等语义,这些语义在一定程度上在运算符系列级别定义。通常,必须单独考虑特定数据类型实现的语义。
核心 PostgreSQL 分发中所含运算符系列遵循的约定是注册一个现成的通用 equalimage
函数。大多数运算符系列都注册 btequalimage()
,这表明总是可以安全地进行重复数据删除。对于 text
等可排序的数据类型的运算符系列,则注册 btvarstrequalimage()
,这表明在确定性校对中可以安全地进行重复数据删除。对于第三方扩展而言,最佳实践是注册自己的自定义函数以保留控件。
选项
B 树运算符系列可以选择提供 options
(“运算符类特定选项”) 支持函数,在支持函数编号 5 下注册。这些函数定义用户可见参数集,这些参数集控制运算符系列行为。
一个 options
支持函数必须具有如下签名
options(relopts
local_relopts *
) returns void
该函数会传递到 local_relopts
结构的指针,该指针需要填充一组运算符类特定的选项。可以使用 PG_HAS_OPCLASS_OPTIONS()
和 PG_GET_OPCLASS_OPTIONS()
宏从其他支持函数访问这些选项。
目前,没有 B 树运算符系列具有 options
支持函数。B 树不允许弹性表示键,而 GiST、SP-GiST、GIN 和 BRIN 允许弹性表示键。因此,在当前 B 树索引访问方法中,options
可能没有什么用武之地。尽管如此,B 树中还是增加了此支持函数以保持一致性,并且它很可能在 PostgreSQL 中 B 树进一步进化期间找到用途。
本节介绍 B 树索引实现细节,高级用户可能会使用到这些细节。有关更详细、更专注于内部结构的 B 树实现说明,请参阅源发行版中的 src/backend/access/nbtree/README
。
PostgreSQL B 树索引是多级树结构,其中树的每一级都可以用作页面的双向链表。单个元页存储在索引的第一个段文件开头处的固定位置。所有其他页面都是叶页面或内部页面。叶页面是树最底层的页面。所有其他层都包含内部页面。每个叶页面都包含指向表行的元组。每个内部页面都包含指向树下一层的元组。通常,超过 99% 的页面都是叶页面。内部页面和叶页面都使用 第 65.6 节 中描述的标准页面格式。
当现有叶页面无法容纳传入元组时,新的叶页面会被添加到 B 树索引。页面分割 操作通过将项目的一部分移动到新页面,为原本属于溢出页面的项目腾出空间。页面分割还必须将一个新的下链插入父页面中的新页面,这又可能导致父页面也进行分割。页面分割会以递归的方式“级联向上”。最终当根页面无法容纳新的下链时,将执行根页面分割 操作。这将通过创建一个位于原始根页面上方一级的新的根页面,为树结构添加一个新层级。
B-Tree 索引并不知悉在 MVCC 下同一个逻辑表行可能有多个现有版本;对于索引,每个元组都是需要有自己索引项的独立对象。“版本混乱”元组有时可能累积,对查询延迟和吞吐量产生不利影响。这通常发生在 UPDATE
-密集型工作负载中,其中大多数单个更新无法应用 HOT 优化。 在 UPDATE
期间仅更改一个列的值(该值由一个索引覆盖)始终需要一组新的索引元组,每个索引元组用于表上的每个索引。请特别注意,这包括 UPDATE
没有“在逻辑上修改”的索引。所有索引都需要一个后继物理索引元组,该索引元组指向表中的最新版本。每个索引中的每个新元组通常需要在短时间内(通常直到 UPDATE
事务提交后不久)与原始“更新的”元组共存。
B-Tree 索引通过执行自下而上的索引删除数据块来逐渐删除版本混乱索引元组。每次删除数据块都是针对预期的“版本混乱页面拆分”而触发的。这种情况只发生在未被 UPDATE
语句在逻辑上修改的索引中,在这种索引中,废弃版本特别集中会出现在特定页面中。页面拆分通常会被避免,但某些实现级启发式可能会无法识别和删除甚至是一个垃圾索引元组(在这种情况下,页面拆分或重复数据删除数据块解决了新进入元组不适合叶页面这一问题)。对于任何索引扫描(对于任何单一逻辑行)必须遍历的版本的最坏情况下数量是整个系统响应和吞吐量的重要影响因素。自下而上的索引删除数据块基于涉及逻辑行和版本的定性区别,以可疑垃圾元组作为目标。这与自动清理工作器执行的“自上而下”索引清理形成对比,后者在超出某些定量表级阈值时触发(请参阅 第 24.1.6 节)。
在 B 树索引中执行的并非所有删除操作都是自底向上的删除操作。一种明确的索引元组删除类别:简单的索引元组删除。这是一个延迟维护操作,用于删除已知可以安全删除的索引元组(那些其项目标识符的 LP_DEAD
位已经设置)。
简单删除是机会主义的,因为它只在最近的索引扫描在经过时设置受影响项目的 LP_DEAD
位时才会执行。在 PostgreSQL 14 之前,B 树删除的唯一类别是简单删除。其与自底向上删除的主要区别在于,只有前者是机会主义地受传递索引扫描的活动控制,而只有后者专门针对不会从逻辑角度修改索引列的 UPDATE
的版本轮换。
自底向上的索引删除对带有特定工作负载的特定索引执行大多数垃圾索引元组清理。对于会因不频繁甚至是永远不会从逻辑角度修改其所涵盖的列的 UPDATE
而导致大量版本轮换的任何 B 树索引,这一点是预期中的。
与 VACUUM
不同,自底向上索引删除不能就最古老的垃圾索引元组可能有多老提供强有力的保证。不能允许任何索引保留在表及其所有索引集体共享的一个保守的截止点之前死亡的“移动的垃圾”索引元组。这种基本表级不变性可以安全地回收表TID。这就是随着时间的推移不同逻辑行能够重复利用相同表TID的原因(尽管这种情况永远不会发生在两个其生命周期跨越同一个 VACUUM
周期的逻辑行上)。
重复项是一个叶页面元组(指向表行的元组),其中所有索引键列中都有值与相同索引中至少一个其他叶页面元组中的相应列值相匹配。重复元组在实践中非常常见。启用可选技术时,B 树索引可以使用特殊的节省空间的表示形式表示重复项:重复数据删除。
重复数据删除通过定期将重复元组分组并合并到一起以形成每个分组的单个过帐单元组来实现。列键值仅在此表示形式中出现一次。这后面是一个排序的数组,其中TID指向表中的行。这显著地减少了每个值(或列值的每个不同组合)平均出现多次的索引的存储大小。查询的延迟可以显著减少。总体查询吞吐量可能会显著增加。常规索引清理的开销也可能会显著减少。
B 树重复数据删除对于包含 NULL 值的“重复项”同样有效,即使 NULL 值根据任何 B 树运算符类的=
成员永远不彼此相等。就理解磁盘上 B 树结构的实现部分而言,NULL 只是索引值域中的另一个值。
当插入一个不能装入现有叶页面中的新项时,重复数据删除过程会以延迟方式进行,但前提是索引元组删除不能为新项释放足够的空间(通常会简单地考虑删除,然后跳过)。与 GIN 过帐单元组不同,B 树过帐单元组在插入新重复项时不需要每次都扩展;它们只是叶页面的原始逻辑内容的备用物理表示形式。这种设计优先考虑对混合读写工作负载的一致性能。大多数客户端应用程序至少会从使用重复数据删除中看到中等程度的性能优势。默认情况下启用重复数据删除。
CREATE INDEX
和REINDEX
应用重复数据删除来创建过帐单元组,但它们使用的策略略有不同。从表中获取的已排序输入中遇到的每组重复普通元组在添加到当前待定的叶页面之前会合并到过帐单元组中。单独的过帐单元组会尽可能多地打包TID。叶页面以通常方式写出,无需任何单独的重复数据删除传递。此策略非常适合CREATE INDEX
和REINDEX
,因为它们是单次批处理操作。
由于索引中重复值少或没有,因此不因去重而受益的写入密集型工作负载会产生较小、固定的性能损失(除非明确禁用去重)。deduplicate_items
存储参数可用于在各个索引中禁用去重。对于只读工作负载,永远不会有任何性能损失,因为读取张贴列表元组至少与读取标准元组表示一样高效。禁用去重通常没有帮助。
有时,唯一索引(以及唯一约束)可以使用去重。这允许叶页面临时“吸收”额外的版本转换重复项。唯一索引中的去重增强了自下而上的索引删除,尤其是在长时间运行的事务持有阻止垃圾收集的快照的情况下。目标是为自下而上的索引删除策略争取时间,以再次生效。延迟页面拆分,直到单个长时间运行的事务自然消失,可以允许自下而上的删除传递成功,而早期的删除传递失败。
应用特殊启发,以确定是否应在唯一索引中执行去重传递。它通常可以直接跳到拆分叶页面,从而避免因在无用的去重传递上浪费周期而产生的性能损失。如果您担心去重的开销,请考虑有选择地将 deduplicate_items = off
设置为禁用。在唯一索引中启用去重几乎没有缺点。
由于实现级别的限制,并非所有情况都可以使用去重。当运行 CREATE INDEX
或 REINDEX
时,将确定去重安全性。
请注意,在涉及相等数据之间的语义差异的情况下,去重被认为是不安全的,不能在以下情况下使用
text
、varchar
和 char
在使用不确定性校对时不能使用去重。相等数据之间必须保留大小写和重音差异。
numeric
不能使用去重。相等数据之间必须保留数字显示比例。
jsonb
不能使用去重,因为 jsonb
B-Tree 运算符类内部使用numeric
。
float4
和 float8
不能使用去重。这些类型对-0
和 0
有不同的表示,但仍被认为是相等的。必须保留此差异。
在未来版本的 PostgreSQL 中可能会解除另一项实现级别的限制
容器类型(如复合类型、数组或范围类型)不能使用去重。
无论使用哪种操作符类或排序规则,还有一个进一步的实现级别限制适用
INCLUDE
索引永远不能使用重复数据删除。