第11章 索引(postgresql v17)

V17
Chapter 11. Indexes

索引是增强数据库性能的常用方法。索引允许数据库服务器查找和检索特定的行,比没有索引要快得多。但是索引也会给整个数据库系统增加开销,因此应该合理使用索引

1、介绍

假设我们有一个类似这样的表:

bash 复制代码
CREATE TABLE test1 (
    id integer,
    content varchar
);

应用程序发出许多这种格式的查询:

bash 复制代码
SELECT content FROM test1 WHERE id = constant;

如果没有事先准备,系统将不得不逐行扫描整个test1表,以找到所有匹配的条目。如果test1中有很多行,而这样的查询只会返回几行(可能是0行或1行),那么这显然是一个效率低下的方法。但是,如果指示系统在id列上维护索引,则可以使用更有效的方法来定位匹配行的位置。例如,它可能只需要在搜索树中走几个层次。

在大多数非小说类书籍中也采用了类似的方法:读者经常查找的术语和概念在书的末尾按字母顺序收集起来。感兴趣的读者可以相对快速地浏览索引并翻到适当的页面,而不是必须阅读整本书才能找到感兴趣的材料。正如作者的任务是预测读者可能查找的条目一样,数据库程序员的任务是预测哪些索引将是有用的。

下面的命令可以在id列上创建索引,如下所示:

bash 复制代码
CREATE INDEX test1_id_index ON test1 (id);

名称test1_id_index可以自由选择,但是您应该选择能够让您稍后记住索引的用途的名称。

需要删除索引,使用DROP INDEX命令。可以随时向表中添加和删除索引。

一旦创建了索引,就不需要进一步的干预:当表被修改时,系统将更新索引,并且当它认为这样做比顺序表扫描更有效时,它将在查询中使用索引。但是,您可能必须定期运行ANALYZE命令来更新统计信息,以便查询规划器能够做出明智的决策 。请参阅第14章,了解如何确定是否使用了索引,以及何时以及为什么计划器可能选择不使用索引。

索引还可以使带有搜索条件的UPDATE和DELETE命令受益。索引还可以用于连接搜索。因此,在作为连接条件一部分的列上定义的索引也可以显著加快使用连接的查询速度。

一般来说,PostgreSQL索引可以用来优化包含一个或多个WHERE或JOIN子句的查询:

bash 复制代码
indexed-column indexable-operator comparison-value

在这里,索引列(indexed-column)是定义了索引的列或表达式。可索引操作符(indexable-operator)是一个操作符,它是索引列的索引操作符类的成员。(详情见下文。)比较值(comparison-value )可以是任何非易变且不引用索引表的表达式。

在某些情况下,查询规划器可以从另一个SQL构造中提取这种形式的可索引子句。一个简单的例子是,如果原条款是

bash 复制代码
comparison-value operator indexed-column

然后,如果原始操作符(operator)有一个交换子操作符,并且该交换子操作符是索引操作符类的成员,则可以将其翻转为可索引的形式。

在大型表上创建索引可能需要很长时间。默认情况下,PostgreSQL允许在创建索引的同时对表进行读操作(SELECT语句),但是写操作(INSERT, UPDATE, DELETE)会被阻塞,直到索引创建完成 。在生产环境中,这通常是不可接受的。允许写操作与索引创建并行进行是可能的,但是需要注意几个注意事项------有关更多信息,请参阅并发构建索引

创建索引后,系统必须使其与表保持同步。这增加了数据操作的开销 。索引还可以防止创建仅限堆的元组。因此,应该删除查询中很少使用或从不使用的索引。

2、Index Types

PostgreSQL提供了几种索引类型:B-tree、Hash、GiST、SP-GiST、GIN、BRIN和扩展bloom 。每种索引类型使用最适合不同类型的可索引子句的不同算法。默认情况下,CREATE INDEX命令创建B-tree索引,这适合最常见的情况 。其他索引类型是通过写入关键字USING和索引类型名称来选择的。例如,要创建一个哈希索引:

bash 复制代码
CREATE INDEX name ON table USING HASH (column);

2.1. B-Tree

b树可以处理按某种顺序排序的数据的相等性和范围查询。特别地,PostgreSQL查询规划器将考虑使用B-tree索引,当索引列涉及到使用以下操作符之一进行比较时:

bash 复制代码
<   <=   =   >=   >

等价于这些操作符的组合的构造,例如BETWEENIN,也可以通过b树索引搜索来实现 。此外,索引列上的IS NULLIS NOT NULL条件也可以与b树索引一起使用

