最長の接頭辞を見つけるアルゴリズム


11

テーブルが2つあります。

最初のものは接頭辞を持つテーブルです

code name price
343  ek1   10
3435 nt     4
3432 ek2    2

2つ目は、電話番号を含む通話記録です

number        time
834353212     10
834321242     20
834312345     30

各レコードのプレフィックスから最長のプレフィックスを見つけるスクリプトを作成し、このすべてのデータを次のように3番目のテーブルに書き込む必要があります。

 number        code   ....
 834353212     3435
 834321242     3432
 834312345     343

番号834353212の場合、「8」をトリミングしてから、プレフィックステーブルから最長のコードである3435
を見つける必要があります。常に最初に「8」を削除し、プレフィックスを先頭に置く必要があります。

私は非常に悪い方法でずっと前にこの課題を解決しました。これは、各レコードに対して多くのクエリを実行する恐ろしいperlスクリプトでした。このスクリプト:

  1. 呼び出しテーブルから数値を取得し、ループ内でlength(number)から1 => $ prefixまでの部分文字列を実行します

  2. クエリを実行します: '$ prefix'のようなコードのプレフィックスからcount(*)を選択します

  3. count> 0の場合、最初のプレフィックスを取得してテーブルに書き込みます

最初の問題はクエリ数です- call_records * length(number)です。第二の問題はLIKE表現です。遅いと思います。

私は2番目の問題を解決しようとしました:

CREATE EXTENSION pg_trgm;
CREATE INDEX prefix_idx ON prefix USING gist (code gist_trgm_ops);

これにより、各クエリが高速化しますが、一般的に問題は解決しませんでした。

私が持っている20Kプレフィックスと170K今の数字を、そして私の古いソリューションは悪いです。ループのない新しいソリューションが必要なようです。

各コールレコードまたはこのようなものに対して1つのクエリのみ。


2
code最初のテーブルが後でプレフィックスと同じであるかどうかは本当にわかりません。明確にしていただけませんか?また、サンプルデータと必要な出力の修正(問題を追跡しやすくするため)も歓迎します。
dezso 2013年

うん。その通り。「8」について書くのを忘れていました。ありがとうございました。
Korjavin Ivan 2013

2
接頭辞は最初にある必要がありますよね?
dezso 2013年

はい。2位から。8 $ prefix $ numbers
Korjavin Ivan 2013

テーブルのカーディナリティは何ですか?10万の数字?プレフィックスはいくつですか?
Erwin Brandstetter 2013年

回答:


21

text関連する列のデータ型を想定しています。

CREATE TABLE prefix (code text, name text, price int);
CREATE TABLE num (number text, time int);

「シンプルな」ソリューション

SELECT DISTINCT ON (1)
       n.number, p.code
FROM   num n
JOIN   prefix p ON right(n.number, -1) LIKE (p.code || '%')
ORDER  BY n.number, p.code DESC;

重要な要素:

DISTINCT ONSQL標準のPostgres拡張ですDISTINCTSOのこの関連する回答で、使用されているクエリ手法の詳細な説明を見つけてください。
ORDER BY p.code DESCは(昇順で)'1234'後にソートされるため、最長の一致を選択し'123'ます。

単純なSQLフィドル

インデックスがないと、クエリは非常に長時間実行されます(クエリが終了するのを待ちませんでした)。これを高速にするには、インデックスのサポートが必要です。追加モジュールによって提供された、言及したトライグラムインデックスpg_trgmは良い候補です。GINインデックスとGiSTインデックスのどちらかを選択する必要があります。数字の最初の文字は単なるノイズであり、インデックスから除外できるため、さらに機能的なインデックスになります。
私のテストでは、機能的なトライグラムGINインデックスがトライグラムGiSTインデックスよりも勝っています(予想通り)。

CREATE INDEX num_trgm_gin_idx ON num USING gin (right(number, -1) gin_trgm_ops);

高度なdbfiddleはこちら

すべてのテスト結果は、設定を減らしたローカルのPostgres 9.1テストインストールからのものです。17kの数値と2kのコード:

  • 合計ランタイム:1719.552 ms(トライグラムGiST)
  • 合計実行時間:912.329 msトライグラムGIN)

まだずっと速い

失敗した試み text_pattern_ops

気を散らす最初のノイズ特性を無視すると、基本的な左アンカーパターンマッチになります。したがって、演算子クラスtext_pattern_opsを使用して機能的なBツリーインデックスを試しました (列の型を想定text)。

CREATE INDEX num_text_pattern_idx ON num(right(number, -1) text_pattern_ops);

これは、単一の検索用語を使用した直接クエリに最適であり、トライグラムインデックスの見た目が悪くなります。

SELECT * FROM num WHERE right(number, -1) LIKE '2345%'
  • 合計実行時間:3.816ミリ秒(trgm_gin_idx)
  • 合計実行時間:0.147 ms(text_pattern_idx)

