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標準のPostgres拡張ですDISTINCT
。SOのこの関連する回答で、使用されているクエリ手法の詳細な説明を見つけてください。
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 lengths
forプレフィックスの数は限られているため、部分インデックスを使用して、ここに示すものと同様のソリューションを構築できます。
言ってやるが、我々はの範囲のプレフィックス持って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;
これらは部分インデックスであるため、すべてを合わせても、単一の完全なインデックスよりもやや大きくなります。
数値に一致するインデックスを追加します(主要なノイズ特性を考慮に入れます):
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;
ただし、このクエリは悪くありませんが、トライグラムの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;
突破口、ついに!
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();
動的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;
または、別の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よりも用途が広く、接頭辞が長いほど短くなります。
code
最初のテーブルが後でプレフィックスと同じであるかどうかは本当にわかりません。明確にしていただけませんか?また、サンプルデータと必要な出力の修正(問題を追跡しやすくするため)も歓迎します。