zoukankan      html  css  js  c++  java
  • PostgreSQL中的索引(七)--GIN

    我们已经熟悉了PostgreSQL索引引擎和访问方法的接口,并讨论了hash索引、b-trees以及GiST和SP-GiST索引。这篇文章将介绍GIN索引。

    GIN

    GIN是广义倒排索引(Generalized Inverted Index)的缩写。这就是所谓的倒排索引。它操作的数据类型的值不是原子的,而是由元素组成的。我们将这些类型称为复合类型。索引的不是复合类型的值,而是单独的元素;每个元素都引用它出现的值。

    与此方法有一个很好的类比,即图书末尾的索引,对于每个术语,它提供了出现该术语的页面列表。访问方法必须确保快速搜索索引元素,就像书中的索引一样。因此,这些元素被存储为熟悉的b树(它使用了另一种更简单的实现,但在本例中并不重要)。 对包含元素复合值的表行的有序引用集链接到每个元素。顺序性对于数据检索并不重要(TIDs的排序顺序没有太大意义),但对于索引的内部结构很重要。

    元素永远不会从GIN索引中删除。我们认为,包含元素的值可以消失、出现或变化,但组成它们的元素集或多或少是稳定的。此解决方案极大地简化了使用索引并行处理多个进程的算法。

    如果TIDs列表非常小,它可以与元素放在同一个页面中(称为«the posting list»)。但如果这个列表很大,就需要一个更高效的数据结构,我们已经意识到了这一点——它还是B-tree。这样的树位于单独的数据页上(称为«the posting tree»)。

    因此,GIN索引由元素的B-tree组成,而TIDs的B-tree或平面列表链接到该B-tree的叶行。

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

    全文检索

    GIN的主要应用领域是加速全文检索,因此,在更详细地讨论该索引时,可以将其用作示例。

    与GiST相关的文章已经提供了关于全文搜索的简单介绍,所以让我们直接切入主题,不要重复。显然,本例中的复合值是文档,而这些文档的元素是*lexemes。

    让我们来看看与GiST相关的文章中的例子:

    postgres=# create table ts(doc text, doc_tsv tsvector);
    
    postgres=# insert into ts(doc) values
      ('Can a sheet slitter slit sheets?'), 
      ('How many sheets could a sheet slitter slit?'),
      ('I slit a sheet, a sheet I slit.'),
      ('Upon a slitted sheet I sit.'), 
      ('Whoever slit the sheets is a good sheet 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.');
    
    postgres=# update ts set doc_tsv = to_tsvector(doc);
    
    postgres=# create index on ts using gin(doc_tsv);
    

    该索引的可能结构如图所示:

    与前面所有的图不同,对表行(tid)的引用是用黑色背景上的数值(页码和页面上的位置)表示的,而不是用箭头表示的。

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

    在这个推测的示例中,所有词素的tid列表可以位于常规页面,但«sheet»、«slit»和«slits»除外。这些词素出现在许多文档中,它们的tid列表已经被放置在单个的b-tree中。

    顺便问一下,我们如何知道一个词素被包含在多少文档?对于一个小的表,下面所示的«direct»技术可以实现,但是我们将进一步学习如何处理较大的表。

    postgres=# select (unnest(doc_tsv)).lexeme, count(*) from ts
    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)
    

    还要注意,与常规的b-树不同,GIN索引的页是通过单向列表连接的,而不是双向列表。这就足够了,因为树遍历只有一种方式。

    查询示例

    执行以下查询会如何执行呢?

    postgres=# 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)
    

    单个词素(搜索键)首先从查询中提取:«mani»和«slitter»。这是由一个专门的API函数来完成的,它考虑到由操作符类决定的数据类型和策略:

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

    在词素的b-树中,我们接下来找到这两个键,并遍历tid的列表。我们得到:

    对于«mani»-(0,2)。

    对于«slitter»-(0,1),(0,2),(1,2),(1,3),(2,2)。

    最后,对于找到的每个TID,将调用一个API一致性函数,该函数必须确定找到的哪一行与搜索查询匹配。由于我们查询中的lexemes是由布尔«and»连接的,所以返回的唯一一行是(0,2):

           |      |         |  consistency
           |      |         |    function
      TID  | mani | slitter | slit & slitter
    -------+------+---------+----------------
     (0,1) |    f |       T |              f 
     (0,2) |    T |       T |              T
     (1,2) |    f |       T |              f
     (1,3) |    f |       T |              f
     (2,2) |    f |       T |              f
    

    结果是:

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

    如果我们将这种方法与已经讨论过的GiST方法进行比较,那么GIN用于全文搜索的优势就很明显了。但这里还有比表面上看到的更多的东西。

    slow update的问题

    问题是GIN索引中的数据插入或更新非常慢。每个文档通常包含许多要建立索引的词素。因此,当只添加或更新一个文档时,我们必须大规模地更新索引树。

    另一方面,如果同时更新几个文档,它们的一些词素可能是相同的,那么总的工作量将比逐个更新文档时要小。

    GIN索引有«fastupdate»存储参数,我们可以在创建和更新索引时指定:

    postgres=# create index on ts using gin(doc_tsv) with (fastupdate = true);
    

    打开此参数后,更新将在一个单独的无序列表中累积(在各个连接的页上)。 当这个列表足够大或在vacuuming期间,所有累积的更新都会立即对索引进行。要考虑的列表«large enough»是由«gin_pending_list_limit»配置参数决定的,或者由索引的同名存储参数决定的。

    但是这种方法也有缺点:首先,搜索速度变慢(因为除了树之外还需要查看无序列表),其次,如果无序列表已经溢出,下一次更新可能会意外地花费很多时间。

    部分匹配的检索

    我们可以在全文搜索中使用部分匹配。例如,考虑以下查询:

    gin=# select doc from ts where doc_tsv @@ to_tsquery('slit:*');
                              doc                           
    --------------------------------------------------------
     Can a sheet slitter slit sheets?
     How many sheets could a sheet slitter slit?
     I slit a sheet, a sheet I slit.
     Upon a slitted sheet I sit.
     Whoever slit the sheets is a good sheet 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.
    (9 rows)
    

    这个查询将会找到包含以«slit»开头的词素的文档。在这个例子中,这样的词素是«slit»和«slitter»。

    即使没有索引,查询也可以正常工作,但GIN还允许加速以下搜索:

    postgres=# explain (costs off)
    select doc from ts where doc_tsv @@ to_tsquery('slit:*');
                             QUERY PLAN                          
    -------------------------------------------------------------
     Bitmap Heap Scan on ts
       Recheck Cond: (doc_tsv @@ to_tsquery('slit:*'::text))
       ->  Bitmap Index Scan on ts_doc_tsv_idx
             Index Cond: (doc_tsv @@ to_tsquery('slit:*'::text))
    (4 rows)
    

    这里,所有在搜索查询中指定前缀的词素都在树中查找,并由布尔«or»连接。

    频繁和不频繁的词素(lexemes)

    为了观察索引是如何在实时数据上工作的,让我们以«pgsql-hacker»电子邮件的归档为例,我们在讨论GiST时已经使用过了。这个版本的存档包含356125条消息,其中包含发送日期、主题、作者和文本。

    fts=# alter table mail_messages add column tsv tsvector;
    
    fts=# update mail_messages set tsv = to_tsvector(body_plain);
    NOTICE:  word is too long to be indexed
    DETAIL:  Words longer than 2047 characters are ignored.
    ...
    UPDATE 356125
    fts=# create index on mail_messages using gin(tsv);
    

    让我们假设一个出现在许多文档中的词素。使用«unnest»的查询将无法在如此大的数据量上工作,正确的技术是使用«ts_stat»函数,它提供关于词素表的信息,它们所出现的文档数量,以及总出现次数。

    fts=# select word, ndoc
    from ts_stat('select tsv from mail_messages')
    order by ndoc desc limit 3;
     word  |  ndoc  
    -------+--------
     re    | 322141
     wrote | 231174
     use   | 176917
    (3 rows)
    

    让我们选择«wrote»。

    我们将采用一些在开发者邮件中不常见的词,比如«tattoo»:

    fts=# select word, ndoc from ts_stat('select tsv from mail_messages') where word = 'tattoo';
      word  | ndoc 
    --------+------
     tattoo |    2
    (1 row)
    

    有没有同时出现这两个词的文档?似乎有:

    fts=# select count(*) from mail_messages where tsv @@ to_tsquery('wrote & tattoo');
     count 
    -------
         1
    (1 row)
    

    出现了如何执行此查询的问题。如果我们得到两个词素的tid列表(如上所述),那么搜索显然效率低下:我们将不得不遍历超过20万个值,最终只留下一个值。幸运的是,使用planner统计数据,算法知道«wrote»经常出现,而«tatoo»很少出现。因此,执行对不常见的词的搜索,然后检查检索到的两个文档是否有«wrote»词。这一点在查询中很清楚,它执行得很快:

    fts=# 	iming on
    
    fts=# select count(*) from mail_messages where tsv @@ to_tsquery('wrote & tattoo');
     count 
    -------
         1
    (1 row)
    Time: 0,959 ms
    

    仅搜索«wrote»就需要相当长的时间:

    fts=# select count(*) from mail_messages where tsv @@ to_tsquery('wrote');
     count  
    --------
     231174
    (1 row)
    Time: 2875,543 ms (00:02,876)
    

    这种优化当然不仅适用于两个词,而且适用于更复杂的情况。

    限制查询结果

    GIN访问方法的一个特性是结果总是作为位图返回:该方法不能按TID返回结果。正因为如此,本文中的所有查询计划都使用bitmap scan。

    因此,使用LIMIT子句对索引扫描结果进行限制并不是很有效。注意操作的预测成本(«Limit»节点的«cost»字段):

    fts=# explain (costs off)
    select * from mail_messages where tsv @@ to_tsquery('wrote') limit 1;
                                           QUERY PLAN
    -----------------------------------------------------------------------------------------
     Limit  (cost=1283.61..1285.13 rows=1)
       ->  Bitmap Heap Scan on mail_messages  (cost=1283.61..209975.49 rows=137207)
             Recheck Cond: (tsv @@ to_tsquery('wrote'::text))
             ->  Bitmap Index Scan on mail_messages_tsv_idx  (cost=0.00..1249.30 rows=137207)
                   Index Cond: (tsv @@ to_tsquery('wrote'::text))
    (5 rows)
    

    估计成本为1285.13,比构建整个位图1249.30(位图索引扫描节点的«cost»字段)的成本略高。

    因此,索引具有限制结果数量的特殊功能。阈值在«gin_fuzzy_search_limit»配置参数中指定,默认为零(不存在限制)。但是我们可以设置阈值:

    fts=# set gin_fuzzy_search_limit = 1000;
    
    fts=# select count(*) from mail_messages where tsv @@ to_tsquery('wrote');
     count 
    -------
      5746
    (1 row)
    fts=# set gin_fuzzy_search_limit = 10000;
    
    fts=# select count(*) from mail_messages where tsv @@ to_tsquery('wrote');
     count 
    -------
     14726
    (1 row)
    

    我们可以看到,查询返回的行数因参数值的不同而不同(如果使用索引访问)。限制并不严格:可以返回比指定的多的行,这就证明了参数名中有«fuzzy»部分是正确的。

    紧凑表示(Compact representation)

    在其他方面,gin索引还是很好的,因为它们很紧凑。首先,如果一个相同的lexeme出现在多个文档中(这是通常的情况),那么它只存储在索引中一次。 其次,TID以有序的方式存储在索引中,这使我们能够使用简单的压缩:列表中存储的下一个TID实际上是与前一个TID是不同点;这通常是一个很小的数字,需要比完整的6字节TID少得多的位。

    为了了解其大小,让我们从消息的文本构建B-tree。但肯定不是公平的比较:

    ·GIN构建在不同的数据类型上(«tsvector»而不是«text»),«tsvector»更小 ·同时,b-树的消息大小必须缩短到大约2kb。

    我们继续:

    fts=# create index mail_messages_btree on mail_messages(substring(body_plain for 2048));
    

    我们还将建立GiST索引:

    fts=# create index mail_messages_gist on mail_messages using gist(tsv);
    

    在«vacuum full»后索引的大小:

    fts=# select pg_size_pretty(pg_relation_size('mail_messages_tsv_idx')) as gin,
                 pg_size_pretty(pg_relation_size('mail_messages_gist')) as gist,
                 pg_size_pretty(pg_relation_size('mail_messages_btree')) as btree;
      gin   |  gist  | btree  
    --------+--------+--------
     179 MB | 125 MB | 546 MB
    (1 row)
    

    由于紧凑性,我们可以尝试在从Oracle迁移的过程中使用GIN索引来替代位图索引(为了便于理解,我提供了Lewis的文章的参考)。作为规则,位图索引用于仅有少数唯一值的字段,这对于GIN也是非常好的。并且,如第一篇文章所示,PostgreSQL可以动态地基于任何索引(包括GIN)构建位图。

    GiST还是GIN

    对于许多数据类型,GiST和GIN都可以使用操作符类,这就产生了使用哪个索引的问题。也许,我们已经可以得出一些结论了。

    通常,GIN在准确性和搜索速度上优于GiST。如果数据更新不频繁,并且需要快速搜索,那么很可能使用GIN。

    另一方面,如果数据是密集更新的,那么更新GIN的开销可能会显得太大。在这种情况下,我们将不得不比较这两种选择。

    Arrays

    使用GIN的另一个例子是数组的索引。在这种情况下,数组元素进入索引,这允许加速对数组的一些操作:

    postgres=# select amop.amopopr::regoperator, amop.amopstrategy
    from pg_opclass opc, pg_opfamily opf, pg_am am, pg_amop amop
    where opc.opcname = 'array_ops'
    and opf.oid = opc.opcfamily
    and am.oid = opf.opfmethod
    and amop.amopfamily = opc.opcfamily
    and am.amname = 'gin'
    and amop.amoplefttype = opc.opcintype;
            amopopr        | amopstrategy 
    -----------------------+--------------
     &&(anyarray,anyarray) |            1  intersection
     @>(anyarray,anyarray) |            2  contains array
     <@(anyarray,anyarray) |            3  contained in array
     =(anyarray,anyarray)  |            4  equality
    (4 rows)
    

    我们的演示数据库有带有航班信息的«routes»视图。在其他视图中,该视图包含«days_of_week»列——发生航班时的工作日数组。例如,从伏努科沃到格伦齐克的航班在周二、周四和周日起飞:

    demo=# select departure_airport_name, arrival_airport_name, days_of_week
    from routes
    where flight_no = 'PG0049';
     departure_airport_name | arrival_airport_name | days_of_week 
    ------------------------+----------------------+--------------
     Vnukovo                | Gelendzhik            | {2,4,7}
    (1 row)
    

    为了构建索引,让我们将视图“物化”到一个表中:

    demo=# create table routes_t as select * from routes;
    
    demo=# create index on routes_t using gin(days_of_week);
    

    现在我们可以用这个索引来了解周二、周四和周日的所有航班:

    demo=# explain (costs off) select * from routes_t where days_of_week = ARRAY[2,4,7];
                            QUERY PLAN                         
    -----------------------------------------------------------
     Bitmap Heap Scan on routes_t
       Recheck Cond: (days_of_week = '{2,4,7}'::integer[])
       ->  Bitmap Index Scan on routes_t_days_of_week_idx
             Index Cond: (days_of_week = '{2,4,7}'::integer[])
    (4 rows)
    

    似乎有六种查询额结果:

    demo=# select flight_no, departure_airport_name, arrival_airport_name, days_of_week from routes_t where days_of_week = ARRAY[2,4,7];
     flight_no | departure_airport_name | arrival_airport_name | days_of_week 
    -----------+------------------------+----------------------+--------------
     PG0005    | Domodedovo             | Pskov                | {2,4,7}
     PG0049    | Vnukovo                | Gelendzhik           | {2,4,7}
     PG0113    | Naryan-Mar             | Domodedovo           | {2,4,7}
     PG0249    | Domodedovo             | Gelendzhik           | {2,4,7}
     PG0449    | Stavropol             | Vnukovo              | {2,4,7}
     PG0540    | Barnaul                | Vnukovo              | {2,4,7}
    (6 rows)
    

    该查询是如何执行的? 和上面描述的完全一样:

    1.从数组{2,4,7}中提取元素(搜索关键字)。显然,这些是«2»、«4»和«7»的值。

    2.在元素树中找到提取的键,并为每个键选择TIDs列表。

    3.在找到的所有TIDs中,一致性函数从查询中选择与操作符匹配的TIDs。 For =操作符,只有那些tid与所有三个列表中出现的匹配(换句话说,初始数组必须包含所有元素)。但这是不够的:它还需要数组不包含任何其他值,我们不能用索引检查这个条件。 因此,在这种情况下,access method要求索引引擎重新检查与表一起返回的所有tid。

    有趣的是,有些策略(例如,«contains in array»)不能检查任何内容,而必须重新检查表中找到的所有tid。

    但是,如果我们需要知道周二、周四和周日从莫斯科起飞的航班,该怎么办呢? 索引将不支持附加条件,它将进入«Filter»列。

    demo=# explain (costs off)
    select * from routes_t where days_of_week = ARRAY[2,4,7] and departure_city = 'Moscow';
                            QUERY PLAN                         
    -----------------------------------------------------------
     Bitmap Heap Scan on routes_t
       Recheck Cond: (days_of_week = '{2,4,7}'::integer[])
       Filter: (departure_city = 'Moscow'::text)
       ->  Bitmap Index Scan on routes_t_days_of_week_idx
             Index Cond: (days_of_week = '{2,4,7}'::integer[])
    (5 rows)
    

    在这里,这是可以的(无论如何索引只选择6行),但如果附加条件增加了选择能力,则需要这样的支持。但是,我们不能仅仅创建索引:

    demo=# create index on routes_t using gin(days_of_week,departure_city);
    ERROR:  data type text has no default operator class for access method "gin"
    HINT:  You must specify an operator class for the index or define a default operator class for the data type.
    

    但是“btree_gin”扩展将提供帮助,它添加了模拟普通b树工作的GIN操作符类。

    demo=# create extension btree_gin;
    
    demo=# create index on routes_t using gin(days_of_week,departure_city);
    
    demo=# explain (costs off)
    select * from routes_t where days_of_week = ARRAY[2,4,7] and departure_city = 'Moscow';
                                 QUERY PLAN
    ---------------------------------------------------------------------
     Bitmap Heap Scan on routes_t
       Recheck Cond: ((days_of_week = '{2,4,7}'::integer[]) AND
                      (departure_city = 'Moscow'::text))
       ->  Bitmap Index Scan on routes_t_days_of_week_departure_city_idx
             Index Cond: ((days_of_week = '{2,4,7}'::integer[]) AND
                          (departure_city = 'Moscow'::text))
    (4 rows)
    

    JSONB

    具有内置GIN支持的复合数据类型的另一个示例是JSON。为了处理JSON值,目前定义了一些操作符和函数,其中一些可以使用索引来加速:

    postgres=# select opc.opcname, amop.amopopr::regoperator, amop.amopstrategy as str
    from pg_opclass opc, pg_opfamily opf, pg_am am, pg_amop amop
    where opc.opcname in ('jsonb_ops','jsonb_path_ops')
    and opf.oid = opc.opcfamily
    and am.oid = opf.opfmethod
    and amop.amopfamily = opc.opcfamily
    and am.amname = 'gin'
    and amop.amoplefttype = opc.opcintype;
        opcname     |     amopopr      | str
    ----------------+------------------+-----
     jsonb_ops      | ?(jsonb,text)    |   9  top-level key exists
     jsonb_ops      | ?|(jsonb,text[]) |  10  some top-level key exists
     jsonb_ops      | ?&(jsonb,text[]) |  11  all top-level keys exist
     jsonb_ops      | @>(jsonb,jsonb)  |   7  JSON value is at top level
     jsonb_path_ops | @>(jsonb,jsonb)  |   7
    (5 rows)
    

    正如我们所看到的,有两个操作符类可用:«jsonb_ops»和«jsonb_path_ops»。

    第一个操作符类«jsonb_ops»默认使用的。所有键、值和数组元素都作为初始JSON文档的元素到达索引。每个元素都添加了一个属性,它指示该元素是否为键(这是«exists»策略所需要的,它区分键和值)。

    例如,让我们用JSON表示«routes»中的几行:

    demo=# create table routes_jsonb as
      select to_jsonb(t) route 
      from (
          select departure_airport_name, arrival_airport_name, days_of_week
          from routes 
          order by flight_no limit 4
      ) t;
    
    demo=# select ctid, jsonb_pretty(route) from routes_jsonb;
     ctid  |                 jsonb_pretty                  
    -------+-------------------------------------------------
     (0,1) | {                                              +
           |     "days_of_week": [                          +
           |         1                                      +
           |     ],                                         +
           |     "arrival_airport_name": "Surgut",          +
           |     "departure_airport_name": "Ust-Ilimsk"     +
           | }
     (0,2) | {                                              +
           |     "days_of_week": [                          +
           |         2                                      +
           |     ],                                         +
           |     "arrival_airport_name": "Ust-Ilimsk",      +
           |     "departure_airport_name": "Surgut"         +
           | }
     (0,3) | {                                              +
           |     "days_of_week": [                          +
           |         1,                                     +
           |         4                                      +
           |     ],                                         +
           |     "arrival_airport_name": "Sochi",           +
           |     "departure_airport_name": "Ivanovo-Yuzhnyi"+
           | }
     (0,4) | {                                              +
           |     "days_of_week": [                          +
           |         2,                                     +
           |         5                                      +
           |     ],                                         +
           |     "arrival_airport_name": "Ivanovo-Yuzhnyi", +
           |     "departure_airport_name": "Sochi"          +
           | }
    (4 rows)
    
    demo=# create index on routes_jsonb using gin(route);
    

    索引看起来如下:

    现在,像这样的查询,例如,可以使用索引执行:

    demo=# explain (costs off) 
    select jsonb_pretty(route) 
    from routes_jsonb 
    where route @> '{"days_of_week": [5]}';
                              QUERY PLAN                           
    ---------------------------------------------------------------
     Bitmap Heap Scan on routes_jsonb
       Recheck Cond: (route @> '{"days_of_week": [5]}'::jsonb)
       ->  Bitmap Index Scan on routes_jsonb_route_idx
             Index Cond: (route @> '{"days_of_week": [5]}'::jsonb)
    (4 rows)
    

    从JSON文档的根开始,@>操作符检查指定的路由(“days_of_week”:[5])是否出现。这里查询将返回一行:

    demo=# select jsonb_pretty(route) from routes_jsonb where route @> '{"days_of_week": [5]}';
                     jsonb_pretty                 
    ------------------------------------------------
     {                                             +
         "days_of_week": [                         +
             2,                                    +
             5                                     +
         ],                                        +
         "arrival_airport_name": "Ivanovo-Yuzhnyi",+
         "departure_airport_name": "Sochi"         +
     }
    (1 row)
    

    查询执行如下:

    1.在搜索查询(“days_of_week”:[5])中提取元素(搜索键):«days_of_week»和«5»。

    2.在元素树中找到提取的键,并为每个键选择tid列表:对于«5»-(0,4),对于«days_of_week»-(0,1),(0,2),(0,3),(0,4)。

    3.在找到的所有TIDs中,一致性函数从查询中选择与操作符匹配的TIDs。 对于@>操作符,不包含来自搜索查询的所有元素的文档将不能确定,因此只剩下(0,4)。 但是我们仍然需要重新检查表中剩下的TID,因为从索引中不清楚找到的元素在JSON文档中出现的顺序。

    要了解其他操作符的更多细节,可以阅读文档。

    除了处理JSON的传统操作外,«jsquery»扩展早就可用了,它定义了一种功能更丰富的查询语言(当然,还支持GIN索引)。此外,2016年发布了新的SQL标准,定义了自己的一套操作和查询语言«SQL/JSON path»。这个标准的实现已经完成,我们相信它会出现在PostgreSQL 11中。

    内部原理

    我们可以使用“pageinspect”扩展查看GIN索引内部。

    fts=# create extension pageinspect;
    

    来自meta页面的信息显示了一般的统计数据:

    fts=# select * from gin_metapage_info(get_raw_page('mail_messages_tsv_idx',0));
    -[ RECORD 1 ]----+-----------
    pending_head     | 4294967295
    pending_tail     | 4294967295
    tail_free_size   | 0
    n_pending_pages  | 0
    n_pending_tuples | 0
    n_total_pages    | 22968
    n_entry_pages    | 13751
    n_data_pages     | 9216
    n_entries        | 1423598
    version          | 2
    

    页面结构提供了访问方法(access method)存储其信息的特殊区域;这个区域对于像vacuum这样的普通程序是«opaque»的。«gin_page_opaque_info»函数显示了GIN的数据。例如,我们可以了解到索引页的集合:

    fts=# select flags, count(*)
    from generate_series(1,22967) as g(id), -- n_total_pages
         gin_page_opaque_info(get_raw_page('mail_messages_tsv_idx',g.id))
    group by flags;
             flags          | count 
    ------------------------+-------
     {meta}                 |     1  meta page
     {}                     |   133  internal page of element B-tree
     {leaf}                 | 13618  leaf page of element B-tree
     {data}                 |  1497  internal page of TID B-tree
     {data,leaf,compressed} |  7719  leaf page of TID B-tree
    (5 rows)
    

    «gin_leafpage_items»函数提供存储在page {data,leaf,compressed}的tid信息:

    fts=# select * from gin_leafpage_items(get_raw_page('mail_messages_tsv_idx',2672));
    -[ RECORD 1 ]---------------------------------------------------------------------
    first_tid | (239,44)
    nbytes    | 248
    tids      | {"(239,44)","(239,47)","(239,48)","(239,50)","(239,52)","(240,3)",...
    -[ RECORD 2 ]---------------------------------------------------------------------
    first_tid | (247,40)
    nbytes    | 248
    tids      | {"(247,40)","(247,41)","(247,44)","(247,45)","(247,46)","(248,2)",...
    ...
    

    这里请注意,TIDs树的leave页面实际上包含指向表行的经过压缩的小指针列表,而不是单个指针。

    属性

    让我们看看GIN access method的属性

     amname |     name      | pg_indexam_has_property 
    --------+---------------+-------------------------
     gin    | can_order     | f
     gin    | can_unique    | f
     gin    | can_multi_col | t
     gin    | can_exclude   | f
    

    有趣的是,GIN支持创建多列索引。但是,与常规b-树不同的是,多列索引将仍然存储单个元素,并且将为每个元素标明列号。

    以下索引层属性可用:

         name      | pg_index_has_property 
    ---------------+-----------------------
     clusterable   | f
     index_scan    | f
     bitmap_scan   | t
     backward_scan | f
    

    注意,不支持按TID(索引扫描)返回结果;只能进行位图扫描。

    也不支持Backward扫描:该特性对index-scan only扫描至关重要,但对位图扫描不支持。

    列层属性如下:

            name        | pg_index_column_has_property 
    --------------------+------------------------------
     asc                | f
     desc               | f
     nulls_first        | f
     nulls_last         | f
     orderable          | f
     distance_orderable | f
     returnable         | f
     search_array       | f
     search_nulls       | f
    

    这里没有可用的内容:没有排序(这很明显),没有使用索引作为覆盖(因为文档本身没有存储在索引中),没有空值操作(因为它对复合类型的元素没有意义)。

    其他数据类型

    还有一些扩展为某些数据类型添加了对GIN的支持。

    ·“pg_trgm”使我们能够通过比较有多少相等的三字母序列(三元组合)可用来确定单词的«likeness»。添加了两个操作符类«gist_trgm_ops»和«gin_trgm_ops»,它们支持各种操作符,包括通过LIKE和正则表达式进行比较。我们可以将此扩展与全文搜索一起使用,以便建议纠正拼写错误的单词选项。 ·“hstore”实现«key-value»存储。对于该数据类型,可以使用用于各种访问方法的操作符类,包括GIN。然而,随着«jsonb»数据类型的引入,也就没有使用«hstore»的理由了。 ·“intarray”扩展了整数数组的功能。索引支持包括GiST以及GIN(«gin__int_ops» 操作符类)。

     

    这两个扩展已经在上面提到过:

    ·“btree_gin”添加了对常规数据类型的GIN支持,以便它们可以与复合类型一起在多列索引中使用。 ·“jsquery”定义了一种用于JSON查询的语言和一个用于支持该语言的索引的操作符类。 这个扩展不包括在标准的PostgreSQL交付中。

     

     原文地址:https://habr.com/en/company/postgrespro/blog/448746/

     

     

  • 相关阅读:
    通俗算法教程04
    微软是如何重写C#编译器并使它开源的
    在 CentOS 7 中安装 MySQL 8
    在 .NET Core 中结合 HttpClientFactory 使用 Polly(下篇)
    在 .NET Core 中结合 HttpClientFactory 使用 Polly(中篇)
    .NET 开源项目 Polly 介绍
    在 .NET Core 中结合 HttpClientFactory 使用 Polly(上篇)
    5年后、10年后,你希望自己是个什么样的人?
    即将发布的 ASP.NET Core 2.2 会有哪些新玩意儿?
    推荐六本前端开发必看的书籍
  • 原文地址:https://www.cnblogs.com/abclife/p/13485422.html
Copyright © 2011-2022 走看看