web-dev-qa-db-ja.com

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

テーブルが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)です。 2番目の問題はLIKE式です。遅いと思います。

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

_CREATE EXTENSION pg_trgm;
CREATE INDEX prefix_idx ON prefix USING Gist (code Gist_trgm_ops);
_

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

20kプレフィックスと170k番号を現在持っているので、私の古い解決策は悪いです。ループのない新しいソリューションが必要なようです。

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

11
Korjavin Ivan

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

シンプル SQL Fiddle

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

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

高度な dbfiddle here

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

  • 合計ランタイム:1719.552 ms(trigram 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ツリーインデックス

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

通常、プレフィックスにはdifferent lengthsの数に制限があるため、部分インデックスを使用して here と同様のソリューションを構築できます。

たとえば、1から5までの範囲のプレフィックスがあるとします。プレフィックスの長さごとに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;

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

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

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;

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

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

数字が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 ms

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

UNION ALL

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

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 ms(!!!)

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

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

さらに、これは接頭辞の長さの任意の範囲で機能します。関数は範囲に対して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 ms

または別の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 ms

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

21

文字列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'を独自のインデックスを持つコードテーブルの列に具体化することもできます。

0
KWillets