1.Base基础/3.Icon图标/操作/search备份
1.Base基础/3.Icon图标/操作/search备份
EN
文档
关于AntDB
部署与升级
快速入门
运维
调优
工具和插件
高级服务
数据安全
参考
  • 文档首页 /
  • 使用教程 /
  • SQL语言 /
  • 索引

索引

更新时间:2024-07-01 14:39:48

简介

索引是提高数据库性能的常用途径。比起没有索引,使用索引可以让数据库服务器更快找到并获取特定行。但是索引同时也会增加数据库系统的日常管理负担,因此应该聪明地使用索引。

假设有一个如下的表:

CREATE TABLE test1 (
    id integer,
    content varchar
);

而应用发出很多以下形式的查询:

SELECT content FROM test1 WHERE id = constant;

在没有事前准备的情况下,系统不得不扫描整个 test1 表,一行一行地去找到所有匹配的项。如果 test1 中有很多行但是只有一小部分行(可能是 0 或者 1)需要被该查询返回,这显然是一种低效的方式。但是如果系统被指示维护一个在 id 列上的索引,它就能使用一种更有效的方式来定位匹配行。例如,它可能仅仅需要遍历一棵搜索树的几层而已。

类似的方法也被用于大部分非小说书籍中:经常被读者查找的术语和概念被收集在一个字母序索引中放在书籍的末尾。感兴趣的读者可以相对快地扫描索引并跳到合适的页而不需要阅读整本书来寻找感兴趣的材料。正如作者的任务是准备好读者可能会查找的术语一样,数据库程序员也需要预见哪些索引会有用。

正如前面讨论的,下列命令可以用来在 id 列上创建一个索引:

CREATE INDEX test1_id_index ON test1 (id);

索引的名字 test1_id_index 可以自由选择,但最好选择一个能想起该索引用途的名字。

为了移除一个索引,可以使用 DROP INDEX 命令。索引可以随时被创建或删除。

一旦一个索引被创建,就不再需要进一步的干预:系统会在表更新时更新索引,而且会在它觉得使用索引比顺序扫描表效率更高时使用索引。但可能需要定期地运行 ANALYZE 命令来更新统计信息以便查询规划器能做出正确的决定。

索引也会使带有搜索条件的 UPDATEDELETE 命令受益。此外索引还可以在连接搜索中使用。因此,一个定义在连接条件列上的索引可以显著地提高连接查询的速度。

在一个大表上创建一个索引会耗费很长的时间。默认情况下,AntDB 允许在索引创建时并行地进行读(SELECT 命令),但写(INSERTUPDATEDELETE)则会被阻塞直到索引创建完成。在生产环境中这通常是不可接受的。在创建索引时允许并行的写是可能的,但是有些警告需要注意,更多信息可以参考 Building Indexes Concurrently。

一个索引被创建后,系统必须保持它与表同步。这增加了数据操作的负担。因此哪些很少或从不在查询中使用的索引应该被移除。

索引类型

AntDB 提供了多种索引类型: B-tree、Hash、GiST、SP-GiST 、GIN 和 BRIN。每一种索引类型使用了 一种不同的算法来适应不同类型的查询。默认情况下, CREATE INDEX 命令创建适合于大部分情况的 B-tree 索引。

B-tree 可以在可排序数据上的处理等值和范围查询。特别地,AntDB 的查询规划器会在任何一种涉及到以下操作符的已索引列上考虑使用 B-tree 索引:

<
<=
=
>=
>

将这些操作符组合起来,例如 BETWEENIN,也可以用 B-tree 索引搜索实现。同样,在索引列上的 IS NULLIS NOT NULL 条件也可以在 B-tree 索引中使用。

优化器也会将 B-tree 索引用于涉及到模式匹配操作符 LIKE~ 的查询,前提是如果模式是一个常量且被固定在字符串的开头—例如:col LIKE 'foo%' 或者 col ~ '^foo',但在 col LIKE '%bar' 上则不会。但是,如果数据库没有使用 C 区域设置,需要创建一个具有特殊操作符类的索引来支持模式匹配查询。同样可以将 B-tree 索引用于 ILIKE~*,但仅当模式以非字母字符开始,即不受大小写转换影响的字符。

B-tree 索引也可以用于检索排序数据。这并不会总是比简单扫描和排序更快,但是总是有用的。

Hash 索引只能处理简单等值比较。不论何时当一个索引列涉及到一个使用了=操作符的比较时,查询规划器将考虑使用一个 Hash 索引。下面的命令将创建一个 Hash 索引:

CREATE INDEX name ON table USING HASH (column);

GiST 索引并不是一种单独的索引,而是可以用于实现很多不同索引策略的基础设施。相应地,可以使用一个 GiST 索引的特定操作符根据索引策略(操作符类)而变化。作为一个例子,AntDB 的标准捐献包中包括了用于多种二维几何数据类型的 GiST 操作符类,它用来支持使用下列操作符的索引化查询:

<<
&<
&>
>>
<<|
&<|
|&>
|>>
@>
<@
~=
&&

GiST索引也有能力优化“最近邻”搜索,例如:

SELECT * FROM places ORDER BY location <-> point '(101,456)' LIMIT 10;

它将找到离给定目标点最近的 10 个位置。能够支持这种查询的能力同样取决于被使用的特定操作符类。

和 GiST 相似,SP-GiST 索引为支持多种搜索提供了一种基础结构。SP-GiST 允许实现众多不同的非平衡的基于磁盘的数据结构,例如四叉树、k-d 树和 radix 树。作为一个例 子,AntDB 的标准捐献包中包含了一个用于二维点的 SP-GiST 操作符类,它用于支持使用下列操作符的索引化查询:

<<
>>
~=
<@
<^
>^

就像 GiST, SP-GiST 支持 “nearest-neighbor” 搜索。

GIN 索引是“倒排索引”,它适合于包含多个组成值的数据值,例如数组。倒排索引中为每一个组成值都包含一个单独的项,它可以高效地处理测试指定组成值是否存在的查询。

与 GiST 和 SP-GiST相似, GIN 可以支持多种不同的用户定义的索引策略,并且可以与一个 GIN 索引配合使用的特定操作符取决于索引策略。作为一个例子,AntDB 的标准贡献包中包含了用于数组的 GIN 操作符类,它用于支持使用下列操作符的索引化查询:

<@
@>
=
&&

BRIN 索引(块范围索引的缩写)存储有关存放在一个表的连续物理块范围上的值摘要信息。与 GiST、SP-GiST 和 GIN 相似,BRIN 可以支持很多种不同的索引策略,并且可以与一个 BRIN 索引配合使用的特定操作符取决于索引策略。对于具有线性排序顺序的数据类型,被索引的数据对应于每个块范围的列中值的最小值和最大值,使用这些操作符来支持用到索引的查询:

<
<=
=
>=
>

多列索引

一个索引可以定义在表的多个列上。例如,有这样一个表:

CREATE TABLE test2 (
  major int,
  minor int,
  name varchar
);

(即将 /dev 目录保存在数据库中)而且经常会做如下形式的查询:

SELECT name FROM test2 WHERE major = constant AND minor = constant;

那么可以在 majorminor 上定义一个索引:

CREATE INDEX test2_mm_idx ON test2 (major, minor);

目前,只有 B-tree、GiST、GIN 和 BRIN 索引类型支持多列索引,最多可以指定 32 个列(该限制可以在源代码文件 pg_config_manual.h 中修改,但是修改后需要重新编译 AntDB)。

一个 B-tree 索引可以用于条件中涉及到任意索引列子集的查询,但是当先导列(即最左边的那些列)上有约束条件时索引最为有效。确切的规则是:在先导列上的等值约束,加上第一个无等值约束的列上的不等值约束,将被用于限制索引被扫描的部分。在这些列右边的列上的约束将在索引中被检查,这样它们适当节约了对表的访问,但它们并未减小索引被扫描的部分。例如,在 (a, b, c) 上有一个索引并且给定一个查询条件 WHERE a = 5 AND b >= 42 AND c < 77,对索引的扫描将从第一个具有 a = 5 和 b = 42 的项开始向上进行,直到最后一个具有 a = 5 的项。在扫描过程中,具有 c >= 77 的索引项将被跳过,但是它们还是会被扫描到。这个索引在原则上可以被用于在 b 和/或 c 上有约束而在 a 上没有约束的查询,但是整个索引都不得不被扫描,因此在大部分情况下规划器宁可使用一个顺序的表扫描来替代索引。

一个多列 GiST 索引可以用于条件中涉及到任意索引列子集的查询。在其余列上的条件将限制由索引返回的项,但是第一列上的条件是决定索引上扫描量的最重要因素。当第一列中具有很少的可区分值时,一个 GiST 索引将会相对比较低效,即便在其他列上有很多可区分值。

一个 GIN 索引可以用于条件中涉及到任意索引列子集的查询。与 B-tree 和 GiST 不同,GIN 的搜索效率与查询条件中使用哪些索引列无关。

多列 BRIN 索引可以被用于涉及该索引被索引列的任意子集的查询条件。和 GIN 相似且不同于 B- 树 或者 GiST,索引搜索效率与查询条件使用哪个索引列无关。在单个表上使用多个 BRIN 索引来取代一个多列 BRIN 索引的唯一原因是为了使用不同的 pages_per_range 存储参数。

当然,要使索引起作用,查询条件中的列必须要使用适合于索引类型的操作符,使用其他操作符的子句将不会被考虑使用索引。

多列索引应该较少地使用。在绝大多数情况下,单列索引就足够了且能节约时间和空间。具有超过三个列的索引不太有用,除非该表的使用是极端程式化的。

索引和 ORDER BY

