第6章 表、约束和索引

第 6 章 表、约束和索引

在表上创建索引是需要经过深思熟虑的,因为一个错误的索引会导致查询效果比全表扫描还差,也就是说创建了还不如不创建。并不是所有的索引都是“生来平等”的,数据库领域的算法专家为不同的数据类型设计出了不同类型的索引,目的是将查询的速度提升到极致。

6.1 表

除了普通的表以外,PostgreSQL 还提供了许多不常见的表,具体包括临时表、无日志表、继承表、基于复合类型的表以及外部表。

6.1.1 基本的建表操作

基本的建表操作

CREATE TABLE logs (
log_id serial PRIMARY KEY, ➊
user_name varchar(50), ➋
description text, ➌
log_ts timestamp with time zone NOT NULL DEFAULT current_timestamp
); ➍
CREATE INDEX idx_logs_log_ts ON logs USING btree (log_ts);

❶ serial 数据类型是一种自增长的数字类型。建表时如果有一个 serial 类型的字段,那么系统会自动在 schema 中同时创建一个对应的序列号生成器。serial 类型字段的值是一个整型数字,它会自动被赋值为序列号生成器的下一个值。每张表一般来说只会有一个 serial 字段,且一般用作主键。对于特别大的表,应该使用 bigserial 类型,因为它能容纳的数值上限更大。
❷ varchar 是 character varying(可变长字符串)的简写,其定义与其他数据库产品中的定义类似。你可以不为 varchar 字段设定最大长度值,此时它与 text 类型几乎是一样的。
❸ text 是一种不定长度的字符串,无最大长度限制。
❹ timestamp with time zone(可简写为 timestamptz)是一种表示日期和时间的类型,总是以国际标准时间(UTC)格式存储。该类型在显示时总是以服务器当前所在时区为基准进行显示,当然你也可以要求使用指定的时区进行显示。

PostgreSQL 10 中新增了对 IDENTITY 关键字的支持。IDENTITY 也可以将字段定义为自增序列号类型。将现有的 log_id 字段的 serial 类型修改为 IDENTITY 类型。

DROP SEQUENCE logs_log_id_seq CASCADE;
ALTER TABLE logs
ALTER COLUMN log_id ADD GENERATED BY DEFAULT AS IDENTITY;

如果此表中已有数据,需要防止序列号再次从 1 生成从而造成重复。

ALTER TABLE logs
ALTER COLUMN log_id RESTART WITH 2000;

使用 IDENTITY 语法创建表

CREATE TABLE logs (
log_id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
user_name varchar(50),
description text,
log_ts timestamp with time zone NOT NULL DEFAULT current_timestamp
);

那么在什么情况下应该使用 IDENTITY 替代 serial 呢?

IDENTITY 语法的主要优点在于一个 identity 总是与所属表绑定的,其值的递增或者重置都是与表本身一体化管理的,不会受其他对象干扰。serial 类型则不是这样,它会在后台自动创建一个序列号生成器,这个序列号生成器可以与别的表共享也可以本表独享,当不需要该序列号生成器时需要手动删除它。如果你需要重置一个 serial 类型字段的初始值,需要修改后台那个自动生成的序列号生成器,这也意味着得先知道那个序列号生成器的名字。很显然,这个管理过程比 IDENTITY 要繁琐很多。

当需要在多张表之间共享一个递增序列号时,serial 类型依然是很有用的。这种情况下,需要创建一个独立的序列号生成器,并把需要共享该序列的每张表的相应字段的默认值设为该序列号生成器的下一个值。从内部实现机制来看,IDENTITY 语法与 serial 类型的做法其实是类似的,都是自动创建一个序列号生成器,只不过 IDENTITY 不会将这个序列号生成器对象暴露给外界去修改。

6.1.2 继承表

