PostgreSQL GIN multi-key search 优化

本文涉及的产品
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
云原生数据库 PolarDB 分布式版,标准版 2核8GB
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介:

标签

PostgreSQL , gin , in , or , multi key , right link scan , skip scan


背景

PostgreSQL中,有一种GIN索引,被广泛应用于多值类型,例如数组,分词,同时也被应用于模糊查询等领域。

gin索引,将列(比如数组,全文检索类型)中的值拿出来,再存储到树形结构中(类似B-TREE,键值+heap行号s),对于低频值,会作为posting list直接存在树的gin的叶子节点中,而对于高频值,行号s会存储在另外树结构(posting tree)中,gin的叶子节点中存储的是指向posting tree的pointer。

pic

pic

pic

GIN本质上是elemet为key的树结构,而value则为"posting tree pointer"或者"posting list"。

Internally, a GIN index contains a B-tree index constructed over keys,   

where each key is an element of one or more indexed items (a member of an array, for example)   

and where each tuple in a leaf page contains either   

a pointer to a B-tree of heap pointers (a “posting tree”), /  

or a simple list of heap pointers (a “posting list”) when the list is small enough to fit into a single index tuple along with the key value.  

关于GIN的一些介绍,可参考

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

posting list\tree

gin 索引的叶子节点中存储的为posting list(heap page number, itempointers),或者posting tree(heap pointers构建的树)。

那么什么时候使用list什么时候使用tree呢?

因为posting list是在GIN的叶子节点里面直接存储的,所以指当heap pointers较少,小于TOAST_INDEX_TARGET时(参考自PostgreSQL数据库内核分析,基于8.4的版本编写),使用posting list.

否则使用posting tree。

gin结构

pic

pic

可以使用pageinspect观察gin索引的内容。

https://www.postgresql.org/docs/devel/static/pageinspect.html

多值搜索例子

多值查询,例如 where column @> aa and column @> bb and column @> cc and ....

多值查询是比较常见的需求,例如有一个表存储的是店家售卖的商品ID,每个店家一行记录,其中列A为数组类型,数组中包含了这家店铺售卖的商品ID。

找出有哪些店铺在售卖热水器(ID=1)、笔记本(ID=2)以及台式机(ID=3)。可以这样把你的需求告诉数据库 where column @> array[1,2,3] 或者 column @> 1 and column @> 2 and column @> 3

这种查询可以使用GIN索引,由于本质上GIN还是个树结构,所以扫描方法和B-Tree实际上是相差不大的,B-Tree类似的优化手段同样适用。

多值搜索优化

2014-01-29, 30 提交的几个patch针对多值搜索场景进行了优化

1. where column @> aa and column @> bb and column @> cc

gin 索引扫描方法是bitmap scan,也就是对gin中每一个命中KEY的posting list/tree中存储的CTID(heap行号)排序后,再开始从HEAP扫描结果。

当找到了一条满足 "某一条件(如column @> aa)" 的记录后,首先对该posting list/tree里面存储的CTIDs进行排序,这个时候就得到了一个有序的ctid列表LIST-A,由于INDEX没有版本信息,所以要从HEAP搜索对应的记录,判断可见性。

当可见时,这条记录的CTID-A会被用于扫描优化,也就是这个patch的优化点。

另一个条件(如column @> bb),也会对该posting list/tree里面存储的CTIDs进行排序,这个时候也会得到一个有序的ctid列表LIST-B。

优化点在于,当LIST-B的ctid < CTID-A时,这些LIST-B的ctid会直接跳过,不需要到HEAP进行CHECK可见性,从而减少IO操作。

为什么可以跳过呢?

因为gin使用了bitmap scan,所以一定是ctid的有序扫描。

那么从逻辑推理角度来看,比如A条件,第一个满足条件的行号为1000,B条件的posting list/tree为(1,2,3,1000,2000),那么1,2,3则可以直接跳过,因为它在A条件中肯定不存在。

https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=e20c70cb0fa74d5bffa080e21a99b44bf0768667

Allow skipping some items in a multi-key GIN search.  

In a multi-key search, ie. something like "col @> 'foo' AND col @> 'bar'",  
as soon as we find the next item that matches the first criteria, we don't  
need to check the second criteria for TIDs smaller the first match. That  
saves a lot of effort, especially if one of the terms is rare, while the  
second occurs very frequently.  

Based on ideas from Alexander Korotkov's fast scan patch.  

