PostgreSQL中的GIN索引有什么作用

57次阅读
没有评论

共计 7046 个字符,预计需要花费 18 分钟才能阅读完成。

本篇内容主要讲解“PostgreSQL 中的 GIN 索引有什么作用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让丸趣 TV 小编来带大家学习“PostgreSQL 中的 GIN 索引有什么作用”吧!

GIN 索引的主要用处是加快全文检索 full-text search 的速度.

全文检索
全文检索 full-text search 的目的是从文档集中找到匹配检索条件的文档(document). 在搜索引擎中, 如果有很多匹配的文档, 那么需要找到最匹配的那些, 但在数据库查询中, 找到满足条件的即可.

在 PG 中, 出于搜索的目的, 文档会被转换为特定的类型 tsvector, 包含词素 (lexemes) 和它们在文档中的位置. 词素 Lexemes 是那些转换适合查询的单词形式(即分词). 比如:

testdb=# select to_tsvector( There was a crooked man, and he walked a crooked mile 
 to_tsvector 
-----------------------------------------
  crook :4,10  man :5  mile :11  walk :8
(1 row)

从本例可以看到, 分词后, 出现了 crook/man/mile 和 walk, 其位置分别是 4,10/5/11/8. 同时, 也可以看到比如 there 等词被忽略了, 因为这些词是 stop words(从搜索引擎的角度来看, 这些词太过普通, 不需要记录), 当然这是可以配置的.

PG 全文检索中的查询通过 tsquery 来表示, 查询条件包含 1 个或多个使用 and(\)/or(|)/not(!)等操作符连接的词素. 同样的, 使用括号来阐明操作的优先级.

testdb=# select to_tsquery(man   (walking | running) 
 to_tsquery 
----------------------------
  man    (  walk  |  run  )
(1 row)

操作符 @@ 用于全文检索

testdb=# select to_tsvector(There was a crooked man, and he walked a crooked mile) @@ to_tsquery(man   (walking | running) 
 ?column? 
----------
 t
(1 row)
select to_tsvector(There was a crooked man, and he walked a crooked mile) @@ to_tsquery(man   (going | running) 
 ?column? 
----------
 f
(1 row)

GIN 简介
GIN 是 Generalized Inverted Index 通用倒排索引的简称, 如熟悉搜索引擎, 这个概念不难理解. 它所操作的数据类型的值由元素组成而不是原子的. 这样的数据类型成为复合数据类型. 索引的是数据值中的元素.
举个例子, 比如书末尾的索引,它为每个术语提供了一个包含该术语出现位置所对应的页面列表。访问方法 (AM) 需要确保索引元素的快速访问, 因此这些元素存储在类似 Btree 中, 引用包含复合值 (内含元素) 数据行的有序集合链接到每个元素上. 有序对于数据检索并不重要 (如 TIDs 的排序), 但对于索引的内部结构很重要.
元素不会从 GIN 索引中删除, 可能有人会认为包含元素的值可以消失 / 新增 / 变化, 但组成这些元素的元素集大多是稳定的. 这样的处理方式大大简化了多进程使用索引的算法.

如果 TIDs 不大, 那么可以跟元素存储在同一个 page 中 (称为 posting list), 但如果链表很大, 会采用 Btree 这种更有效的数据结构, 会存储在分开的数据页中(称为 posting tree).
因此,GIN 索引包含含有元素的 Btree,TIDs Btree 或者普通链表会链接到该 Btree 的叶子行上.

与前面讨论的 GiST 和 SP-GiST 索引一样,GIN 为应用程序开发人员提供了接口,以支持复合数据类型上的各种操作。

举个例子, 下面是表 ts, 为 ts 创建 GIN 索引:

testdb=# drop table if exists ts;
psql: NOTICE: table  ts  does not exist, skipping
DROP TABLE
testdb=# create table ts(doc text, doc_tsv tsvector);
CREATE TABLE
testdb=# truncate table ts;
 slitter. ), 
 (I am a sheet slitter.),
 (I slit sheets.),
 (I am the sleekest sheet slitter that ever slit sheets.),
 ( She slits the sheet she sits on. 
update ts set doc_tsv = to_tsvector(doc);
create index on ts using gin(doc_tsv);
TRUNCATE TABLE
testdb=# insert into ts(doc) values
testdb-# (Can a sheet slitter slit sheets?), 
testdb-# (How many sheets could a sheet slitter slit?),
testdb-# (I slit a sheet, a sheet I slit.),
testdb-# (Upon a slitted sheet I sit.), 
testdb-# (Whoever slit the sheets is a good sheet slitter.), 
testdb-# (I am a sheet slitter.),
testdb-# (I slit sheets.),
testdb-# (I am the sleekest sheet slitter that ever slit sheets.),
testdb-# ( She slits the sheet she sits on. 
INSERT 0 9
testdb=# 
testdb=# update ts set doc_tsv = to_tsvector(doc);
UPDATE 9
testdb=# 
testdb=# create index on ts using gin(doc_tsv);
CREATE INDEX

在这里, 使用黑底 (page 编号 + page 内偏移) 而不是箭头来表示对 TIDs 的引用.
与常规的 Btree 不同, 因为遍历只有一种方法,GIN 索引由单向链表连接, 而不是双向链表.

testdb=# select ctid, left(doc,20), doc_tsv from ts;
 ctid | left | doc_tsv 
--------+----------------------+---------------------------------------------------------
 (0,10) | Can a sheet slitter |  sheet :3,6  slit :5  slitter :4
 (0,11) | How many sheets coul |  could :4  mani :2  sheet :3,6  slit :8  slitter :7
 (0,12) | I slit a sheet, a sh |  sheet :4,6  slit :2,8
 (0,13) | Upon a slitted sheet |  sheet :4  sit :6  slit :3  upon :1
 (0,14) | Whoever slit the she |  good :7  sheet :4,8  slit :2  slitter :9  whoever :1
 (0,15) | I am a sheet slitter |  sheet :4  slitter :5
 (0,16) | I slit sheets. |  sheet :3  slit :2
 (0,17) | I am the sleekest sh |  ever :8  sheet :5,10  sleekest :4  slit :9  slitter :6
 (0,18) | She slits the sheet |  sheet :4  sit :6  slit :2
(9 rows)

在这个例子中,sheet/slit/slitter 使用 Btree 存储而其他元素则使用简单的链表.

如果我们希望知道元素的个数, 如何获取?

testdb=# select (unnest(doc_tsv)).lexeme, count(*) from ts
testdb-# group by 1 order by 2 desc;
 lexeme | count 
----------+-------
 sheet | 9
 slit | 8
 slitter | 5
 sit | 2
 upon | 1
 mani | 1
 whoever | 1
 sleekest | 1
 good | 1
 could | 1
 ever | 1
(11 rows)

下面举例说明如何通过 GIN 索引进行扫描:

testdb=# explain(costs off)
testdb-# select doc from ts where doc_tsv @@ to_tsquery( many   slitter 
 QUERY PLAN 
-----------------------------------------------------------
 Seq Scan on ts
 Filter: (doc_tsv @@ to_tsquery( many   slitter ::text))
(2 rows)
testdb=# set enable_seqscan=off;
testdb=# explain(costs off)
select doc from ts where doc_tsv @@ to_tsquery( many   slitter 
 QUERY PLAN 
---------------------------------------------------------------------
 Bitmap Heap Scan on ts
 Recheck Cond: (doc_tsv @@ to_tsquery( many   slitter ::text))
 -  Bitmap Index Scan on ts_doc_tsv_idx
 Index Cond: (doc_tsv @@ to_tsquery( many   slitter ::text))
(4 rows)

执行此查询首先需要提取单个词素(lexeme, 亦即检索键):mani/slitter.PG 中有专门的 API 函数来完成, 该函数考虑了由 op class 确定的数据类型和使用场景.

testdb=# select amop.amopopr::regoperator, amop.amopstrategy
testdb-# from pg_opclass opc, pg_opfamily opf, pg_am am, pg_amop amop
testdb-# where opc.opcname =  tsvector_ops 
testdb-# and opf.oid = opc.opcfamily
testdb-# and am.oid = opf.opfmethod
testdb-# and amop.amopfamily = opc.opcfamily
testdb-# and am.amname =  gin 
testdb-# and amop.amoplefttype = opc.opcintype;
 amopopr | amopstrategy 
-----------------------+--------------
 @@(tsvector,tsquery) | 1
 @@@(tsvector,tsquery) | 2
(2 rows)

回到本例中, 在词素 Btree 中, 下一步会同时检索键并进入 TIDs 链表中, 得到:
mani — (0,2)
slitter — (0,1), (0,2), (1,2), (1,3), (2,2)

对于每一个找到的 TID, 调用 consistency function
API, 由此函数确定找到的行是否匹配检索键. 因为查询为 AND, 因此只返回(0,2).

testdb=# select doc from ts where doc_tsv @@ to_tsquery( many   slitter 
 doc 
---------------------------------------------
 How many sheets could a sheet slitter slit?
(1 row)

Slow Update
对 GIN index 的列进行 DML(主要是 insert update)是相当慢的, 每一个文档通常包含许多需要索引的词素. 因此, 虽然只添加或更新一个文档, 但也需要更新大量索引树. 换句话说, 如果多个文档同时更新, 这些文档中的词素可能是一样的, 因此总的消耗可能比逐个更新文档要小.
PG 提供了 fastupdate 选项, 用打开此参数后, 更新将在一个单独的无序链表中处理, 当这个链表超过阈值 (参数:gin_pending_list_limit 或索引同名存储参数) 时才会对索引进行更新. 这种技术也有负面影响, 一是降低了查询效率(需额外扫描该链表), 二是某个更新恰好碰上索引更新, 那么该次更新会相对很久.

Limiting the query result
GIN AM 的其中一个特性时通常会返回 bitmap 而不是逐个返回 TID, 因此执行计划都是 bitmap scan.
这样的特性胡导致 LIMIT 子句不会太有效:

testdb=# explain verbose 
select doc from ts where doc_tsv @@ to_tsquery( many   slitter 
 QUERY PLAN 
------------------------------------------------------------------------------
 Bitmap Heap Scan on public.ts (cost=12.25..16.51 rows=1 width=32)
 Output: doc
 Recheck Cond: (ts.doc_tsv @@ to_tsquery( many   slitter ::text))
 -  Bitmap Index Scan on ts_doc_tsv_idx (cost=0.00..12.25 rows=1 width=0)
 Index Cond: (ts.doc_tsv @@ to_tsquery( many   slitter ::text))
(5 rows)
testdb=# explain verbose
select doc from ts where doc_tsv @@ to_tsquery(many   slitter) limit 1;
 QUERY PLAN 
------------------------------------------------------------------------------------
 Limit (cost=12.25..16.51 rows=1 width=32)
 Output: doc
 -  Bitmap Heap Scan on public.ts (cost=12.25..16.51 rows=1 width=32)
 Output: doc
 Recheck Cond: (ts.doc_tsv @@ to_tsquery( many   slitter ::text))
 -  Bitmap Index Scan on ts_doc_tsv_idx (cost=0.00..12.25 rows=1 width=0)
 Index Cond: (ts.doc_tsv @@ to_tsquery( many   slitter ::text))
(7 rows)

这是因为 Bitmap Heap Scan 的启动成本与 Bitmap Index Scan 不会差太多.

基于这样的情况,PG 提供了 gin_fuzzy_search_limit 参数控制返回的结果行数(默认为 0, 即全部返回).

testdb=# show gin_fuzzy_search_limit ;
 gin_fuzzy_search_limit 
------------------------
 0
(1 row)

到此,相信大家对“PostgreSQL 中的 GIN 索引有什么作用”有了更深的了解,不妨来实际操作一番吧!这里是丸趣 TV 网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-24发表,共计7046字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)