利用pg_trgm的gist和gin索引加速字符匹配查询

  1. 云栖社区>
  2. 博客>
  3. 正文

利用pg_trgm的gist和gin索引加速字符匹配查询

小桥河西 2015-02-06 18:19:19 浏览563
展开阅读全文
pg_trgm是用来做相似度匹配的,在一些情况下也可以拿来代替全文检索做字符匹配。
从大量数据中通过字符串的匹配查找数据的关键是索引,对字符串的精确相等匹配,前缀匹配(like 'x%')和后缀匹配(like '%x')可以使用btree索引,对中缀匹配(like '%x%')和正则表达式匹配就可以用pg_trgm的索引了。下面用一个例子说明一下。

1.环境

CentOS 6.5
PostgreSQL 9.4.0

2.测试数据

没有特意找什么测试数据,正好手头有一份翻译中的PostgreSQL9.3中文手册,就拿它当测试数据了。

建表

点击(此处)折叠或打开

  1. create table t1(id serial,c1 text);

导数据,手册中的每一行作为一条记录。

点击(此处)折叠或打开

  1. -bash-4.1$ tar xf pg9.3.1.html.tar.gz
  2. -bash-4.1$ find html -name *.html -exec cat {} \;|iconv -f GBK -t UTF8|sed 's\[\]\\g'|psql -p 5433 testdb -c "copy t1(c1) from stdin"
注)翻译中的PostgreSQL9.3中文手册(pg9.3.1.html.tar.gz)可从以下位置下载:http://58.58.27.50:8079/doc/doc/postdoc_download.php

查看数据大小

点击(此处)折叠或打开

  1. testdb=# select count(*) from t1;
  2.  count
  3. --------
  4.  702476
  5. (1 row)
  6. testdb=# select pg_table_size('t1');
  7.  pg_table_size
  8. ---------------
  9.       37519360
  10. (1 row)
  11. testdb=# select * from t1 where c1 like '%存储过程%';
  12.    id | c1
  13. --------+------------------------------------------------------------------------------------------------------------------------
  14.  132113 | >有些其它数据库系统定义动态的数据库规则。这些通常是存储过程和触发器,
  15.  132119 | >规则系统(更准确地说是查询重写规则系统)是和存储过程和触发器完全不同的东西。
  16.  260249 | >如果你的需求超过这些条件表达式的能力,你可能会希望用一种更富表现力的编程语言写一个存储过程。/P
  17.  473911 | >下面是一个用 C 写的存储过程语言处理器的模版:
  18.  562282 | >请注意开销估计函数必须用 C 写,而不能用 SQL 或者任何可用的存储过程语言,因为它们必须访问规划器/优化器的内部数据结构。
  19.  566142 | >登记编程语言,你可以用这些语言或接口写函数或者存储过程。
  20. (6 rows)

3.模糊匹配测试

3.1 通过全表扫描做模糊匹配

点击(此处)折叠或打开

  1. testdb=# explain (analyze,buffers) select * from t1 where c1 like '%存储过程%';
  2.                                              QUERY PLAN
  3. -----------------------------------------------------------------------------------------------------
  4.  Seq Scan on t1 (cost=0.00..13354.95 rows=30 width=21) (actual time=34.920..186.967 rows=6 loops=1)
  5.    Filter: (c1 ~~ '%存储过程%'::text)
  6.    Rows Removed by Filter: 702470
  7.    Buffers: shared hit=4574
  8.  Planning time: 0.121 ms
  9.  Execution time: 187.003 ms
  10. (6 rows)

  11. Time: 187.635 ms

3.2 使用pg_trgm的gist索引扫描做模糊匹配

点击(此处)折叠或打开

  1. testdb=# create extension pg_trgm;
  2. CREATE EXTENSION
  3. testdb=# create index t1_c1_gist_idx on t1 using gist(c1 gist_trgm_ops);
  4. LOG: checkpoints are occurring too frequently (22 seconds apart)
  5. HINT: Consider increasing the configuration parameter "checkpoint_segments".
  6. LOG: checkpoints are occurring too frequently (5 seconds apart)
  7. HINT: Consider increasing the configuration parameter "checkpoint_segments".
  8. CREATE INDEX
  9. Time: 15988.394 ms

  10. testdb=# select pg_relation_size('t1_c1_gist_idx');
  11.  pg_relation_size
  12. ------------------
  13.          55214080
  14. (1 row)

  15. Time: 0.461 ms

  16. testdb=# explain (analyze,buffers) select * from t1 where c1 like '%存储过程%';
  17.                                                         QUERY PLAN
  18. --------------------------------------------------------------------------------------------------------------------------
  19.  Bitmap Heap Scan on t1 (cost=4.52..117.60 rows=30 width=21) (actual time=71.292..71.303 rows=6 loops=1)
  20.    Recheck Cond: (c1 ~~ '%存储过程%'::text)
  21.    Heap Blocks: exact=5
  22.    Buffers: shared hit=5249
  23.    -> Bitmap Index Scan on t1_c1_gist_idx (cost=0.00..4.51 rows=30 width=0) (actual time=71.268..71.268 rows=6 loops=1)
  24.          Index Cond: (c1 ~~ '%存储过程%'::text)
  25.          Buffers: shared hit=5244
  26.  Planning time: 0.146 ms
  27.  Execution time: 71.344 ms
  28. (9 rows)

  29. Time: 71.976 ms