PostgreSQL 是唯一提供表继承功能的数据库。如果创建一张表(子表)时指定为继承自另一张表(父表),则建好的子表除了含有自己的字段外还会含有父表的所有字段。PostgreSQL 会记录下这个继承关系,这样一旦父表的结构发生了变化,子表的结构也会自动跟着变化。这种父子继承结构的表可以完美地适用于需要数据分区的场景。当查询父表时,PostgreSQL 会自动把子表的记录也取出来。值得注意的是,并不是所有父表的特征都会被子表继承下来,比如主表的主键约束、唯一性约束以及索引就不会被继承。check 约束会被继承,但子表还可以另建自己的 check 约束。(随着版本的发展,主表的主键约束、唯一性约束以及索引也可以被继承,PG12版本)

创建继承表

CREATE TABLE logs_2011 (PRIMARY KEY (log_id)) INHERITS (logs);
CREATE INDEX idx_logs_2011_log_ts ON logs_2011 USING btree(log_ts);
ALTER TABLE logs_2011
ADD CONSTRAINT chk_y2011
CHECK (
log_ts >= '2011-1-1'::timestamptz AND log_ts < '2012-1-1'::timestamptz
);

➊ 我们定义了一个 check 约束来限制只能录入 2011 年的数据。该 check 约束告诉查询规划器在查询父表时跳过不满足条件的子表。

PostgreSQL 9.5 新增支持了在本地表和外部表之间做表继承,而且可以互相继承。支持该特性主要是为了实现表的分布式存储

6.1.3 原生分区表支持

尽管在很多场景下分区表都可以替代原来的表继承功能,但还不能完全替代。以下是表继承和分区表这两个功能的关键区别。

  • 分区表使用声明式的 CREATE TABLE … PARTITION BY RANGE … 语法,后台会默认创建一个分区表组。

  • 使用分区表时,如果对主表插入数据,记录会按照分区规则被路由到相应的分区中。但使用表继承时情况就不是这样,你需要直接把数据插入正确的子表中,或者在主表上挂载触发器来把记录路由到子表中。

  • 分区表的所有子分区都必须具备相同的字段结构,而表继承中的子表完全可以比父表拥有更多字段。

  • 分区表的每个分区都隶属于一个共同的分区表组,这意味着它们只能有一个父表,然而表继承功能中的一张子表可以继承自多个父表。

  • 分区表的父表是一个逻辑对象而非物理实体,因此它上面不可以定义主键、唯一键或者索引,但是每个子分区可以定义这些。表继承机制中的情况与此不同:父表和每个子表都可以有主键,而且主键只需在本表内部唯一即可,并不一定要在所有子表范围内都唯一。(PG12中的,索引直接可以继承)。

  • 与表继承机制中的父表不同,分区表的父表不能存储自己的记录。所有针对父表插入的记录都会被路由到相应的子分区中,如果没有符合条件的子分区则会报错。

创建分区表时,必须使用 PARTITION BY 语法来表明这是一个分区表。我们先创建了一张普通表。另外,请注意我们并没有定义主键,因为分区表的主表并不支持主键。

CREATE TABLE logs (
log_id int GENERATED BY DEFAULT AS IDENTITY,
user_name varchar(50),
description text,
log_ts timestamp with time zone NOT NULL DEFAULT current_timestamp
) PARTITION BY RANGE (log_ts);

与表继承机制类似的是,分区表机制中也需要单独创建子分区表,不同之处是使用了 FOR VALUES 语法来指明每张子表能容纳的数据范围,而表继承中使用的是 check 约束。

CREATE TABLE logs_2011 PARTITION OF logs ➊
FOR VALUES FROM ('2011-1-1') TO ('2012-1-1');
CREATE INDEX idx_logs_2011_log_ts ON logs_2011 USING btree(log_ts); ➌
ALTER TABLE logs_2011 ADD CONSTRAINT pk_logs_2011 PRIMARY KEY (log_id);

➊ 定义一张新表,作为 logs 表的一个子分区。
➋ 定义该分区中能够存储的数据范围。子分区之间的数据存储范围不能有重叠,如果违反此规则,子分区的 CREATE TABLE 语句会失败。
➌➍ 子分区表上可以定义索引和主键。与表继承类似的是,子分区表的主键并不需要在所有子分区表中全局唯一。