除了简单地查找查询要返回的行外,一个索引可能还需要将它们以指定的顺序传递。这使得查询中的 ORDER BY 不需要独立的排序步骤。在 AntDB 当前支持的索引类型中,只有 B-tree 可以产生排序后的输出,其他索引类型会把行以一种没有指定的且与实现相关的顺序返回。

规划器会考虑以两种方式来满足一个 ORDER BY 说明:扫描一个符合说明的可用索引,或者先以物理顺序扫描表然后再显式排序。对于一个需要扫描表的大部分的查询,一个显式的排序很可能比使用一个索引更快,因为其顺序访问模式使得它所需要的磁盘 I/O 更少。只有在少数行需要被取出时,索引才会更有用。一种重要的特殊情况是 ORDER BYLIMIT n 联合使用:一个显式的排序将会处理所有的数据来确定最前面的 n 行,但如果有一个符合 ORDER BY 的索引,前 n 行将会被直接获取且根本不需要扫描剩下的数据。

默认情况下,B-tree 索引将它的项以升序方式存储,并将空值放在最后(表 TID 被处理为其它相等条目之间的分线器列)。这意味着对列x 上索引的一次前向扫描将产生满足 ORDER BY x(或者更长的形式:ORDER BY x ASC NULLS LAST)的结果。索引也可以被后向扫描,产生满足 ORDER BY x DESCORDER BY x DESC NULLS FIRSTNULLS FIRSTORDER BY DESC 的默认情况)。

可以在创建 B-tree 索引时通过 ASCDESCNULLS FIRSTNULLS LAST 选项来改变索引的排序,例如:

CREATE INDEX test2_info_nulls_low ON test2 (info NULLS FIRST);
CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);

一个以升序存储且将空值前置的索引可以根据扫描方向来支持 ORDER BY x ASC NULLS FIRSTORDER BY x DESC NULLS LAST

读者可能会疑惑为什么要麻烦地提供所有四个选项,因为两个选项连同可能的后向扫描可以覆盖所有 ORDER BY 的变体。在单列索引中这些选项确实有冗余,但是在多列索引中它们却很有用。考虑 (x, y) 上的一个两列索引:它可以通过前向扫描满足 ORDER BY x, y,或者通过后向扫描满足 ORDER BY x DESC, y DESC。但是应用可能需要频繁地使用 ORDER BY x ASC, y DESC。这样就没有办法从通常的索引中得到这种顺序,但是如果将索引定义为 (x ASC, y DESC) 或者 (x DESC, y ASC) 就可以产生这种排序。

显然,具有非默认排序的索引是相当专门的特性,但是有时它们会为特定查询提供巨大的速度提升。是否值得维护这样一个索引取决于会多频繁地使用需要特殊排序的查询。

组合多个索引

只有查询子句中在索引列上使用了索引操作符类中的操作符并且通过 AND 连接时才能使用单一索引。例如,给定一个 (a, b) 上的索引,查询条件 WHERE a = 5 AND b = 6 可以使用该索引,而查询 WHERE a = 5 OR b = 6 不能直接使用该索引。

幸运的是,AntDB 具有组合多个索引(包括多次使用同一个索引)的能力来处理那些不 能用单个索引扫描实现的情况。系统能在多个索引扫描之间安排 ANDOR 条件。例如, WHERE x = 42 OR x = 47 OR x = 53 OR x = 99 这样一个查询可以被分解成为四个独立的在 x 上索引扫描,每一个扫描使用其中一个条件。这些查询的结果将被“或”起来形成最后的结果。另一个例子是如果在 xy 上都有独立的索引,WHERE x = 5 AND y = 6 这样的查询的一种可能的实现方式就是分别使用两个索引配合相应的条件,然后将结果“与”起来得到最后的结果行。

为了组合多个索引,系统扫描每一个所需的索引并在内存中准备一个位图用于指示表中符合索引条件的行的位置。然后这些位图会被根据查询的需要“与”和“或”起来。最后,实际的表行将被访问并返回。表行将被以物理顺序访问,因为位图就是以这种顺序布局的。这意味着原始索引中的任何排序都会被丢失,并且如果存在一个 ORDER BY 子句就需要一个单独的排序步骤。由于这个原因以及每一个附加的索引都需要额外的时间,即使有额外的索引可用,规划器有时也会选择使用单一索引扫描。

在所有的应用(除了最简单的应用)中,可能会有多种有用的索引组合,数据库开发人员必须做出权衡以决定提供哪些索引。有时候多列索引最好,但是有时更好的选择是创建单独的索引并依赖于索引组合特性。例如,如果查询中有时只涉及到列 x,有时候只涉及到列y,还有时候会同时涉及到两列,可以选择在 x 和 y 上创建两个独立索引然后依赖索引组合来处理同时涉及到两列的查询。当然也可以创建一个 (x, y) 上的多列索引。当查询同时涉及到两列时,该索引会比组合索引效率更高,但是正如第 11.3 节中讨论的,它在只涉及到 y 的查询中几乎完全无用,因此它不能是唯一的一个索引。一个多列索引和一个 y 上的独立索引的组合将会工作得很好。多列索引可以用于那些只涉及到 x 的查询,尽管它比 x 上的独立索引更大且更慢。最后一种选择是创建所有三个索引,但是这种选择最适合表经常被执行所有三种查询但是很少被更新的情况。如果其中一种查询要明显少于其他类型的查询,可能需要只为常见类型的查询创建两个索引。