性能提升了1倍多(187.635/71.976),可以说效果并不明显,尤其需要注意的是索引太大,而且扫描的索引数据块太多(5244),几乎把整个索引都扫了一遍。而匹配的记录其实只有6条。下面看看gin索引的效果。

3.3 使用pg_trgm的gin索引扫描做模糊匹配

点击(此处)折叠或打开

  1. testdb=# drop index t1_c1_gist_idx;
  2. DROP INDEX
  3. Time: 22.827 ms
  4. testdb=# create index t1_c1_gin_idx on t1 using gin(c1 gin_trgm_ops);
  5. CREATE INDEX
  6. Time: 4995.957 ms
  7. testdb=# select pg_relation_size('t1_c1_gin_idx');
  8.  pg_relation_size
  9. ------------------
  10.          29663232
  11. (1 row)

  12. Time: 0.670 ms

  13. testdb=# explain (analyze,buffers) select * from t1 where c1 like '%存储过程%';
  14.                                                        QUERY PLAN
  15. ------------------------------------------------------------------------------------------------------------------------
  16.  Bitmap Heap Scan on t1 (cost=28.23..141.32 rows=30 width=21) (actual time=0.115..0.135 rows=6 loops=1)
  17.    Recheck Cond: (c1 ~~ '%存储过程%'::text)
  18.    Heap Blocks: exact=5
  19.    Buffers: shared hit=12
  20.    -> Bitmap Index Scan on t1_c1_gin_idx (cost=0.00..28.22 rows=30 width=0) (actual time=0.093..0.093 rows=6 loops=1)
  21.          Index Cond: (c1 ~~ '%存储过程%'::text)
  22.          Buffers: shared hit=7
  23.  Planning time: 1.348 ms
  24.  Execution time: 0.265 ms
  25. (9 rows)

  26. Time: 2.721 ms

gin_trgm索引的效果好多了。性能提升了69倍(187.635/2.721)
很妙的是除了like,gin_trgm索引还可以用在正则表达式匹配上。比如,查出所有不是出现在句子结尾的“存储过程”。

点击(此处)折叠或打开

  1. testdb=# select * from t1 where c1 ~ '存储过程[^。]';
  2.    id | c1
  3. --------+------------------------------------------------------------------------------------------------------------------------
  4.  132113 | >有些其它数据库系统定义动态的数据库规则。这些通常是存储过程和触发器,
  5.  132119 | >规则系统(更准确地说是查询重写规则系统)是和存储过程和触发器完全不同的东西。
  6.  473911 | >下面是一个用 C 写的存储过程语言处理器的模版:
  7.  562282 | >请注意开销估计函数必须用 C 写,而不能用 SQL 或者任何可用的存储过程语言,因为它们必须访问规划器/优化器的内部数据结构。
  8. (4 rows)

  9. Time: 0.978 ms
  10. testdb=# explain (analyze,buffers) select * from t1 where c1 ~ '存储过程[^。]';
  11.                                                        QUERY PLAN
  12. ------------------------------------------------------------------------------------------------------------------------
  13.  Bitmap Heap Scan on t1 (cost=28.23..141.32 rows=30 width=21) (actual time=0.141..0.172 rows=4 loops=1)
  14.    Recheck Cond: (c1 ~ '存储过程[^。]'::text)
  15.    Rows Removed by Index Recheck: 2
  16.    Heap Blocks: exact=5
  17.    Buffers: shared hit=12
  18.    -> Bitmap Index Scan on t1_c1_gin_idx (cost=0.00..28.22 rows=30 width=0) (actual time=0.120..0.120 rows=6 loops=1)
  19.          Index Cond: (c1 ~ '存储过程[^。]'::text)
  20.          Buffers: shared hit=7
  21.  Planning time: 0.441 ms
  22.  Execution time: 0.281 ms
  23. (10 rows)

  24. Time: 1.261 ms

3.4 对比一下使用zhparser插件的全文检索的效果

zhparser的使用方法参考前一篇博客《PostgreSQL的全文检索插件zhparser的中文分词效果》。
libscws的分词模式设成短词+重要单字的复合分词。

