优化 postgres 相似性查询(pg_trgm + gin 索引)
Optimizing a postgres similarity query (pg_trgm + gin index)
我定义了以下索引:
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, ) s_username,
similarity(first_name, ) s_first_name,
similarity(last_name, ) s_last_name
WHERE
username % OR
first_name % OR
last_name %
ORDER BY
rank DESC
LIMIT ;
auth_user
table 有 620 万行。
查询速度似乎在很大程度上取决于 return 可能由 similarity
查询编辑的结果数量。
通过 set_limit
提高相似性阈值会有所帮助,但会通过消除部分匹配降低结果的有用性。
一些搜索 return 在 200 毫秒内完成,其他搜索大约需要 10 秒。
我们已经使用 Elasticsearch 实现了此功能,return任何查询都在 < 200 毫秒内完成,同时进行更复杂(更好)的排名。
我想知道是否有任何方法可以改进它以获得更一致的性能?
据我了解,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
服务器是 Postgres 9.6.1 运行ning on Amazon RDS
更新
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)
这有了很大的改进(以前 > 10s)!
1.5s 对于类似的查询仍然比 ES 慢,所以我仍然想听听任何优化查询的建议。
2.
为了回应评论并看到这个问题 (),我尝试了完全相同的设置,用 GIST 索引代替 GIN 索引。
尝试与上面相同的搜索,它 return 在 ~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 %
子句在它下面有三个 Bitmap Index Scan
...然后这些结果与 BitmapOr
步骤组合。这些部分都挺快的。
但即使在我们没有运行 out of work mem 的情况下,我们仍然会在 Bitmap Heap Scan
.
中花费将近一整秒的时间
我希望使用这种方法多更快的结果:
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()
简化?
- Combine two columns and add into one new column
- Faster query with pattern-matching on multiple text fields
- Combine two columns and add into one new column
2.
使用正确的 nearest-neighbor 查询,匹配以上索引:
SELECT username, email, first_name, last_name
, similarity(username , ) AS s_username
, similarity(first_name, ) AS s_first_name
, similarity(last_name , ) AS s_last_name
, row_number() OVER () AS rank -- greatest similarity first
FROM auth_user
WHERE (username || ' ' || first_name || ' ' || last_name) % -- !!
ORDER BY (username || ' ' || first_name || ' ' || last_name) <-> -- !!
LIMIT ;
WHERE
和ORDER BY
中的表达式必须匹配索引表达式!
特别是 ORDER BY rank
(就像你有的那样)对于从更大的合格行池中进行的小型 LIMIT
选择总是表现不佳,因为它不能直接使用索引:复杂的rank
后面的表达式必须针对每个 每个 符合条件的行进行计算,然后必须在返回最佳匹配的小选择之前对所有内容进行排序。这 比真正的最近邻查询 贵得多 .
具有空 window 定义的 row_number()
仅反映由相同 SELECT
.
的 ORDER BY
产生的排序
相关回答:
- Best index for similarity function
- Search in 300 million addresses with pg_trgm
关于你的项目3.
,我添加了你提到的问题的答案,应该解释一下:
我定义了以下索引:
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, ) s_username,
similarity(first_name, ) s_first_name,
similarity(last_name, ) s_last_name
WHERE
username % OR
first_name % OR
last_name %
ORDER BY
rank DESC
LIMIT ;
auth_user
table 有 620 万行。
查询速度似乎在很大程度上取决于 return 可能由 similarity
查询编辑的结果数量。
通过 set_limit
提高相似性阈值会有所帮助,但会通过消除部分匹配降低结果的有用性。
一些搜索 return 在 200 毫秒内完成,其他搜索大约需要 10 秒。
我们已经使用 Elasticsearch 实现了此功能,return任何查询都在 < 200 毫秒内完成,同时进行更复杂(更好)的排名。
我想知道是否有任何方法可以改进它以获得更一致的性能?
据我了解,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
服务器是 Postgres 9.6.1 运行ning on Amazon RDS
更新
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)
这有了很大的改进(以前 > 10s)!
1.5s 对于类似的查询仍然比 ES 慢,所以我仍然想听听任何优化查询的建议。
2.为了回应评论并看到这个问题 (
尝试与上面相同的搜索,它 return 在 ~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 %
子句在它下面有三个 Bitmap Index Scan
...然后这些结果与 BitmapOr
步骤组合。这些部分都挺快的。
但即使在我们没有运行 out of work mem 的情况下,我们仍然会在 Bitmap Heap Scan
.
我希望使用这种方法多更快的结果:
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()
简化?
- Combine two columns and add into one new column
- Faster query with pattern-matching on multiple text fields
- Combine two columns and add into one new column
2.
使用正确的 nearest-neighbor 查询,匹配以上索引:
SELECT username, email, first_name, last_name
, similarity(username , ) AS s_username
, similarity(first_name, ) AS s_first_name
, similarity(last_name , ) AS s_last_name
, row_number() OVER () AS rank -- greatest similarity first
FROM auth_user
WHERE (username || ' ' || first_name || ' ' || last_name) % -- !!
ORDER BY (username || ' ' || first_name || ' ' || last_name) <-> -- !!
LIMIT ;
WHERE
和ORDER BY
中的表达式必须匹配索引表达式!
特别是 ORDER BY rank
(就像你有的那样)对于从更大的合格行池中进行的小型 LIMIT
选择总是表现不佳,因为它不能直接使用索引:复杂的rank
后面的表达式必须针对每个 每个 符合条件的行进行计算,然后必须在返回最佳匹配的小选择之前对所有内容进行排序。这 比真正的最近邻查询 贵得多 .
row_number()
仅反映由相同 SELECT
.
ORDER BY
产生的排序
相关回答:
- Best index for similarity function
- Search in 300 million addresses with pg_trgm
关于你的项目3.
,我添加了你提到的问题的答案,应该解释一下: