03.PostgreSQL常用索引与优化

PostgreSQL常用索引与优化

主要内容转载自《PostgreSQL 开发指南》

索引(Index)可以用于提高数据库的查询性能;但是索引也需要进行读写,同时还会占用更多的存储空间;因此了解并适当利用索引对于数据库的优化至关重要。本篇我们就来介绍如何高效地使用 PostgreSQL 索引。

索引简介

假设存在以下数据表:

sql 复制代码
CREATE TABLE test (
  id integer,
  name text
);

insert into test
select v,'val:'||v from generate_series(1, 10000000) v;

我们经常需要使用类似以下的查询返回结果:

sql 复制代码
SELECT name FROM test WHERE id = 10000;

如果没有索引,数据库需要扫描整个表才能找到相应的数据。利用EXPLAIN命令可以看到数据库的执行计划,也就是 PostgreSQL 执行 SQL 语句的具体步骤:

sql 复制代码
explain analyze
SELECT name FROM test WHERE id = 10000;
QUERY PLAN                                                                                                              |
------------------------------------------------------------------------------------------------------------------------|
Gather  (cost=1000.00..107137.70 rows=1 width=11) (actual time=50.266..12082.777 rows=1 loops=1)                        |
  Workers Planned: 2                                                                                                    |
  Workers Launched: 2                                                                                                   |
  ->  Parallel Seq Scan on test  (cost=0.00..106137.60 rows=1 width=11) (actual time=7674.992..11553.964 rows=0 loops=3)|
        Filter: (id = 10000)                                                                                            |
        Rows Removed by Filter: 3333333                                                                                 |
Planning Time: 16.480 ms                                                                                                |
Execution Time: 12093.016 ms                                                                                            |

Parallel Seq Scan 表示并行顺序扫描,执行消耗了 12 s;由于表中有包含大量数据,而查询只返回一行数据,显然这种方法效率很低。

关于执行计划的更多信息,可以参考这篇文章

此时,如果在 id 列上存在索引,则可以通过索引快速找到匹配的结果。我们先创建一个索引:

sql 复制代码
CREATE INDEX test_id_index ON test (id);

创建索引需要消耗一定的时间。然后再次查看数据库的执行计划:

sql 复制代码
explain analyze
SELECT name FROM test WHERE id = 10000;
QUERY PLAN                                                                                                           |
---------------------------------------------------------------------------------------------------------------------|
Index Scan using test_id_index on test  (cost=0.43..8.45 rows=1 width=11) (actual time=20.410..20.412 rows=1 loops=1)|
  Index Cond: (id = 10000)                                                                                           |
Planning Time: 14.989 ms                                                                                             |
Execution Time: 20.521 ms                                                                                            |

Index Scan 表示索引扫描,执行消耗了 20 ms;这种方式类似于图书最后的关键字索引,读者可以相对快速地浏览索引并翻到适当的页面,而不必阅读整本书来找到感兴趣的内容。

索引不仅仅能够优化查询语句,某些包含WHERE条件的UPDATEDELETE语句也可以利用索引提高性能,因为修改数据的前提是找到数据。

此外,索引也可以用于优化连接查询,基于连接条件中的字段创建索引可以提高连接查询的性能。索引甚至还能优化分组或者排序操作,因为索引自身是按照顺序进行组织存储的。

另一方面,系统维护索引需要付出一定的代价,从而增加数据修改操作的负担。所以,我们需要合理创建索引,一般只为经常使用到的字段创建索引。就像图书一样,不可能为书中的每个关键字都创建一个索引。

索引类型

详细介绍可参考: PostgreSQL 9种索引的原理和应用场景

PostgreSQL 提高了多种索引类型:B-树、哈希、GiST、SP-GiST、GIN 以及 BRIN 等索引。每种索引基于不同的存储结构和算法,用于优化不同类型的查询。默认情况下,PostgreSQL 创建 B-树索引,因为它适合大部分情况下的查询。