点击(此处)折叠或打开

  1. testdb=# create index t1_c1_fts_idx on t1 using gin(to_tsvector('testzhcfg',c1));
  2. CREATE INDEX
  3. Time: 4523.277 ms
  4. testdb=# select pg_relation_size('t1_c1_fts_idx');
  5.  pg_relation_size
  6. ------------------
  7.           8765440
  8. (1 row)

  9. Time: 0.543 ms

  10. testdb=# explain (analyze,buffers) select * from t1 where to_tsvector('testzhcfg',c1) @@ to_tsquery('testzhcfg','存储过程');
  11.                                                              QUERY PLAN
  12. -------------------------------------------------------------------------------------------------------------------------------------
  13.  Bitmap Heap Scan on t1 (cost=76.00..80.02 rows=1 width=21) (actual time=0.437..0.465 rows=13 loops=1)
  14.    Recheck Cond: (to_tsvector('testzhcfg'::regconfig, c1) @@ '''存储'' & ''存'' & ''储'' & ''过程'' & ''过'' & ''程'''::tsquery)
  15.    Heap Blocks: exact=12
  16.    Buffers: shared hit=35
  17.    -> Bitmap Index Scan on t1_c1_fts_idx (cost=0.00..76.00 rows=1 width=0) (actual time=0.424..0.424 rows=13 loops=1)
  18.          Index Cond: (to_tsvector('testzhcfg'::regconfig, c1) @@ '''存储'' & ''存'' & ''储'' & ''过程'' & ''过'' & ''程'''::tsquery)
  19.          Buffers: shared hit=23
  20.  Planning time: 0.192 ms
  21.  Execution time: 0.506 ms
  22. (9 rows)

  23. Time: 1.486 ms

全文检索果然索引更小,搜索更快(在我的测试条件下,性能其实相差不大,而且每次测试结果都不一样;随着数据量的增大中文分词会更有性能优势)。但是pg_trgm贵在不需要涉及中文分词的那么多不确定性。比如上面的例子,用全文检索搜出来的就是13条记录,而不是6条。因为用于分词的词典里没有“存储过程”这个词,“存储过程”被拆成了“存储”和“过程”两个词,只要记录里有这两个词,不管是否连在一起,都被认为匹配。如下:

点击(此处)折叠或打开

  1. testdb=# select * from t1 where to_tsvector('testzhcfg',c1) @@ to_tsquery('testzhcfg','存储过程');
  2.    id | c1
  3. --------+------------------------------------------------------------------------------------------------------------------------
  4.  120827 | 在描述这些变化的日志记录刷新到永久存储器之后。如果我们遵循这个过程,
  5.  132113 | >有些其它数据库系统定义动态的数据库规则。这些通常是存储过程和触发器,
  6.  132119 | >规则系统(更准确地说是查询重写规则系统)是和存储过程和触发器完全不同的东西。
  7.  260249 | >如果你的需求超过这些条件表达式的能力,你可能会希望用一种更富表现力的编程语言写一个存储过程。/P
  8.  263598 | >表存储关于函数(或过程)的信息。参阅A
  9.  320664 | 然后在使用过程中大概需要在一个平面文本文件里存放同等数据量五倍的空间存储数据。
  10.  376663 | 实现节点将数据保存在存储器中,因为它被读取,然后从存储器每个后续过程中返回每一个数据。/P
  11.  436633 | >存储有关与访问方法操作符族相关联的支持过程的信息。
  12.  455406 | >表为过程语言存储SPAN
  13.  473911 | >下面是一个用 C 写的存储过程语言处理器的模版:
  14.  552426 | 我们加速了存储器分配,优化,表联合以及行传输过程。/P
  15.  562282 | >请注意开销估计函数必须用 C 写,而不能用 SQL 或者任何可用的存储过程语言,因为它们必须访问规划器/优化器的内部数据结构。
  16.  566142 | >登记编程语言,你可以用这些语言或接口写函数或者存储过程。
  17. (13 rows)
pg_trgm本质上虽然也相当于一种特殊的分词方法,但是pg_trgm索引只有用来做初次筛选,最终结果还有recheck来把关,所以结果是确定的。

4 pg_trgm做索引的注意事项

4.1 不支持小于3个字的匹配条件

pg_trgm的工作原理是把字符串切成N个3元组,然后对这些3元组做匹配,所以如果作为查询条件的字符串小于3个字符它就罢工了。
对pg_trgm的gist索引,走的还是索引扫描。但是悲剧的是,索引没有起到任何筛选的作用,702476条记录一个不落全给提出来了,这样还不如直接全表扫描呢。

