空間インデックスは「範囲-順序-制限」クエリに役立ちますか


29

R-tree / spatialインデックスに適しているので、特に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)が有効であると思いlsetますが、(@High, @Low)範囲内の値が多すぎるとうまく機能しない可能性があります。

(frequency DESC)そのインデックスを使用した検索@Nが範囲条件に一致する行を早期に生成する場合、単純なインデックスで十分な場合もあります。

しかし、パフォーマンスはパラメーター値に大きく依存するようです。

範囲(@Low, @High)が広いか狭いかに関係なく、また、頻度の高い単語が幸運にも選択された範囲内にあるかどうかにかかわらず、高速に実行する方法はありますか?

Rツリー/空間インデックスは役立ちますか?

インデックスの追加、クエリの書き換え、テーブルの再設計、制限はありません。


3
カバーインデックスは、9.2(現在のベータ版)で導入されました。PostgreSQLの人々は、インデックスのみのスキャンについて語っています。:この関連の回答を参照してくださいdba.stackexchange.com/a/7541/3684PostgreSQLのWikiページ
アーウィンBrandstetter

2つの質問:(1)テーブルにはどのような使用パターンが予想されますか?主に読み取りがありますか、それとも頻繁に更新されますか(特にネストされたセット変数)?(2)ネストされたセット整数変数lsetおよびrsetとテキスト変数wordの間に接続はありますか?
jp

@jug:ほとんど読み取ります。間の接続なしlset,rsetword
ypercubeᵀᴹ

3
多くの更新がある場合、ネストされたセットモデルはパフォーマンスに関して悪い選択になります( "The art of SQL"にアクセスできる場合は、階層モデルに関する章を参照してください)。しかし、とにかく、主な問題は、間隔で(独立変数の)最大値/最大値を見つけることに似ています。そのため、インデックス付け方法を設計するのは困難です。私の知る限り、必要なインデックスに最も近いのはknngistモジュールですが、ニーズに合わせて変更する必要があります。空間インデックスが役立つことはほとんどありません。
jp

回答:


30

より高い周波数の行を最初に検索することにより、パフォーマンスを向上させることができます。これは、次のように、周波数を「粒状化」してから手順的に周波数をステップ実行することで実現できます。

-テストベッドおよび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範囲とサイズの期待値も重要です。


前のレベルと前のレベルのwidth_granule=8間にギャップがあるのはなぜですか?granulae_startgranulae_end
vyegorov

@vyegorovは、1809と1810の値がないためですか?これはランダムに生成されたデータなので、YMMV :)
ジャックダグラス

うーん、それはランダム性とは何の関係もないようですが、むしろ方法frequencyが生成されます:1e6 / 2と1e6 / 3の間の大きなギャップは、行番号が大きくなるほど、ギャップは小さくなります。とにかく、この素晴らしいアプローチをありがとう!
-vyegorov

@vyegorovごめん、はい、そうです。まだ行っていない場合は、Erwinsの改善点を必ず確認してください!
ジャックダグラス

23

セットアップ

私は@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を取りました。

  1. 次の形式の生のSQLクエリ:

    SELECT * 
    FROM   lexikon 
    WHERE  lset >= 20000
    AND    lset <= 30000
    ORDER  BY frequency DESC
    LIMIT  5;
  2. このインデックスを作成した後も同じ

    CREATE INDEX ON lexikon(lset);

    すべての部分インデックスをまとめたものとほぼ同じスペースが必要です。

    SELECT pg_size_pretty(pg_total_relation_size('lexikon')) -- 93 MB
  3. 関数

    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するとパフォーマンスが向上する場合があります。スイートスポットを見つけるためにテストします。ここで紹介するツールを使用すると、テストが簡単になります。


1

単語列をインデックスに含める理由はありません。このインデックス

CREATE INDEX lexikon_lset_frequency ON lexicon (lset, frequency DESC)

クエリを高速に実行します。

UPD

現在、PostgreSQLでカバリングインデックスを作成する方法はありません。PostgreSQLメーリングリストhttp://archives.postgresql.org/pgsql-performance/2012-06/msg00114.phpでこの機能に関する議論がありました。


1
インデックスを「カバー」するために含まれていました。
ypercubeᵀᴹ

しかし、クエリ決定ツリーでその用語を検索しないことで、カバリングインデックスがここで役立つことを確信していますか?
jcolebrand

さて、私は今見ます。現在、PostgreSQLでカバリングインデックスを作成する方法はありません。メーリングリストarchives.postgresql.org/pgsql-performance/2012-06/msg00114.phpでこの機能に関する議論がありました。
グレイヘンプ

PostgreSQLの「インデックスのカバー」については、質問に対するErwin Brandstetterのコメントも参照してください。
jp

1

GISTインデックスを使用する

範囲(@ Low、@ High)が広いか狭いかにかかわらず、また、上位の頻度の単語が幸運にも(狭い)選択された範囲にあるかどうかに関係なく、高速に実行する方法はありますか?

それは、あなたが断食するときのあなたの意味に依存します:あなたのクエリはなので、明らかに範囲内のすべての行を訪問しなければなりませんORDER freq DESC。質問を理解していれば、クエリプランナーが既にこれをカバーしているので、

ここでは、10,000行のテーブルを作成します (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でインデックスのみのスキャンを完了することを確認できます。

                                                                 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)

範囲を拡張してテーブル全体を含めると、論理的には別のseqスキャンが生成され、さらに10億行で拡張すると、別のインデックススキャンが生成されます。

要約すると、

  • データ量に対して、高速に実行されます。
  • 範囲が十分に選択的でない場合、シーケンシャルスキャンは可能な限り高速になる可能性があります。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.