为当前年份再创建一个分区

CREATE TABLE logs_gt_2011 PARTITION OF logs
FOR VALUES FROM ('2012-1-1') TO (unbounded);

使用了 unbounded 关键字来表示分区的截止范围,这样将来的日期也都能匹配到这个分区。请注意,在实际项目中不是只创建完分区就可以,我们需要针对新建的分区创建索引和主键以提升查询效率。

与表继承机制类似的是,当查询父表时,所有不符合条件的子分区都会被跳过。规划器自动跳过不符合条件的分区。

EXPLAIN ANALYZE SELECT * FROM logs WHERE log_ts > '2017-05-01';
Append (cost=0.00..15.25 rows=140 width=162)
(actual time=0.008..0.009 rows=1 loops=1)
-> Seq Scan on logs_gt_2011 (cost=0.00..15.25 rows=140 width=162)
(actual time=0.008..0.008 rows=1 loops=1)
Filter: (log_ts > '2017-05-01 00:00:00-04'::timestamp with time zone)
Planning time: 0.152 ms
Execution time: 0.022 ms

如果你使用的是 PostgreSQL 10 自带的 PSQL,能看到每个分区能容纳数据的范围。

\d+ logs
Table "public.logs"
:
Partition key: RANGE (log_ts)
Partitions: logs_2011
FOR VALUES FROM ('2011-01-01 00:00:00-05') TO ('2012-01-01 00:00:00-05'),
logs_gt_2011
FOR VALUES FROM ('2012-01-01 00:00:00-05') TO (UNBOUNDED)

6.1.4 无日志表

对于发生磁盘故障或者系统崩溃后可以被重建的临时数据来说,其操作速度比可靠性更重要。PostgreSQL从 9.1 版开始支持 UNLOGGED 修饰符,使用该修饰符可以创建无日志的表,系统不会为这种表记录任何事务日志。无日志表的一大优势是其写入记录的速度远远超过普通的有日志表,依照我们的经验,大概会快 10 到 15 倍。

如果你的服务器不小心被掉电重启,那么无日志表中的数据会在事务回滚过程中被全部清除。无日志表的另一个特性是它无法被纳入 PostgreSQL 的复制机制,因为复制机制依赖事务日志。pg_dump 有一个选项可以允许你跳过备份无日志的表。

创建无日志表

CREATE UNLOGGED TABLE web_sessions (
session_id text PRIMARY KEY,
add_ts timestamptz,
upd_ts timestamptz,
session_state xml);

在 PostgreSQL 9.3 之前,无日志表不支持 GiST 索引,该类型的索引一般适用于高级的数据类型,比如数组、范围、JSON、全文检索以及空间类型等。不过任何 PostgreSQL 版本中的无日志表都可以使用 B-树索引和 GIN 索引。

在 PostgreSQL 9.5 之前,要想把无日志表改为有日志表是很麻烦的。9.5 版之后,只需执行以下命令即可:

ALTER TABLE some_table SET LOGGED;

6.1.5 TYPE OF

PostgreSQL 在创建一张表时,会自动在后台创建一个结构完全相同的复合数据类型,反之则不是这样。首先创建一个复合数据类型。

CREATE TYPE basic_user AS (user_name varchar(50), pwd varchar(10));

以复合数据类型为模板来创建一张表

CREATE TABLE super_users OF basic_user (CONSTRAINT pk_su PRIMARY KEY (user_name));

当基于数据类型来创建表时,你不能指定表字段的定义,一切以数据类型本身的定义为准。然而,为复合数据类型新增或者移除字段时,PostgreSQL 会自动修改相应的表结构。这种机制的优点是,如果你的系统中有很多结构相同的表,而你可能需要同时对所有表结构进行相同的修改,那么此时只需要修改此基础数据类型即可,这一点与表继承机制很相似。

为super_users 表增加一个电话号码字段。

ALTER TYPE basic_user ADD ATTRIBUTE phone varchar(10) CASCADE;

