如何提高 PostgreSQL LIKE %text% 查询性能

Posted

技术标签:

【中文标题】如何提高 PostgreSQL LIKE %text% 查询性能【英文标题】:How to improve PostgreSQL LIKE %text% query performance 【发布时间】:2020-11-25 12:04:31 【问题描述】:

我有 3 个表:请求、步骤和有效负载。每个请求有 N 个步骤(所以是一对多),每个步骤有 1 个有效载荷(一对一)。

现在,每当我想按有效负载主体进行过滤时,执行时间都很糟糕。

这是简化的请求:

select rh.id
from request_history_step_payload rhsp
join request_history_step rhs on rhs.id =  rhsp.step_id
join request_history rh on rhs.request_id = rh.id
where rh.id> 35000 and rhs.step_type = 'CONSUMER_REQUEST' and rhsp.payload like '%09141%'

这是EXPLAIN ANALYZE(在VACUUM ANALYZE 之后立即运行):

Nested Loop  (cost=0.71..50234.28 rows=1 width=8) (actual time=120.093..2494.929 rows=12 loops=1)
  ->  Nested Loop  (cost=0.42..50233.32 rows=3 width=8) (actual time=120.083..2494.900 rows=14 loops=1)
        ->  Seq Scan on request_history_step_payload rhsp  (cost=0.00..50098.28 rows=16 width=8) (actual time=120.063..2494.800 rows=25 loops=1)
              Filter: ((payload)::text ~~ '%09141%'::text)
              Rows Removed by Filter: 164512
        ->  Index Scan using request_history_step_pkey on request_history_step rhs  (cost=0.42..8.44 rows=1 width=16) (actual time=0.003..0.003 rows=1 loops=25)
              Index Cond: (id = rhsp.step_id)
              Filter: ((step_type)::text = 'CONSUMER_REQUEST'::text)
              Rows Removed by Filter: 0
  ->  Index Only Scan using request_history_pkey on request_history rh  (cost=0.29..0.32 rows=1 width=8) (actual time=0.001..0.001 rows=1 loops=14)
        Index Cond: ((id = rhs.request_id) AND (id > 35000))
        Heap Fetches: 0
Planning Time: 0.711 ms
Execution Time: 2494.964 ms

现在,我想以某种方式建议计划者将LIKE 操作作为最后一站,但想不出任何方法。我已经尝试了各种连接,并对它们重新排序,并将条件移动到 ON 子句和 WHERE 子句之间,反之亦然。一切都无济于事!无论如何,它首先查看payload 表中的所有行,这显然是最糟糕的想法,考虑到其他条件,这可能会大大减少需要应用的LIKE 操作的数量。所以,我希望它会首先应用id 条件,它已经整理出所有记录的90%;然后它将应用step_type 条件,这将像其余的 85% 一样进行排序;因此只会将LIKE 条件应用于不到 5% 的所有有效负载。

我会怎么做呢?我正在使用 Postgres 11。

UPD:有人建议我为这些表添加索引信息,所以:

request_history - 在 id 字段上有 2 个唯一索引(我不知道为什么有 2 个) request_history_step - 有 2 个唯一索引,都在 id 字段上 request_history_step_payload - 在 id 字段上有 1 个唯一索引

UPD2:steppayload 表也定义了 FK(分别在 payload.step_id->step.id 和 step.request_id -> request_id 上)

我还尝试了几个(简化的)查询 w/sub-SELECT:

explain analyze select rhs.id from request_history_step rhs
join (select step_id from request_history_step_payload rhsp where rhsp.payload like '%09141%') rhsp on rhs.id = rhsp.step_id 
where rhs.step_type = 'CONSUMER_REQUEST';

explain analyze select rhsp.step_id from request_history_step_payload rhsp
join (select id from request_history_step rhs where rhs.step_type = 'CONSUMER_REQUEST') rhs on rhs.id = rhsp.step_id 
where rhsp.payload like '%09141%';


explain analyze select rhsp.step_id from request_history_step_payload rhsp
where rhsp.step_id  in (select id from request_history_step rhs where rhs.step_type = 'CONSUMER_REQUEST')  
and rhsp.payload like '%09141%';

(也使用JOIN LATERAL 而不仅仅是JOIN)- 每个 都给出了完全相同的计划,即嵌套循环,在它的嵌套循环中,以及“外部”(第一个)该循环中的一条腿是 SeqScan。这..让我发疯。为什么它要对尽可能多的行集执行最昂贵的操作??

