使用pg_trgm索引进行相似性搜索的查询时间慢


9

我们在表中添加了两个pg_trgm索引,以启用按电子邮件地址或名称的模糊搜索,因为我们需要按名称或注册过程中拼写错误的电子邮件地址(例如“ @ gmail.con”)查找用户。ANALYZE在创建索引后运行。

但是,在绝大多数情况下,对这两个索引中的任何一个进行排名搜索都非常缓慢。也就是说,随着超时的增加,查询可能会在60秒内返回,在极少数情况下可能会很快返回15秒,但通常查询会超时。

pg_trgm.similarity_threshold是的默认值0.3,但将其提高0.8似乎没有什么不同。

这个特定的表有超过2500万行,并且不断地对其进行查询,更新和插入(每个表的平均时间小于2ms)。设置为PostgreSQL 9.6.6,在具有通用SSD存储和或多或少默认参数的RDS db.m4.large实例上运行。pg_trgm扩展是1.3版。

查询:

  • SELECT *
    FROM users
    WHERE email % 'chris@example.com'
    ORDER BY email <-> 'chris@example.com' LIMIT 10;
  • SELECT *
    FROM users
    WHERE (first_name || ' ' || last_name) % 'chris orr'
    ORDER BY (first_name || ' ' || last_name) <-> 'chris orr' LIMIT 10;

这些查询不需要经常运行(一天运行数十次),但是它们应基于当前表状态,理想情况下大约在10秒内返回。


架构:

=> \d+ users
                                          Table "public.users"
          Column   |            Type             | Collation | Nullable | Default | Storage  
-------------------+-----------------------------+-----------+----------+---------+----------
 id                | uuid                        |           | not null |         | plain    
 email             | citext                      |           | not null |         | extended 
 email_is_verified | boolean                     |           | not null |         | plain    
 first_name        | text                        |           | not null |         | extended 
 last_name         | text                        |           | not null |         | extended 
 created_at        | timestamp without time zone |           |          | now()   | plain    
 updated_at        | timestamp without time zone |           |          | now()   | plain    
                  | boolean                     |           | not null | false   | plain    
                  | character varying(60)       |           |          |         | extended 
                  | character varying(6)        |           |          |         | extended 
                  | character varying(6)        |           |          |         | extended 
                  | boolean                     |           |          |         | plain    
Indexes:
  "users_pkey" PRIMARY KEY, btree (id)
  "users_email_key" UNIQUE, btree (email)
  "users_search_email_idx" gist (email gist_trgm_ops)
  "users_search_name_idx" gist (((first_name || ' '::text) || last_name) gist_trgm_ops)
  "users_updated_at_idx" btree (updated_at)
Triggers:
  update_users BEFORE UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE update_modified_column()
Options: autovacuum_analyze_scale_factor=0.01, autovacuum_vacuum_scale_factor=0.05

(我知道,我们也许应该还可以添加unaccent()users_search_name_idx与名称查询...)


说明:

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE (first_name || ' ' || last_name) % 'chris orr' ORDER BY (first_name || ' ' || last_name) <-> 'chris orr' LIMIT 10;

Limit  (cost=0.42..40.28 rows=10 width=152) (actual time=58671.973..58676.193 rows=10 loops=1)
  Buffers: shared hit=66227 read=231821
  ->  Index Scan using users_search_name_idx on users  (cost=0.42..100264.13 rows=25153 width=152) (actual time=58671.970..58676.180 rows=10 loops=1)
        Index Cond: (((first_name || ' '::text) || last_name) % 'chris orr'::text)
        Order By: (((first_name || ' '::text) || last_name) <-> 'chris orr'::text"
        Buffers: shared hit=66227 read=231821
Planning time: 0.125 ms
Execution time: 58676.265 ms

电子邮件搜索比名称搜索更有可能超时,但这大概是因为电子邮件地址是如此相似(例如,许多 @ gmail.com地址)。

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE email % 'chris@example.com' ORDER BY email <-> 'chris@example.com' LIMIT 10;

Limit  (cost=0.42..40.43 rows=10 width=152) (actual time=58851.719..62181.128 rows=10 loops=1)
  Buffers: shared hit=83 read=428918
  ->  Index Scan using users_search_email_idx on users  (cost=0.42..100646.36 rows=25153 width=152) (actual time=58851.716..62181.113 rows=10 loops=1)
        Index Cond: ((email)::text % 'chris@example.com'::text)
        Order By: ((email)::text <-> 'chris@example.com'::text)
        Buffers: shared hit=83 read=428918
Planning time: 0.100 ms
Execution time: 62181.186 ms

查询时间慢的原因可能是什么?与读取的缓冲区数量有关?我找不到有关优化这种特殊查询的更多信息,无论如何,这些查询与pg_trgm文档中的查询非常相似。

这是我们可以优化的东西,还是可以在Postgres中更好地实现,还是希望像Elasticsearch这样的东西更适合此特定用例?


1
您的版本是否pg_trgm至少为1.3?您可以在中使用“ \ dx”进行检查psql
jjanes '18

您是否能够使用<->不使用索引的运算符重现任何排名靠前的查询?
Colin't Hart,

假设设置为默认设置,我将使用相似性阈值进行播放。这样,你可以得到较小的结果,也许整体成本可以去...
米哈尔Zaborowski

@jjanes感谢您的指导。是的,版本是1.3。
Christopher Orr

1
@MichałZaborowski正如问题中提到的,我尝试了这一点,但不幸的是没有看到任何改善。
Christopher Orr

Answers:


1

使用gin_trgm_ops而不是,您也许可以获得更好的性能gist_trgm_ops。哪个更好是几乎不可预测的,它对数据和查询条件中文本模式和长度的分布很敏感。您几乎只需尝试一下,看看它如何为您工作。一件事是pg_trgm.similarity_threshold,与GiST方法不同,GIN方法将非常敏感。这也取决于您使用的pg_trgm版本。如果您使用的是PostgreSQL的早期版本,但使用进行了更新,则pg_upgrade可能没有最新版本。计划者在预测哪种索引类型比我们能做的更好方面做得更好。因此,要测试它,您不能只创建两个,而必须删除另一个,以迫使计划者使用您想要的一个。

在电子邮件列的特定情况下,最好将它们分为用户名和域,然后查询具有确切域的相似用户名,反之亦然。然后,主要的云电子邮件提供商的极端普遍性不太可能使用添加了很少信息的三字母组合来污染索引。

最后,用例是什么?知道为什么需要运行这些查询可能会带来更好的建议。特别是,一旦电子邮件被验证为可以发送并送达正确的人,为什么还要对电子邮件进行相似性搜索?也许您可以仅对尚未验证的电子邮件子集建立部分索引?


谢谢(你的)信息。我将尝试使用GIN索引,并使用阈值。另外,是的,这对于为未验证的地址提供部分索引是很重要的一点。但是,即使对于经过验证的电子邮件地址,也可能需要进行模糊匹配(例如,人们忘记了@ gmail.com地址中的点),但是正如您提到的那样,这可能是使用具有标准化本地部分和域列的单独表的情况。
Christopher Orr
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.