一般来说,如果表依赖于某个类型,那么你就不能更改该类型的定义。CASCADE 修饰符凌驾于此限制之上,对所有相关表应用相同的更改。

6.2 约束机制

6.2.1 外键约束

建立外键约束和相应的索引

SET search_path=census, public;
ALTER TABLE facts ADD CONSTRAINT fk_facts_1 FOREIGN KEY (fact_type_id)
REFERENCES lu_fact_types (fact_type_id) ➊ ON UPDATE CASCADE ON DELETE RESTRICT;
➋
CREATE INDEX fki_facts_1 ON facts (fact_type_id);

❶ 我们在 facts 表和 lu_fact_types 表之间定义了一个外键约束关系。有了这个约束以后,如果主表 lu_fact_types 中不存在某 fact_type_id 的记录,那么从表 fact 中就不能插入该 fact_type_id 的记录。
❷ 我们定义了一个级联规则,实现了以下功能:(1) 如果主表 lu_fact_type 的 fact_type_id 字段值发生了变化,那么从表 fact 中相应记录的 fact_type_id 字段值会自动进行相应修改,以维持外键引用关系不变;(2) 如果从表 fact 中还存在某 fact_type_id 字段值的记录,那么主表 lu_fact_type 中相同 fact_type_id 字段值的记录就不允许被删除。ON DELETE RESTRICT 是默认行为模式,也就是说这个子句不加也可以,但为了清晰起见最好加上。
❸ PostgreSQL 在建立主键约束和唯一性约束时,会自动为相应字段建立索引,但在建立外键约束时却不会,这一点需要注意。你需要为外键字段手动建立索引,以加快关联引用时的查询速度。

6.2.2 唯一性约束

主键字段的值是唯一的,但每张表只能定义一个主键,因此如果你需要保证别的字段值唯一,那么必须在该字段上建立唯一性约束或者唯一索引。建立唯一性约束时会自动在后台创建一个相应的唯一索引。与主键字段类似,建立了唯一性约束的字段可以作为外键字段被别的表引用,但它可以为空。不过请注意:建了唯一索引却没有唯一性约束的字段是可以输入空值的,而且还可以使用函数来定义。

ALTER TABLE logs_2011 ADD CONSTRAINT uq UNIQUE (user_name, log_ts);

你可能经常会遇到仅需要保证表中部分记录行唯一的情况,PostgreSQL 不支持带筛选条件的唯一性约束,但你可以通过使用唯一性的部分索引来达到相同的目的。

6.2.3 check约束

check 约束能够给表的一个或者多个字段加上一个条件,表中每一行记录必须满足此条件。查询规划器也会利用 check 约束来优化执行速度,比如有些查询附带的条件与待查询表的 check 约束无交集,那么规划器会立即认定该查询未命中目标并返回。

例如,以下 check 约束可以限制 logs 表中所有用户名必须都小写。

ALTER TABLE logs ADD CONSTRAINT chk CHECK (user_name = lower(user_name));

特别值得注意的一点是,当表间存在继承关系时,子表会继承父表的 check 约束,但主键、外键、唯一性这三种约束却不会继承。

6.2.4 排他性约束

传统的唯一性约束在比较算法中仅使用了“等于”运算符,即保证了指定字段的值在本表的任意两行记录中都不相等,而排他性约束机制拓展了唯一性比较算法机制,可以使用更多的运算符来进行比较运算,该类约束特别适用于解决有关时间安排的问题。

PostgreSQL 9.2 中引入了区间数据类型,该类型特别适合使用排他性约束。排他性约束一般是基于 GiST 类型的索引来实现的,使用基于 B-树算法的 GiST 多列复合索引也是可以的,不过需要先安装 btree_gist 扩展包才能建立这种索引。多列排他性约束的一个经典应用场景就是用于安排资源

假设你的办公场所有固定数量的会议室,各项目组在使用会议室前必须预订。如何避免发生预订冲突?该示例中使用了 && 运算符来判定时间区段是否重叠,还使用了 = 运算符来判定会议室房间号是否重复,请注意观察和思考此用法。

