Trigram搜索随着搜索字符串变长而变慢


16

在Postgres 9.1数据库中,我有一个table1约150万行和一列的表label(为方便起见,使用简化名称)。

上有一个功能性trigram-index lower(unaccent(label))unaccent()已使其不可变,以允许在索引中使用)。

以下查询非常快:

SELECT count(*) FROM table1
WHERE (lower(unaccent(label)) like lower(unaccent('%someword%')));
 count 
-------
     1
(1 row)

Time: 394,295 ms

但是以下查询速度较慢:

SELECT count(*) FROM table1
WHERE (lower(unaccent(label)) like lower(unaccent('%someword and some more%')));
 count 
-------
     1
(1 row)

Time: 1405,749 ms

即使搜索更加严格,添加更多单词的速度甚至会更慢。

我尝试了一个简单的技巧,即先对第一个单词运行子查询,然后对完整的搜索字符串进行查询,但是(不幸的是)查询计划者看到了我的想法:

EXPLAIN ANALYZE
SELECT * FROM (
   SELECT id, title, label from table1
   WHERE lower(unaccent(label)) like lower(unaccent('%someword%'))
   ) t1
WHERE lower(unaccent(label)) like lower(unaccent('%someword and some more%'));
对table1进行位图堆扫描(成本= 16216.01..16220.04行= 1宽度= 212)(实际时间= 1824.017..1824.019行= 1循环= 1)
  重新检查条件:((lower(unaccent((label):: text))~~'%someword%':: text)AND(lower(unaccent((label):: text))~~'%someword等%'::文本))
  ->对table1_label_hun_gin_trgm进行位图索引扫描(成本= 0.00..16216.01行= 1宽度= 0)(实际时间= 1823.900..1823.900行= 1循环= 1)
        索引条件:((lower(unaccent((label):: text))~~'%someword%':: text)AND(lower(unaccent((label):: text))~~'%someword等%'::文本))
总运行时间:1824.064毫秒

我的最终问题是搜索字符串来自Web界面,该Web界面可能会发送很长的字符串,因此发送速度很慢,并且还可能构成DOS向量。

所以我的问题是:

  • 如何加快查询速度?
  • 有没有一种方法可以将其分为子查询,以便更快?
  • 也许Postgres的更高版本更好?(我尝试了9.4,它似乎并不快:效果还是一样。也许是更高版本?)
  • 也许需要不同的索引策略?

1
必须提到的是,unaccent()它也是由附加模块提供的,并且Postgres 默认情况下支持该函数上的索引,因为它不支持IMMUTABLE。您必须进行了一些更改,并且应该提及您在问题中所做的工作。我的常规建议:stackoverflow.com/a/11007216/939860。另外,trigram索引支持开箱即用的不区分大小写的匹配。您可以简化为:WHERE f_unaccent(label) ILIKE f_unaccent('%someword%')-具有匹配的索引。详细信息:stackoverflow.com/a/28636000/939860
Erwin Brandstetter

我只是宣布unaccent不可变。我将此添加到问题中。
P.Péter

请注意,更新unaccent模块时,hack将被覆盖。我建议使用函数包装的原因之一。
Erwin Brandstetter

Answers:


34

在PostgreSQL 9.6中,将有一个新版本的pg_trgm 1.2,对此会更好。稍加努力,您也可以使该新版本在PostgreSQL 9.4下工作(您必须应用补丁,然后自己编译扩展模块并安装)。

最旧的版本所做的是搜索查询中的每个三联词并将它们的并集,然后应用过滤器。新版本将执行的操作是在查询中选择最稀有的三元组,然后仅搜索该三元组,然后稍后过滤其余的三元组。

9.1中不存在执行此操作的机制。在9.4中添加了机械,但是pg_trgm当时不适合使用。

您仍然可能会遇到DOS问题,因为恶意人员可以制作仅具有常见三字组的查询。例如“%and%”,甚至是“%a%”


如果您不能升级到pg_trgm 1.2,那么欺骗计划者的另一种方法是:

WHERE (lower(unaccent(label)) like lower(unaccent('%someword%'))) 
AND   (lower(unaccent(label||'')) like 
      lower(unaccent('%someword and some more%')));

通过串联空字符串以进行标记,您可以使计划者欺骗,使其认为无法使用where子句那部分的索引。因此,它仅对%someword%使用索引,并对这些行仅应用过滤器。


另外,如果您始终在搜索整个单词,则可以使用函数将字符串标记为单词数组,并在该数组返回函数上使用常规的内置GIN索引(而非pg_trgm)。


13
值得一提的是您是编写补丁的人。初步的性能测试令人印象深刻。这确实值得更多投票(也用于当前版本的解释和解决方法)。
Erwin Brandstetter

至少对您用来实现9.1中不存在的补丁的机器的引用,我会更感兴趣。但是,我同意瓦特(Erwin)不好的回答。
埃文·卡洛尔

3

我找到了一种骗取查询计划器的方法,这是一个非常简单的技巧:

SELECT *
FROM (
   select id, title, label
   from   table1
   where  lower(unaccent(label)) like lower(unaccent('%someword%'))
   ) t1
WHERE lower(lower(unaccent(label))) like lower(unaccent('%someword and more%'))

EXPLAIN 输出:

table1上的位图堆扫描(成本= 6749.11..7332.71行= 1宽度= 212)(实际时间= 256.607..256.609行= 1循环= 1)
  重新检查条件:(lower(unaccent((label_hun):: text))~~'%someword%':: text)
  过滤器:(lower(lower(unaccent((label):: text))))~~'%someword和更多的%':: text)
  ->在table1_label_hun_gin_trgm上进行位图索引扫描(成本= 0.00..6749.11行= 147宽度= 0)(实际时间= 256.499..256.499行= 1循环= 1)
        索引条件:(lower(unaccent((label):: text))~~'%someword%':: text)
总运行时间:256.653毫秒

因此,由于没有索引lower(lower(unaccent(label))),这将创建顺序扫描,因此它变成了一个简单的过滤器。而且,一个简单的AND也将执行相同的操作:

SELECT id, title, label
FROM table1
WHERE lower(unaccent(label)) like lower(unaccent('%someword%'))
AND   lower(lower(unaccent(label))) like lower(unaccent('%someword and more%'))

当然,如果索引扫描中使用的切除部分非常常见,则这种启发式方法可能无法很好地起作用。但是在我们的数据库中,如果我使用大约10-15个字符,实际上并没有那么多重复。

剩下两个小问题:

  • 为什么postgres不能弄清楚这样的事情会是有益的?
  • postgres在0..256.499时间范围内做什么(请参阅分析输出)?

1
在0到256.499之间的时间范围内,它正在构建位图。在256.499,它产生其第一个输出,即位图。这也是它的最后输出,因为它只产生一个输出-一个完整的位图。
jjanes
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.