PostgreSQL SQL 语言:索引

  1. 云栖社区>
  2. 博客>
  3. 正文

PostgreSQL SQL 语言:索引

琴瑟 2017-08-18 13:44:07 浏览1526
展开阅读全文

本文档为PostgreSQL 9.6.0文档,本转载已得到原译者彭煜玮授权。

1. 简介

假设我们有一个如下的表:


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命令来更新统计信息以便查询规划器能做出正确的决定。通过Chapter 14的信息可以了解如何找出一个索引是否被使用以及规划器在何时以及为什么会选择不使用索引。

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

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

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

2. 索引类型

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

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


<
<=
=
>=
>

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

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

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


CREATE INDEX name ON table USING HASH (column);

Caution

Hash索引操作目前不被WAL记录,因此存在未写入修改,在数据库崩溃后需要用REINDEX命令重建Hash索引。同样,在完成初始的基础备份后,对于Hash索引的改变也不会通过流式或基于文件的复制所复制,所以它们会对其后使用它们的查询给出错误的答案。正因为这些原因,Hash索引已不再被建议使用。

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


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

Table 61-1中给出了标准发布中所包括的 GiST 操作符类。contrib集合中还包括了很多其他GiST操作符类。
GiST索引也有能力优化"最近邻"搜索,例如:

SELECT * FROM places ORDER BY location <-> point '(101,456)' LIMIT 10;
它将找到离给定目标点最近的10个位置。能够支持这种查询的能力同样取决于被使用的特定操作符类。 在Table 61-1中,"Ordering Operators"列中列出了可以在这种方法中使用的操作符。

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


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

Table 62-1中给出了标准发布中所包括的 SP-GiST 操作符类。

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

与 GiST 和 SP-GiST相似, GIN可以支持多种不同的用户定义的索引策略和特定操作符,通过它一个GIN索引可以被根据索引策略被使用。作为一个例子,PostgreSQL的标准捐献包中包含了用于一维数组的GIN操作符类,它用于支持使用下列操作符的索引化查询:


<@
@>
=
&&

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


<
<=
=
>=
>

3. 多列索引

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


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

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


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

那么我们可以在major和minor上定义一个索引:


CREATE INDEX test2_mm_idx ON test2 (major, minor);

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

一个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存储参数。

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

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

4. 索引和ORDER BY

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

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

默认情况下,B-tree索引将它的项以升序方式存储,并将空值放在最后。这意味着对列x上索引的一次前向扫描将产生满足ORDER BY x(或者更长的形式:ORDER BY x ASC NULLS LAST)的结果。索引也可以被后向扫描,产生满足ORDER BY x DESC(ORDER BY x DESC NULLS FIRST, NULLS FIRST是ORDER BY DESC的默认情况)。

我们可以在创建B-tree索引时通过ASC、DESC、NULLS FIRST和NULLS 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 FIRST或 ORDER 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)就可以产生这种排序。

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

5. 组合多个索引

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

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

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

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

6. 唯一索引

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

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

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

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

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

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

7. 表达式索引

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

例如,一种进行大小写不敏感比较的常用方法是使用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',因此查询的速度将等同于其他简单索引查询。因此,表达式索引对于检索速度远比插入和更新速度重要的情况非常有用。

8. 部分索引

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

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

Example 11-1. 建立一个部分索引来排除公值

假设我们要在一个数据库中保存网页服务器访问日志。大部分访问都来自于我们组织内的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';

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


SELECT *
FROM access_log
WHERE client_ip = inet '192.168.100.23';

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

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

Example 11-2. 建立一个部分索引来排除不感兴趣的值

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


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可能在已上账订单或未上账订单中。

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

部分索引的第三种可能的用途并不要求索引被用于查询。其思想是在一个表的子集上创建一个唯一索引,如Example 11-3所示。这对那些满足索引谓词的行强制了唯一性,而对那些不满足的行则没有影响。

Example 11-3. 建立一个部分唯一索引

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


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

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

当有少数成功测试和很多不成功测试时这是一种特别有效的方法。

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

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

9. 操作符类和操作符族

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

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

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

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

操作符类text_pattern_ops、varchar_pattern_ops和 bpchar_pattern_ops分别支持类型text、varchar和 char上的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;

10. 索引和排序规则

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

考虑这些语句:


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");

11. 只用索引的扫描