唯一索引

索引也可以被用来强制列值的唯一性,或者是多个列组合值的唯一性。

CREATE UNIQUE INDEX name ON table (column [, ...]);

当前,只有 B-tree 能够被声明为唯一。

当一个索引被声明为唯一时,索引中不允许多个表行具有相同的索引值。空值被视为不相同。一个多列唯一索引将会拒绝在所有索引列上具有相同组合值的表行。

AntDB 会自动为定义了一个唯一约束或主键的表创建一个唯一索引。该索引包含组成主键或唯一约束的所有列(可能是一个多列索引),它也是用于强制这些约束的机制。

注意

不需要手工在唯一列上创建索引,如果那样做也只是重复了自动创建的索引而已。

表达式索引

一个索引列并不一定是底层表的一个列,也可以是从表的一列或多列计算而来的一个函数或者标量表达式。这种特性对于根据计算结果快速获取表中内容是有用的。

例如,一种进行大小写不敏感比较的常用方法是使用 lower 函数:

SELECT * FROM test1 WHERE lower(col1) = 'value';

这种查询可以利用一个建立在 lower(col1) 函数结果之上的索引:

CREATE INDEX test1_lower_col1_idx ON test1 (lower(col1));

如果将该索引声明为 UNIQUE,它将阻止创建在 col1 值上只有大小写不同的行。

另外一个例子,如果经常进行如下的查询:

SELECT * FROM people WHERE (first_name || ' ' || last_name) = 'John Smith';

那么值得创建一个这样的索引:

CREATE INDEX people_names ON people ((first_name || ' ' || last_name));

正如第二个例子所示,CREATE INDEX 命令的语法通常要求在被索引的表达式周围书写圆括号。而如第一个例子所示,当表达式只是一个函数调用时可以省略掉圆括号。

索引表达式的维护代价较为昂贵,因为在每一个行被插入或更新时都得为它重新计算相应的表达式。然而,索引表达式在进行索引搜索时却需要重新计算,因为它们的结果已经被存储在索引中了。在上面两个例子中,系统将会发现查询的条件是 WHERE indexedcolumn = 'constant',因此查询的速度将等同于其他简单索引查询。因此,表达式索引对于检索速度远比插入和更新速度重要的情况非常有用。

部分索引

一个部分索引是建立在表的一个子集上,而该子集则由一个条件表达式(被称为部分索引的谓词)定义。而索引中只包含那些符合该谓词的表行的项。部分索引是一种专门的特性,但在很多种情况下它们也很有用。

使用部分索引的一个主要原因是避免索引公值。由于搜索一个公值的查询(一个在所有表行中占比超过一定百分比的值)不会使用索引,所以完全没有理由将这些行保留在索引中。这可以减小索引的尺寸,同时也将加速使用索引的查询。它也将加速很多表更新操作,因为这种索引并不需要在所有情况下都被更新。

举例:建立一个部分索引来排除公值

假设要在一个数据库中保存网页服务器访问日志。大部分访问都来自于组织内的 IP 地址,但是有些来自于其他地方(如使用拨号连接的员工)。如果主要通过 IP 搜索来自于外部的访问,就没有必要索引对应于组织内网的 IP 范围。

假设有这样一个表:

CREATE TABLE access_log (
    url varchar,
    client_ip inet,
    ...
);

用以下命令可以创建适用于部分索引:

CREATE INDEX access_log_client_ip_ix ON access_log (client_ip)
WHERE NOT (client_ip > inet '192.168.100.0' AND
           client_ip < inet '192.168.100.255');

一个使用该索引的典型查询是:

SELECT *
FROM access_log
WHERE url = '/index.html' AND client_ip = inet '212.78.10.32';

此处查询的 IP 地址由部分索引覆盖。以下查询无法使用部分索引,因为它使用从索引中排除的 IP 地址:

SELECT *
FROM access_log
WHERE url = '/index.html' AND client_ip = inet '192.168.100.23';

可以看到部分索引查询要求公值能被预知,因此部分索引最适合于数据分布不会改变的情况。这样的索引也可以偶尔被重建来适应新的数据分布,但是这会增加维护负担。

下面的例子展示了部分索引的另一个可能的用途:从索引中排除那些查询不感兴趣的值。这导致了上述相同的好处,但它防止了通过索引来访问“不感兴趣的”值,即便在这种情况下一个索引扫描是有益的。显然,为这种场景建立部分索引需要很多考虑和实验。