1.B-树索引

注意B树就是B-树,"-"是个连字符号,不是减号。

B-树是一种平衡 的多路**查找(又称排序)**树,在文件系统中有所应用。主要用作文件的索引。其中的B就表示平衡(Balance),与B+树区别可参考:https://xiaolincoding.com/mysql/index/why_index_chose_bpuls_tree.html#_3、范围查询

实现原理可参考:《深入浅出PostgreSQL B-Tree索引结构》

B-树是一个自平衡树(self-balancing tree),按照顺序存储数据,支持对数时间复杂度(O(logN))的搜索、插入、删除和顺序访问。举例来说,假如 100 条数据时需要 1 次磁盘 I/O,也就是说 N 等于 100;10000 条数据时只需要 2 次 I/O,1 亿数据时只需要 4 次 I/O。

对于索引列上的以下比较运算符,PostgreSQL 优化器都会考虑使用 B-树索引:

  • <
  • <=
  • =
  • >=
  • BETWEEN
  • IN
  • IS NULL
  • IS NOT NULL

另外,如果模式匹配运算符LIKE~中模式的开头不是通配符,优化器也可以使用 B-树索引,例如:

sql 复制代码
col  LIKE 'foo%' 
col  ~ '^foo'

对于不区分大小的的ILIKE~*运算符,如果匹配的模式以非字母的字符(不受大小写转换影响)开头,也可以使用 B-树索引。

B-树索引还可以用于优化排序操作,例如:

sql 复制代码
SELECT col1, col2
  FROM t
 WHERE col1 BETWEEN 100 AND 200
 ORDER BY col1;

col1 上的索引不仅能够优化查询条件,也可以避免额外的排序操作;因为基于该索引访问时本身就是按照排序返回结果。

2.哈希索引

哈希索引(Hash index)只能用于简单的等值查找(=),也就是说索引字段被用于等号条件判断。因为对数据进行哈希运算之后不再保留原来的大小关系。

hash索引特别适用于字段VALUE非常长(不适合b-tree索引,因为b-tree一个PAGE至少要存储3个ENTRY,所以不支持特别长的VALUE)的场景,例如很长的字符串,并且用户只需要等值搜索,建议使用hash index。

创建哈希索引需要使用HASH关键字:

sql 复制代码
CREATE INDEX index_name 
ON table_name USING HASH (column_name);

CREATE INDEX语句用于创建索引,USING子句指定索引的类型,具体参考下文。

3.GiST 索引

GiST 代表通用搜索树(Generalized Search Tree),GiST 索引单个索引类型,而是一种支持不同索引策略的框架。GiST 索引常见的用途包括几何数据的索引和全文搜索。GiST 索引也可以用于优化"最近邻"搜索,例如:

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

该语句用于查找距离某个目标地点最近的 10 个地方。

GiST是一个通用的索引接口,可以使用GiST实现b-tree, r-tree等索引结构。

不同的类型,支持的索引检索也各不一样。例如:

1、几何类型,支持位置搜索(包含、相交、在上下左右等),按距离排序。

2、范围类型,支持位置搜索(包含、相交、在左右等)。

3、IP类型,支持位置搜索(包含、相交、在左右等)。

4、空间类型(PostGIS),支持位置搜索(包含、相交、在上下左右等),按距离排序。

5、标量类型,支持按距离排序。

实现原理可参考:《PostgreSQL 百亿地理位置数据 近邻查询性能》

4.SP-GiST 索引

SP-GiST 代表空间分区 GiST,主要用于 GIS、多媒体、电话路由以及 IP 路由等数据的索引。

与 GiST 类似,SP-GiST 也支持"最近邻"搜索。是一个通用的索引接口,但是SP-GIST使用了空间分区的方法,使得SP-GiST可以更好的支持非平衡数据结构,例如quad-trees, k-d tree, radis tree.

实现原理可参考:《Space-partitioning trees in PostgreSQL》

