テーブルが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スクリプトでした。このスクリプト:
呼び出しテーブルから数値を取得し、ループ内でlength(number)から1 => $ prefixまでの部分文字列を実行します
クエリを実行します: '$ prefix'のようなコードのプレフィックスからcount(*)を選択します
最初の問題はクエリ数です-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つのクエリのみ。
関連する列のデータ型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コード:
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%'
ただし、の場合、クエリプランナーはこのインデックスを2つのテーブルの結合に考慮しません。以前にこの制限を見てきました。まだ意味のある説明はありません。
代わりに、部分インデックスを持つ部分文字列に対して等価チェックを使用します。この 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 を使用した非常にエレガントなソリューションを望んでいました:
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;
ただし、このクエリは悪くありませんが、トライグラムのGINインデックスを使用した単純なバージョンとほぼ同じくらいのパフォーマンスが得られますが、目的とする結果が得られません。再帰的な用語は1回だけ計画されているため、最適なインデックスを使用できません。非再帰的な用語のみが可能です。
少数の再帰を扱っているので、繰り返し再帰的に書くことができます。これにより、それぞれの計画を最適化できます。 (ただし、すでに成功した数値の再帰的な除外は失われます。したがって、 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;
突破口、ついに!
これを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();
この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;
または別の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();
必要な計画オーバーヘッドのために少し遅くなります。ただし、SQLよりも用途が広く、接頭辞が長いほど短くなります。
文字列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'を独自のインデックスを持つコードテーブルの列に具体化することもできます。