优化器还可以对涉及模式匹配操作符LIKE~的查询使用b树索引,如果模式是常量并且锚定在字符串的开头---例如,col LIKE 'foo%'col ~ '^foo',但不能使用col LIKE '%bar'。但是,如果您的数据库不使用C语言环境,则需要使用特殊的操作符类创建索引,以支持模式匹配查询的索引;参见下面第11.10节。也可以为ILIKE~*使用b -树索引,但前提是模式以非字母字符开始,即不受大小写转换影响的字符。

b树索引还可以用于按排序顺序检索数据。这并不总是比简单的扫描和排序更快,但它通常是有用的。

2.2. Hash

哈希索引存储从索引列的值派生的32位哈希码。因此,这样的索引只能处理简单的相等比较。查询规划器将考虑在使用相等操作符进行比较时使用散列索引:

bash 复制代码
=

2.3. GiST

GiST索引不是一种索引,而是一种基础设施,可以在其中实现许多不同的索引策略。因此,可以使用GiST索引的特定操作符取决于索引策略(操作符类)。作为一个例子,PostgreSQL的标准发行版包含了一些二维几何数据类型的GiST操作符类,它们支持使用这些操作符进行索引查询:

bash 复制代码
<<   &<   &>   >>   <<|   &<|   |&>   |>>   @>   <@   ~=   &&

(这些操作符的含义参见9.11节。)标准发行版中包含的GiST操作符类见表64.1。许多其他GiST操作符类可以在贡献contrib 中或作为单独的项目使用。有关更多信息,请参见第64.2节

GiST索引还能够优化"最近邻"搜索,例如

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

它会找出离给定目标点最近的10个位置。这样做的能力同样依赖于所使用的特定操作符类。在表64.1中,可以以这种方式使用的操作符列在"排序操作符"中。

2.4. SP-GiST

与GiST索引一样,SP-GiST索引提供了支持各种搜索的基础设施。SP-GiST允许实现各种不同的基于磁盘的非平衡数据结构,例如四叉树、k-d树和基数树(尝试)。举个例子,PostgreSQL的标准发行版包含了用于二维点的SP-GiST操作符类,它支持使用以下操作符进行索引查询:

bash 复制代码
<<   >>   ~=   <@   <<|   |>>

(这些操作符的含义参见9.11节。)标准发行版中包含的SP-GiST操作符类见表64.2。有关更多信息,请参见第64.3节

和GiST一样,SP-GiST支持"最近邻"搜索。对于支持距离排序的SP-GiST操作符类,相应的操作符列在表64.2的"排序操作符"列中。

2.5. GIN

GIN索引是"倒排索引(inverted indexes)",适用于包含多个组件值的数据值,比如数组。倒排索引包含每个组件值的单独条目,并且可以有效地处理测试特定组件值是否存在的查询。

与GiST和SP-GiST一样,GIN可以支持许多不同的用户定义索引策略,并且可以使用GIN索引的特定操作符因索引策略而异。例如,PostgreSQL的标准发行版包含了一个用于数组的GIN操作符类,它支持使用以下操作符进行索引查询:

bash 复制代码
<@   @>   =   &&

(这些操作符的含义见第9.19节。)表64.3记录了标准分布中包含的GIN操作符类。许多其他GIN操作符类可以在贡献(contrib )集合中或作为单独的项目使用。有关更多信息,请参见第64.4节

2.6. BRIN

BRIN索引(块范围索引的简写)存储存储在表中的连续物理块范围中的值的摘要。因此,对于那些值与表行物理顺序密切相关的列,它们是最有效的。像GiST, SP-GiST和GIN一样,BRIN可以支持许多不同的索引策略,并且可以使用BRIN索引的特定操作符根据索引策略而变化。对于具有线性排序顺序的数据类型,索引的数据对应于每个块范围的列中值的最小值和最大值。它支持使用以下操作符的索引查询:

bash 复制代码
<   <=   =   >=   >

标准分布中包含的BRIN操作符类见表64.4。有关更多信息,请参见第64.5节

3. 多列索引

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

bash 复制代码
CREATE TABLE test2 (
  major int,
  minor int,
  name varchar
);

(比如,你把/dev目录保存在一个数据库中...),你经常发出这样的查询:

bash 复制代码
SELECT name FROM test2 WHERE major = constant AND minor = constant;

那么在majorminor列上同时定义索引可能是合适的,例如:

bash 复制代码
CREATE INDEX test2_mm_idx ON test2 (major, minor);

目前,只有B-tree、GiST、GIN和BRIN索引类型支持多键列索引 。是否可以有多个键列与是否可以向索引添加INCLUDE列无关。索引最多可以有32列,包括INCLUDE列。(此限制可以在构建PostgreSQL时更改;请参阅pg_config_manual.h文件。)