5.GIN 索引

GIN 代表广义倒排索引(generalized inverted indexes),主要用于单个字段中包含多个值的数据,例如 hstore、array、jsonb 以及 range 数据类型。一个倒排索引为每个元素值都创建一个单独的索引项,可以有效地查询某个特定元素值是否存在。Google、百度这种搜索引擎利用的就是倒排索引。

实现原理可参考:《PostgreSQL GIN索引实现原理》

应用场景

1、当需要搜索多值类型内的VALUE时,适合多值类型,例如数组、全文检索、TOKEN。(根据不同的类型,支持相交、包含、大于、在左边、在右边等搜索)

2、当用户的数据比较稀疏时,如果要搜索某个VALUE的值,可以适应btree_gin支持普通btree支持的类型。(支持btree的操作符)

3、当用户需要按任意列进行搜索时,gin支持多列展开单独建立索引域,同时支持内部多域索引的bitmapAnd, bitmapOr合并,快速的返回按任意列搜索请求的数据。

6.BRIN 索引

BRIN 代表块区间索引(block range indexes),存储了连续物理范围区间内的数据摘要信息。BRIN 也相比于 B-树索引要小很多,维护也更容易。对于不进行水平分区就无法使用 B-树索引的超大型表,可以考虑 BRIN。

BRIN 通常用于具有线性排序顺序的字段,例如订单表的创建日期。

例如时序数据,在时间或序列字段创建BRIN索引,进行等值、范围查询时效果很棒。

应用场景

《BRIN (block range index) index》

《PostgreSQL 物联网黑科技 - 瘦身几百倍的索引(BRIN index)》

《PostgreSQL 聚集存储 与 BRIN索引 - 高并发行为、轨迹类大吞吐数据查询场景解说》

《PostgreSQL 并行写入堆表,如何保证时序线性存储 - BRIN索引优化》

7.rum 索引

原理可参考:https://github.com/postgrespro/rum

rum 是一个索引插件,由Postgrespro开源,适合全文检索,属于GIN的增强版本。

增强包括:

1、在RUM索引中,存储了lexem的位置信息,所以在计算ranking时,不需要回表查询(而GIN需要回表查询)。

2、RUM支持phrase搜索,而GIN无法支持。

3、在一个RUM索引中,允许用户在posting tree中存储除ctid(行号)以外的字段VALUE,例如时间戳。

这使得RUM不仅支持GIN支持的全文检索,还支持计算文本的相似度值,按相似度排序等。同时支持位置匹配,例如(速度与激情,可以采用"速度" <2> "激情" 进行匹配,而GIN索引则无法做到)

应用场景

《PostgreSQL 全文检索加速 快到没有朋友 - RUM索引接口(潘多拉魔盒)》

《从难缠的模糊查询聊开 - PostgreSQL独门绝招之一 GIN , GiST , SP-GiST , RUM 索引原理与技术背景》

《PostgreSQL结合余弦、线性相关算法 在文本、图片、数组相似 等领域的应用 - 3 rum, smlar应用场景分析》

8.bloom 索引

bloom索引接口是PostgreSQL基于bloom filter构造的一个索引接口,属于lossy索引,可以收敛结果集(排除绝对不满足条件的结果,剩余的结果里再挑选满足条件的结果),因此需要二次check,bloom支持任意列组合的等值查询。

bloom存储的是签名,签名越大,耗费的空间越多,但是排除更加精准。有利有弊。

复制代码
CREATE INDEX bloomidx ON tbloom USING bloom (i1,i2,i3)
       WITH (length=80, col1=2, col2=2, col3=4);

签名长度 80 bit, 最大允许4096 bits
col1 - col32,分别指定每列的bits,默认长度2,最大允许4095 bits.

应用场景

bloom索引适合多列任意组合查询。

《PostgreSQL 9.6 黑科技 bloom 算法索引,一个索引支撑任意列组合查询》

