セットアップ
私は@Jackのセットアップを基に、人々がより簡単にフォローして比較できるようにしています。PostgreSQL 9.1.4でテスト済み。
CREATE TABLE lexikon (
lex_id serial PRIMARY KEY
, word text
, frequency int NOT NULL -- we'd need to do more if NULL was allowed
, lset int
);
INSERT INTO lexikon(word, frequency, lset)
SELECT 'w' || g -- shorter with just 'w'
, (1000000 / row_number() OVER (ORDER BY random()))::int
, g
FROM generate_series(1,1000000) g
ここから私は別のルートを取ります:
ANALYZE lexikon;
補助テーブル
このソリューションは、元のテーブルに列を追加するのではなく、小さなヘルパーテーブルが必要なだけです。スキーマに配置し、public
選択した任意のスキーマを使用します。
CREATE TABLE public.lex_freq AS
WITH x AS (
SELECT DISTINCT ON (f.row_min)
f.row_min, c.row_ct, c.frequency
FROM (
SELECT frequency, sum(count(*)) OVER (ORDER BY frequency DESC) AS row_ct
FROM lexikon
GROUP BY 1
) c
JOIN ( -- list of steps in recursive search
VALUES (400),(1600),(6400),(25000),(100000),(200000),(400000),(600000),(800000)
) f(row_min) ON c.row_ct >= f.row_min -- match next greater number
ORDER BY f.row_min, c.row_ct, c.frequency DESC
)
, y AS (
SELECT DISTINCT ON (frequency)
row_min, row_ct, frequency AS freq_min
, lag(frequency) OVER (ORDER BY row_min) AS freq_max
FROM x
ORDER BY frequency, row_min
-- if one frequency spans multiple ranges, pick the lowest row_min
)
SELECT row_min, row_ct, freq_min
, CASE freq_min <= freq_max
WHEN TRUE THEN 'frequency >= ' || freq_min || ' AND frequency < ' || freq_max
WHEN FALSE THEN 'frequency = ' || freq_min
ELSE 'frequency >= ' || freq_min
END AS cond
FROM y
ORDER BY row_min;
テーブルは次のようになります。
row_min | row_ct | freq_min | cond
--------+---------+----------+-------------
400 | 400 | 2500 | frequency >= 2500
1600 | 1600 | 625 | frequency >= 625 AND frequency < 2500
6400 | 6410 | 156 | frequency >= 156 AND frequency < 625
25000 | 25000 | 40 | frequency >= 40 AND frequency < 156
100000 | 100000 | 10 | frequency >= 10 AND frequency < 40
200000 | 200000 | 5 | frequency >= 5 AND frequency < 10
400000 | 500000 | 2 | frequency >= 2 AND frequency < 5
600000 | 1000000 | 1 | frequency = 1
列cond
はさらに下の動的SQLで使用されるため、このテーブルを安全にする必要があります。適切なcurrentを確認できない場合は常にテーブルをスキーマ修飾し、search_path
書き込み権限public
(およびその他の信頼できないロール)を取り消します。
REVOKE ALL ON public.lex_freq FROM public;
GRANT SELECT ON public.lex_freq TO public;
このテーブルにlex_freq
は3つの目的があります。
インデックス
このDO
ステートメントは、必要なすべてのインデックスを作成します。
DO
$$
DECLARE
_cond text;
BEGIN
FOR _cond IN
SELECT cond FROM public.lex_freq
LOOP
IF _cond LIKE 'frequency =%' THEN
EXECUTE 'CREATE INDEX ON lexikon(lset) WHERE ' || _cond;
ELSE
EXECUTE 'CREATE INDEX ON lexikon(lset, frequency DESC) WHERE ' || _cond;
END IF;
END LOOP;
END
$$
これらの部分インデックスはすべて、一度にテーブル全体に広がります。これらは、テーブル全体の1つの基本インデックスとほぼ同じサイズです。
SELECT pg_size_pretty(pg_relation_size('lexikon')); -- 50 MB
SELECT pg_size_pretty(pg_total_relation_size('lexikon')); -- 71 MB
これまでのところ、50 MBのテーブルのインデックスは21 MBのみです。
で部分インデックスのほとんどを作成します(lset, frequency DESC)
。2番目の列は、特別な場合にのみ役立ちます。ただし、PostgreSQLのMAXALIGNと組み合わせたinteger
データアラインメントの仕様により、関係する列は両方ともtype であるため、2番目の列はインデックスをそれ以上大きくしません。ほとんどコストがかからず、小さな勝利です。
単一の周波数のみにまたがる部分インデックスに対して、それを行う意味はありません。それらはちょうど上にあり(lset)
ます。作成されたインデックスは次のようになります。
CREATE INDEX ON lexikon(lset, frequency DESC) WHERE frequency >= 2500;
CREATE INDEX ON lexikon(lset, frequency DESC) WHERE frequency >= 625 AND frequency < 2500;
-- ...
CREATE INDEX ON lexikon(lset, frequency DESC) WHERE frequency >= 2 AND frequency < 5;
CREATE INDEX ON lexikon(lset) WHERE freqency = 1;
関数
関数のスタイルは@Jackのソリューションと多少似ています:
CREATE OR REPLACE FUNCTION f_search(_lset_min int, _lset_max int, _limit int)
RETURNS SETOF lexikon
$func$
DECLARE
_n int;
_rest int := _limit; -- init with _limit param
_cond text;
BEGIN
FOR _cond IN
SELECT l.cond FROM public.lex_freq l ORDER BY l.row_min
LOOP
-- RAISE NOTICE '_cond: %, _limit: %', _cond, _rest; -- for debugging
RETURN QUERY EXECUTE '
SELECT *
FROM public.lexikon
WHERE ' || _cond || '
AND lset >= $1
AND lset <= $2
ORDER BY frequency DESC
LIMIT $3'
USING _lset_min, _lset_max, _rest;
GET DIAGNOSTICS _n = ROW_COUNT;
_rest := _rest - _n;
EXIT WHEN _rest < 1;
END LOOP;
END
$func$ LANGUAGE plpgsql STABLE;
主な違い:
を使用しRETURN QUERY EXECUTE
た動的SQL
ステップをループするとき、異なるクエリプランが受益者になる場合があります。静的SQLのクエリプランは、一度生成されてから再利用されます。これにより、オーバーヘッドを節約できます。ただし、この場合、クエリは単純で、値は大きく異なります。動的SQLは大きな勝利になります。
LIMIT
クエリステップごとに動的。
これは複数の方法で役立ちます。まず、必要な場合にのみ行がフェッチされます。動的SQLと組み合わせて、これは異なるクエリプランを生成することもあります。2番目:LIMIT
余剰を削除するために、関数呼び出しで追加する必要はありません。
基準
セットアップ
4つの例を選択し、それぞれで3つの異なるテストを実行しました。ウォームキャッシュと比較するために、ベスト5を取りました。
次の形式の生のSQLクエリ:
SELECT *
FROM lexikon
WHERE lset >= 20000
AND lset <= 30000
ORDER BY frequency DESC
LIMIT 5;
このインデックスを作成した後も同じ
CREATE INDEX ON lexikon(lset);
すべての部分インデックスをまとめたものとほぼ同じスペースが必要です。
SELECT pg_size_pretty(pg_total_relation_size('lexikon')) -- 93 MB
関数
SELECT * FROM f_search(20000, 30000, 5);
結果
SELECT * FROM f_search(20000, 30000, 5);
1:合計ランタイム:315.458ミリ秒
2:合計ランタイム:36.458ミリ秒
3:合計ランタイム:0.330ミリ秒
SELECT * FROM f_search(60000, 65000, 100);
1:合計ランタイム:294.819ミリ秒
2:合計ランタイム:18.915ミリ秒
3:合計ランタイム:1.414ミリ秒
SELECT * FROM f_search(10000, 70000, 100);
1:合計ランタイム:426.831ミリ秒
2:合計ランタイム:217.874ミリ秒
3:合計ランタイム:1.611ミリ秒
SELECT * FROM f_search(1, 1000000, 5);
1:合計ランタイム:2458.205ミリ秒
2:合計ランタイム:2458.205ミリ秒-lsetの範囲が広い場合、seqスキャンはインデックスよりも高速です。
3:合計ランタイム:0.266ミリ秒
結論
予想されるように、関数の利点はの範囲が大きくlset
なり、が小さくなるにつれて大きくなりLIMIT
ます。
非常に小さな範囲lset
、インデックスとの組み合わせで、生のクエリは実際に速いです。あなたはテストしたいかもしれませんし、分岐するかもしれませんlset
。それを「両方の世界のベスト」のための機能に組み込むことさえできます-それが私がやることです。
データの分布と一般的なクエリによっては、より多くのステップを実行lex_freq
するとパフォーマンスが向上する場合があります。スイートスポットを見つけるためにテストします。ここで紹介するツールを使用すると、テストが簡単になります。