Rツリー/空間インデックスに優れたサポートがあるため、特にPostgresにこの質問をします。
次の表に、単語とその頻度のツリー構造(ネストされたセットモデル)を示します。
lexikon
-------
_id integer PRIMARY KEY
Word text
frequency integer
lset integer UNIQUE KEY
rset integer UNIQUE KEY
そしてクエリ:
SELECT Word
FROM lexikon
WHERE lset BETWEEN @Low AND @High
ORDER BY frequency DESC
LIMIT @N
(lset, frequency, Word)
のカバリングインデックスは便利だと思いますが、(@High, @Low)
の範囲にあるlset
の値が多すぎると、うまく機能しない可能性があります。
(frequency DESC)
の単純なインデックスでも十分な場合があります。そのインデックスを使用して検索すると、範囲条件に一致する@N
行が早期に得られる場合があります。
ただし、パフォーマンスはパラメータ値に大きく依存するようです。
範囲(@Low, @High)
が広いか狭いかに関係なく、また最高頻度の単語が幸運にも選択された(狭い)範囲にあるかどうかに関係なく、高速に実行する方法はありますか?
Rツリー/空間インデックスは役に立ちますか?
インデックスの追加、クエリの書き換え、テーブルの再設計など、制限はありません。
周波数の高い行を最初に検索すると、パフォーマンスが向上する可能性があります。これは、たとえば次のように、周波数を「細かく」設定し、手続き的にそれらをステップ実行することによって実現できます。
--testbedおよびlexikon
ダミーデータ:
begin;
set role dba;
create role stack;
grant stack to dba;
create schema authorization stack;
set role stack;
--
create table lexikon( _id serial,
Word text,
frequency integer,
lset integer,
width_granule integer);
--
insert into lexikon(Word, frequency, lset)
select Word, (1000000/row_number() over(order by random()))::integer as frequency, lset
from (select 'Word'||generate_series(1,1000000) Word, generate_series(1,1000000) lset) z;
--
update lexikon set width_granule=ln(frequency)::integer;
--
create index on lexikon(width_granule, lset);
create index on lexikon(lset);
-- the second index is not used with the function but is added to make the timings 'fair'
granule
分析(主に情報とチューニング用):
create table granule as
select width_granule, count(*) as freq,
min(frequency) as granule_start, max(frequency) as granule_end
from lexikon group by width_granule;
--
select * from granule order by 1;
/*
width_granule | freq | granule_start | granule_end
---------------+--------+---------------+-------------
0 | 500000 | 1 | 1
1 | 300000 | 2 | 4
2 | 123077 | 5 | 12
3 | 47512 | 13 | 33
4 | 18422 | 34 | 90
5 | 6908 | 91 | 244
6 | 2580 | 245 | 665
7 | 949 | 666 | 1808
8 | 349 | 1811 | 4901
9 | 129 | 4926 | 13333
10 | 47 | 13513 | 35714
11 | 17 | 37037 | 90909
12 | 7 | 100000 | 250000
13 | 2 | 333333 | 500000
14 | 1 | 1000000 | 1000000
*/
alter table granule drop column freq;
--
最初に高周波をスキャンする機能:
create function f(p_lset_low in integer, p_lset_high in integer, p_limit in integer)
returns setof lexikon language plpgsql set search_path to 'stack' as $$
declare
m integer;
n integer := 0;
r record;
begin
for r in (select width_granule from granule order by width_granule desc) loop
return query( select *
from lexikon
where width_granule=r.width_granule
and lset>=p_lset_low and lset<=p_lset_high );
get diagnostics m = row_count;
n = n+m;
exit when n>=p_limit;
end loop;
end;$$;
結果(タイミングはおそらく少々のソルトで取得する必要がありますが、キャッシングに対抗するために各クエリは2回実行されます)
最初に、作成した関数を使用します。
\timing on
--
select * from f(20000, 30000, 5) order by frequency desc limit 5;
/*
_id | Word | frequency | lset | width_granule
-----+-----------+-----------+-------+---------------
141 | Word23237 | 7092 | 23237 | 9
246 | Word25112 | 4065 | 25112 | 8
275 | Word23825 | 3636 | 23825 | 8
409 | Word28660 | 2444 | 28660 | 8
418 | Word29923 | 2392 | 29923 | 8
Time: 80.452 ms
*/
select * from f(20000, 30000, 5) order by frequency desc limit 5;
/*
_id | Word | frequency | lset | width_granule
-----+-----------+-----------+-------+---------------
141 | Word23237 | 7092 | 23237 | 9
246 | Word25112 | 4065 | 25112 | 8
275 | Word23825 | 3636 | 23825 | 8
409 | Word28660 | 2444 | 28660 | 8
418 | Word29923 | 2392 | 29923 | 8
Time: 0.510 ms
*/
そして単純なインデックススキャンで:
select * from lexikon where lset between 20000 and 30000 order by frequency desc limit 5;
/*
_id | Word | frequency | lset | width_granule
-----+-----------+-----------+-------+---------------
141 | Word23237 | 7092 | 23237 | 9
246 | Word25112 | 4065 | 25112 | 8
275 | Word23825 | 3636 | 23825 | 8
409 | Word28660 | 2444 | 28660 | 8
418 | Word29923 | 2392 | 29923 | 8
Time: 218.897 ms
*/
select * from lexikon where lset between 20000 and 30000 order by frequency desc limit 5;
/*
_id | Word | frequency | lset | width_granule
-----+-----------+-----------+-------+---------------
141 | Word23237 | 7092 | 23237 | 9
246 | Word25112 | 4065 | 25112 | 8
275 | Word23825 | 3636 | 23825 | 8
409 | Word28660 | 2444 | 28660 | 8
418 | Word29923 | 2392 | 29923 | 8
Time: 51.250 ms
*/
\timing off
--
rollback;
実際のデータに応じて、グラニュルの数と、それらに行を配置するために使用される関数を変更する必要があります。 limit
句の期待値と求められるlset
範囲のサイズと同様に、ここでは頻度の実際の分布が重要です。
私は@ 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でさらに使用されるため、このテーブルをsecureにする必要があります。現在の適切な_search_path
_が不明な場合は、常にテーブルをスキーマ修飾し、public
(およびその他の信頼されていないロール)から書き込み権限を取り消します。
_REVOKE ALL ON public.Lex_freq FROM public;
GRANT SELECT ON public.Lex_freq TO public;
_
テーブル_Lex_freq
_は3つの目的を果たします。
次のDO
ステートメントは、all必要なインデックスを作成します。
_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
$$
_
これらすべてのpartialインデックスは一緒にテーブルにまたがります。それらは、テーブル全体の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番目の列は、特別な場合にのみ役立ちます。ただし、関連する両方の列のタイプはinteger
であるため、PostgreSQLのデータの詳細 MAXALIGNとの組み合わせ のため、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;
_
主な違い:
動的SQLと_RETURN QUERY EXECUTE
_。
手順をループする際に、別のクエリプランが有効になる場合があります。静的SQLのクエリプランは1回生成されてから再利用されるため、オーバーヘッドを節約できます。ただし、この場合、クエリは単純で、値は大きく異なります。動的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:合計実行時間:.330 ms
SELECT * FROM f_search(60000, 65000, 100);
1:合計ランタイム:294.819ミリ秒
2:合計実行時間:18.915ミリ秒
3:合計実行時間:1.414 ms
SELECT * FROM f_search(10000, 70000, 100);
1:合計実行時間:426.831ミリ秒
2:合計実行時間:217.874ミリ秒
3:合計実行時間:1.611 ms
SELECT * FROM f_search(1, 1000000, 5);
1:合計ランタイム:2458.205 ms
2:合計実行時間:2458.205 ms-lsetの範囲が広い場合、seqスキャンはインデックスよりも高速です。
3:合計実行時間:.266 ms
予想通り、lset
の範囲が大きく、LIMIT
の範囲が小さいほど、関数のメリットは大きくなります。
非常に小さい範囲のlset
を使用すると、インデックスと組み合わせた生のクエリは実際に高速になります。あなたはテストしたいと思うかもしれません、そして分岐するかもしれません:lset
の小さな範囲の生のクエリ、そうでなければ関数呼び出し。あなたは単にそれを関数に組み込む「両方の世界のベスト」のために-それが私がすることです。
データの分布と一般的なクエリによっては、_Lex_freq
_の手順を増やすとパフォーマンスが向上する場合があります。スイートスポットを見つけるためにテストします。ここで紹介するツールを使用すると、テストが簡単になります。
範囲(@ Low、@ High)が広いか狭いかに関係なく、また最高頻度の単語が幸運にも選択された(狭い)範囲にあるかどうかに関係なく、高速に実行する方法はありますか?
それはあなたが断食するときのあなたの意味次第です。あなたのクエリはORDER freq DESC
なので、明らかに範囲内のすべての行にアクセスする必要があります。質問を理解していれば、クエリプランナーはすでにこれをカバーしています。
ここでは、1万行の(5::int,random()::double precision)
のテーブルを作成します
CREATE EXTENSION IF NOT EXISTS btree_gin;
CREATE TABLE t AS
SELECT 5::int AS foo, random() AS bar
FROM generate_series(1,1e4) AS gs(x);
インデックスを付けます
CREATE INDEX ON t USING Gist (foo, bar);
ANALYZE t;
問い合わせます
EXPLAIN ANALYZE
SELECT *
FROM t
WHERE foo BETWEEN 1 AND 6
ORDER BY bar DESC
FETCH FIRST ROW ONLY;
Seq Scan on t
を取得します。これは単に、選択性の見積もりにより、pgがヒープアクセスがインデックスをスキャンして再チェックするよりも高速であると結論付けるためです。そのため、「範囲」に合わない(42::int,random()::double precision)
の行を1,000,000行以上挿入することで、よりジューシーになります。
INSERT INTO t(foo,bar)
SELECT 42::int, x
FROM generate_series(1,1e6) AS gs(x);
VACUUM ANALYZE t;
次に、再クエリします。
EXPLAIN ANALYZE
SELECT *
FROM t
WHERE foo BETWEEN 1 AND 6
ORDER BY bar DESC
FETCH FIRST ROW ONLY;
ここでは、4.6 MSでIndex Only Scanを使用して完了することを確認できます。
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=617.64..617.64 rows=1 width=12) (actual time=4.652..4.652 rows=1 loops=1)
-> Sort (cost=617.64..642.97 rows=10134 width=12) (actual time=4.651..4.651 rows=1 loops=1)
Sort Key: bar DESC
Sort Method: top-N heapsort Memory: 25kB
-> Index Only Scan using t_foo_bar_idx on t (cost=0.29..566.97 rows=10134 width=12) (actual time=0.123..3.623 rows=10000 loops=1)
Index Cond: ((foo >= 1) AND (foo <= 6))
Heap Fetches: 0
Planning time: 0.144 ms
Execution time: 4.678 ms
(9 rows)
範囲を拡大してテーブル全体を含めると、別のシーケンススキャンが生成されます-論理的には、さらに10億行で拡張すると、別のインデックススキャンが生成されます。
要約すると、
インデックスにWord列を含める理由は何もありません。このインデックスは
CREATE INDEX lexikon_lset_frequency ON Lexicon (lset, frequency DESC)
クエリを高速に実行します。
[〜#〜]更新[〜#〜]
現在、PostgreSQLでカバリングインデックスを作成する方法はありません。 PostgreSQLメーリングリストでこの機能についての議論がありました http://archives.postgresql.org/pgsql-performance/2012-06/msg00114.php