点击(此处)折叠或打开

  1. testdb=# explain analyze select * from t1 where c1 like '%存储%';
  2.                                                          QUERY PLAN
  3. ----------------------------------------------------------------------------------------------------------------------------
  4.  Bitmap Heap Scan on t1 (cost=4.52..117.60 rows=30 width=21) (actual time=106.022..221.730 rows=640 loops=1)
  5.    Recheck Cond: (c1 ~~ '%存储%'::text)
  6.    Rows Removed by Index Recheck: 701836
  7.    Heap Blocks: exact=4574
  8.    -> Bitmap Index Scan on t1_c1_gist_idx (cost=0.00..4.51 rows=30 width=0) (actual time=105.184..105.184 rows=702476 loops=1)
  9.          Index Cond: (c1 ~~ '%存储%'::text)
  10.  Planning time: 0.102 ms
  11.  Execution time: 221.855 ms
  12. (8 rows)

  13. Time: 222.311 ms

对pg_trgm的gin索引,优化器看出来这时候走索引是白费功夫,所以就直接走的全表扫描。

点击(此处)折叠或打开

    1. testdb=# explain analyze select * from t1 where c1 like '%存储%';
    2.                                                          QUERY PLAN 
  1. ------------------------------------------------------------------------------------------------------
  2.  Seq Scan on t1 (cost=0.00..13354.95 rows=30 width=21) (actual time=0.541..207.416 rows=640 loops=1)
  3.    Filter: (c1 ~~ '%存储%'::text)
  4.    Rows Removed by Filter: 701836
  5.    Buffers: shared hit=4574
  6.  Planning time: 0.183 ms
  7.  Execution time: 207.608 ms
  8. (6 rows)

  9. Time: 208.288 ms

4.2 数据库的LC_CTYPE需要设置为中文区域

在你的环境下,可能会发现pg_trgm不支持中文,中文字符都被截掉了。就像这样:

点击(此处)折叠或打开

  1. utf8_C=# select show_trgm('存储过程');
  2.  show_trgm
  3. -----------
  4.  {}
  5. (1 row)

正确的应该是这样:

点击(此处)折叠或打开

  1. testdb=# select show_trgm('存储过程');
  2.                    show_trgm
  3. ------------------------------------------------
  4.  {0x9acb56,0xa61c30,0xaad876,0xd07577,0x5e9b60}
  5. (1 row)

这是因为,pg_trgm调用了系统的isalpha()函数判断字符,而isalpha()依赖于LC_CTYPE,如果数据库的LC_CTYPE是C,isalpha()就不能识别中文。所以需要把数据库的LC_CTYPE设成zh_CN。

trgm.h:

点击(此处)折叠或打开

  1. #define t_isdigit(x)    isdigit(TOUCHAR(x))
  2. ...
  3. #define t_isalpha(x)    isalpha(TOUCHAR(x))
  4. ...
  5. #define ISWORDCHR(c)    (t_isalpha(c) || t_isdigit(c))
trgm_opt.c:

点击(此处)折叠或打开

  1. /*
  2.  * Finds first word in string, returns pointer to the word,
  3.  * endword points to the character after word
  4.  */
  5. static char *
  6. find_word(char *str, int lenstr, char **endword, int *charlen)
  7. {
  8.     char     *beginword = str;

  9.     while (beginword - str lenstr && !ISWORDCHR(beginword))
  10.         beginword += pg_mblen(beginword);

  11.     if (beginword - str >= lenstr)
  12.         return NULL;

  13.     *endword = beginword;
  14.     *charlen = 0;
  15.     while (*endword - str lenstr && ISWORDCHR(*endword))
  16.     {
  17.         *endword += pg_mblen(*endword);
  18.         (*charlen)++;
  19.     }

  20.     return beginword;
  21. }

数据库的LC_CTYPE:

点击(此处)折叠或打开

  1. testdb=#  \l
                                     List of databases
       Name    |  Owner   | Encoding |  Collate   |   Ctype    |   Access privileges   
    -----------+----------+----------+------------+------------+-----------------------
     postgres  | postgres | UTF8     | zh_CN.utf8 | zh_CN.utf8 | 
     template0 | postgres | UTF8     | zh_CN.utf8 | zh_CN.utf8 | =c/postgres          +
               |          |          |            |            | postgres=CTc/postgres
     template1 | postgres | UTF8     | zh_CN.utf8 | zh_CN.utf8 | postgres=CTc/postgres+
               |          |          |            |            | =c/postgres
     testdb    | postgres | UTF8     | zh_CN.utf8 | zh_CN.utf8 | 
     utf8_C    | postgres | UTF8     | C          | C          | 
    (5 rows)

5.参考

http://blog.2ndquadrant.com/text-search-strategies-in-postgresql/
http://www.postgresql.org/docs/9.4/static/pgtrgm.html 
http://my.oschina.net/Kenyon/blog/366505

网友评论

登录后评论
0/500
评论
小桥河西
+ 关注