CREATE TABLE schedules(id serial primary key, room int, time_slot tstzrange);
ALTER TABLE schedules ADD CONSTRAINT ex_schedules
EXCLUDE USING gist (room WITH =, time_slot WITH &&);

同唯一性约束一样,PostgreSQL 会自动为排他性约束中涉及的字段建立索引。

排他性约束适用的另一个场景是处理数组类型的数据。假设有若干房间要分配给一群人搞聚会,我们把一场聚会所使用的房间称为一个 block。方便起见,我们为每个聚会生成一条记录,但需要确保两场聚会不会共享同一个房间。建表如下:

CREATE TABLE room_blocks(block_id integer primary key, rooms int[]);

为保证每两个 block 之间不会有共享的房间,可以设置一个排他性约束来防止分配重叠。很遗憾的是,排他性约束仅对 GiST 索引类型生效,而 GiST 索引又不支持在数组上建立,所以我们先安装一个扩展包。

CREATE EXTENSION IF NOT EXISTS intarray;
ALTER TABLE room_blocks
ADD CONSTRAINT ex_room_blocks_rooms
EXCLUDE USING gist(rooms WITH &&);

intarray 扩展包的功能是支持在整型数组(支持 int4 和 int8)上建立 GiST 索引。intarray 安装好以后就可以对数组类型的数据建立 GiST 了,接着就可以在整型数组数据上建立排他性约束。

6.3 索引

PostgreSQL 原生支持若干种类型的索引。如果你觉得还不够,PostgreSQL 还允许你为这几种索引类型自定义新的索引运算符和修饰符以作为其功能补充。如果这样还不能满足你的要求,你可以创建自己的索引类型。

普通表和物化视图上均可创建索引,但外部表不行。

6.3.1 PostgreSQL原生支持的索引类型

B-树索引
  B-树是一种关系型数据库中常见的通用索引类型。如果你对别的索引类型不感兴趣,那么一般使用B-树索引就可以了。有的场景下 PostgreSQL 会自动创建索引(比如创建主键约束或者唯一性约束时),那么创建出来的索引就是 B-树类型的;如果你自己创建索引时未指定索引类型,那么默认也会创建 B-树类型的索引。主键约束和唯一性约束唯一支持的后台索引就是 B-树索引。

BRIN 索引
  BRIN(block range index,块范围索引)是 PostgreSQL 9.4 中引入的一种索引类型,其设计目的是针对超大表做索引,在这种表上创建 B-树索引耗费的空间过大,以至于无法全部容纳在内存中,这会导致内存和磁盘间的索引数据块换入换出,从而严重影响查询速度。BRIN 索引的思路就是把一个范围内的数据页面当作一个单元来处理,这样就可以大大压缩需要索引的目标单元数。BRIN 索引占用的空间要比 B-树索引和其他索引小得多,同时建立起来也更快。但其查询时的速度相对较慢,也不能用于确保唯一主键,另外还有一些场景页不适用该类索引。

GiST 索引
  GiST(generalized search tree,通用搜索树)主要的适用场景包括全文检索以及空间数据、科学数据、非结构化数据和层次化数据的搜索。该类索引不能用于保障字段的唯一性,也就是说建立了该类型索引的字段上可插入重复值,但如果把该类索引用于排他性约束就可以实现唯一性保障。
  GiST 是一种有损索引,也就是说它不存储被索引字段的值,而仅仅存储字段值的一个取样,这种取样是失真的,就像把一个盒子变成了一个多边形。这就意味着需要一个额外的查找步骤以获得真正记录的值。