当某个条件比较罕见,而另一个条件很常见时,有立竿见影的效果。(也就是说一个有很多行记录满足条件,另一个则只有少量记录满足条件)

例子(posting list/tree最小的先扫描,所以直接跳过若干ctid的扫描)

pic

pic

pic

pic

2. 依旧是multi-key的优化,优化点和1类似,对于所有tid都更小的posting list segments,连decoding都不做。

https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=25b1dafab63f465a65c63b26834dc18857f0fa0c

Further optimize multi-key GIN searches.  

If we're skipping past a certain TID, avoid decoding posting list segments  
that only contain smaller TIDs.  

Extracted from Alexander Korotkov's fast scan patch, heavily modified.  
+       GinPostingList *seg = GinDataLeafPageGetPostingList(page);  
        Size        len = GinDataLeafPageGetPostingListSize(page);  
+       Pointer     endptr = ((Pointer) seg) + len;  
+       GinPostingList *next;  
+  
+       /* Skip to the segment containing advancePast+1 */  
+       if (ItemPointerIsValid(&advancePast))  
+       {  
+           next = GinNextPostingListSegment(seg);  
+           while ((Pointer) next < endptr &&  
+                  ginCompareItemPointers(&next->first, &advancePast) <= 0)  
+           {  
+               seg = next;  
+               next = GinNextPostingListSegment(seg);  
+           }  
+           len = endptr - (Pointer) seg;  
+       }  

-       result = ginPostingListDecodeAllSegments(ptr, len, nitems);  
+       if (len > 0)  
+           result = ginPostingListDecodeAllSegments(seg, len, nitems);  
+       else  
+       {  
+           result = NULL;  
+           *nitems = 0;  
+       }  

3. 跳跃扫描优化,指posting tree的扫描优化,当skip的element已经不在当前posting tree的当前page时,返回posting tree的root开始扫描。

https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=626a120656a75bf4fe64b1d0d83c23cb38d3771a

Further optimize GIN multi-key searches.  

When skipping over some items in a posting tree, re-find the new location  
by descending the tree from root, rather than walking the right links.  
This can save a lot of I/O.  

Heavily modified from Alexander Korotkov's fast scan patch.  
+   bool        stepright;  
+  
+   if (!BufferIsValid(entry->buffer))  
+   {  
+       entry->isFinished = true;  
+       return;  
+   }  
+  
+   /*  
+    * We have two strategies for finding the correct page: step right from  
+    * the current page, or descend the tree again from the root. If  
+    * advancePast equals the current item, the next matching item should be  
+    * on the next page, so we step right. Otherwise, descend from root.  
+    */  
+   if (ginCompareItemPointers(&entry->curItem, &advancePast) == 0)  
+   {  
+       stepright = true;  
+       LockBuffer(entry->buffer, GIN_SHARE);  
+   }  
+   else  
+   {  
+       GinBtreeStack *stack;  
+  
+       ReleaseBuffer(entry->buffer);  
+  
+       /*  
+        * Set the search key, and find the correct leaf page.  
+        */  
+       if (ItemPointerIsLossyPage(&advancePast))  
+       {  
+           ItemPointerSet(&entry->btree.itemptr,  
+                          GinItemPointerGetBlockNumber(&advancePast) + 1,  
+                          FirstOffsetNumber);  
+       }  
+       else  
+       {  
+           entry->btree.itemptr = advancePast;  
+           entry->btree.itemptr.ip_posid++;  
+       }  
+       entry->btree.fullScan = false;  
+       stack = ginFindLeafPage(&entry->btree, true);  
+  
+       /* we don't need the stack, just the buffer. */  
+       entry->buffer = stack->buffer;  
+       IncrBufferRefCount(entry->buffer);  
+       freeGinBtreeStack(stack);  
+       stepright = false;  
+   }  
+  
+   elog(DEBUG2, "entryLoadMoreItems, %u/%u, skip: %d",  
+        GinItemPointerGetBlockNumber(&advancePast),  
+        GinItemPointerGetOffsetNumber(&advancePast),  
+        !stepright);  

跳跃扫描优化类似于递归查询的优化

参考

《时序数据合并场景加速分析和实现 - 复合索引,窗口分组查询加速,变态递归加速》

《distinct xx和count(distinct xx)的变态递归优化方法 - 索引收敛(skip scan)扫描》

