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` 索引

Postgres:强制分析器使用位图扫描而不是索引扫描

postgres中大型数据库的索引

PostgreSQL GIN 索引比 pg_trgm 的 GIST 慢?

LIKE查询的最佳Postgres文本索引?

Postgresql GIN索引