9.zombodb 索引

zombodb是PostgreSQL与ElasticSearch结合的一个索引接口,可以直接读写ES。

https://github.com/zombodb/zombodb

应用场景

与ES结合,实现SQL接口的搜索引擎,实现数据的透明搜索。

创建索引

PostgreSQL 使用CREATE INDEX语句创建新的索引:

sql 复制代码
CREATE INDEX index_name ON table_name 
[USING method]
(column_name [ASC | DESC] [NULLS FIRST | NULLS LAST]);

其中:

  • index_name 是索引的名称,table_name 是表的名称;
  • method 表示索引的类型,例如 btree、hash、gist、spgist、gin 或者 brin。默认为 btree
  • column_name 是字段名,ASC表示升序排序(默认值),DESC表示降序索引;
  • NULLS FIRSTNULLS LAST表示索引中空值的排列顺序,升序索引时默认为NULLS LAST,降序索引时默认为NULLS FIRST

如果我们经常使用 name 字段作为查询条件,可以为 test 表创建以下索引:

sql 复制代码
CREATE INDEX test_name_index ON test (name);

创建索引之后,优化器会自动选择是否使用索引,例如:

sql 复制代码
explain analyze
SELECT * FROM test WHERE name IS NULL;
QUERY PLAN                                                                                                           |
---------------------------------------------------------------------------------------------------------------------|
Index Scan using test_name_index on test  (cost=0.43..5.77 rows=1 width=15) (actual time=0.036..0.037 rows=0 loops=1)|
  Index Cond: (name IS NULL)                                                                                         |
Planning Time: 1.067 ms                                                                                              |
Execution Time: 0.048 ms                                                                                             |

基于索引字段的IS NULL运算符同样可以利用索引进行优化。

唯一索引

在创建索引时,可以使用UNIQUE关键字指定唯一索引:

sql 复制代码
CREATE UNIQUE INDEX index_name
ON table_name (column_name [ASC | DESC] [NULLS FIRST | NULLS LAST]);

唯一索引可以用于实现唯一约束,PostgreSQL 目前只支持 B-树类型的唯一索引。多个 NULL 值被看作是不同的值,因此唯一索引字段可以存在多个空值。

对于主键和唯一约束,PostgreSQL 会自动创建一个唯一索引,从而确保唯一性。

多列索引

PostgreSQL 支持基于多个字段的索引,也就是多列索引(复合索引)。默认情况下,一个多列索引最多可以使用 32 个字段。只有B-树、GIST、GIN 和 BRIN 索引支持多列索引。

sql 复制代码
CREATE [UNIQUE] INDEX index_name ON table_name
[USING method]
(column1 [ASC | DESC] [NULLS FIRST | NULLS LAST], ...);

对于多列索引,应该将最常作为查询条件使用的字段放在左边,较少使用的字段放在右边。例如,基于(c1, c2, c3)创建的索引可以优化以下查询:

sql 复制代码
WHERE c1 = v1 and c2 = v2 and c3 = v3;
WHERE c1 = v1 and c2 = v2;
WHERE c1 = v1;

但是以下查询无法使用该索引:

sql 复制代码
WHERE c2 = v2;
WHERE c3 = v3;
WHERE c2 = v2 and c3 = v3;

对于多列唯一索引,字段的组合值不能重复;但是如果某个字段是空值,其他字段可以出现重复值。

函数索引

函数索引,也叫表达式索引,是指基于某个函数或者表达式的值创建的索引。PostgreSQL 中创建函数索引的语法如下:

sql 复制代码
CREATE [UNIQUE] INDEX index_name 
ON table_name (expression);

expression 是基于字段的表达式或者函数。

以下查询在 name 字段上使用了 upper 函数:

sql 复制代码
explain analyze
SELECT * FROM test WHERE upper(name) ='VAL:10000';
QUERY PLAN                                                                                                                 |
---------------------------------------------------------------------------------------------------------------------------|
Gather  (cost=1000.00..122556.19 rows=50001 width=15) (actual time=18.629..7310.422 rows=1 loops=1)                        |
  Workers Planned: 2                                                                                                       |
  Workers Launched: 2                                                                                                      |
  ->  Parallel Seq Scan on test  (cost=0.00..116556.09 rows=20834 width=15) (actual time=4746.266..7171.452 rows=0 loops=3)|
        Filter: (upper(name) = 'VAL:10000'::text)                                                                          |
        Rows Removed by Filter: 3333333                                                                                    |
Planning Time: 0.100 ms                                                                                                    |
Execution Time: 7310.444 ms                                                                                                |

虽然 name 字段上存在索引 test_name_index,但是函数会导致优化器无法使用该索引。为了优化这种不分区大小写的查询语句,可以基于 name 字段创建一个函数索引:

sql 复制代码
drop index test_name_index;
create index test_name_index on test(upper(name));

再次查看该语句的执行计划:

sql 复制代码
explain analyze
SELECT * FROM test WHERE upper(name) ='VAL:10000';
QUERY PLAN                                                                                                                     |
-------------------------------------------------------------------------------------------------------------------------------|
Bitmap Heap Scan on test  (cost=1159.93..57095.47 rows=50000 width=15) (actual time=17.046..17.047 rows=1 loops=1)             |
  Recheck Cond: (upper(name) = 'VAL:10000'::text)                                                                              |
  Heap Blocks: exact=1                                                                                                         |
  ->  Bitmap Index Scan on test_name_index  (cost=0.00..1147.43 rows=50000 width=0) (actual time=17.032..17.032 rows=1 loops=1)|
        Index Cond: (upper(name) = 'VAL:10000'::text)                                                                          |
Planning Time: 1.985 ms                                                                                                        |
Execution Time: 17.080 ms                                                                                                      |

函数索引的维护成本比较高,因为插入和更新时都需要进行函数计算。

部分索引

部分索引(partial index)是只针对表中部分数据行创建的索引通过一个WHERE子句指定需要索引的行。例如,对于订单表 orders,绝大部的订单都处于完成状态;我们只需要针对未完成的订单进行查询跟踪,可以创建一个部分索引:

sql 复制代码
create table orders(order_id int primary key, order_ts timestamp, finished boolean);

create index orders_unfinished_index
on orders (order_id)
where finished is not true;

该索引只包含了未完成的订单 id,比直接基于 finished 字段创建的索引小很多。它可以用于优化未完成订单的查询:

sql 复制代码
explain analyze
select order_id
from orders
where finished is not true;
QUERY PLAN                                                                                                                      |
--------------------------------------------------------------------------------------------------------------------------------|
Bitmap Heap Scan on orders  (cost=4.38..24.33 rows=995 width=4) (actual time=0.010..0.010 rows=0 loops=1)                       |
  Recheck Cond: (finished IS NOT TRUE)                                                                                          |
  ->  Bitmap Index Scan on orders_unfinished_index  (cost=0.00..4.13 rows=995 width=0) (actual time=0.004..0.004 rows=0 loops=1)|
Planning Time: 0.130 ms                                                                                                         |
Execution Time: 0.049 ms                                                                                                        |

覆盖索引

PostgreSQL 中的索引都属于二级索引,意味着索引和数据是分开存储的。因此通过索引查找数据即需要访问索引,又需要访问表,而表的访问是随机 I/O。

为了解决这个性能问题,PostgreSQL 支持 Index-Only 扫描,只需要访问索引的数据就能获得需要的结果,而不需要再次访问表中的数据。例如:

sql 复制代码
CREATE TABLE t (a int, b int, c int);
CREATE UNIQUE INDEX idx_t_ab ON t USING btree (a, b) INCLUDE (c);

以上语句基于字段 a 和 b 创建了多列索引,同时利用INCLUDE在索引的叶子节点存储了字段 c 的值。以下查询可以利用 Index-Only 扫描:

sql 复制代码
explain analyze
select a, b, c 
from t 
where a = 100 and b = 200;
QUERY PLAN                                                                                                      |
----------------------------------------------------------------------------------------------------------------|
Index Only Scan using idx_t_ab on t  (cost=0.15..8.17 rows=1 width=12) (actual time=0.007..0.007 rows=0 loops=1)|
  Index Cond: ((a = 100) AND (b = 200))                                                                         |
  Heap Fetches: 0                                                                                               |
Planning Time: 0.078 ms                                                                                         |
Execution Time: 0.021 ms                                                                                        |

以上查询只返回索引字段(a、b)和覆盖的字段(c),可以仅通过扫描索引即可返回结果。

B-树索引支持 Index-Only 扫描,GiST 和 SP-GiST 索引支持某些运算符的 Index-Only 扫描,其他索引不支持这种方式。

查看索引

PostgreSQL 提供了一个关于索引的视图 pg_indexes,可以用于查看索引的信息:

sql 复制代码
select * from pg_indexes where tablename = 'test';
schemaname|tablename|indexname      |tablespace|indexdef                                                             |
----------|---------|---------------|----------|---------------------------------------------------------------------|
public    |test     |test_id_index  |          |CREATE INDEX test_id_index ON public.test USING btree (id)           |
public    |test     |test_name_index|          |CREATE INDEX test_name_index ON public.test USING btree (upper(name))|

该视图包含的字段依次为:模式名、表名、索引名、表空间以及索引的定义语句。

如果使用 psql 客户端连接,可以使用\d table_name命令查看表的结构,包括表中的索引信息。

维护索引

PostgreSQL 提供了一些修改和重建索引的方法:

sql 复制代码
ALTER INDEX index_name RENAME TO new_name;
ALTER INDEX index_name SET TABLESPACE tablespace_name;

REINDEX [ ( VERBOSE ) ] { INDEX | TABLE | SCHEMA | DATABASE | SYSTEM }  index_name;

两个ALTER INDEX语句分别用于重命名索引和移动索引到其他表空间;REINDEX用于重建索引数据,支持不同级别的索引重建。

另外,索引被创建之后,系统会在修改数据的同时自动更新索引。不过,我们需要定期执行ANALYZE命令更新数据库的统计信息,以便优化器能够合理使用索引。

删除索引

如果需要删除一个已有的索引,可以使用以下命令:

sql 复制代码
DROP INDEX index_name [ CASCADE | RESTRICT ];

CASCADE 表示级联删除其他依赖该索引的对象;RESTRICT 表示如果存在依赖于该索引的对象,将会拒绝删除操作。默认为 RESTRICT。

我们可以使用以下语句删除 test 上的索引:

sql 复制代码
DROP INDEX test_id_index, test_name_index;
相关推荐
Hello.Reader3 小时前
Redis 延迟监控深度指南
数据库·redis·缓存
ybq195133454313 小时前
Redis-主从复制-分布式系统
java·数据库·redis
好奇的菜鸟6 小时前
如何在IntelliJ IDEA中设置数据库连接全局共享
java·数据库·intellij-idea
tan180°6 小时前
MySQL表的操作(3)
linux·数据库·c++·vscode·后端·mysql
满昕欢喜7 小时前
SQL Server从入门到项目实践(超值版)读书笔记 20
数据库·sql·sqlserver
优创学社28 小时前
基于springboot的社区生鲜团购系统
java·spring boot·后端
why技术8 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
幽络源小助理8 小时前
SpringBoot基于Mysql的商业辅助决策系统设计与实现
java·vue.js·spring boot·后端·mysql·spring
Hello.Reader8 小时前
Redis 延迟排查与优化全攻略
数据库·redis·缓存
ai小鬼头9 小时前
AIStarter如何助力用户与创作者?Stable Diffusion一键管理教程!
后端·架构·github