举例:建立一个部分索引来排除不感兴趣的值

如果有一个表包含已上账和未上账的订单,其中未上账的订单在整个表中占据一小部分且它们是最经常被访问的行。可以通过只在未上账的行上创建一个索引来提高性能。创建索引的命令如下:

CREATE INDEX orders_unbilled_index ON orders (order_nr)
    WHERE billed is not true;

使用该索引的一个可能查询是:

SELECT * FROM orders WHERE billed is not true AND order_nr < 10000;

然而,索引也可以用于完全不涉及 order_nr 的查询,例如:

SELECT * FROM orders WHERE billed is not true AND amount > 5000.00;

这并不如在 amount 列上部分索引有效,因为系统必须扫描整个索引。然而,如果有相对较少的未上账订单,使用这个部分索引来查找未上账订单将会更好。

注意这个查询将不会使用该索引:

SELECT * FROM orders WHERE order_nr = 3501;

订单 3501 可能在已上账订单或未上账订单中。

显示索引列和谓词中使用的列并不需要匹配。AntDB 支持使用任意谓词的部分索引,只要其中涉及的只有被索引表的列。然而,记住谓词必须匹配在将要受益于索引的查询中使用的条件。更准确地,只有当系统能识别查询的 WHERE 条件从数学上索引的谓词时,一个部分索引才能被用于一个查询。AntDB 并不能给出一个精致的定理证明器来识别写成不同形式在数学上等价的表达式(一方面创建这种证明器极端困难,另一方面即便能创建出来对于实用也过慢)。系统可以识别简单的不等蕴含,例如“x < 1”蕴含“x < 2”;否则谓词条件必须准确匹配查询的 WHERE 条件中的部分,或者索引将不会被识别为可用。匹配发生在查询规划期间而不是运行期间。因此,参数化查询子句无法配合一个部分索引工作。例如,对于参数的所有可能值来说,一个具有参数“x < ?”的预备查询绝不会蕴含“x < 2”。

部分索引的第三种可能的用途并不要求索引被用于查询。其思想是在一个表的子集上创建一个唯一索引。

举例:建立一个部分唯一索引

假设有一个描述测试结果的表。希望保证其中对于一个给定的主题和目标组合只有一个“成功”项,但其中可能会有任意多个“不成功”项。实现它的方式是:

CREATE TABLE tests (
    subject text,
    target text,
    success boolean,
    ...
);

CREATE UNIQUE INDEX tests_success_constraint ON tests (subject, target)
    WHERE success;

当有少数成功测试和很多不成功测试时这是一种特别有效的方法。通过创建具有 IS NULL 限制的惟一部分索引,也可以允许列中仅有一个空。

最后,一个部分索引也可以被用来重载系统的查询规划选择。同样,具有特殊分布的数据集可能导致系统在它并不需要索引的时候选择使用索引。在此种情况下可以被建立,这样它将不会被那些无关的查询所用。通常,AntDB 会对索引使用做出合理的选择(例如,它会在检索公值时避开索引,这样前面的例子只能节约索引尺寸,它并非是避免索引使用所必需的),非常不正确的规划选择则需要作为故障报告。

记住建立一个部分索引意味着知道的至少和查询规划器所知的一样多,尤其是知道什么时候一个索引会是有益的。 构建这些知识需要经验和对于 AntDB 中索引工作方式的理解。 在大部分情况下,一个部分索引相对于一个普通索引的优势很小。在某些情况下,它们会完全相反。

举例:不要使用部分索引代替分区

可能想尝试创建一组巨大的、不重叠的部分索引,例如

CREATE INDEX mytable_cat_1 ON mytable (data) WHERE category = 1;
CREATE INDEX mytable_cat_2 ON mytable (data) WHERE category = 2;
CREATE INDEX mytable_cat_3 ON mytable (data) WHERE category = 3;
...
CREATE INDEX mytable_cat_N ON mytable (data) WHERE category = N;

这是个个坏主意!几乎可以肯定,使用一个非部分索引会更好一些,声明如

CREATE INDEX mytable_cat_data ON mytable (category, data);

虽然在这个更大的索引中进行搜索可能比在更小的索引中进行搜索要下降两倍以上的树级别, 但这几乎肯定会比选择适当的部分索引中的一个所需的规划器的开销更便宜。 问题的核心是系统不理解部分索引之间的关系,并将费力地测试每个索引,以确定它是否适用于当前查询。

如果表足够大,单个索引确实是一个坏主意,应该考虑使用分区代替。 通过这种机制,系统理解表和索引是不重叠的,就此而言可以获得更好的性能。

只用索引的扫描和覆盖索引

AntDB 中的所有索引是二级索引,这意味着每个索引都是与表的主数据区(在 AntDB 术语称为表的中)分开存储。这意味着在普通索引扫描中,每行检索都需要从索引和堆中取数据。 此外,虽然匹配给定的可索引 WHERE 条件的索引条目通常在一起靠近存储,但它们引用的表行可能在堆中的任何地方。 因此索引扫描的堆访问部分涉及到对堆的大量随机访问,这可能很慢,特别是在传统旋转媒介上。