GIN 索引
  GIN(generalized inverted index,通用逆序索引)主要适用于 PostgreSQL 内置的全文搜索引擎以及二进制 json 数据类型。其他一些扩展包(比如 hstore 和 pg_trgm)也会使用这种索引。GIN其实是从 GiST 派生出来的一种索引类型,但它是无损的,也就是说索引中会包含有被索引字段的值。如果你需要查询的字段都已被索引,那么只读取索引即可获取查询结果,这种情况下 GIN 的查询速度是快于 GiST 的。然而,由于 GIN 比 GiST 在更新操作时要多出一个字段值复制动作,因此此时 GIN 索引体积更大并且更新速度慢于 GiST 索引。另外,GIN 的索引树内部每一个索引行的长度是有限制的,所以它不能用于对 hstore 文档或者 text 等大对象类型进行索引。如果你需要把一个 600 页的手册内容存入一张表的某个字段,那么绝对不要在该字段上建立 GIN 类型的索引。在 9.3 版中,用于实现字符串模糊匹配和相似度查询的 pg_trgm 扩展包中做了一个功能强化:支持正则表达式条件查询时用上 GIN 索引,这大大增加了 pg_trgm 的适用场景。

SP-GiST 索引
  SP-GiST 是指基于空间分区树(space-partitioning trees)算法的 GiST 索引。该类型的索引与 GiST 索引的适用领域相同,但对于某些特定领域的数据算法,其效率会更高一些。PostgreSQL 的 point 和 box 等原生几何类型以及 text 类型是最先支持该类索引的数据类型。从 9.3 版开始,区间类型也开始支持此类型的索引。

散列索引
  散列索引在 GiST 和 GIN 索引出现前就已经得到了广泛使用。业界普遍认为 GiST 和 GIN 索引在性能和事务安全性方面要胜过散列索引。PostgreSQL 10 之前的版本中,事务日志中不会记录散列索引的变化,那么在流式复制环境中就不能使用散列索引,否则会导致修改无法被同步。尽管有一段时期散列索引被 PostgreSQL 官方列为不推荐使用状态,但是在 PostgreSQL 10 中它再次得到了强化。该版本中,散列索引强化了事务一致性以及一些性能提升,有的场景中它会比 B-树更快。

基于 B-树算法的 GiST 和 GIN 索引
  如果你想了解 PostgreSQL 除了原生索引以外还有哪些索引,不管是出于业务需要还是仅仅出于好奇,都可以从了解基于 B-树算法的 GiST 和 GIN 索引开始。二者都可以用扩展包形式安装,并且大多数PostgreSQL 发行版中都含有这两个扩展包。这两类混合算法索引的优势在于,它们既能够支持 GiST 和 GIN 索引特有的运算符,又具有 B-树索引对于“等于”运算符的良好支持。当需要建立同时包含简单和复杂数据类型的多列复合索引时,你会发现这两类索引不可或缺。例如,我们建立的复合索引中既有普通文本类型也有 tsvector 和 tsquery 这两种专用于全文检索的数据类型 。一般来说,类似全文检索、ltree、geometric 和空间类型这些高级数据类型,只能使用 GIN 或者 GiST 索引,因此这类字段不可能与只能建立 B-树索引的普通字段构成复合索引。此时基于这两种索引就可以实现这个目标,基于它们的混合算法可以把建立了 GiST 索引的字段和建立了 B-树索引的字段联合起来,组成为单一的复合索引。

除了 PostgreSQL 原生附带的索引类型外,还有一些额外的索引类型以扩展包形式存在。其中比较流行的是 VODKA 和 RUM(基于 GIN 索引的一个变种)这两种,适用于PostgreSQL 9.6 以及之后的版本。RUM 索引适用于 full-text 之类的复杂类型,如果你需要全文词组搜索,那么就必须使用 RUM 类型。另外,它还提供了一些距离运算符。

另一个索引类型是 pgroonga,它以扩展包的形式存在,当前仅支持 PostgreSQL 9.5 和 PostgreSQL 9.6。该扩展把作为全文搜索引擎同时也是一个列式存储容器的 roonga 的能力带入了 PostgreSQL。PGRoonga 扩展包中含有一个名为 pgroonga 的索引类型以及相应的运算符。PGRoonga 扩展可以实现对普通文本进行索引,以支持对其进行全文检索,但不像 PostgreSQL 原生的全文检索机制那样需要创建全文检索向量。PGRoonga 还能够实现让 ILIKE 和 LIKE ‘%something%’ 这种操作用上索引,效果类似于 pg_trgm 扩展。此外,它还支持对数组类型和 JSONB 类型建立索引。