PostgreSQL中的所有索引都是二级索引,表示每一个索引都被存储在表的主数据区域(在PostgreSQL术语中被称为该表的堆)之外。这意味着在一次普通索引扫描中,每次取一行需要从索引和堆中取得数据。此外,虽然满足一个给定的可索引WHERE条件的索引项通常在索引中都靠拢在一起,但是它们所引用的表行可能分布在堆中的任何地方。因此一次索引扫描的堆访问部分可能会涉及到很多对堆的随机访问,这可能会很慢,尤其在传统的磁盘上。

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

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

查询必须只引用存储在该索引中的列。例如,给定的索引建立在表的列x和y上,而该表还有一个列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;

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

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

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

为了有效地使用只用索引的扫描,可以选择创建时只让前导列来匹配WHERE子句,而让后面的列保存"载荷"数据用以返回查询所需的数据。例如,如果经常要运行这样的查询


SELECT y FROM tab WHERE x = 'key';

加速这类查询的传统方法是只在x上创建一个索引。但是,在(x, y)上创建一个索引可以将这种查询实现为只用索引的扫描。正如之前讨论的,这样一个索引比只在x上的索引更大并且因此开销更大,因此只有知道表几乎是静态时这种方法才具有吸引力。注意,很重要的一点是,该索引应该定义在(x, y)而不是(y, x)上。这是因为对于大部分索引类型(特别是 B-树)来说,不限制前导索引列的搜索效率不高。

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


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

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

部分索引也和只用索引的扫描之间有着有趣的关系。考虑Example 11-3中所展示的部分索引:


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,因此在计划中检查这个部分的需要并不明显。PostgreSQL 9.6 和以后的版本将会识别这种情况,并且允许生成只用索引的扫描,但是旧版本无法这样做。

12. 检查索引使用

Although indexes in 尽管PostgreSQL中的索引并不需要维护或调优,但是检查真实的查询负载实际使用了哪些索引仍然非常重要。检查一个独立查询的索引使用情况可以使用EXPLAIN命令,它应用于这种目的的内容在Section 14.1中有介绍。也可以在一个运行中的服务器上收集有关索引使用的总体统计情况,如Section 28.2所述。

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

  • 总是先运行ANALYZE。这个命令会收集有关表中值分布情况的统计信息。估计一个查询将要返回的行数需要这些信息,而结果行数则被规划器用来为每一个可能的查询计划分配实际的代价。如果没有任何真实的统计信息,将会假定一些默认值,这几乎肯定是不准确的。在没有运行的情况下检查一个应用的索引使用情况是注定要失败的。
  • 使用真实数据进行实验。使用测试数据来建立索引将会告诉你测试数据需要什么样的索引,但这并不代表真实数据的需要。
  • 使用非常小的测试数据集是特别致命的。在从100000行中选出1000行时可能会用到索引,但是从100行里选出1行是很难用到索引的,因为100行完全可能放入到一个磁盘页面中,而没有任何计划能够比得上从一个磁盘页面顺序获取的计划。
  • 在创建测试数据时也要小心,特别是当应用还没有产生时通常是不可避免的。值非常相似、完全随机或以排好序的方式被插入都将是的统计信息倾斜于真实数据中的值分布。
  • 如果索引没有被用到,强制使用它们将会对测试非常有用。有一些运行时参数可以关闭多种计划类型。例如,关闭顺序扫描(enable_seqscan)以及嵌套循环连接(enable_nestloop)将强制系统使用一种不同的计划。如果系统仍然选择使用一个顺序扫描或嵌套循环连接,则索引没有被使用的原因可能更加根本,例如查询条件不匹配索引(哪种查询能够使用哪种索引已经在前面的小节中解释过了)。
  • 如果强制索引使用确实使用了索引,则有两种可能性:系统是正确的并且索引确实不合适,或者查询计划的代价估计并没有反映真实情况。因此你应该对用索引的查询和不用索引的查询计时。此时EXPLAIN ANALYZE命令就能发挥作用了。
  • 如果发现代价估计是错误的,也分为两种可能性。总代价是用每个计划节点的每行代价乘以计划节点的选择度估计来计算的。计划节点的代价估计可以通过运行时参数调整(如Section 19.7.2所述)。不准确的选择度估计可能是由于缺乏统计信息,可以通过调节统计信息收集参数(见ALTER TABLE)来改进。

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

网友评论

登录后评论
0/500
评论
琴瑟
+ 关注