ただし、クエリプランナーは、2つのテーブルを結合するためにこのインデックスを考慮しません。以前にこの制限を見てきました。まだ意味のある説明はありません。

部分的/機能的Bツリーインデックス

代わりに、部分インデックスを持つ部分文字列で等価チェックを使用します。これで使用できますJOIN

通常、different lengthsforプレフィックスの数は限られているため、部分インデックスを使用して、ここに示すものと同様のソリューションを構築できます。

言ってやるが、我々はの範囲のプレフィックス持って15つの文字が。プレフィックスの長さごとに1つずつ、多数の部分的な機能インデックスを作成します。

CREATE INDEX prefix_code_idx5 ON prefix(code) WHERE length(code) = 5;
CREATE INDEX prefix_code_idx4 ON prefix(code) WHERE length(code) = 4;
CREATE INDEX prefix_code_idx3 ON prefix(code) WHERE length(code) = 3;
CREATE INDEX prefix_code_idx2 ON prefix(code) WHERE length(code) = 2;
CREATE INDEX prefix_code_idx1 ON prefix(code) WHERE length(code) = 1;

これらは部分インデックスであるため、すべてを合わせても、単一の完全なインデックスよりもやや大きくなります。

数値に一致するインデックスを追加します(主要なノイズ特性を考慮に入れます):

CREATE INDEX num_number_idx5 ON num(substring(number, 2, 5)) WHERE length(number) >= 6;
CREATE INDEX num_number_idx4 ON num(substring(number, 2, 4)) WHERE length(number) >= 5;
CREATE INDEX num_number_idx3 ON num(substring(number, 2, 3)) WHERE length(number) >= 4;
CREATE INDEX num_number_idx2 ON num(substring(number, 2, 2)) WHERE length(number) >= 3;
CREATE INDEX num_number_idx1 ON num(substring(number, 2, 1)) WHERE length(number) >= 2;

これらのインデックスは部分文字列のみを保持し、部分的ですが、それぞれがテーブルのほとんどまたはすべてをカバーします。そのため、長い数値を除いて、それらは単一の合計インデックスよりもはるかに大きくなります。そして、書き込み操作により多くの作業を課します。それは驚くべき速度の代償です。

そのコストが高すぎる場合(書き込みパフォーマンスが重要/書き込み操作が多すぎる/ディスク容量に問題がある)場合は、これらのインデックスをスキップできます。残りの部分はまだ高速ですが、可能な限り高速ではありません...

数値がn文字より短くなることがない場合はWHERE、一部またはすべてから冗長な句を削除し、それにWHERE続くすべてのクエリから対応する句も削除します。

再帰CTE

これまでのすべてのセットアップで、再帰的なCTEを使用した非常にエレガントなソリューションを望んでいました:

WITH RECURSIVE cte AS (
   SELECT n.number, p.code, 4 AS len
   FROM   num n
   LEFT    JOIN prefix p
            ON  substring(number, 2, 5) = p.code
            AND length(n.number) >= 6  -- incl. noise character
            AND length(p.code) = 5

   UNION ALL 
   SELECT c.number, p.code, len - 1
   FROM    cte c
   LEFT   JOIN prefix p
            ON  substring(number, 2, c.len) = p.code
            AND length(c.number) >= c.len+1  -- incl. noise character
            AND length(p.code) = c.len
   WHERE    c.len > 0
   AND    c.code IS NULL
   )
SELECT number, code
FROM   cte
WHERE  code IS NOT NULL;
  • 合計実行時間:1045.115ミリ秒

ただし、このクエリは悪くありませんが、トライグラムのGINインデックスを使用した単純なバージョンと同じくらいのパフォーマンスですが、目的とする結果が得られません。再帰的な用語は一度だけ計画されるため、最適なインデックスを使用できません。非再帰的な用語のみが可能です。

UNION ALL

少数の再帰を扱っているので、繰り返し再スペルアウトすることができます。これにより、それぞれの計画を最適化できます。(ただし、既に成功した数値の再帰的な除外は失われます。そのため、特により広い範囲のプレフィックス長については、まだ改善の余地があります)。):

SELECT DISTINCT ON (1) number, code
FROM  (
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 5) = p.code
            AND length(n.number) >= 6  -- incl. noise character
            AND length(p.code) = 5
   UNION ALL 
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 4) = p.code
            AND length(n.number) >= 5
            AND length(p.code) = 4
   UNION ALL 
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 3) = p.code
            AND length(n.number) >= 4
            AND length(p.code) = 3
   UNION ALL 
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 2) = p.code
            AND length(n.number) >= 3
            AND length(p.code) = 2
   UNION ALL 
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 1) = p.code
            AND length(n.number) >= 2
            AND length(p.code) = 1
   ) x