6.3.2 运算符类

“为什么规划器没用上我的索引?”

各种数据类型均有自身的特点,因此适用的索引类型不同,会用到的比较运算符也不同。例如,对于基于区间类型(range)的索引来说,最常用的运算符是重叠运算符(&&),然而该运算符对于本文搜索领域来说却毫无意义。对于中文这类表意文字来说,建立的索引基本上不会用到“不等于”运算符;而对英文这类表音文字建立索引时,字母 A 到 Z 的排序操作是不可或缺的。

基于以上特点,PostgreSQL 把一类应用领域相近的运算符以及这些运算符适用的数据类型组合在一起,称为一个运算符类。例如,int4_ops 运算符类包含适用于 int4(也就是日常所说的 integer)类型的 = < > > < 运算符。PostgreSQL 提供了一张叫作 pg_class 的系统表,从中可以查到完整的运算符类列表,其中既包含了系统原生支持的类,也包含了通过扩展包机制添加的类。一种类型的索引会使用特定的若干种运算符类。

查询 B-树索引支持的数据类型以及运算符类

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 INNER JOIN pg_opclass opc ON opc.opcmethod = am.oid
WHERE am.amname = 'btree'
ORDER BY index_method, indexed_type, opclass_name;
index_method | opclass_name        | indexed_type | is_default
-------------+---------------------+--------------+------------
btree        | bool_ops            | boolean      | t
⋮
btree        | text_ops            | text         | t
btree        | text_pattern_ops    | text         | f
btree        | varchar_ops         | text         | f
btree        | varchar_pattern_ops | text         | f
⋮

请注意,每类索引都会有多个运算符类,而其中仅有一个会被标记为默认运算符类。如果建立索引时未指定使用哪个运算符类,那么 PostgreSQL 会使用默认运算符类。

例如,B-树索引默认的 text_ops 运算符类(又名 varchar_ops)中并不支持~~运算符(即 LIKE 运算符),所以如果创建 B-树索引时选择了该运算符类,那么所有使用 LIKE 的查询都无法在 text_ops 运算符类中使用索引。因此,如果你的业务场景需要对 varchar 或者 text 类型进行大量 LIKE 模糊查询,那么创建索引时最好显式指定使用 text_pattern_ops 或者 varchar_pattern_ops 这两个运算符类。指定运算符类的语法很简单,只需要在创建索引时加在被索引字段名的后面即可。

CREATE INDEX idx1 ON census.lu_tracts USING btree (tract_name text_pattern_ops);

最后请牢记这一条:你创建的每一个索引都只会使用一个运算符类。如果希望一个字段上的索引使用多个运算符类,那么请创建多个索引。

CREATE INDEX idx2 ON census.lu_tracts USING btree (tract_name);

规划器处理等值查询时会使用 idx2,处理 like 模糊查询时会使用 idx1

6.3.3 函数索引

PostgreSQL 的函数索引功能可以基于字段值的函数运算结果建立索引。函数索引的用途也是很广泛的,例如可用于对大小写混杂的文本数据建立索引。PostgreSQL 是一个区分大小写的数据库,如果要实现不区分大小写的查询,可以借助如下的函数索引:

CREATE INDEX idx ON featnames_short
USING btree (upper(fullname) varchar_pattern_ops);

由于查询语句中使用的函数与我们在建立索引时使用的函数相同,因此规划器会对此查询语句使用索引:

SELECT fullname FROM featnames_short WHERE upper(fullname) LIKE 'S%';

6.3.4 基于部分记录的索引

基于部分记录的索引(有时也称为已筛选索引)是一种仅针对表中部分记录的索引,而且这部分记录需要满足 WHERE 语句设置的筛选条件。例如,假设某表中有 1 000 000 条记录,但你只会查询其中一个记录数为 10 000 的子集,那么该场景就非常适合使用基于部分记录的索引。这种索引比全量索引快,因为其体积小,所以可以把更多索引数据缓存到内存中。另外,该类索引占用的磁盘空间也更小。

