在PostgreSQL中使用LIKE,SIMILAR TO或正则表达式进行模式匹配


94

我必须编写一个简单的查询,以查找以B或D开头的人的名字:

SELECT s.name 
FROM spelers s 
WHERE s.name LIKE 'B%' OR s.name LIKE 'D%'
ORDER BY 1

我想知道是否有办法重写它以提高性能。所以我可以避免or和/或like


你为什么要重写?性能?整洁吗?被s.name索引了吗?
马丁·史密斯

我想为性能而写,未对s.name进行索引。
卢卡斯·考夫曼

8
当您搜索时没有前导通配符,也没有选择任何其他列时,name如果您关心性能,则在此处使用索引可能会很有用。
马丁·史密斯

Answers:


161

您的查询几乎是最佳的。语法不会变短,查询不会变快:

SELECT name
FROM   spelers
WHERE  name LIKE 'B%' OR name LIKE 'D%'
ORDER  BY 1;

如果您确实想缩短语法,请使用带有分支的正则表达式:

...
WHERE  name ~ '^(B|D).*'

或稍快一点,使用一个字符类

...
WHERE  name ~ '^[BD].*'

SIMILAR TO对我而言,没有索引的快速测试所产生的结果比任何一种情况下都更快。
有了适当的B树索引,就可以在LIKE这场比赛中胜出几个数量级。

阅读手册中有关模式匹配的基础知识。

性能指标

如果您担心性能,请为更大的表创建这样的索引:

CREATE INDEX spelers_name_special_idx ON spelers (name text_pattern_ops);

使这种查询的速度提高几个数量级。特殊注意事项适用于特定于语言环境的排序顺序。在手册中阅读有关操作符类的更多信息。如果您使用标准的“ C”语言环境(大多数人不使用),则将使用纯索引(具有默认的运算符类)。

这样的索引仅适用于左锚模式(从字符串开头开始匹配)。

SIMILAR TO或带有基本左锚表达式的正则表达式也可以使用此索引。但包含分支(B|D)或字符类[BD](至少在我对PostgreSQL 9.0的测试中)。

Trigram匹配或文本搜索使用特殊的GIN或GiST索引。

模式匹配运算符概述

  • LIKE~~)简单,快速,但功能有限。
    ILIKE~~*)不区分大小写的变体。
    pg_trgm扩展了两者的索引支持。

  • ~ (正则表达式匹配)功能强大但更复杂,除基本表达式外,其他任何东西的运行速度都可能很慢。

  • SIMILAR TO仅仅是毫无意义的LIKE正则表达式的特殊混血。我从不使用它。见下文。

  • 是附加模块提供的“相似”运算符pg_trgm。见下文。

  • @@是文本搜索运算符。见下文。

pg_trgm-三元组匹配

PostgreSQL 9.1开始,您可以方便扩展使用GIN或GiST索引pg_trgm任何 LIKE / ILIKE模式(以及带有的简单正则表达式模式~)提供索引支持。

详细信息,示例和链接:

pg_trgm还提供以下运算符

  • % -“相似性”运算符
  • <%(换向器:%>)-Postgres 9.6或更高版本中的“ word_similarity”运算符
  • <<%(换向器:%>>)-Postgres 11或更高版本中的“ strict_word_similarity”运算符

文字搜寻

是与单独的基础结构和索引类型匹配的一种特殊类型的模式。它使用字典和词干,是在文档中查找单词的好工具,特别是对于自然语言。

还支持前缀匹配

从Postgres 9.6开始,还有短语搜索

请考虑手册中介绍以及操作员和功能概述

模糊字符串匹配的其他工具

附加模块Fuzzystrmatch提供了更多选项,但是性能通常不如上述所有。

特别地,levenshtein()功能的各种实现可以是有用的。

为什么正则表达式(~)总是比快SIMILAR TO

答案很简单。SIMILAR TO表达式在内部被重写为正则表达式。因此,对于每个SIMILAR TO表达式,至少有一个更快的正则表达式(这节省了重写表达式的开销)。使用SIMILAR TO ever不会提高性能。

无论如何,可以使用LIKE~~)完成的简单表达式都更快LIKE

SIMILAR TO仅在PostgreSQL中受支持,因为它最终出现在SQL标准的早期草案中。他们仍然没有摆脱它。但是有计划将其删除,而改为包含正则表达式匹配项-或我听说。

EXPLAIN ANALYZE揭示它。自己尝试使用任何桌子!

EXPLAIN ANALYZE SELECT * FROM spelers WHERE name SIMILAR TO 'B%';

揭示了:

...  
Seq Scan on spelers  (cost= ...  
  Filter: (name ~ '^(?:B.*)$'::text)

SIMILAR TO已使用正则表达式(~)重写。

此特定情况的最终性能

EXPLAIN ANALYZE揭示更多。尝试使用上述索引:

EXPLAIN ANALYZE SELECT * FROM spelers WHERE name ~ '^B.*;

揭示了:

...
 ->  Bitmap Heap Scan on spelers  (cost= ...
       Filter: (name ~ '^B.*'::text)
        ->  Bitmap Index Scan on spelers_name_text_pattern_ops_idx (cost= ...
              Index Cond: ((prod ~>=~ 'B'::text) AND (prod ~<~ 'C'::text))

在内部,与未识别语言代码的索引(text_pattern_ops或使用区域C)简单的左锚定表达式用这些文字图案运营商改写为:~>=~~<=~~>~~<~。这是的情况下~~~SIMILAR TO相似。

varchar具有varchar_pattern_opschar具有的类型上的索引也是如此bpchar_pattern_ops

因此,将其应用于原始问题,这是最快的方法

SELECT name
FROM   spelers  
WHERE  name ~>=~ 'B' AND name ~<~ 'C'
    OR name ~>=~ 'D' AND name ~<~ 'E'
ORDER  BY 1;

当然,如果您碰巧要搜索相邻的缩写,则可以进一步简化:

WHERE  name ~>=~ 'B' AND name ~<~ 'D'   -- strings starting with B or C

~或的普通使用相比,收益~~很小。如果性能不是您的首要要求,那么您应该坚持使用标准操作员-得出问题中已有的内容。


OP没有名称索引,但是您是否知道,如果有,他们的原始查询会涉及2个范围搜索和similar一次扫描吗?
马丁·史密斯

2
@MartinSmith:快速测试,EXPLAIN ANALYZE显示2个位图索引扫描。多个位图索引扫描可以相当快速地组合在一起。
Erwin Brandstetter,2012年

谢谢。因此,会有与更换任何milage ORUNION ALL或更换name LIKE 'B%'name >= 'B' AND name <'C'Postgres里?
马丁·史密斯

1
@MartinSmith:UNION不会,但是,是的,将范围合并为一个WHERE子句将加快查询速度。我在答案中添加了更多内容。当然,您必须考虑到您的语言环境。区域设置感知搜索总是较慢。
Erwin Brandstetter,2012年

2
@a_horse_with_no_name:我希望不会。具有GIN索引的pg_tgrm的新功能可用于通用文本搜索。在一开始就进行锚定的搜索已经比以前更快。
Erwin Brandstetter,2012年

11

如何在表中添加一列。根据您的实际要求:

person_name_start_with_B_or_D (Boolean)

person_name_start_with_char CHAR(1)

person_name_start_with VARCHAR(30)

PostgreSQL 在SQL Server中不支持基表中的计算列,但可以通过触发器维护新列。显然,该新列将被索引。

另外,在表达式上使用索引也可以使您便宜。例如:

CREATE INDEX spelers_name_initial_idx ON spelers (left(name, 1)); 

在其条件下与表达式匹配的查询可以利用此索引。

这样,在创建或修改数据时会导致性能下降,因此可能仅适用于低活动性环境(即写入次数少于读取次数)。


8

你可以试试

SELECT s.name
FROM   spelers s
WHERE  s.name SIMILAR TO '(B|D)%' 
ORDER  BY s.name

我不知道上面或您的原始表达式是否在Postgres中都是可修饰的。

如果您创建建议的索引,也将有兴趣了解它与其他选项的比较。

SELECT name
FROM   spelers
WHERE  name >= 'B' AND name < 'C'
UNION ALL
SELECT name
FROM   spelers
WHERE  name >= 'D' AND name < 'E'
ORDER  BY name

1
它奏效了,我得到了1.19的成本,而我只有1.25。谢谢 !
卢卡斯·考夫曼

2

面对类似的性能问题,我过去所做的就是增加最后一个字母的ASCII字符,然后执行BETWEEN。然后,对于LIKE功能的一部分,您将获得最佳性能。当然,它仅在某些情况下有效,但是对于例如您正在搜索名称的超大型数据集,它会使性能从糟糕到可接受。


2

很老的问题,但是我找到了解决这个问题的另一种快速解决方案:

SELECT s.name 
FROM spelers s 
WHERE ascii(s.name) in (ascii('B'),ascii('D'))
ORDER BY 1

由于函数ascii()仅查看字符串的第一个字符。


1
这会在上使用索引(name)吗?
ypercubeᵀᴹ

2

为了检查首字母缩写,我经常使用强制转换为"char"(带有双引号)。它不是便携式的,但速度很快。在内部,它简单地删除文本并返回第一个字符,并且“ char”比较操作非常快,因为类型为1字节固定长度:

SELECT s.name 
FROM spelers s 
WHERE s.name::"char" =ANY( ARRAY[ "char" 'B', 'D' ] )
ORDER BY 1

请注意,强制转换为to "char"的速度比ascii()@ Sole021的速度快,但它与UTF8不兼容(或与此相关的任何其他编码),仅返回第一个字节,因此仅应在与普通旧7比较的情况下使用位ASCII字符。


1

有两种尚未提及的用于处理此类情况的方法:

  1. 局部(或分区-如果手动创建,则是为全范围创建的)索引-仅在需要数据子集时(例如在某些维护期间或某些报告的临时期间)最有用:

    CREATE INDEX ON spelers WHERE name LIKE 'B%'
  2. 对表本身进行分区(使用第一个字符作为分区键)-在PostgreSQL 10+(减轻痛苦的分区)和11+(查询执行过程中的分区修剪)中,这种技术特别值得考虑。

此外,如果对表中的数据进行了排序,则可以使用BRIN索引(在第一个字符上)而受益。


-4

进行单个字符比较可能更快:

SUBSTR(s.name,1,1)='B' OR SUBSTR(s.name,1,1)='D'

1
并不是的。 column LIKE 'B%'比在该列上使用子字符串函数更有效。
ypercubeᵀᴹ
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.