为了解决这种性能问题,AntDB 支持只用索引的扫描,这类扫描可以仅用一个索引来回答查询而不产生任何堆访问。其基本思想是直接从每一个索引项中直接返回值,而不是去参考相关的堆项。在使用这种方法时有两个根本的限制:

  • 索引类型必须支持只用索引的扫描。B- 树索引总是支持只用索引的扫描。GiST 和 SP-GiST 索引只对某些操作符类支持只用索引的扫描。其他索引类型不支持这种扫描。底层的要求是索引必须在物理上存储或者可以重构出每一个索引项对应的原始数据值。GIN 索引是一个不支持只用索引的扫描的反例,因为它的每一个索引项通常只包含原始数据值的一部分。

  • 查询必须只引用存储在该索引中的列。例如,给定的索引建立在表的列 xy 上,而该表还有一个列 z,这些查询可以使用只用索引的扫描:

    SELECT x, y FROM tab WHERE x = 'key';
    SELECT x FROM tab WHERE x = 'key' AND y < 42;
    

    但是这些查询不能使用只用索引的查询:

    SELECT x, z FROM tab WHERE x = 'key';
    SELECT x FROM tab WHERE x = 'key' AND z < 42;
    

    (如下面所讨论的,表达式索引和部分索引会让这条规则更加复杂)。

如果符合这两个根本要求,那么该查询所要求的所有数据值都可以从索引得到,因此才可能使用只用索引的扫描。但是对 AntDB 中的任何表扫描还有一个额外的要求:必须验证每一个检索到的行对该查询的 MVCC 快照是“可见的”。可见性信息并不存储在索引项中,只存储在堆项中。因此,乍一看似乎每一次行检索无论如何都会要求一次堆访问。如果表行最近被修改过,确实是这样。但是,对于很少更改的数据有一种方法可以解决这个问题。AntDB 为表堆中的每一个页面跟踪是否其中所有的行的年龄都足够大,以至于对所有当前以及未来的事务都可见。这个信息存储在该表的可见性映射的一个位中。在找到一个候选索引项后,只用索引的扫描会检查对应堆页面的可见性映射位。如果该位被设置,那么这一行就是可见的并且该数据库可以直接被返回。如果该位没有被设置,那么就必须访问堆项以确定这一行是否可见,这种情况下相对于标准索引扫描就没有性能优势。即便是在成功的情况下,这种方法也是把对堆的访问换成了对可见性映射的访问。不过由于可见性映射比它所描述的堆要小四个数量级,所以访问可见性映射所需的物理 I/O 要少很多。在大部分情况下,可见性映射总是会被保留在内存中的缓冲中。

总之,虽然当两个根本要求满足时可以使用只用索引的扫描,但是只有该表的堆页面中有很大一部分的“所有都可见”映射位被设置时这种索引才有优势。不过,有很大一部分行不被更改的表是很常见的,这也让这一类扫描在实际中非常有用。

为了有效利用仅索引扫描功能,可以选择创建一个覆盖索引,它是一个特别设计的索引,包含经常运行的特殊类型查询所需要的列。由于查询通常需要检索的列不仅仅是他们搜索的列,AntDB 允许创建索引,这个索引中有些列只是“负荷”而不是搜索键的一部分。这可以通过添加 INCLUDE 来完成子句来列出了额外的列。 例如,如果通常可以运行这样的查询:

SELECT y FROM tab WHERE x = 'key';

加快此类查询的传统方法是仅在 x 上的索引。但是,一个索引定义为

CREATE INDEX tab_x_y ON tab(x) INCLUDE (y);

可以将这些查询作为仅索引扫描处理,因为 y 可以从索引中获取而不需要访问堆。

因为列 y 不是搜索键的一部分,它不必是索引可以处理的数据类型;它只存储在索引中,不由索引机解释。另外,如果索引是唯一的索引,则

CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);

唯一性条件仅适用于 x 列,而不是 xy 的组合。(如果使用和在索引中设置的类似语法,一个 INCLUDE 子句可以写在 UNIQUEPRIMARY KEY 约束中。)

保守地将非键负载列添加到索引是明智的,尤其是宽列。 如果索引元组超过索引类型允许的最大大小,数据插入将失败。在任何情况下,非键列都将复制索引表中的数据并放大了索引的大小,从而有可能减慢搜索速度。请记住,除非一个表足够慢以至于仅索引扫描可能不必访问堆,否则没有什么理由在一个索引中包含负载列。无论如何,如果必须访问堆元组,从堆里获取列的值并不会带来更高的开销。其他限制是表达式不被作为包含的来支持。只有 B 树和 GiST 索引当前支持包含的列。

在 AntDB 有 INCLUDE 特性之前,人们有时会通过写负载列作为普通索引列来制作覆盖索引。它这样写:

CREATE INDEX tab_x_y ON tab(x, y);

即使他们无意将 y 用作 WHER 子句的一部分,只要额外的列是尾列就可以很好的工作。 让它们成为前导字段是不明智的。但是,此方法不支持希望索引在键列上实施唯一性。

Suffix truncation 总是从 B-Tree 的上层移除非键列。作为有效负载列,它们从不用于指导索引扫描。 当键列的其余前缀恰好足以描述最低 B-Tree 级别上的元组时,截断过程还会删除一个或多个尾随的键列。 实际中,不使用 INCLUDE 子句覆盖索引通常会避免存储在上层有效负载的列。 然而,显式地将有效负载列定义为非键列 reliably 使上层元组保持较小。

原则上,只用索引的扫描可以被用于表达式索引。例如,给定一个 f(x) 上的索引(x 是一个表列),可以把

SELECT f(x) FROM tab WHERE f(x) < 1;

作为只用索引的扫描执行,如果f()是一个计算代价昂贵的函数,这会非常有吸引力。不过,AntDB 的规划器当前面对这类情况时并不是很聪明。只有在索引中有查询所需要的所有时,规划器才会考虑用只用索引的扫描来执行一个查询。在这个例子中,除了在 f(x) 环境中之外,查询的其他部分不需要 x,但是规划器并不能意识到这一点,因此它会得出不能使用只用索引的扫描的结论。如果只用索引的扫描足够有价值,有一种解决方法是把该索引定义在 (f(x), x) 上,其中第二个列实际上并不会被使用,它只是用来说服规划器可以使用只用索引的扫描而已。如果目标是避免重复计算 f(x),一个额外的警示是规划器不一定会把不在可索引 WHERE 子句中对 f(x) 的使用匹配到索引列。通常在上述那种简单查询中一切正常,但是涉及到连接的查询中就不行了。这些不足将在未来的 AntDB 版本中修正。

部分索引也和只用索引的扫描之间有着有趣的关系:

CREATE UNIQUE INDEX tests_success_constraint ON tests (subject, target)
    WHERE success;

原则上,可以在这个索引上使用只用索引的扫描来满足查询

SELECT target FROM tests WHERE subject = 'some-subject' AND success;

但是有一个问题:WHERE 子句引用的是不能作为索引结果列的 success。尽管如此,还是可以使用只用索引的扫描,因为在运行时计划不需要重新检查 WHERE 子句的那个部分:在该索引中找到的所有项必定具有 success = true,因此在计划中检查这个部分的需要并不明显。

操作符类和操作符族

一个索引定义可以为索引中的每一列都指定一个操作符类

CREATE INDEX name ON table (column opclass [ ( opclass_options ) ] [sort options] [, ...]);

操作符类标识该列上索引要使用的操作符。例如,一个 int4 类型上的B树索引会使用 int4_ops 类,这个操作符类包括用于 int4 类型值的比较函数。实际上列的数据类型的默认操作符类通常就足够了。存在多个操作符类的原因是,对于某些数据类型可能会有多于一种的有意义的索引行为。例如,可能想要对一种复数数据类型按照绝对值排序或者按照实数部分排序。可以通过为该数据类型定义两个操作符类来实现,并且在创建一个索引时选择合适的类。操作符类会决定基本的排序顺序(可以通过增加排序选项 COLLATEASC/DESC 和/或 NULLS FIRST/NULLS LAST来修改)。

除了默认的操作符类,还有一些内建的操作符类:

  • 操作符类 text_pattern_opsvarchar_pattern_opsbpchar_pattern_ops 分别支持类型 textvarcharchar上的 B树索引。它们与默认操作符类的区别是值的比较是严格按照字符进行而不是根据区域相关的排序规则。这使得这些操作符类适合于当一个数据库没有使用标准“C”区域时被使用在涉及模式匹配表达式(LIKE 或 POSIX 正则表达式)的查询中。一个例子是,可以这样索引一个 varchar 列:

    CREATE INDEX test_index ON test_table (col varchar_pattern_ops);
    

    注意如果希望涉及到<<=>>=比较的查询使用一个索引,也应该创建一个使用默认操作符类的索引。这些查询不能使用*xxx*_pattern_ops 操作符类(但是普通的等值比较可以使用这些操作符类)。可以在同一个列上创建多个使用不同操作符类的索引。如果正在使用 C 区域,并不需要 *xxx*_pattern_ops 操作符类,因为在 C 区域中的模式匹配查询可以用带有默认操作符类的索引。

下面的查询展示了所有已定义的操作符类:

SELECT am.amname AS index_method,
       opc.opcname AS opclass_name,
       opc.opcintype::regtype AS indexed_type,
       opc.opcdefault AS is_default
    FROM pg_am am, pg_opclass opc
    WHERE opc.opcmethod = am.oid
    ORDER BY index_method, opclass_name;