《用PostgreSQL找回618秒逝去的青春 - 递归收敛优化》

posting list 压缩优化

posting list的压缩优化也是9.4对GIN的优化之一。

fastupdate, pending list 优化

由于多值类型的变更,插入,可能影响GIN索引的若干个KEY,所以IO巨大,为了减少这种IO,提高数据的写入\变更速度,提出了pending list的结构,类似缓冲区,这部分数据非树结构,可以有效合并IO,使用速度提升非常明显。

但是要注意pending list的存在,使得查询效率有一定的下降,特别是pending list中有大量数据时,使用vacuum可以手动将pending list合并到gin tree中。

或者等pending list写满后触发合并的动作,或者等待autovcauum来合并。

https://www.postgresql.org/docs/9.6/static/gin-tips.html

其他

btree_gin

https://www.postgresql.org/docs/9.6/static/btree-gin.html

btree_gin为普通类型的GIN索引接口。

int2, int4, int8, float4, float8, timestamp with time zone, timestamp without time zone, time with time zone, time without time zone, date, interval, oid, money, "char", varchar, text, bytea, bit, varbit, macaddr, inet, and cidr  

它主要是GIN的开发例子,或者复合索引(如int, tsvector的复合查询,可以建立GIN的单一索引)

Also, for queries that test both a GIN-indexable column and a B-tree-indexable column, it might be more efficient to create a multicolumn GIN index that uses one of these operator classes than to create two separate indexes that would have to be combined via bitmap ANDing.  

由于这些标量类型默认只有B-Tree和hash索引扫描方法,当查询需求包含数组列,同时还包含这些标量数据列的查询时。

1. 如果有两个索引,那么会对两个索引的CTID进行合并

bitmap anding

例子

create table t1(id int , c1 int[]);

create index idx1 on t1 using btree (id);
create index idx2 on t1 using gin (c1);

select ? from t1 where id=? and c1 @> ....;

2. 而如果是一个GIN复合索引(标量+多值类型),则不需要bitmap anding操作。

例子 , 使用gin复合索引

create extension btree_gin;  

create index idx3 on t1 using gin (id, c1);  

select ? from t1 where id=? and c1 @> ....;

参考

GIN in 9.4 and further

https://www.postgresql.org/docs/current/static/indexes-bitmap-scans.html

https://www.postgresql.org/docs/current/static/indexes-multicolumn.html

A multicolumn B-tree index can be used with query conditions that involve any subset of the index's columns, but the index is most efficient when there are constraints on the leading (leftmost) columns. The exact rule is that equality constraints on leading columns, plus any inequality constraints on the first column that does not have an equality constraint, will be used to limit the portion of the index that is scanned. Constraints on columns to the right of these columns are checked in the index, so they save visits to the table proper, but they do not reduce the portion of the index that has to be scanned. For example, given an index on (a, b, c) and a query condition WHERE a = 5 AND b >= 42 AND c < 77, the index would have to be scanned from the first entry with a = 5 and b = 42 up through the last entry with a = 5. Index entries with c >= 77 would be skipped, but they'd still have to be scanned through. This index could in principle be used for queries that have constraints on b and/or c with no constraint on a — but the entire index would have to be scanned, so in most cases the planner would prefer a sequential table scan over using the index.

A multicolumn GiST index can be used with query conditions that involve any subset of the index's columns. Conditions on additional columns restrict the entries returned by the index, but the condition on the first column is the most important one for determining how much of the index needs to be scanned. A GiST index will be relatively ineffective if its first column has only a few distinct values, even if there are many distinct values in additional columns.

A multicolumn GIN index can be used with query conditions that involve any subset of the index's columns. Unlike B-tree or GiST, index search effectiveness is the same regardless of which index column(s) the query conditions use.

https://www.postgresql.org/docs/devel/static/gin-implementation.html

Multicolumn GIN indexes are implemented by building a single B-tree over composite values (column number, key value). The key values for different columns can be of different types.

src/backend/access/gin/README

* In a single-column index, a key tuple just contains the key datum, but
in a multi-column index, a key tuple contains the pair (column number,
key datum) where the column number is stored as an int2.  This is needed
to support different key data types in different columns.  This much of
the tuple is built by index_form_tuple according to the usual rules.
The column number (if present) can never be null, but the key datum can
be, in which case a null bitmap is present as usual.  (As usual for index
tuples, the size of the null bitmap is fixed at INDEX_MAX_KEYS.)