多列b树索引可以用于涉及索引列的任何子集的查询条件,但是当对最左边的列有约束时,索引是最有效的确切的规则是,前导列上的相等约束,加上没有相等约束的第一列上的任何不相等约束,将用于限制扫描的索引部分。在索引中检查这些列右边的列的约束,因此它们节省了对表的访问,但是它们并没有减少必须扫描的索引部分。例如,给定(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树或GiST不同,无论查询条件使用哪个索引列,索引搜索的有效性都是相同的

多列BRIN索引可以用于涉及索引列的任何子集的查询条件。与GIN一样,与B-tree或GiST不同,无论查询条件使用哪个索引列,索引搜索的有效性都是相同的。在单个表上使用多个BRIN索引而不是一个多列BRIN索引的唯一原因是使用不同的pages_per_range存储参数。

当然,每个列必须与适合索引类型的操作符一起使用;涉及其他操作符的条款将不予考虑。

应该谨慎地使用多列索引。在大多数情况下,单列上的索引就足够了,可以节省空间和时间 。具有超过三列的索引不太可能有帮助,除非表的使用非常程式化。另请参阅第11.5节和第11.9节,了解不同索引配置的优点。

4. 索引和 ORDER BY

除了简单地查找查询要返回的行之外,索引还可以按照特定的排序顺序提供它们。这允许在没有单独排序步骤的情况下执行查询的ORDER BY规范。在PostgreSQL目前支持的索引类型中,只有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 DESC的输出(或者更详细地说,ORDER BY x DESC NULLS FIRST,因为NULLS FIRSTORDER BY DESC的默认值)。

在创建索引时,您可以通过包括ASC, DESC, NULLS FIRST和/或NULLS LAST选项来调整b树索引的顺序;例如:

bash 复制代码
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),则可以。

显然,具有非默认排序顺序的索引是一种相当专门化的特性,但有时它们可以为某些查询带来极大的加速。是否值得维护这样一个索引取决于您使用需要特殊排序排序的查询的频率。

5. 组合多个索引

单个索引扫描只能使用使用索引列及其操作符类的操作符并使用AND连接的查询子句。例如,给定(a, b)上的索引,像WHERE a = 5 AND b = 6这样的查询条件可以使用索引,但是像WHERE a = 5 OR b = 6这样的查询不能直接使用索引。

幸运的是,PostgreSQL有能力组合多个索引(包括同一索引的多个使用)来处理单索引扫描无法实现的情况 。系统可以跨多个索引扫描形成ANDOR条件。例如,WHERE x = 42 OR x = 47 OR x = 53 OR x = 99这样的查询可以分解为对x上的索引的四次单独扫描,每次扫描使用一个查询子句。然后将这些扫描的结果放在一起生成结果。另一个例子是,如果我们在xy上有单独的索引,那么WHERE x = 5 and y = 6这样的查询的一种可能实现是将每个索引与适当的查询子句一起使用,然后将索引结果and在一起以标识结果行。

为了组合多个索引,系统扫描每个需要的索引,并在内存中准备一个位图(bitmap ),给出报告为匹配该索引条件的表行的位置 。然后根据查询的需要将位图andor放在一起。最后,访问并返回实际的表行。表的行是按物理顺序访问的,因为这就是位图的布局方式;这意味着原始索引的任何排序都将丢失,因此如果查询具有ORDER BY子句,则需要单独的排序步骤。由于这个原因,并且由于每次额外的索引扫描都会增加额外的时间,所以规划器有时会选择使用简单的索引扫描,即使可以使用其他可用的索引。

除了最简单的应用程序之外,在所有应用程序中都有各种可能有用的索引组合,数据库开发人员必须做出权衡,以决定提供哪些索引。有时多列索引是最好的,但有时最好创建单独的索引并依赖于索引组合特性 。例如,如果您的工作负载包含多种查询,这些查询有时只涉及列x,有时只涉及列y,有时涉及两个列,那么您可以选择在xy上创建两个单独的索引,依靠索引组合来处理使用两个列的查询。您也可以在(x, y)上创建一个多列索引。对于涉及两列的查询,这个索引通常比索引组合更有效,但正如第11.3节所讨论的,对于只涉及y的查询,它几乎是无用的,所以它不应该是唯一的索引。多列索引和y上的单独索引的组合可以很好地服务。对于只涉及x的查询,可以使用多列索引,尽管它比单独针对x的索引更大,因此速度更慢。最后一种选择是创建所有三个索引,但这可能只有在表的搜索频率比更新频率高得多并且所有三种查询类型都很常见的情况下才合理。如果其中一种查询类型比其他查询类型少得多,那么您可能会满足于只创建最适合常见类型的两个索引。

6. Unique Indexes

索引还可以用于强制列值的唯一性,或者强制多个列的组合值的唯一性。

