既然您知道LIKE (~~) 查询速度快而RegEx (~) 查询速度慢,那么最简单的解决方案是将这两个条件结合起来(\m / \M 是@987654321 @到[[:<:]]/[[:>:]]):
SELECT value FROM blocked_items
WHERE lower(unaccent('my input text')) ~~ ('%'||value||'%')
AND lower(unaccent('my input text')) ~ ('\m'||value||'\M')
LIMIT 1;
这样,快速查询条件过滤掉大部分行,然后慢速条件丢弃剩余的行。
假设value 已经标准化,我正在使用更快的区分大小写的运算符。如果不是这种情况,请删除(然后是多余的)lower(),并像在原始查询中一样使用区分大小写的版本。
在我的具有 370k 行的测试集上,将查询从 6s(暖)加速到 90ms:
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.00..1651.85 rows=1 width=10) (actual time=89.702..89.702 rows=1 loops=1)
-> Seq Scan on blocked_items (cost=0.00..14866.61 rows=9 width=10) (actual time=89.701..89.701 rows=1 loops=1)
Filter: ((lower(unaccent('my input text'::text)) ~~ (('%'::text || value) || '%'::text)) AND (lower(unaccent('my input text'::text)) ~ (('\m'::text || value) || '\M'::text)))
Rows Removed by Filter: 153281
Planning Time: 0.097 ms
Execution Time: 89.717 ms
(6 rows)
但是,我们仍在进行全表扫描,性能会因表中的位置而异。
理想情况下,我们可以使用索引在几乎恒定的时间内回答查询。
让我们重写查询以使用Text Search Functions and Operators:
SELECT value FROM blocked_items
WHERE to_tsvector('simple', unaccent('my input text'))
@@ phraseto_tsquery('simple', value)
LIMIT 1;
首先我们将输入拆分为search vectors,然后检查被阻止的短语是否与这些向量匹配。
测试查询大约需要 440 毫秒 - 从我们的组合查询中退一步:
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.00..104.01 rows=1 width=10) (actual time=437.761..437.761 rows=1 loops=1)
-> Seq Scan on blocked_items (cost=0.00..192516.05 rows=1851 width=10) (actual time=437.760..437.760 rows=1 loops=1)
Filter: (to_tsvector('simple'::regconfig, unaccent('my input text'::text)) @@ phraseto_tsquery('simple'::regconfig, value))
Rows Removed by Filter: 153281
Planning Time: 0.063 ms
Execution Time: 437.772 ms
(6 rows)
由于我们不能使用tsvector @@ tsquery 来索引tsquery,我们可以再次重写查询以检查被阻止的短语是否是使用tsquery @> tsquery Text Search Operator 的输入短语的子查询,然后可以使用来自GiST Operator Classes 的tsquery_ops 进行索引:
CREATE INDEX blocked_items_search ON blocked_items
USING gist (phraseto_tsquery('simple', value));
ANALYZE blocked_items; -- update query planner stats
SELECT value FROM blocked_items
WHERE phraseto_tsquery('simple', unaccent('my input text'))
@> phraseto_tsquery('simple', value)
LIMIT 1;
查询现在可以使用索引扫描,并且需要 20ms 处理相同的数据。
由于 GiST 是有损索引,查询时间可能会根据需要重新检查的次数而有所不同:
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.54..4.23 rows=1 width=10) (actual time=19.215..19.215 rows=1 loops=1)
-> Index Scan using blocked_items_search on blocked_items (cost=0.54..1367.01 rows=370 width=10) (actual time=19.214..19.214 rows=1 loops=1)
Index Cond: (phraseto_tsquery('simple'::regconfig, value) <@ phraseto_tsquery('simple'::regconfig, unaccent('my input text'::text)))
Rows Removed by Index Recheck: 4028
Planning Time: 0.093 ms
Execution Time: 19.236 ms
(6 rows)
使用全文搜索的一大优势是您现在可以通过搜索配置 (regconfig) 使用特定语言的单词匹配。
以上查询都使用默认的'simple' regconfig 来匹配原始查询的行为。通过切换到'english',您还可以匹配同一个词的变体,例如 cat 和 cats (stemming) 以及没有意义的普通词,例如 the em> 或 my 将被忽略 (stop words)。