web-dev-qa-db-ja.com

LIKE、SIMILAR TO、またはPostgreSQLの正規表現によるパターンマッチング

BまたはDで始まる人の名前を探す簡単なクエリを作成する必要がありました。

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

これをよりパフォーマンスの高いものに書き換える方法はないかと思っていました。だから私はorと/またはlikeを避けることができますか?

103
Lucas Kauffman

あなたのクエリはほとんど最適です。構文はそれほど短くならず、クエリはそれほど速くなりません:

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

本当に構文を短くしたい場合は、branchesで正規表現を使用します

...
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]を含むnot(少なくともPostgreSQL 9.0のテストでは)。

トライグラム一致またはテキスト検索では、特別なGINまたはGistインデックスを使用します。

パターンマッチング演算子の概要

  • LIKE~~)はシンプルで高速ですが、機能に制限があります。
    ILIKE~~*)大文字と小文字を区別しないバリアント。
    pg_trgmは、両方のインデックスサポートを拡張します。

  • ~ (正規表現の一致)は強力ですが、より複雑であり、基本的な表現以外の場合は遅くなる可能性があります。

  • SIMILAR TOpointlessです。 LIKEと正規表現の独特の混血。私はそれを使用することはありません。下記参照。

  • は、追加モジュールpg_trgmによって提供される「類似性」演算子です。下記参照。

  • @@ はテキスト検索演算子です。下記参照。

pg_trgm-トライグラム一致

PostgreSQL 9.1以降、拡張機能 pg_trgm を使用して、anyLIKE/ILIKEパターン(および~を使用した単純な正規表現パターン)のインデックスサポートを提供できます。 GINまたはGistインデックス。

詳細、例、リンク:

pg_trgmこれらの演算子も提供します

  • %-「類似性」演算子
  • <%(commutator:%>)-Postgres 9.6以降の「Word_similarity」演算子
  • <<%(commutator:%>>)-Postgres 11以降の「strict_Word_similarity」演算子

テキスト検索

インフラストラクチャタイプとインデックスタイプが別個の特別なタイプのパターンマッチングです。辞書とステミングを使用し、特に自然言語の場合、ドキュメント内の単語を検索するための優れたツールです。

Prefix matchingもサポートされています:

また、Postgres 9.6以降のphrase search

マニュアルの はじめに および 演算子と関数の概要 を検討してください。

ファジー文字列マッチングのための追加ツール

追加のモジュール fuzzystrmatch はいくつかのオプションを提供しますが、一般的にパフォーマンスは上記のすべてよりも劣ります。

特に、levenshtein()関数のさまざまな実装が役立つ場合があります。

なぜ正規表現(~)は常にSIMILAR TOより速いのですか?

答えは簡単です。 SIMILAR TO式は内部で正規表現に書き換えられます。したがって、すべてのSIMILAR TO式について、少なくとも高速正規表現が1つあります(これにより、式を書き換えるオーバーヘッドが節約されます)。 SIMILAR TOeverを使用してもパフォーマンスは向上しません。

そして、LIKE~~)で実行できる単純な式は、LIKEを使用すると高速になります。

SIMILAR TOは、SQL標準の初期ドラフトになってしまったため、PostgreSQLでのみサポートされています。彼らはまだそれを取り除いていません。しかし、それを削除し、代わりに正規表現の一致を含める計画があります-そう私は聞いた。

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_pattern_opsを使用するvarchar型またはbpchar_pattern_opsを使用するcharのインデックスにも当てはまります。

したがって、元の質問に適用すると、これは最速の方法です。

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

~または~~の単純な使用に対する利点はごくわずかです。パフォーマンスが最も重要な要件でない場合は、標準の演算子をそのまま使用する必要があります。問題にすでに到達しているものに到達します。

171

テーブルに列を追加するのはどうでしょう。実際の要件に応じて:

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

条件の式に一致するクエリは、このインデックスを利用できます。

この方法では、データが作成または修正されたときにパフォーマンスヒットが発生するため、アクティビティが少ない環境(つまり、読み取りより書き込みがはるかに少ない)にのみ適している可能性があります。

11
onedaywhen

あなたはできます try

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
8
Martin Smith

イニシャルのチェックには、_"char"_へのキャスト(二重引用符付き)をよく使用します。ポータブルではありませんが、非常に高速です。内部的には、単純にテキストをデトーストして最初の文字を返します。また、型が1バイトの固定長であるため、 "char"比較演算は非常に高速です。

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

_"char"_へのキャストは、@ Sole021によるascii()ソリューションよりも高速ですが、UTF8互換ではない(またはその他のエンコーディング)ので、最初のバイトを返すだけなので、比較が単純な古い7ビットASCII文字との比較である場合に使用されます。

非常に古い質問ですが、この問題の別の迅速な解決策を見つけました:

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

関数ascii()は文字列の最初の文字のみを参照するため。

2
Sole021

同様のパフォーマンスの問題に直面して過去に私が行ったことは、最後の文字のASCII文字をインクリメントし、BETWEENを実行することです。次に、サブセットに対して最高のパフォーマンスを取得します。もちろん、これは特定の状況でのみ機能しますが、たとえば名前で検索している超大規模なデータセットの場合、パフォーマンスはひどいものから許容できるものまでになります。

2
Mel Padden

そのような場合に対処するには、まだ言及されていない2つの方法があります。

  1. 部分的(またはパーティション化-フルレンジ用に手動で作成した場合)インデックス-データのサブセットのみが必要な場合(たとえば、一部のメンテナンス中または一部のレポートのために一時的)に最も役立ちます。

    CREATE INDEX ON spelers WHERE name LIKE 'B%'
    
  2. テーブル自体をパーティション化する(パーティション化キーとして最初の文字を使用)-この手法は、PostgreSQL 10以降(パーティション分割がそれほど簡単ではない)および11以降(クエリ実行中のパーティションプルーニング)で検討する価値があります。

さらに、テーブル内のデータが並べ替えられている場合、 BRIN index (最初の文字よりも)を使用することでメリットが得られます。

1
Tomasz Pala