bash 复制代码
CREATE UNIQUE INDEX name ON table (column [, ...]) [ NULLS [ NOT ] DISTINCT ];

目前,只有b树索引可以被声明为唯一的。

当一个索引被声明为唯一时,不允许有多个索引值相等的表行。默认情况下,不认为唯一列中的空值相等,从而允许列中出现多个空值NULLS NOT DISTINCT选项修改了这一点,并导致索引将空值视为相等。多列唯一索引只拒绝在多个行中所有索引列相等的情况。

当为表定义了唯一约束或主键时,PostgreSQL会自动创建一个唯一索引。索引涵盖构成主键或唯一约束的列(如果合适的话,是多列索引),并且是强制约束的机制。

不需要在唯一的列上手动创建索引;这样做只会复制自动创建的索引。

7. 表达式上的索引

索引列不必仅仅是基础表的一列,也可以是从表的一个或多个列计算的函数或标量表达式。这个特性对于基于计算结果获得对表的快速访问非常有用。

例如,进行不区分大小写比较的一种常用方法是使用下面的函数:

bash 复制代码
SELECT * FROM test1 WHERE lower(col1) = 'value';

如果在lower(col1)函数的结果上定义了索引,则该查询可以使用索引:

bash 复制代码
CREATE INDEX test1_lower_col1_idx ON test1 (lower(col1));

如果我们声明这个索引为UNIQUE,它将防止创建col1值仅在大小写上不同的行,以及col1值实际上相同的行。因此,表达式上的索引可用于强制不能定义为简单唯一约束的约束。

再举一个例子,如果一个人经常做这样的查询:

bash 复制代码
SELECT * FROM people WHERE (first_name || ' ' || last_name) = 'John Smith';

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

bash 复制代码
CREATE INDEX people_names ON people ((first_name || ' ' || last_name));

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

索引表达式的维护成本相对较高,因为必须为每个行插入和非hot更新计算派生表达式。但是,在索引搜索期间不会重新计算索引表达式,因为它们已经存储在索引中。在上面的两个示例中,系统将查询视为WHERE indexedcolumn = 'constant',因此搜索速度与任何其他简单索引查询相同。因此,当检索速度比插入和更新速度更重要时,表达式上的索引很有用。

8. 部分索引

部分索引是建立在一个表的子集上的索引;子集由条件表达式(称为部分索引的谓词)定义。索引只包含满足谓词的表行的条目。部分索引是一种专门的特性,但在几种情况下它们很有用。

使用部分索引的一个主要原因是避免为公共值建立索引 。由于搜索公共值(占所有表行数超过百分之几的值)的查询无论如何都不会使用索引,因此将这些行保留在索引中根本没有意义。这减少了索引的大小,这将加快那些使用索引的查询。它还将加快许多表更新操作,因为在所有情况下都不需要更新索引。例11.1展示了这种思想的一种可能的应用。

例11.1。设置部分索引以排除公共值

假设您将web服务器访问日志存储在数据库中。大多数访问来自您组织的IP地址范围,但也有一些来自其他地方(例如,拨号连接的员工)。如果按IP搜索主要用于外部访问,则可能不需要索引与组织的子网对应的IP范围。

假设一个表是这样的:

sql 复制代码
CREATE TABLE access_log (
    url varchar,
    client_ip inet,
    ...
);

要创建适合我们示例的部分索引,使用如下命令:

sql 复制代码
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');

可以使用此索引的典型查询是:

sql 复制代码
SELECT *
FROM access_log
WHERE url = '/index.html' AND client_ip = inet '212.78.10.32';

在这里,查询的IP地址由部分索引覆盖。下面的查询不能使用部分索引,因为它使用的IP地址被排除在索引之外:

sql 复制代码
SELECT *
FROM access_log
WHERE url = '/index.html' AND client_ip = inet '192.168.100.23';

注意,这种部分索引要求预先确定公共值,因此这种部分索引最适合用于不变化的数据分布。可以偶尔重新创建这样的索引,以适应新的数据分布,但这会增加维护工作。

部分索引的另一个可能用途是从索引中排除典型查询工作负载不感兴趣的值如例11.2所示。这将产生与上面列出的相同的优点,但是它阻止了通过该索引访问"不感兴趣"的值,即使在这种情况下索引扫描可能是有利可图的。显然,为这种场景设置部分索引需要大量的小心和实验。

例11.2。设置部分索引以排除不感兴趣的值

如果您有一个表,其中包含已计费和未计费的订单,其中未计费的订单占整个表的一小部分,但这些是访问最多的行,您可以通过仅在未计费的行上创建索引来提高性能。创建索引的命令如下所示:

sql 复制代码
CREATE INDEX orders_unbilled_index ON orders (order_nr)
    WHERE billed is not true;

使用此索引的可能查询是:

sql 复制代码
SELECT * FROM orders WHERE billed is not true AND order_nr < 10000;

然而,这个索引也可以用在根本不涉及order_nr的查询中,例如:

sql 复制代码
SELECT * FROM orders WHERE billed is not true AND amount > 5000.00;

由于系统必须扫描整个索引,因此这不如在amount列上创建部分索引那么高效。然而,如果未付款订单相对较少,使用这个部分索引来查找未付款订单可能是一种胜利。

注意,这个查询不能使用这个索引:

sql 复制代码
SELECT * FROM orders WHERE order_nr = 3501;

订单3501可能在帐单或未帐单的订单中。

例11.2还说明了索引列和谓词中使用的列不需要匹配 。PostgreSQL支持任意谓词的部分索引,只要只涉及被索引表的列。但是,请记住,谓词必须匹配应该从索引中受益的查询中使用的条件 。确切地说,只有当系统能够识别出查询的WHERE条件在数学上暗示了索引的谓词时,才可以在查询中使用部分索引。PostgreSQL没有一个复杂的定理证明器,可以识别以不同形式写成的数学等价表达式。(这样的一般定理证明不仅极其难以创建,而且可能太慢而无法真正使用。)系统可以识别简单的不等式含义 ,例如"x < 1"意味着"x < 2";否则,谓词条件必须完全匹配查询的WHERE条件的一部分,否则将无法识别索引是可用的。匹配在查询规划时进行,而不是在运行时进行。因此,参数化查询子句不适用于部分索引。例如,一个准备好的带有参数的查询可能指定"x < ?",这不会意味着该参数的所有可能值都是"x < 2"。

部分索引的第三种可能用法根本不需要在查询中使用索引。这里的思想是在表的子集上创建唯一索引,如例11.3所示。这将强制满足索引谓词的行具有唯一性,而不约束不满足索引谓词的行

例11.3。建立部分唯一索引

假设我们有一个描述测试结果的表。我们希望确保给定主题和目标组合只有一个"成功"条目,但可能有任意数量的"不成功"条目。这里有一种方法:

sql 复制代码
CREATE TABLE tests (
    subject text,
    target text,
    success boolean,
    ...
);

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

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

最后,还可以使用部分索引来覆盖系统的查询计划选择 。此外,具有特殊分布的数据集可能会导致系统在不应该使用索引的情况下使用索引。在这种情况下,可以设置索引,使其不可用于违规查询。通常,PostgreSQL会对索引的使用做出合理的选择(例如,在检索公共值时,它会避免它们,所以前面的例子实际上只是节省了索引的大小,而不需要避免索引的使用),而严重错误的计划选择会导致错误报告

请记住,设置一个部分索引表明,您知道的至少和查询规划者知道的一样多,特别是当一个索引可能是有利可图的时候 。形成这些知识需要经验和理解PostgreSQL工作的索引。在大多数情况下,在常规索引上的部分索引的优势将是最小的。有些情况下,他们会适得其反 ,比如11.4

`例如11.4。不要使用部分索引作为分区的替代品

例如,您可能会想要创建一组不重叠的部分索引

sql 复制代码
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;

这是个坏主意!几乎可以肯定的是,你会更好地使用单一的非部分索引

sql 复制代码
CREATE INDEX mytable_cat_data ON mytable (category, data);

(根据第11.3节中所述的原因,首先将类别列放入。)虽然在这个较大的索引中进行搜索可能需要比在较小的索引中搜索更多的树级别,但这几乎肯定比计划器选择适当的部分索引所需的工作要便宜。问题的核心是系统不理解部分索引之间的关系,并且会费力地测试每一个索引,看看它是否适用于当前的查询

如果您的表足够大,单个索引真的是个坏主意,您应该考虑使用分区(见第5.12节)。通过这种机制,系统确实理解表和索引是非重叠的,因此可以更好地实现性能。

更多关于部分索引的信息可以在[stone 89b],[olson93][seshadri95]中找到。

9. 仅索引扫描和覆盖索引

PostgreSQL中的所有索引都是二级索引,这意味着每个索引都与表的主数据区(在PostgreSQL术语中称为表的堆heap)分开存储 。这意味着在普通索引扫描中,每次检索都需要从索引和堆中获取数据。此外,虽然匹配给定可索引WHERE条件的索引条目通常在索引中靠近,但它们引用的表行可能位于堆中的任何位置。因此,索引扫描的堆访问部分涉及到对堆的大量随机访问,这可能很慢,特别是在传统的旋转媒体上。(如第11.5节所述,位图扫描试图通过按排序顺序进行堆访问来减轻这种成本,但这也仅此而已。)

为了解决这个性能问题,PostgreSQL支持仅索引扫描 ,它可以在没有任何堆访问的情况下从索引单独回答查询。基本思想是直接从每个索引项返回值,而不是咨询相关的堆项。这种方法的使用有两个基本限制:

  1. 索引类型必须支持仅索引扫描 。b树索引总是这样。GiST和SP-GiST索引支持某些操作符类的仅索引扫描,但不支持其他操作符类。其他索引类型不支持。基本要求是索引必须物理地存储,或者能够重建每个索引条目的原始数据值。作为反例,GIN索引不能支持仅索引扫描,因为每个索引项通常只保存原始数据值的一部分。
  2. 查询必须只引用存储在索引中的列。例如,给定一个表的列x和列y上的索引,该表也有列z,这些查询可以使用仅索引扫描:
sql 复制代码
SELECT x, y FROM tab WHERE x = 'key';
SELECT x FROM tab WHERE x = 'key' AND y < 42;

但这些查询不能:

sql 复制代码
SELECT x, z FROM tab WHERE x = 'key';
SELECT x FROM tab WHERE x = 'key' AND z < 42;

(表达式索引和部分索引使该规则复杂化,如下所述。)

如果满足这两个基本要求,那么查询所需的所有数据值都可以从索引中获得,因此物理上可以进行仅索引扫描 。但是在PostgreSQL中对任何表扫描都有一个额外的要求:它必须验证每个检索到的行对查询的MVCC快照是"可见的",如第13章所讨论的。可见性信息不存储在索引项中,只存储在堆项中;因此,乍一看,似乎每个行检索无论如何都需要一个堆访问。如果表行最近被修改过,确实会出现这种情况。然而,对于很少变化的数据,有一种方法可以解决这个问题。PostgreSQL跟踪表堆中的每一页,是否该页上存储的所有行都足够旧,可以对所有当前和未来的事务可见。该信息存储在表可见性映射( visibility map)中的位中。在找到候选索引项后,只执行索引扫描,检查相应堆页的可见性映射位。如果设置了该行,则该行是已知可见的,因此无需进一步操作即可返回数据。如果没有设置,则必须访问堆条目以确定它是否可见,因此与标准索引扫描相比,没有性能优势。即使在成功的情况下,这种方法也会用可见性映射访问换取堆访问;但是由于可见性映射比它所描述的堆小四个数量级,因此访问它所需的物理I/O要少得多。在大多数情况下,可见性映射始终缓存在内存中

简而言之,尽管在满足这两个基本要求的情况下,仅索引扫描是可能的,但只有当表堆页面的很大一部分设置了它们的全可见映射位时,这种扫描才会成功。但是,大部分行不变的表非常常见,因此这种类型的扫描在实践中非常有用。

为了有效地利用仅索引扫描特性,可以选择创建覆盖索引(covering index),这是一种专门设计的索引,用于包含经常运行的特定查询类型所需的列。由于查询通常需要检索更多的列,而不仅仅是他们搜索的列,PostgreSQL允许你创建一个索引,其中一些列只是"有效负载",而不是搜索键的一部分。这是通过添加一个包含额外列的INCLUDE子句来实现的。例如,如果您经常运行这样的查询:

sql 复制代码
SELECT y FROM tab WHERE x = 'key';

加速此类查询的传统方法是仅在x上创建索引。但是,索引定义为:

sql 复制代码
CREATE INDEX tab_x_y ON tab(x) INCLUDE (y);

可以将这些查询作为仅索引扫描来处理,因为y可以在不访问堆的情况下从索引获得。

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

sql 复制代码
CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);

唯一性条件只适用于列x,而不适用于xy的组合(INCLUDE子句也可以写在UNIQUEPRIMARY KEY约束中,为建立这样的索引提供了另一种语法)。

在向索引中添加非键有效负载列时保持保守是明智的,尤其是宽列。如果索引元组超过索引类型允许的最大大小,则数据插入将失败。在任何情况下,非键列都会复制索引表中的数据,使索引的大小膨胀,从而可能减慢搜索速度。请记住,在索引中包含有效负载列没有什么意义,除非表的变化足够慢,以至于仅索引扫描可能不需要访问堆。如果无论如何都必须访问堆元组,那么从堆元组中获取列的值不会增加任何开销。其他限制是表达式目前不支持包含列,并且目前只有B-tree、GiST和SP-GiST索引支持包含列。

在PostgreSQL有INCLUDE特性之前,人们有时通过将有效负载列写成普通索引列来覆盖索引,也就是写

sql 复制代码
CREATE INDEX tab_x_y ON tab(x, y);

即使他们从未打算将y用作WHERE子句的一部分。这工作很好,只要额外的列是尾列;由于第11.3节中解释的原因,将它们作为主导列是不明智的。但是,此方法不支持希望索引在键列 column(s)上强制惟一的情况。

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

原则上,仅索引扫描可以与表达式索引一起使用。例如,给定f(x)上的索引,其中x是表列,应该可以执行

sql 复制代码
SELECT f(x) FROM tab WHERE f(x) < 1;

作为仅索引扫描;如果f()是一个计算成本很高的函数,这是非常吸引人的。然而,PostgreSQL的计划器目前对这种情况并不是很聪明。只有当查询所需的所有列(all columns)都从索引中可用时,它才认为查询可能通过仅索引扫描可执行。在本例中,除了上下文f(x)之外,不需要x,但规划器没有注意到这一点,并得出结论,不可能进行仅索引扫描。如果仅索引扫描看起来足够值得,那么可以通过添加x作为包含列来解决这个问题,例如

sql 复制代码
CREATE INDEX tab_f_x ON tab (f(x)) INCLUDE (x);

另外需要注意的是,如果目标是避免重新计算f(x),那么规划器不一定会将不在可索引WHERE子句中的f(x)的使用匹配到索引列。在上面所示的简单查询中,它通常会正确处理,但在涉及连接的查询中则不然。这些缺陷可能会在未来的PostgreSQL版本中得到弥补。

部分索引还与仅索引扫描有有趣的交互。考虑例11.3所示的部分索引:

sql 复制代码
CREATE UNIQUE INDEX tests_success_constraint ON tests (subject, target)
    WHERE success;

原则上,我们可以对该索引执行仅索引扫描,以满足像以下查询:

sql 复制代码
SELECT target FROM tests WHERE subject = 'some-subject' AND success;

但是有一个问题:WHERE子句引用的是成功,而它不是索引的结果列。尽管如此,仅索引扫描是可能的,因为计划不需要在运行时重新检查WHERE子句的那一部分:在索引中找到的所有条目都必须具有success = true,因此不需要在计划中显式检查。PostgreSQL 9.6及以后的版本将识别这种情况,并允许仅生成索引扫描,但旧版本不会。

10. 操作符类和操作符族

索引定义可以为索引的每一列指定一个操作符类(operator class )。

bash 复制代码
CREATE INDEX name ON table (column opclass [ ( opclass_options ) ] [sort options] [, ...]);

操作符类标识该列的索引要使用的操作符 。例如,类型int4上的b树索引将使用int4_ops类;这个操作符类包括int4类型值的比较函数。实际上,列数据类型的默认操作符类通常就足够了。使用操作符类的主要原因是,对于某些数据类型,可能存在不止一个有意义的索引行为。例如,我们可能希望按绝对值或实数部分对复数数据类型进行排序。为此,可以为数据类型定义两个操作符类,然后在创建索引时选择适当的类。操作符类确定基本排序顺序(然后可以通过添加排序选项COLLATE、ASC/DESC和/或NULLS FIRST/NULLS LAST来修改排序顺序)。

设置PostgreSQL排序规则(Collation)

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

操作符类text_pattern_opsvarchar_pattern_opsbpchar_pattern_ops分别支持对textvarcharchar类型进行b树索引。与默认操作符类的不同之处在于,值严格地逐个字符进行比较,而不是根据特定于语言环境的排序规则进行比较 。这使得这些操作符类适合在数据库不使用标准"C"语言环境时用于涉及模式匹配表达式(LIKE或POSIX正则表达式)的查询。作为一个例子,你可以像这样索引一个varchar列:

bash 复制代码
CREATE INDEX test_index ON test_table (col varchar_pattern_ops);

注意,如果希望查询涉及普通<<=>>=比较,则还应该使用默认操作符类创建索引 。这样的查询不能使用 xxx_pattern_ops操作符类。(不过,普通的相等比较可以使用这些操作符类。)可以使用不同的操作符类在同一列上创建多个索引。如果使用C语言环境,则不需要xxx_pattern_ops操作符类,因为带有默认操作符类的索引可用于C语言环境中的模式匹配查询。

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

bash 复制代码
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;

操作符类实际上只是称为操作符族(operator family)的更大结构的一个子集。在几种数据类型具有相似行为的情况下,定义跨数据类型操作符并允许它们与索引一起工作通常很有用。为此,必须将每种类型的操作符类分组到相同的操作符族中。交叉类型操作符是家族的成员,但不与家族中的任何单个类相关联。

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

bash 复制代码
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;

该查询显示所有已定义的操作符族以及每个操作符族中包含的所有操作符:

bash 复制代码
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;

11. 索引和排序

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

考虑这些陈述:

sql 复制代码
CREATE TABLE test1c (
    id integer,
    content varchar COLLATE "x"
);

CREATE INDEX test1c_content_index ON test1c (content);

索引自动使用基础列的排序规则。查询的形式是

sql 复制代码
SELECT * FROM test1c WHERE content > constant;

可以使用索引,因为比较在默认情况下将使用列的排序。但是,此索引不能加速涉及其他排序的查询。如果查询的形式是,

sql 复制代码
SELECT * FROM test1c WHERE content > constant COLLATE "y";

同样感兴趣的是,可以创建一个支持"y"排序的额外索引,如下所示:

sql 复制代码
CREATE INDEX test1c_content_y_index ON test1c (content COLLATE "y");

12. 检查索引使用情况

虽然PostgreSQL中的索引不需要维护或调优,但检查实际查询工作负载实际使用的索引仍然很重要 。使用EXPLAIN命令检查单个查询的索引使用情况;它在这方面的应用将在第14.1节中说明。还可以收集运行服务器中索引使用情况的总体统计信息,如第27.2节所述。

很难制定一个确定要创建哪些索引的通用过程。在前面几节的示例中已经展示了许多典型的案例。大量的实验常常是必要的。本节的其余部分给出了一些建议:

  • 总是先运行ANALYZE。该命令用于统计表中值的分布情况。计划器需要这些信息来估计查询返回的行数,从而为每个可能的查询计划分配实际的成本。在没有任何实际统计数据的情况下,假设了一些默认值,这些值几乎肯定是不准确的。因此,在没有运行ANALYZE的情况下检查应用程序的索引使用情况是失败的 。更多信息请参见24.1.3节24.1.6节

  • 使用真实数据进行实验。使用测试数据来设置索引将告诉您测试数据需要哪些索引,但仅此而已。
    使用非常小的测试数据集是非常致命的 。虽然从100000行中选择1000行可以作为索引的候选,但从100行中选择1行很难作为索引的候选,因为这100行可能只适合一个磁盘页,并且没有比顺序获取1个磁盘页更好的计划

  • 当不使用索引时,通过测试强制使用索引是很有用的 。运行时参数可以关闭各种计划类型(参见第19.7.1节)。例如,关闭顺序扫描enable_seqscan)和嵌套循环连接enable_nestloop),这是最基本的计划,将迫使系统使用不同的计划。如果系统仍然选择顺序扫描或嵌套循环连接,那么可能有一个更根本的原因导致索引没有被使用;例如,查询条件与索引不匹配。(前几节解释了哪种查询可以使用哪种索引。)

  • 如果强制使用索引确实使用了索引,那么有两种可能:要么系统是正确的,使用索引确实不合适,要么查询计划的成本估计没有反映实际情况。因此,您应该对有索引和没有索引的查询进行计时。EXPLAIN ANALYZE命令在这里很有用。

  • 如果结果证明成本估算是错误的,那么又有两种可能。总成本由每个计划节点的每行成本乘以计划节点的选择性估计值计算得出。计划节点的估计成本可以通过运行时参数进行调整(见第19.7.2节)。不准确的选择性估计是由于统计不足造成的。可以通过调优统计数据收集参数(参见ALTER TABLE)来改进这一点。

    如果您不能成功地将成本调整为更合适的成本,那么您可能不得不显式地强制使用索引。您可能还需要联系PostgreSQL开发人员来检查这个问题。

IMDB数据集导入PostgreSQL和join order benchmark(JOB)查询生成

相关推荐
云和数据.ChenGuang14 分钟前
Django 应用安装脚本 – 如何将应用添加到 INSTALLED_APPS 设置中 原创
数据库·django·sqlite
woshilys42 分钟前
sql server 查询对象的修改时间
运维·数据库·sqlserver
Hacker_LaoYi43 分钟前
SQL注入的那些面试题总结
数据库·sql
建投数据2 小时前
建投数据与腾讯云数据库TDSQL完成产品兼容性互认证
数据库·腾讯云
Hacker_LaoYi3 小时前
【渗透技术总结】SQL手工注入总结
数据库·sql
岁月变迁呀3 小时前
Redis梳理
数据库·redis·缓存
独行soc3 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍06-基于子查询的SQL注入(Subquery-Based SQL Injection)
数据库·sql·安全·web安全·漏洞挖掘·hw
你的微笑,乱了夏天3 小时前
linux centos 7 安装 mongodb7
数据库·mongodb
工业甲酰苯胺3 小时前
分布式系统架构:服务容错
数据库·架构
独行soc4 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍08-基于时间延迟的SQL注入(Time-Based SQL Injection)
数据库·sql·安全·渗透测试·漏洞挖掘