web-dev-qa-db-ja.com

多くの重複する値で使用するインデックスは何ですか?

いくつかの仮定をしてみましょう:

次のようなテーブルがあります。

_ a | b
---+---
 a | -1
 a | 17
  ...
 a | 21
 c | 17
 c | -3
  ...
 c | 22
_

私のセットについての事実:

  • テーブル全体のサイズは〜10です10 行。

  • 他の値と同様に、列aに値aを含む約10万行があります(例:c)。

  • つまり、列 'a'に約10万個の異なる値があります。

  • ほとんどのクエリは、a内の特定の値のすべてまたはほとんどの値を読み取ります。 select sum(b) from t where a = 'c'

  • テーブルは、連続する値が物理的に近い方法で書き込まれます(順番に書き込まれるか、または CLUSTER がそのテーブルと列で使用されたと想定しますa )。

  • テーブルが更新されることはめったにないので、読み取り速度のみを考慮します。

  • テーブルは比較的狭いです(たとえば、タプルあたり約25バイト、+ 23バイトのオーバーヘッド)。

今問題は、どのような種類のインデックスを使用する必要があるかです。私の理解は:

  • BTreeここでの問題は、重複する値を格納することがわかっている限り、BTreeインデックスが巨大になることです(できるので、テーブルが物理的にソートされていると仮定します)。 BTreeが巨大な場合、インデックスと、インデックスが指すテーブルの部分の両方を読み取る必要があります。 (_fillfactor = 100_を使用して、インデックスのサイズを少し減らすことができます。)

  • [〜#〜] brin [〜#〜]私の理解では、役に立たないページを読むことを犠牲にして、ここに小さなインデックスを付けることができます。小さな_pages_per_range_を使用すると、インデックスが大きくなります(インデックス全体を読み取る必要があるため、BRINで問題になります)。大きな_pages_per_range_を使用すると、多くの無駄なページが読み取られます。これらのトレードオフを考慮した_pages_per_range_の適切な値を見つけるための魔法の数式はありますか?

  • GIN/Gistそれらは主に全文検索に使用されているため、ここでの関連性は不明ですが、取引に優れていると聞いていますキーが重複しています。 GINまたはGistインデックスのどちらがここで役立ちますか?

もう1つの質問は、PostgresがクエリプランナーでテーブルがCLUSTERed(更新がないと想定)であるという事実を(たとえば、関連する開始/終了ページのバイナリ検索によって)使用するかどうかです。ある程度関連していますが、すべての列をBTreeに格納して、テーブルを完全に削除できますか(または同等のものを実現できますか、それらはSQLサーバーのクラスター化インデックスであると思います)?ここで役立つハイブリッドBTree/BRINインデックスはありますか?

クエリがそのように読みにくくなるため、配列を使用して値を格納するのは避けたいです(タプルの数を減らすことで、タプルのオーバーヘッドあたり23バイトのコストが削減されることを理解しています)。

14
foo

BTree

ここでの私の問題は、BTreeインデックスが重複する値を格納するため、巨大になるということです(テーブルが物理的にソートされているとは想定できないため、BTreeインデックスもそうです)。 BTreeが巨大な場合、インデックスと、インデックスがポイントするテーブルの部分の両方を読み取る必要があります...

必ずしもそうとは限りません— 'covering' であるbtreeインデックスを使用すると、最速の読み取り時間になります。それが必要な場合(つまり、追加のストレージを用意できる場合)、それはあなたの最善の策です。

BRIN

私の理解は、役に立たないページを読むことを犠牲にして、ここで小さなインデックスを持つことができるということです。小さいpages_per_rangeを使用すると、インデックスが大きくなります(インデックス全体を読み取る必要があるため、BRINで問題になります)。大きいpages_per_rangeを使用すると、多くの無駄なページを読み取る。

カバーするbtreeインデックスのストレージオーバーヘッドを許容できない場合は、クラスタリングが既に整っているため、BRINが理想的です(これは BRINが役立つために重要です ) 。 BRINインデックス は小さい ので、pages_per_rangeの適切な値を選択すると、すべてのページがメモリにある可能性があります。

それらのトレードオフを考慮に入れたpages_per_rangeの適切な値を見つけるための魔法の公式はありますか?

魔法の公式はありませんが、pages_per_rangeから始めて、平均のa値が占める平均サイズ(ページ数)よりも少し小さい 。典型的なクエリでは、(スキャンされたBRINページの数)+(スキャンされたヒープページの数)を最小化しようとしています。 Heap Blocks: lossy=nの実行プランでpages_per_range=1を探し、pages_per_rangeの他の値と比較します。つまり、スキャンされている不要なヒープブロックの数を確認します。

GIN/Gist

これらは主に全文検索に使用されているため、ここでの関連性は不明ですが、重複キーの処理にも優れていると聞いています。 GIN/Gistインデックスはここで役に立ちますか?

GINは検討に値するかもしれませんが、おそらくGistはそうではありません—しかし、自然なクラスタリングが本当に良い場合は、おそらくBRINの方が適しています。

以下は、ダミーデータのさまざまなインデックスタイプの比較例です。

テーブルとインデックス:

create table foo(a,b,c) as
select *, lpad('',20)
from (select chr(g) a from generate_series(97,122) g) a
     cross join (select generate_series(1,100000) b) b
order by a;
create index foo_btree_covering on foo(a,b);
create index foo_btree on foo(a);
create index foo_gin on foo using gin(a);
create index foo_brin_2 on foo using brin(a) with (pages_per_range=2);
create index foo_brin_4 on foo using brin(a) with (pages_per_range=4);
vacuum analyze;

関係サイズ:

select relname "name", pg_size_pretty(siz) "size", siz/8192 pages, (select count(*) from foo)*8192/siz "rows/page"
from( select relname, pg_relation_size(C.oid) siz
      from pg_class c join pg_namespace n on n.oid = c.relnamespace
      where nspname = current_schema ) z;
名前|サイズ|ページ|行/ページ
:----------------- | :------ | ----:| --------:
 foo | 149 MB | 19118 | 135 
 foo_btree_covering | 56 MB | 7132 | 364 
 foo_btree | 56 MB | 7132 | 364 
 foo_gin | 2928 kB | 366 | 7103 
 foo_brin_2 | 264 kB | 33 | 78787 
 foo_brin_4 | 136 kB | 17 | 152941 

btreeのカバー:

explain analyze select sum(b) from foo where a='a';
 |クエリプラン| 
 | :------------------------------------------------- -------------------------------------------------- ------------------------------------------- | 
 |集計(コスト= 3282.57..3282.58行= 1幅= 8)(実際の時間= 45.942..45.942行= 1ループ= 1)| 
 | -> fooのfoo_btree_coveringを使用したインデックスのみのスキャン(コスト= 0.43..3017.80行= 105907幅= 4)(実際の時間= 0.038..27.286行= 100000ループ= 1)| 
 |インデックス条件:(a = 'a' :: text)| 
 |ヒープフェッチ:0 | 
 |計画時間:0.099ミリ秒| 
 |実行時間:45.968ミリ秒| 

プレーンなbtree:

drop index foo_btree_covering;
explain analyze select sum(b) from foo where a='a';
 |クエリプラン| 
 | :------------------------------------------------- -------------------------------------------------- ----------------------------- | 
 |集計(コスト= 4064.57..4064.58行= 1幅= 8)(実際の時間= 54.242..54.242行= 1ループ= 1)| 
 | -> fooのfoo_btreeを使用したインデックススキャン(コスト= 0.43..3799.80行= 105907幅= 4)(実際の時間= 0.037..33.084行= 100000ループ= 1)| 
 |インデックス条件:(a = 'a' :: text)| 
 |計画時間:0.135 ms | 
 |実行時間:54.280 ms | 

BRIN pages_per_range = 4:

drop index foo_btree;
explain analyze select sum(b) from foo where a='a';
 |クエリプラン| 
 | :------------------------------------------------- -------------------------------------------------- ----------------------------- | 
 |集計(コスト= 21595.38..21595.39行= 1幅= 8)(実際の時間= 52.455..52.455行= 1ループ= 1)| 
 | -> fooのビットマップヒープスキャン(コスト= 888.78..21330.61行= 105907幅= 4)(実際の時間= 2.738..31.967行= 100000ループ= 1)| 
 |条件を再確認:(a = 'a' :: text)| 
 |インデックスの再チェックによって削除された行:96 | 
 |ヒープブロック:lossy = 736 | 
 | -> foo_brin_4のビットマップインデックススキャン(コスト= 0.00..862.30行= 105907幅= 0)(実際の時間= 2.720..2.720行= 7360ループ= 1)| 
 |インデックス条件:(a = 'a' :: text)| 
 |計画時間:0.101 ms | 
 |実行時間:52.501 ms | 

BRIN pages_per_range = 2:

drop index foo_brin_4;
explain analyze select sum(b) from foo where a='a';
 |クエリプラン| 
 | :------------------------------------------------- -------------------------------------------------- ----------------------------- | 
 |集計(コスト= 21659.38..21659.39行= 1幅= 8)(実際の時間= 53.971..53.971行= 1ループ= 1)| 
 | -> fooのビットマップヒープスキャン(コスト= 952.78..21394.61行= 105907幅= 4)(実際の時間= 5.286..33.492行= 100000ループ= 1)| 
 |条件を再確認:(a = 'a' :: text)| 
 |インデックスの再チェックによって削除された行:96 | 
 |ヒープブロック:lossy = 736 | 
 | -> foo_brin_2のビットマップインデックススキャン(コスト= 0.00..926.30行= 105907幅= 0)(実際の時間= 5.275..5.275行= 7360ループ= 1)| 
 |インデックス条件:(a = 'a' :: text)| 
 |計画時間:0.095 ms | 
 |実行時間:54.016ミリ秒| 

ジン:

drop index foo_brin_2;
explain analyze select sum(b) from foo where a='a';
 |クエリプラン| 
 | :------------------------------------------------- -------------------------------------------------- ------------------------------ | 
 |集計(コスト= 21687.38..21687.39行= 1幅= 8)(実際の時間= 55.331..55.331行= 1ループ= 1)| 
 | -> fooのビットマップヒープスキャン(コスト= 980.78..21422.61行= 105907幅= 4)(実際の時間= 12.77..33.956行= 100000ループ= 1)| 
 |条件を再確認:(a = 'a' :: text)| 
 |ヒープブロック:exact = 736 | 
 | -> foo_ginでのビットマップインデックススキャン(コスト= 0.00..954.30行= 105907幅= 0)(実際の時間= 12.271..12.271行= 100000ループ= 1)| 
 |インデックス条件:(a = 'a' :: text)| 
 |計画時間:0.118 ms | 
 |実行時間:55.366 ms | 

dbfiddle ここに

btreebrinの他に、最も賢明なオプションであると思われる他のいくつか、調査する価値があるかもしれないエキゾチックなオプション-それらはあなたの場合に役立つかそうでないかもしれません:

  • INCLUDEインデックス。それらは-うまくいけば-2017年9月頃のPostgresの次のメジャーバージョン(10)になる予定です。_(a) INCLUDE (b)_のインデックスは、_(a)_のインデックスと同じ構造ですが、リーフページに含まれますbのすべての値(ただし順不同)。つまり、たとえば_SELECT * FROM t WHERE a = 'a' AND b = 2 ;_には使用できません。インデックスが使用される場合がありますが、_(a,b)_インデックスは1回のシークで一致する行を見つけますが、インクルードインデックスは_a = 'a'_に一致する(場合によっては100K)値を通過する必要がありますbの値を確認してください。
    一方、インデックスは_(a,b)_インデックスよりも少し幅が狭く、クエリでSUM(b)を計算するためにbで順序を指定する必要はありません。 。たとえば、_(a) INCLUDE (b,c,d)_を使用して、3つの列すべてを集計するクエリに類似したクエリに使用することもできます。

  • フィルターされた(部分)インデックス。少しおかしく聞こえるかもしれない提案* 最初は:

    _CREATE INDEX flt_a  ON t (b) WHERE (a = 'a') ;
    ---
    CREATE INDEX flt_xy ON t (b) WHERE (a = 'xy') ;
    _

    a値ごとに1つのインデックス。あなたの場合、約100Kのインデックス。これはよく聞こえますが、各インデックスはサイズ(行数)と幅(b値のみを格納するため)の両方が非常に小さくなることを考慮してください。ただし、他のすべての側面では、それ(100Kのインデックスをまとめて)は、_(a,b)_インデックスのスペースを使用しながら、_(b)_のBツリーインデックスとして機能します。
    欠点は、aの新しい値がテーブルに追加されるたびに、自分で作成して管理する必要があることです。テーブルはかなり安定しており、挿入(または更新)が多数(またはまったく)ないため、問題に見えません。

  • サマリーテーブル。テーブルは安定しているため、サマリーテーブルを作成して、必要な最も一般的な集計(sum(b), sum(c), sum(d), avg(b), count(distinct b)など)。これは小さく(10万行のみ)、メインテーブルで行が挿入、更新、または削除されたときにのみ、一度だけデータを入力して更新する必要があります。

*:本番システムで1,000万のインデックスを実行するこの会社からコピーされたアイデア: ヒープ:本番環境で1,000万のPostgresqlインデックスを実行(およびカウント)

6
ypercubeᵀᴹ