基于部分记录的索引能够实现仅针对部分记录的唯一性约束。假设你手上有一家报纸在过去10 年间的订阅用户数据。

CREATE TABLE subscribers (
id serial PRIMARY KEY,
name varchar(50) NOT NULL, type varchar(50),
is_active boolean);

我们建立一个基于当前活跃用户的部分记录索引即可:

CREATE UNIQUE INDEX uq ON subscribers USING btree(lower(name)) WHERE is_active;

索引的 WHERE 条件中使用的函数必须是确定性函数,即固定的输入一定能够得到固定输出的函数。这意味着有几类函数是不能用作筛选条件的:一类是 CURRENT_DATE 这种输出结果不停在变的函数;一类是依赖于其他表数据进行运算的函数,其输出结果受其他表的数据的影响,因此输出也是不固定的;还有一类是依赖当前表中的其他记录行进行运算的函数,其输出也不会受控。

需要特别强调的一点是,当使用 SELECT 语句查询数据时,要想规划器用上部分记录索引,那么该查询语句的 WHERE 条件中必须包含创建部分记录索引时所使用的 WHERE 条件;如果该索引同时还是个函数索引,那么该查询语句的 WHERE 条件中必须包含建立索引时所使用的函数。

建立一个视图,视图条件就是建立索引的条件,那么针对此视图进行查询就永远不会漏掉条件了。

CREATE OR REPLACE VIEW vw_subscribers_current AS
SELECT id, lower(name) As name FROM subscribers WHERE is_active = true;

视图的本质就是一个保存下来的查询语句,创建视图时所用的 WHERE 条件以及其中
的函数条件部分都会被完整代入到任何针对该视图的查询语句中。查询视图的语句通过两个代入步骤后即可被翻译为针对基表的查询语句,之后就可以用上基础表的部分数据索引。

SELECT * FROM vw_subscribers_current WHERE name = 'sandy';

6.3.5 多列索引

多列索引(也称为复合索引),也可以基于多个字段来创建函数索引。复合索引的多个字段中也可以包含函数,此时建立的索引既是复合索引也是函数索引。

CREATE INDEX idx ON subscribers
USING btree (type, upper(name) varchar_pattern_ops);

PostgreSQL 的规划器在语句执行过程中会自动使用一种被称为“位图索引扫描”的策略来同时使用多个索引。该策略可以使得多个单列索引同时发挥作用,达到的效果与使用单个复合索引相同。如果你不能确定业务的应用模式是以单列作为查询条件的场景多一些,还是同时以多列作为查询条件的场景多一些,那么最好针对可能作为查询条件的每个列单独建立索引,这是最灵活的做法,规划器会决定如何组合使用这些索引。

假设你建了一个 B-树多列索引,其中包含 type 和 upper(name) 两个字段,那么完全没必要针对 type 字段再单独建立一个索引,因为规划器即使在遇到只有 type 单字段的查询条件时,也会自动使用该多列索引,这是规划器的一项基本能力。如果查询条件字段没有从多列索引中的第一个字段开始匹配,规划器其实也能用上索引,但请尽量避免这种情况,因为从索引原理上说,从索引的第一个字段开始匹配才是最高效的

规划器支持一种仅依赖索引内数据的查询策略(index-only scan),也就是说如果查询的目标字段在索引内都有,那么直接扫描索引就可以得到查询结果,根本不需要访问表的本体了。这个功能的引入使得复合索引的作用更为凸显,因为复合索引可以提供更多数据,因此更适合使用此种查询方法。如果你的业务场景中查询的目标字段和条件字段是相同的那几个,那么就应该建立复合索引以提升查询速度。不过,索引中包含的字段越多也就意味着索引占用的空间会越大,能在内存中缓存的索引条目就越少,因此请不要滥用复合索引。

猜你喜欢

转载自blog.csdn.net/qq_42226855/article/details/110203336
今日推荐