UPD3:受原始问题下的 cmets 启发,我进行了一些进一步的实验。我用更简单的查询解决了:

select rhs.request_id 
from request_history_step_payload rhsp
join request_history_step rhs on rhs.id =  rhsp.step_id
where rhs.step_type = 'CONSUMER_REQUEST' and rhsp.payload like '%09141%';

现在,它的执行计划基本上和原来的一样,只是少了一个“嵌套循环”。

现在,我为payload.step_id 添加了索引:

create index request_history_step_payload_step_id on request_history_step_payload(step_id);

运行VACUUM ANALYZE;使用explain analyze 运行查询 - 没有任何变化。嗯。

现在我已经运行set enable_seqscan to off现在我们在谈

Gather  (cost=1000.84..88333.90 rows=3 width=8) (actual time=530.273..589.650 rows=14 loops=1)
  Workers Planned: 1
  Workers Launched: 1
  ->  Nested Loop  (cost=0.84..87333.60 rows=2 width=8) (actual time=544.639..580.608 rows=7 loops=2)
        ->  Parallel Index Scan using request_history_step_pkey on request_history_step rhs  (cost=0.42..15913.04 rows=20867 width=16) (actual time=0.029..28.667 rows=17620 loops=2)
              Filter: ((step_type)::text = 'CONSUMER_REQUEST'::text)
              Rows Removed by Filter: 64686
        ->  Index Scan using request_history_step_payload_step_id on request_history_step_payload rhsp  (cost=0.42..3.41 rows=1 width=8) (actual time=0.031..0.031 rows=0 loops=35239)
              Index Cond: (step_id = rhs.id)
              Filter: ((payload)::text ~~ '%09141%'::text)
              Rows Removed by Filter: 1
Planning Time: 0.655 ms
Execution Time: 589.688 ms

现在,随着 SeqScan 的成本飙升,我认为我们可以看到问题的要点:这个执行计划的成本被认为高于原来的(88k vs 50k),尽管执行时间实际上要短得多(590ms vs 2700ms)。这显然是 Postgre planner 一直选择“SeqScan first”的原因,尽管我努力说服他不这样做。

我也尝试为step.step_type 字段添加索引;基于hashbtree。它们中的每一个仍然会生成一个成本超过 50k 的计划,因此将 enable_seqscan 设置为 on(默认值),计划者将始终忽略这些。

有人知道对此有什么缓解措施吗?我担心正确的解决方案可能需要更改计划变量的权重,当然我不倾向于这样做。但很高兴听到任何建议!

