web-dev-qa-db-ja.com

PostgreSQLを使用してアンカー文字列の出現をどのようにカウントしますか?

このようにテーブルの行の列に文字列がある場合

1 2 2 2 2 2 2

文字列内の部分文字列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
3
Evan Carroll

あなたはこれで解決することができます

  1. [〜#〜] fastest [〜#〜]は、pl/Perlを必要とするため、このリストの最後に配置したpl/Perlメソッドでした。ほとんどのワークロードではおそらく必要ありません。
  2. [〜#〜] fast [〜#〜]以下に説明するパターンなどの文字列関数

    length(str) - length(*replace(str, replaceStr))
      / length(replaceStr)
    
  3. 文字列から配列に変換するもの。
  4. [〜#〜] slow [〜#〜]文字列からテーブルに変換するもの。

可能な解決策

ストリング

lengthregexp_replaceの使用

ほとんどのRDBMSは、このような部分文字列の出現を計算する方法を提供しています。

SELECT length(data) - length(replace(data, '2', ''))
  / length('2')
FROM foo;

アンカーがないと、something-space-delimited内の部分文字列を置き換えているかどうかわからないため、このメソッドはここでは適用できません。例として、上記は2329を置き換えます。これを修正するには、regexp_replaceを使用して部分文字列を固定します。

SELECT length(data) - length(regexp_replace(data, '\m2\M', '', 'g'))
  / length('2')
FROM foo;

より複雑にすることもできますが、単純なスペース( '')で分割していないため、 次のように異なる長さのサブストリングに対応することもできます)この質問 。これが/ length('2')を明示的に含める理由です。これは何もしないことになりますが、1文字より長いものを検索する場合は必須です。

SELECT length(data) - length(regexp_replace(data, '\m42\M', '', 'g'))
  / length('42')
FROM foo;

ARRAY []の使用

ARRAY[]に分割する

ここでは、1つの一致を減算して文字列を2つのフラグメントに分割する必要があるため、発生はフラグメントカウントより1つ少なくなります。このxyxでのyの分割は{'x', 'x'}を生成し、yの発生に対応する長さを1にする必要があります。

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);

または、string_to_arrayを使用して、スペースで区切られたものを分離し、一致をカウントすることもできます。

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

TABLEの使用

regexp_split_to_tableでテーブルに分割する

ここでは、正規表現をテーブルに分割しています。このメソッドでは、GROUP BYcount()を使用しています。

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)

そして、そこから通常の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

regex_matchesの使用

ここでは、分割を回避し、代わりにWord境界に\mおよび\Mアンカーを使用します。

SELECT count(*)
FROM foo
CROSS JOIN LATERAL regexp_matches(data, '\m2\M', 'g');

手続き言語

Perl

この方法は全体的に最速であることが判明しました。

CREATE LANGUAGE plperl

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

要約とパフォーマンスへの影響

同じ形式のデータに従うと、パフォーマンスへの影響が次のように得られます。

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
;

これらの制約の下で、plperlを使用した手続き型メソッドが最速であることがわかりました。次に、次のものが最速のネイティブメソッドであることがわかりました。

length(str) - regexp_replace(str, replacement, g)
  / length(replacement)

文字列をアンカーする必要がないことを除いて、試行錯誤された文字列置換の方法は、いちばん不格好かもしれませんが、いちばん速くて最も効率的なネイティブ方法です。

length(str) - replace(str, replacement)
  / length(replacement)

つまり、ARRAY[]メソッドは、テーブルに分割するよりも大幅に高速です。

4
Evan Carroll

完全一致を検索(スタンドアロン2対。 2 以内に 329


@AndriyMによる改善

with      foo (data) as (select '1 2 329 2 2 2 272 2 22 2 2'::text)      

select    (
              length (data) + 2
           -  length (replace (replace (' ' || data || ' ',' 2 ','  '),' 2 ','  '))
           ) / length('2')

from    foo
;

7


元のソリューション

with      foo (data) as (select '1 2 329 2 2 2 272 2 22 2 2'::text)     

select    (
                length (replace (data,' ','  ')) + 2 
              - length (replace (' ' || replace (data,' ','  ') || ' ', ' 2 ', ''))
          )  /  length (' 2 ')  

from      foo
;

7