【问题标题】:Optimizing a postgres similarity query (pg_trgm + gin index)优化 postgres 相似度查询(pg_trgm + gin index)
【发布时间】:2017-10-07 14:59:58
【问题描述】:

我已经定义了以下索引:

CREATE INDEX
    users_search_idx
ON
    auth_user
USING
    gin(
        username gin_trgm_ops,
        first_name gin_trgm_ops,
        last_name gin_trgm_ops
    );

我正在执行以下查询:

PREPARE user_search (TEXT, INT) AS
    SELECT
        username,
        email,
        first_name,
        last_name,
        ( -- would probably do per-field weightings here
            s_username + s_first_name + s_last_name
        ) rank
    FROM
        auth_user,
        similarity(username, $1) s_username,
        similarity(first_name, $1) s_first_name,
        similarity(last_name, $1) s_last_name
    WHERE
        username % $1 OR
        first_name % $1 OR
        last_name % $1
    ORDER BY
        rank DESC
    LIMIT $2;

auth_user 表有 620 万行。

查询的速度似乎在很大程度上取决于similarity 查询可能返回的结果数量。

通过set_limit 提高相似度阈值会有所帮助,但会通过消除部分匹配降低结果的有用性。

有些搜索会在 200 毫秒内返回,有些则需要 10 秒左右。

我们已经使用 Elasticsearch 实现了此功能,它可以在

我想知道是否有任何方法可以改进这一点以获得更一致的性能?

据我了解,GIN 索引(倒排索引)与 Elasticsearch 使用的基本方法相同,因此我认为可以进行一些优化。

EXPLAIN ANALYZE EXECUTE user_search('mel', 20) 显示:

Limit  (cost=54099.81..54099.86 rows=20 width=52) (actual time=10302.092..10302.104 rows=20 loops=1)
  ->  Sort  (cost=54099.81..54146.66 rows=18739 width=52) (actual time=10302.091..10302.095 rows=20 loops=1)
        Sort Key: (((s_username.s_username + s_first_name.s_first_name) + s_last_name.s_last_name)) DESC
        Sort Method: top-N heapsort  Memory: 26kB
        ->  Nested Loop  (cost=382.74..53601.17 rows=18739 width=52) (actual time=118.164..10293.765 rows=8380 loops=1)
              ->  Nested Loop  (cost=382.74..53132.69 rows=18739 width=56) (actual time=118.150..10262.804 rows=8380 loops=1)
                    ->  Nested Loop  (cost=382.74..52757.91 rows=18739 width=52) (actual time=118.142..10233.990 rows=8380 loops=1)
                          ->  Bitmap Heap Scan on auth_user  (cost=382.74..52383.13 rows=18739 width=48) (actual time=118.128..10186.816 rows=8380loops=1)"
                                Recheck Cond: (((username)::text % 'mel'::text) OR ((first_name)::text % 'mel'::text) OR ((last_name)::text %'mel'::text))"
                                Rows Removed by Index Recheck: 2434523
                                Heap Blocks: exact=49337 lossy=53104
                                ->  BitmapOr  (cost=382.74..382.74 rows=18757 width=0) (actual time=107.436..107.436 rows=0 loops=1)
                                      ->  Bitmap Index Scan on users_search_idx  (cost=0.00..122.89 rows=6252 width=0) (actual time=40.200..40.200rows=88908 loops=1)"
                                            Index Cond: ((username)::text % 'mel'::text)
                                      ->  Bitmap Index Scan on users_search_idx  (cost=0.00..122.89 rows=6252 width=0) (actual time=43.847..43.847rows=102028 loops=1)"
                                            Index Cond: ((first_name)::text % 'mel'::text)
                                      ->  Bitmap Index Scan on users_search_idx  (cost=0.00..122.89 rows=6252 width=0) (actual time=23.387..23.387rows=58740 loops=1)"
                                            Index Cond: ((last_name)::text % 'mel'::text)
                          ->  Function Scan on similarity s_username  (cost=0.00..0.01 rows=1 width=4) (actual time=0.004..0.004 rows=1 loops=8380)
                    ->  Function Scan on similarity s_first_name  (cost=0.00..0.01 rows=1 width=4) (actual time=0.002..0.002 rows=1 loops=8380)
              ->  Function Scan on similarity s_last_name  (cost=0.00..0.01 rows=1 width=4) (actual time=0.002..0.002 rows=1 loops=8380)
Execution time: 10302.559 ms

服务器是在 Amazon RDS 上运行的 Postgres 9.6.1

更新

1.

发布问题后不久,我发现了以下信息:https://www.postgresql.org/message-id/464F3C5D.2000700@enterprisedb.com

所以我尝试了

-> SHOW work_mem;
4MB
-> SET work_mem='12MB';
-> EXECUTE user_search('mel', 20);
(results returned in ~1.5s)

这取得了很大的进步(之前 > 10 秒)!

对于类似的查询,1.5s 仍然比 ES 慢很多,所以我仍然想听听任何优化查询的建议。

2.