ORDER BY number, code DESC;
  • 合計実行時間:57.578ミリ秒(!!)

突破口、ついに!

SQL関数

これをSQL関数にラップすると、繰り返し使用するためのクエリ計画のオーバーヘッドがなくなります。

CREATE OR REPLACE FUNCTION f_longest_prefix()
  RETURNS TABLE (number text, code text) LANGUAGE sql AS
$func$
SELECT DISTINCT ON (1) number, code
FROM  (
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 5) = p.code
            AND length(n.number) >= 6  -- incl. noise character
            AND length(p.code) = 5
   UNION ALL 
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 4) = p.code
            AND length(n.number) >= 5
            AND length(p.code) = 4
   UNION ALL 
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 3) = p.code
            AND length(n.number) >= 4
            AND length(p.code) = 3
   UNION ALL 
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 2) = p.code
            AND length(n.number) >= 3
            AND length(p.code) = 2
   UNION ALL 
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 1) = p.code
            AND length(n.number) >= 2
            AND length(p.code) = 1
   ) x
ORDER BY number, code DESC
$func$;

コール:

SELECT * FROM f_longest_prefix_sql();
  • 合計実行時間:17.138ミリ秒(!!!)

動的SQLを使用したPL / pgSQL関数

このplpgsql関数は上記の再帰CTEによく似ていますが、動的SQLをEXECUTE使用すると、反復ごとにクエリが強制的に再計画されます。今では、すべてのテーラードインデックスを利用しています。

さらに、これは任意の範囲のプレフィックス長で機能します。この関数は範囲に対して2つのパラメーターを取りますが、値を指定して準備したDEFAULTので、明示的なパラメーターなしでも機能します。

CREATE OR REPLACE FUNCTION f_longest_prefix2(_min int = 1, _max int = 5)
  RETURNS TABLE (number text, code text) LANGUAGE plpgsql AS
$func$
BEGIN
FOR i IN REVERSE _max .. _min LOOP  -- longer matches first
   RETURN QUERY EXECUTE '
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(n.number, 2, $1) = p.code
            AND length(n.number) >= $1+1  -- incl. noise character
            AND length(p.code) = $1'
   USING i;
END LOOP;
END
$func$;

最後のステップを1つの関数に簡単にラップすることはできません。 次のように呼び出すだけです。

SELECT DISTINCT ON (1)
       number, code
FROM   f_longest_prefix_prefix2() x
ORDER  BY number, code DESC;
  • 合計実行時間:27.413ミリ秒

または、別のSQL関数をラッパーとして使用します。

CREATE OR REPLACE FUNCTION f_longest_prefix3(_min int = 1, _max int = 5)
  RETURNS TABLE (number text, code text) LANGUAGE sql AS
$func$
SELECT DISTINCT ON (1)
       number, code
FROM   f_longest_prefix_prefix2($1, $2) x
ORDER  BY number, code DESC
$func$;

コール:

SELECT * FROM f_longest_prefix3();
  • 合計実行時間:37.622ミリ秒

必要な計画オーバーヘッドのために少し遅くなります。ただし、SQLよりも用途が広く、接頭辞が長いほど短くなります。


まだ確認中ですが、見栄えが良いです!演算子のような「逆」のアイデア-素晴らしい。なぜ私がそんなに愚かだったのか;(
Korjavin Ivan

5
わぁ!それはかなりの編集です。また投票できるといいのですが。
swasheck 2013年

3
過去2年間よりも、あなたのすばらしい答えから多くを学びます。ループソリューションで数時間に対して17〜30 ms それは魔法です。
Korjavin Ivan 2013

1
@KorjavinIvan:まあ、文書化されているように、2Kのプレフィックス/ 17000の数値の設定を減らしてテストしました。しかし、これはかなり適切にスケーリングするはずであり、私のテストマシンは小さなサーバーでした。したがって、実際のケースでは1秒以内に十分に留まる必要があります。
Erwin Brandstetter 2013年

1
いい答えです... dimitriのプレフィックス拡張子を知っていますか?テストケースの比較にそれを含めることができますか?
MatheusOl 2013年

0

文字列Sは、文字列Tのプレフィックスであり、TがSとSZの間である場合に限ります。ただし、Zは他の文字列よりも辞書順で大きくなります(たとえば、99999999は、データセットで可能な最長の電話番号を超えるのに十分な9があり、場合によっては0xFFが機能します)。

特定のTの最も長い共通の接頭辞も辞書編集的に最大であるため、単純なgroup byおよびmaxがそれを見つけます。

select n.number, max(p.code) 
from prefixes p
join numbers n 
on substring(n.number, 2, 255) between p.code and p.code || '99999999'
group by n.number

これが遅い場合は、計算された式が原因である可能性が高いため、p.code || '999999'を、独自のインデックスを使用してコードテーブルの列に具体化することもできます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.