如何使用 PostgreSQL 计算锚定字符串的出现次数?

Eva*_*oll 5 postgresql count string substring

如果我在这样的表中的行的列中有一个字符串

1 2 2 2 2 2 2
Run Code Online (Sandbox Code Playgroud)

我如何计算字符串2中子字符串的出现次数。假设除了空格分隔符 之外没有其他任何内容" "

为此,我们将数字视为子字符串

样本数据

CREATE TABLE foo
  AS
    SELECT 1 AS id, '1 2 2 2 2 2 2'::text AS data;

TABLE foo
 id |     data      
----+---------------
  1 | 1 2 2 2 2 2 2
Run Code Online (Sandbox Code Playgroud)

Eva*_*oll 8

你可以用以下方法解决这个问题

  1. 最快的是 pl/perl 方法,我将其放在该列表的最后,因为它需要 pl/perl,并且大多数工作负载可能不需要。
  2. FAST字符串函数,例如下面解释的模式之一

    length(str) - length(*replace(str, replaceStr))
      / length(replaceStr)
    
    Run Code Online (Sandbox Code Playgroud)
  3. 从字符串转换为数组的东西。
  4. SLOW从字符串转换为表的东西。

可能的解决方案

细绳

使用lengthregexp_replace

大多数 RDBMS 提供了一些计算子字符串出现次数的方法,如下所示:

SELECT length(data) - length(replace(data, '2', ''))
  / length('2')
FROM foo;
Run Code Online (Sandbox Code Playgroud)

此方法在这里不适用,因为如果没有锚点,我们无法确定是否要替换以空格分隔的内容内的子字符串。例如,上面的内容替换了2in 329。我们可以通过使用regexp_replace锚定子字符串来解决这个问题。

SELECT length(data) - length(regexp_replace(data, '\m2\M', '', 'g'))
  / length('2')
FROM foo;
Run Code Online (Sandbox Code Playgroud)

因为我们不是简单的空格('')上进行分割,尽管我们可以更复杂地进行分割,所以我们可能还想容纳不同长度的子字符串,就像这个问题一样。这就是为什么我们明确包含/ length('2'). 这减少了一项无操作,但如果我们要搜索的内容长于一个字符,那么它就是必需的。

SELECT length(data) - length(regexp_replace(data, '\m42\M', '', 'g'))
  / length('42')
FROM foo;
Run Code Online (Sandbox Code Playgroud)

使用数组[]

分裂成一个ARRAY[]

在这里,我们必须减去一个匹配项,将字符串分割成两个片段,因此出现次数比片段计数少 1:对 , 的xyx分割y产生{'x', 'x'},并且我们希望长度与1的出现次数相对应y

SELECT array_length(x, 1) - 1
FROM foo
CROSS JOIN LATERAL regexp_split_to_array(data, '\m2\M') AS t(x);
-- un-anchored version for reference.
-- CROSS JOIN LATERAL string_to_array(data, '2') AS t(x);
Run Code Online (Sandbox Code Playgroud)

或者,我们可以使用string_to_array空格分隔的内容,然后计算匹配数,

SELECT id, array_length(array_positions(x, '2'), 1)
FROM foo
CROSS JOIN LATERAL string_to_array(data, ' ') AS t(x);
Run Code Online (Sandbox Code Playgroud)

使用表格

拆分成一个表regexp_split_to_table

在这里,我们将正则表达式拆分为一个表。在此方法中,您使用的是GROUP BYand count()

SELECT id, x
FROM foo
CROSS JOIN LATERAL regexp_split_to_table(data, ' ')
  AS t(x);

 id | x 
----+---
  1 | 1
  1 | 2
  1 | 2
  1 | 2
  1 | 2
  1 | 2
  1 | 2
(7 rows)
Run Code Online (Sandbox Code Playgroud)

并且,您可以从那里运行常规 SQL。

SELECT id, x, count(*)
FROM (
    SELECT id, x
    FROM foo
    CROSS JOIN LATERAL regexp_split_to_table(data, ' ')
      AS t(x)
) AS t(id,x)
GROUP BY id, x;

 id | x | count 
----+---+-------
  1 | 1 |     1
  1 | 2 |     6
Run Code Online (Sandbox Code Playgroud)

使用regex_matches

在这里,我们摆脱了分割,而是使用\m, 和\M锚点作为单词边界。

SELECT count(*)
FROM foo
CROSS JOIN LATERAL regexp_matches(data, '\m2\M', 'g');
Run Code Online (Sandbox Code Playgroud)

过程语言

珀尔

事实证明,这种方法是总体上最快的,

CREATE LANGUAGE plperl

CREATE FUNCTION count_occurances(inputStr text, regex text)
RETURNS smallint
AS $BODY$
  scalar @{[ $_[0] =~ m/$_[1]/g ]} 
$BODY$
LANGUAGE plperl
IMMUTABLE;
Run Code Online (Sandbox Code Playgroud)

总结和性能影响

遵循相同的数据格式,可以通过以下方式获得性能影响

CREATE TABLE foo
AS
  SELECT
    1 AS id,
    array_to_string(
      ARRAY(SELECT trunc(random()*100+1)::int % 100 FROM generate_series(1,5000) AS t(x)),
      ' '
    ) AS data
;
Run Code Online (Sandbox Code Playgroud)

在这些限制下,我发现使用 plperl 的过程方法是最快的。接下来我发现以下是最快的本机方法,

length(str) - regexp_replace(str, replacement, g)
  / length(replacement)
Run Code Online (Sandbox Code Playgroud)

请记住,除了需要锚定字符串之外,经过验证的真实字符串替换方法仍然是最快、最有效的本机方法,无论它可能有多笨拙,

length(str) - replace(str, replacement)
  / length(replacement)
Run Code Online (Sandbox Code Playgroud)

也就是说,该ARRAY[]方法比拆分到表要快得多