针对 cme​​ts,在看到这个问题 (Postgresql GIN index slower than GIST for pg_trgm) 后,我尝试了完全相同的设置,用 GIST 索引代替 GIN 索引。

尝试与上面相同的搜索,它在大约 3.5 秒内返回,使用默认 work_mem='4MB'。增加work_mem 没有任何区别。

由此我得出结论,GIST 索引的内存效率更高(没有像 GIN 那样遇到病态情况),但是当 GIN 正常工作时,它比 GIN 慢。这与推荐 GIN 索引的文档中的描述一致。

3.

我还是不明白为什么要花这么多时间:

 ->  Bitmap Heap Scan on auth_user  (cost=382.74..52383.13 rows=18739 width=48) (actual time=118.128..10186.816 rows=8380loops=1)"
     Recheck Cond: (((username)::text % 'mel'::text) OR ((first_name)::text % 'mel'::text) OR ((last_name)::text %'mel'::text))"
     Rows Removed by Index Recheck: 2434523
     Heap Blocks: exact=49337 lossy=53104

我不明白为什么需要这一步或它在做什么。

对于每个username % $1 子句,它下面都有三个Bitmap Index Scan...然后这些结果与BitmapOr 步骤相结合。这些部分都非常快。

但即使在我们没有用完工作内存的情况下,我们仍然在Bitmap Heap Scan 上花费了将近一秒钟的时间。

【问题讨论】:

  • 所以...大部分时间都花在了昂贵的Bitmap Heap Scan on auth_user Recheck Cond 上。出于某种原因,这必须删除 240 万行,尽管内部索引扫描返回的总行数
  • 为什么要在 FROM 子句中进行相似性调用?除了在 SELECT 子句中,您似乎没有对它们做任何事情。请注意,那里使用的总时间是“实际时间”*“循环”,尽管我不会太相信“实际时间”的数字 - 它太小而不能成为可靠​​的数字。
  • FROM 中的相似性是我从某处的示例查询中得到的……它使它更具可读性。我尝试进入 SELECT 部分,但性能没有差异。此查询的实际时间是 10 秒...对于某些搜索,我得到的最坏情况与上面的 EXPLAIN 中报告的完全一样
  • 我实际上刚刚发现了一些有很大不同的东西......SET work_mem='8MB'(从 4MB 开始)将最坏的情况减少到 ~1.3 秒。所以我想主要问题是这里描述的postgresql.org/message-id/464F3C5D.2000700@enterprisedb.com
  • 1.3s 不是很好,但比 10s 好很多

标签: postgresql similarity postgresql-9.6 trigram pg-trgm


【解决方案1】:

我希望通过这种方法获得更快的结果

1.

创建一个 GiST 索引,其中 1 列包含连接值:

CREATE INDEX users_search_idx ON auth_user
USING gist((username || ' ' || first_name || ' ' || last_name) gist_trgm_ops);

这假定所有 3 列都定义为 NOT NULL(您没有指定)。否则你需要做更多。
为什么不用concat_ws() 简化呢?

2.

使用正确的 查询,匹配上面的索引:

SELECT username, email, first_name, last_name
     , similarity(username  , $1) AS s_username
     , similarity(first_name, $1) AS s_first_name
     , similarity(last_name , $1) AS s_last_name
     , row_number() OVER () AS rank  -- greatest similarity first
FROM   auth_user
WHERE     (username || ' ' || first_name || ' ' || last_name) %   $1  -- !!
ORDER  BY (username || ' ' || first_name || ' ' || last_name) <-> $1  -- !!
LIMIT  $2;

WHEREORDER BY 中的表达式必须匹配索引表达式!

特别是ORDER BY rank(就像您拥有的那样)对于从更大的合格行池中挑选的小型LIMIT 总是表现不佳,因为它不能直接使用索引:rank 背后的复杂表达式有要为每个符合条件的行进行计算,则必须先对所有符合条件的行进行排序,然后才能返回一小部分最佳匹配。这比真正的最近邻查询要贵得多,后者可以直接从索引中挑选最佳结果,甚至无需查看其余部分。

带有空窗口定义的row_number() 仅反映同一SELECTORDER BY 产生的排序。

相关答案:


至于您的3.,我添加了对您引用的问题的答案,应该可以解释一下:

【讨论】:

  • 如果我理解正确的话,最重要的变化是ORDER BY (username || ' ' || first_name || ' ' || last_name) &lt;-&gt; $1。我一直希望稍后调整我的 rank 值以提高用户名匹配度,例如 ((s_username * 2) + s_first_name + s_last_name) rank 但似乎在优化版本中这是不可能的
  • @Anentropic:优化的重点是查询可以直接从索引中选择最佳匹配,这比检索所有候选者、计算每个候选者的排名、对所有候选者进行排序要快得多他们按这个排名,然后丢弃除第一个之外的所有 - 而LIMIT 远小于合格行的总数。
猜你喜欢
  • 2011-01-16
  • 2021-05-16
  • 1970-01-01
  • 2022-01-24
  • 2011-02-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多