一个操作符类实际上只是一个更大的被称为操作符族的结构的一个子集。在多种数据类型具有相似行为的情况下,常常会定义跨数据类型的操作符并且允许索引使用它们。为了实现该目的,这些类型的操作符类必须被分组到同一个操作符族中。跨类型的操作符是该族的成员,但是并不与族内任意一个单独的类相关联。

前一个查询的扩展版本展示了每个操作符类所属的操作符族:

SELECT am.amname AS index_method,
       opc.opcname AS opclass_name,
       opf.opfname AS opfamily_name,
       opc.opcintype::regtype AS indexed_type,
       opc.opcdefault AS is_default
    FROM pg_am am, pg_opclass opc, pg_opfamily opf
    WHERE opc.opcmethod = am.oid AND
          opc.opcfamily = opf.oid
    ORDER BY index_method, opclass_name;

这个查询展示所有已定义的操作符族和每一个族中包含的所有操作符:

SELECT am.amname AS index_method,
       opf.opfname AS opfamily_name,
       amop.amopopr::regoperator AS opfamily_operator
    FROM pg_am am, pg_opfamily opf, pg_amop amop
    WHERE opf.opfmethod = am.oid AND
          amop.amopfamily = opf.oid
    ORDER BY index_method, opfamily_name, opfamily_operator;

索引和排序规则

一个索引在每一个索引列上只能支持一种排序规则。如果需要多种排序规则,可能需要多个索引。

考虑这些语句:

CREATE TABLE test1c (
    id integer,
    content varchar COLLATE "x"
);

CREATE INDEX test1c_content_index ON test1c (content);

该索引自动使用下层列的排序规则。因此一个如下形式的查询:

SELECT * FROM test1c WHERE content > constant;

可以使用该索引,因为比较会默认使用列的排序规则。但是,这个索引无法加速涉及到某些其他排序规则的查询。因此对于下面形式的查询:

SELECT * FROM test1c WHERE content > constant COLLATE "y";

可以创建一个额外的支持"y"排序规则的索引,例如:

CREATE INDEX test1c_content_y_index ON test1c (content COLLATE "y");

检查索引使用

尽管 AntDB 中的索引并不需要维护或调优,但是检查真实的查询负载实际使用了哪些索引仍然非常重要。检查一个独立查询的索引使用情况可以使用 EXPLAIN 命令。也可以在一个运行中的服务器上收集有关索引使用的总体统计情况。

很难明确地表达决定创建哪些索引的通用过程。在之前的小节中的例子里有一些典型的情况。通常需要大量的实验才能决定应该创建哪些索引。本小节剩余的部分将给出一些创建索引的提示:

  • 总是先运行 ANALYZE。这个命令会收集有关表中值分布情况的统计信息。估计一个查询将要返回的行数需要这些信息,而结果行数则被规划器用来为每一个可能的查询计划分配实际的代价。如果没有任何真实的统计信息,将会假定一些默认值,这几乎肯定是不准确的。在没有运行的情况下检查一个应用的索引使用情况是注定要失败的。

  • 使用真实数据进行实验。使用测试数据来建立索引将会告诉测试数据需要什么样的索引,但这并不代表真实数据的需要。

    使用非常小的测试数据集是特别致命的。在从 100000 行中选出 1000 行时可能会用到索引,但是从 100 行里选出 1 行是很难用到索引的,因为 100 行完全可能放入到一个磁盘页面中,而没有任何计划能够比得上从一个磁盘页面顺序获取的计划。

    在创建测试数据时也要小心,特别是当应用还没有产生时通常是不可避免的。值非常相似、完全随机或以排好序的方式被插入都将使得统计信息倾斜于真实数据中的值分布。

  • 如果索引没有被用到,强制使用它们将会对测试非常有用。有一些运行时参数可以关闭多种计划类型。例如,关闭顺序扫描(enable_seqscan)以及嵌套循环连接(enable_nestloop)将强制系统使用一种不同的计划。如果系统仍然选择使用一个顺序扫描或嵌套循环连接,则索引没有被使用的原因可能更加根本,例如查询条件不匹配索引(哪种查询能够使用哪种索引已经在前面的小节中解释过了)。

  • 如果强制索引使用确实使用了索引,则有两种可能性:系统是正确的并且索引确实不合适,或者查询计划的代价估计并没有反映真实情况。因此应该对用索引的查询和不用索引的查询计时。此时 EXPLAIN ANALYZE 命令就能发挥作用了。

  • 如果发现代价估计是错误的,也分为两种可能性。总代价是用每个计划节点的每行代价乘以计划节点的选择度估计来计算的。计划节点的代价估计可以通过运行时参数调整。不准确的选择度估计可能是由于缺乏统计信息,可以通过调节统计信息收集参数来改进。

    如果不能成功地把代价调整得更合适,那么可能必须依靠显式地强制索引使用。也可能希望联系 AntDB 开发者来检查该问题。

问题反馈