Postgres 文本搜索与 GIN 索引并在其他列上排序 DESC
Posted
技术标签:
【中文标题】Postgres 文本搜索与 GIN 索引并在其他列上排序 DESC【英文标题】:Postgres text search with GIN index and sorted DESC on other column 【发布时间】:2020-06-03 02:29:39 【问题描述】:我目前正在开发一种搜索功能,该功能最终会通过 LIKE 查询访问数据库。它曾经是形式
WHERE some_id = blah AND some_timestamp > blah AND (field1 LIKE '%some_text%' OR field2 LIKE '%some_text%' OR ...) ORDER BY some_timestamp DESC
。
由于该表的大小为数千万行,因此扩展性不佳,尤其是在使用非常旧的时间戳进行过滤时。
经过一些研究,看起来三元组索引可能更适合文本搜索。 所以我在所有连接的文本字段上添加了一个三元索引,最初得到了很好的结果。尽管我发现了回归,但在更改了新查询之后。不再命中旧索引(some_id 和 some_timestamp DESC 上的 btree)。 因此,新的文本搜索有助于某些过去非常慢的文本查询,以及由于 btree 索引而过去非常快(几毫秒)的其他文本查询现在超级慢(见下文)。
有没有办法两全其美?快速三元组文本搜索和快速 btree 索引以用于需要它的查询?
注意事项:
Postgres 11.6
我也尝试使用 btree_gin 索引来索引时间戳列,但性能几乎相同。
我稍微修改了我的查询(连接空格)以绕过三元组索引,并验证了慢查询返回到 btree 索引和
我尝试了一些查询重排,试图让两个索引都命中但无济于事。
表:
table1
---------------------------------
some_id | bigint
field1 | text
field2 | text
field3 | text
field4 | text
field5 | text
field6 | bigint
some_timestamp | timestamp without time zone
三元索引:
CREATE INDEX CONCURRENTLY IF NOT EXISTS trgm_idx ON table1 USING gin ((COALESCE(field1, '') || ' ' || COALESCE(field2, '') || COALESCE(field3, '') || ' ' || COALESCE(field4, '') || ' ' || COALESCE(field5, '') || ' ' || field6::text) gin_trgm_ops);
查询:
SELECT *
FROM table1 i
WHERE i.some_id = 1
AND (COALESCE(field1, '') || ' ' || COALESCE(field2, '') || COALESCE(field3, '') || ' ' || COALESCE(field4, '') || ' ' || COALESCE(field5, '') || ' ' || field6::text) ILIKE '%some_text%'
AND i.some_timestamp > '2015-01-00 00:00:00.0'
ORDER BY some_timestamp DESC limit 20;
解释:
Limit (cost=1043.06..1043.11 rows=20 width=446) (actual time=37240.094..37240.099 rows=20 loops=1)
-> Sort (cost=1043.06..1043.15 rows=39 width=446) (actual time=37240.092..37240.095 rows=20 loops=1)
Sort Key: some_timestamp
Sort Method: top-N heapsort Memory: 36kB
-> Bitmap Heap Scan on table1 i (cost=345.01..1042.03 rows=39 width=446) (actual time=1413.415..37202.331 rows=83066 loops=1)
Recheck Cond: ((((((((((COALESCE(field1, ''::text) || ' '::text) || COALESCE(field2, ''::text)) || COALESCE(field3, ''::text)) || ' '::text) || COALESCE(field4, ''::text)) || ' '::text) || COALESCE(field5, ''::text)) || ' '::text) || (field6)::text) ~~* '%some_text%'::text)
Rows Removed by Index Recheck: 23
Filter: ((some_timestamp > '2015-01-00 00:00:00'::timestamp without time zone) AND (some_id = 1))
Rows Removed by Filter: 5746666
Heap Blocks: exact=395922
-> Bitmap Index Scan on trgm_idx (cost=0.00..345.00 rows=667 width=0) (actual time=1325.867..1325.867 rows=5833670 loops=1)
Index Cond: ((((((((((COALESCE(field1, ''::text) || ' '::text) || COALESCE(field2, ''::text)) || COALESCE(field3, ''::text)) || ' '::text) || COALESCE(field4, ''::text)) || ' '::text) || COALESCE(field5, ''::text)) || ' '::text) || (field6)::text) ~~* '%some_text%'::text)
Planning Time: 0.252 ms
Execution Time: 37243.205 ms
(14 rows)
【问题讨论】:
【参考方案1】:创建一个附加索引:
CREATE INDEX ON table1 (some_id, some_timestamp);
那么您很有可能获得对两个索引的扫描位图或,这应该比过滤器删除超过 500 万行要快得多。
【讨论】:
这就是我所说的“不再命中旧索引(some_id 和 some_timestamp DESC 上的 btree)”的意思。我已经在这些列上建立了索引,但新索引到位后它不再受到打击。 那么问题是PostgreSQL错误估计了结果行数并决定不使用其他索引。我不知道 PostgreSQL 是否为三元组收集有意义的统计信息。ANALYZE
设置较高的default_statistics_target
有帮助吗?如果没有,那我就别无选择了。
在碰到default_statistics_target
之后运行良好的ANALYZE
确实做了一些事情。现在规划器正在回退到旧的 btree 索引,根本不使用三元组。很有意思。我想知道规划器是否不够聪明,无法同时使用这两个索引。
@user3112658 据推测,旧的 btree 索引正用于按 ORDER BY 的顺序获取数据,然后在满足 LIMIT 时提前停止。这种类型的索引扫描(提供顺序)不能与同一张表上的第二个索引一起使用以提供选择性。这不仅仅是计划者。执行者目前不知道如何实现,所以规划没有意义。【参考方案2】:
我稍微修改了我的查询(连接空白)以绕过三元索引,并验证了慢查询返回到 btree 索引和
这很难看,但几乎可以肯定它是解决方案。也许我们可以修复一些问题,以便 PostgreSQL 的某些(远)未来版本不需要此类修复,但这对您今天没有帮助。
Bitmap Index Scan on trgm_idx (cost=0.00..345.00 rows=667 width=0) (actual time=1325.867..1325.867 rows=5833670 loops=1)
这个估计显然是错误的,这可能是问题的根源。但是 trigram 索引和 ILIKE 查询对实际查询文本非常敏感。只有(显然)匿名值 '%some_text%'
不足以进行更深入的研究。
另一种方法是使用 GiST 而不是 GIN。 GiST 索引可以有益地同时使用多个列。
CREATE INDEX CONCURRENTLY IF NOT EXISTS trgm_idx ON table1 USING gist
(some_id, some_timestamp, (COALESCE(field1, '') || ' ' || COALESCE(field2, '') || COALESCE(field3, '') || ' ' || COALESCE(field4, '') || ' ' || COALESCE(field5, '') || ' ' || field6::text) gist_trgm_ops);
您可能只想包含前两列中的一列或另一列,具体取决于每列提供的选择性。您可能需要做一些实验。
我不喜欢将 GiST 与 pg_trgm 一起使用,我发现性能(在使用和构建中)不稳定。但是对于这种方式的有用的多列索引,您别无选择。
无论如何,您的索引已经运行良好,只是没有使用它。制作 GiST 索引可能“足够好”以诱使查询远离 GIN,但它可能只会让其他查询选择错误的计划。
另一种方法是使用 RUM 索引,这允许您使用存储在索引中的数据进行排序,但我认为您必须 write some code 才能使它们支持 pg_trgm。
【讨论】:
以上是关于Postgres 文本搜索与 GIN 索引并在其他列上排序 DESC的主要内容,如果未能解决你的问题,请参考以下文章
未使用 Postgres `gin_trgm_ops` 索引