UPD4:现在我玩得更多了,我可以报告更多结果(enable_seqscan 设置为on

这个很慢,应用seqscan,即使step.step_type上有索引:

explain analyze 
select rhsp.step_id 
from (select request_id, id from request_history_step rhs2 where rhs2.step_type = 'CONSUMER_REQUEST') rhs
join request_history_step_payload rhsp on rhs.id =  rhsp.step_id
where rhsp.payload like '%09141%';

这是基于 O. Jones 的建议,仍然很慢:

explain analyze
with rhs as (select request_id, id from request_history_step rhs2 where rhs2.step_type = 'CONSUMER_REQUEST') 
select rhsp.step_id from request_history_step_payload rhsp 
join rhs on rhs.id = rhsp.step_id 
where rhsp.payload like '%09141%';

但是这个稍作修改,

explain analyze
with rhs as (select request_id, id from request_history_step rhs2 where rhs2.step_type = 'CONSUMER_REQUEST') 
select rhsp.step_id   
from request_history_step_payload rhsp 
join rhs on rhs.id = rhsp.step_id 
where rhsp.step_id  in (select id from rhs) and rhsp.payload like '%09141%';

它的执行计划是:

Hash Join  (cost=9259.55..10097.04 rows=2 width=8) (actual time=1157.984..1162.199 rows=14 loops=1)
  Hash Cond: (rhs.id = rhsp.step_id)
  CTE rhs
    ->  Bitmap Heap Scan on request_history_step rhs2  (cost=1169.28..6918.06 rows=35262 width=16) (actual time=3.899..19.093 rows=35241 loops=1)
          Recheck Cond: ((step_type)::text = 'CONSUMER_REQUEST'::text)
          Heap Blocks: exact=3120
          ->  Bitmap Index Scan on request_history_step_step_type_hash  (cost=0.00..1160.46 rows=35262 width=0) (actual time=3.047..3.047 rows=35241 loops=1)
                Index Cond: ((step_type)::text = 'CONSUMER_REQUEST'::text)
  ->  CTE Scan on rhs  (cost=0.00..705.24 rows=35262 width=8) (actual time=3.903..5.976 rows=35241 loops=1)
  ->  Hash  (cost=2341.39..2341.39 rows=8 width=16) (actual time=1153.976..1153.976 rows=14 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 9kB
        ->  Nested Loop  (cost=793.81..2341.39 rows=8 width=16) (actual time=104.170..1153.919 rows=14 loops=1)
              ->  HashAggregate  (cost=793.39..795.39 rows=200 width=8) (actual time=33.315..44.875 rows=35241 loops=1)
                    Group Key: rhs_1.id
                    ->  CTE Scan on rhs rhs_1  (cost=0.00..705.24 rows=35262 width=8) (actual time=0.001..23.590 rows=35241 loops=1)
              ->  Index Scan using request_history_step_payload_step_id on request_history_step_payload rhsp  (cost=0.42..7.72 rows=1 width=8) (actual time=0.031..0.031 rows=0 loops=35241)
                    Index Cond: (step_id = rhs_1.id)
                    Filter: ((payload)::text ~~ '%09141%'::text)
                    Rows Removed by Filter: 1
Planning Time: 1.318 ms
Execution Time: 1162.618 ms

同样,成本大幅下降,而执行时间却没有那么多

【问题讨论】:

为了让条件like '%09141%' 使用索引,您需要创建一个 tirgram 索引 @a_horse_with_no_name 这不是我要解决的问题;我完全理解这样的事情需要很长时间;我不太明白为什么它被应用于整个表,而不仅仅是行的子集 将 enable_seqscan 设置为 off 的计划是什么? 您应该在request_history_step_payload (step_id)request_history_step(request_id) 上有一个索引(可能是外键) 我的意思是在这些列上创建索引。外键约束不会自动创建索引 【参考方案1】:

从您的计划看来,您对request_history_step_payload 操作的step_type 谓词没有太大帮助。

因此,让我们尝试使用包含包含列的文本(三元组)索引来帮助该搜索步骤更快地进行。

 CREATE INDEX CONCURRENTLY rhsp_type_payload 
     ON request_history_step_payload
  USING GIN (step_type gin_trgm_ops)
INCLUDE (rhs_step_type, rhs_step_id);

这可能会有所帮助。试一试。

当您拥有该索引时,您还可以尝试像这样重新处理您的查询:

select rh.id
from (  select step_id
          from request_history_step_payload
         where  rhs.step_type = 'CONSUMER_REQUEST'
           and rhsp.payload like '%09141%'
     ) rhsp
join request_history_step rhs on rhs.id =  rhsp.step_id
join request_history rh on rhs.request_id = rh.id
where rh.id> 35000

这会将您对该 request_history_step_payload 表的搜索移动到子查询中。您可以单独优化子查询,因为您尝试让所有这些都以足够快的速度为您的应用程序运行。

并且,删除所有重复的索引。他们无缘无故减慢了 INSERT 和 UPDATE 操作。

【讨论】:

谢谢,@o-jones!我已在我的答案中添加了 UPD3 部分,您可以添加任何内容吗?我目前不能使用三元组(这需要更改数据库,这必须等到我们的数据库管理员从假期回来),但仍然感觉这里有一些可以改进的地方。你怎么看? 当然,WHERE col LIKE '%constant%' 是一个臭名昭著的性能反模式。 postgreSql 有 trigram 索引来帮助减轻它。但你最好的选择永远是避免它。如果您认为您的系统会扩大规模,那么明智的做法是尽快重组您的数据。 不得不承认,这现在的工作速度快得离谱。太酷了,我什至认识写这个扩展的人 - Oleg Bartunov。必须给他几瓶啤酒,如果他是那个会喝的人(他不是)

以上是关于如何提高 PostgreSQL LIKE %text% 查询性能的主要内容,如果未能解决你的问题,请参考以下文章

在 python 中,如何执行缓解 SQL 注入的 postgresql 'LIKE %name%' 查询?

如何在 Postgresql 中使用 LIKE 和 ANY?

如何使用 PredicateBuilder、EF Core 5 和 Postgresql 10+ 执行不区分大小写和重音的 LIKE(子字符串)查询?

怎样提高SQLSERVER的like查询速度?

PostgreSQL 反向 LIKE

PostgreSQL LIKE 子句