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

Chr*_*Orr 9 postgresql performance postgresql-9.6 query-performance

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

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

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

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

查询:

这些查询不需要经常运行(一天几十次),但它们应该基于当前的表状态,并且最好在 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
Run Code Online (Sandbox Code Playgroud)

(我知道我们可能还应该添加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
Run Code Online (Sandbox Code Playgroud)

电子邮件搜索比名称搜索更可能超时,但这可能是因为电子邮件地址非常相似(例如,很多@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
Run Code Online (Sandbox Code Playgroud)

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

这是我们可以优化的东西,还是可以在 Postgres 中更好地实现,或者寻找像 Elasticsearch 这样的东西更适合这个特定用例?

jja*_*nes 2

gin_trgm_ops使用而不是可以获得更好的性能gist_trgm_ops。哪个更好是很难预测的,它对数据和查询术语中文本模式和长度的分布很敏感。您几乎只需要尝试一下,看看它对您有何作用。有一件事是,与 GiST 方法不同,GIN 方法对 非常敏感pg_trgm.similarity_threshold。它还取决于您拥有的 pg_trgm 版本。如果您开始使用较旧版本的 PostgreSQL,但使用 更新了它pg_upgrade,则您可能没有最新版本。规划器在预测哪种索引类型优于我们方面并不比我们更好。因此,要测试它,您不能只创建两者,您必须删除另一个,以强制规划器使用您想要的那个。

在电子邮件列的特定情况下,您可能最好将它们拆分为用户名和域,然后查询具有确切域的相似用户名,反之亦然。然后,主要云电子邮件提供商的极度流行不太可能用添加很少信息的三元组污染索引。

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