如何提高 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:step
和 payload
表也定义了 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
字段添加索引;基于hash
和btree
。它们中的每一个仍然会生成一个成本超过 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(子字符串)查询?