backend/access/gin/ginentrypage.c: itup = index_form_tuple(ginstate->tupdesc[attnum - 1], datums, isnull);

backend/access/nbtree/nbtree.c: itup = index_form_tuple(RelationGetDescr(rel), values, isnull);

backend/access/common/indextuple.c:index_form_tuple(TupleDesc tupleDescriptor,

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
6月前
|
SQL 关系型数据库 测试技术
沉浸式学习PostgreSQL|PolarDB 20: 学习成为数据库大师级别的优化技能
在上一个实验《沉浸式学习PostgreSQL|PolarDB 19: 体验最流行的开源企业ERP软件 odoo》 中, 学习了如何部署odoo和polardb|pg. 由于ODOO是非常复杂的ERP软件, 对于关系数据库的挑战也非常大, 所以通过odoo业务可以更快速提升同学的数据库优化能力, 发现业务对数据库的使用问题(如索引、事务对锁的运用逻辑问题), 数据库的代码缺陷, 参数或环境配置问题, 系统瓶颈等.
764 1
|
22天前
|
存储 JSON 关系型数据库
PostgreSQL Json应用场景介绍和Shared Detoast优化
PostgreSQL Json应用场景介绍和Shared Detoast优化
|
3月前
|
弹性计算 关系型数据库 数据库
开源PostgreSQL在倚天ECS上的最佳优化实践
本文基于倚天ECS硬件平台,以自顶向下的方式从上层应用、到基础软件,再到底层芯片硬件,通过应用与芯片的硬件特性的亲和性分析,实现PostgreSQL与倚天芯片软硬协同的深度优化,充分使能倚天硬件性能,帮助开源PostgreSQL应用实现性能提升。
|
7月前
|
SQL 弹性计算 测试技术
如何在PolarDB-X中优化慢SQL
《PolarDB-X动手实践》系列第六期,本场景带您体验如何使用PolarDB-X提供的解决慢SQL的相关工具。
733 0
|
8月前
|
存储 Java 测试技术
深度优化 | PolarDB-X 基于向量化SIMD指令的探索
本文将介绍PolarDB-X对于向量化SIMD指令的探索和实践,包括基本用法及实现原理,以及在具体算子实现中的思考和沉淀。
|
8月前
|
关系型数据库 测试技术 分布式数据库
PolarDB | PostgreSQL 高并发队列处理业务的数据库性能优化实践
在电商业务中可能涉及这样的场景, 由于有上下游关系的存在, 1、用户下单后, 上下游厂商会在自己系统中生成一笔订单记录并反馈给对方, 2、在收到反馈订单后, 本地会先缓存反馈的订单记录队列, 3、然后后台再从缓存取出订单并进行处理. 如果是高并发的处理, 因为大家都按一个顺序获取, 容易产生热点, 可能遇到取出队列遇到锁冲突瓶颈、IO扫描浪费、CPU计算浪费的瓶颈. 以及在清除已处理订单后, 索引版本未及时清理导致的回表版本判断带来的IO浪费和CPU运算浪费瓶颈等. 本文将给出“队列处理业务的数据库性能优化”优化方法和demo演示. 性能提升10到20倍.
596 4
|
9月前
|
存储 缓存 NoSQL
[译]解锁TOAST的秘密:如何优化PostgreSQL的大型列存储以最佳性能和可扩展性
[译]解锁TOAST的秘密:如何优化PostgreSQL的大型列存储以最佳性能和可扩展性
130 0
|
10月前
|
SQL 关系型数据库 MySQL
深度解析PolarDB DDL锁的优化和演进
DDL是数据库所有SQL操作中最繁重的一种,本文总结介绍了云原生数据库PolarDB中DDL全链路MDL锁治理的经验和进展,持续优化用户的使用体验,为用户打造最佳的云原生数据库。
|
11月前
|
SQL 关系型数据库 API
【PostgreSQL】PostgreSQL扩展:pg_stat_statements 优化SQL
【PostgreSQL】PostgreSQL扩展:pg_stat_statements 优化SQL
|
11月前
|
SQL 存储 缓存
开源分布式数据库PolarDB-X源码解读——PolarDB-X源码解读(十二):谈谈in常量查询的设计与优化
开源分布式数据库PolarDB-X源码解读——PolarDB-X源码解读(十二):谈谈in常量查询的设计与优化
185 0

相关产品

  • 云原生数据库 PolarDB