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


14

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

次のような表があります。

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

私のセットに関する事実:

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

  • 私は値で〜100kの行を持ってa列内のa他の値(例えばについても同様、c)。

  • これは、列 'a'に〜100k個の異なる値があることを意味します。

  • 私のクエリのほとんどは、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 CLUSTERがクエリプランナーのテーブルが更新されていないという事実を使用するかどうかです(関連する開始/終了ページのバイナリ検索など)。やや関連性がありますが、すべての列をBTreeに格納し、テーブルを完全に削除できますか(または同等のものを達成できます。これらはSQLサーバーのクラスター化インデックスであると思います)。ここで役立つBTree / BRINハイブリッドインデックスはありますか?

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


「主に全文検索に使用されます」GiSTはPostGISで非常に広く使用されています。
jpmc26

回答:


15

BTree

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

必ずしもそうではありません— 「カバー」しているbtreeインデックスを持っているのが最速の読み取り時間であり、それだけが必要な場合(つまり、追加のストレージを購入できる場合)が最善の策です。

ブリン

私の理解では、役に立たないページを読むことを犠牲にして、ここに小さなインデックスを作成できるということです。小さな値を使用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を使用したインデックスのみのスキャン(cost = 0.43..3017.80 rows = 105907 width = 4)(actual time = 0.038..27.286 rows = 100000 loops = 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を使用したインデックススキャン(cost = 0.43..3799.80 rows = 105907 width = 4)(actual time = 0.037..33.084 rows = 100000 loops = 1)|
| インデックス条件:(a = 'a' :: text)|
| 計画時間:0.135ミリ秒|
| 実行時間:54.280ミリ秒|

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のビットマップヒープスキャン(cost = 888.78..21330.61 rows = 105907 width = 4)(実際の時間= 2.738..31.967 rows = 100000ループ= 1)|
| Condの再確認:(a = 'a' :: text)|
| インデックス再チェックによって削除された行:96 |
| ヒープブロック:lossy = 736 |
| -> foo_brin_4のビットマップインデックススキャン(cost = 0.00..862.30 rows = 105907 width = 0)(実際の時間= 2.720..2.720 rows = 7360ループ= 1)|
| インデックス条件:(a = 'a' :: text)|
| 計画時間:0.101ミリ秒|
| 実行時間:52.501ミリ秒|

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のビットマップヒープスキャン(cost = 952.78..21394.61 rows = 105907 width = 4)(実際の時間= 5.286..33.492 rows = 100000ループ= 1)|
| Condの再確認:(a = 'a' :: text)|
| インデックス再チェックによって削除された行:96 |
| ヒープブロック:lossy = 736 |
| -> foo_brin_2のビットマップインデックススキャン(cost = 0.00..926.30 rows = 105907 width = 0)(actual time = 5.275..5.275 rows = 7360 loops = 1)|
| インデックス条件:(a = 'a' :: text)|
| 計画時間:0.095ミリ秒|
| 実行時間: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のビットマップヒープスキャン(cost = 980.78..21422.61 rows = 105907 width = 4)(実際の時間= 12.377..33.956 rows = 100000ループ= 1)|
| Condを再確認します:(a = 'a' :: text)|
| ヒープブロック:exact = 736 |
| -> foo_ginのビットマップインデックススキャン(cost = 0.00..954.30 rows = 105907 width = 0)(actual time = 12.271..12.271 rows = 100000 loops = 1)|
| インデックス条件:(a = 'a' :: text)|
| 計画時間:0.118ミリ秒|
| 実行時間:55.366ミリ秒|

ここに dbfiddle


したがって、カバーインデックスは、ディスクスペースを犠牲にしてテーブルの読み取りを完全にスキップしますか?良いトレードオフのようです。私たちは(私が間違っている場合は、正しい私)「インデックス全体を読ん」でBRINインデックスに同じことを意味し、私は私は何が起こっているかだと思う全体BRINインデックススキャン意味だと思うdbfiddle.uk/...なしに、?
foo

「(テーブルが物理的にソートされているとは想定できないため、これもあります)」に関する@foo テーブルの物理的な順序(クラスターかどうか)は関係ありません。インデックスには正しい順序で値があります。しかし、Postgres Bツリーインデックスはすべての値を格納する必要があります(そして、はい、複数回)。それが彼らの設計方法です。各個別の値を1回だけ保存すると、優れた機能/改善になります。Postgresの開発者に提案することもできます(さらに、実装を支援することさえできます)。
ypercubeᵀᴹ

1
@foo —あなたは完全に正しい、BRINインデックスのスキャンは常にインデックス全体(pgcon.org/2016/schedule/attachments/…、最後から2番目のスライド)をスキャンします—それはフィドルのEXPLAIN PLANには表示されません、 それは...ですか?
ジャックは

2
@ypercubeᵀᴹOracleでCOMPRESSを使用して、ブロックごとに1つの個別のプレフィックスを保存できます。
ジャックは

@JackDouglas私はBitmap Index Scan「ブリンインデックス全体を読む」という意味で読んでいますが、それは間違った読みかもしれません。Oracle COMPRESSはBツリーのサイズを縮小するので、ここで役立つもののように見えますが、私はpgにこだわっています!
foo

6

ほかBTREEブリンた最も賢明な選択肢、価値調査かもしれないいくつかの他、エキゾチック・オプションようだ-彼らは可能性があなたのケースに役立つかどうかを:

  • 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)インデックスよりもわずかに狭くb、クエリを計算するための順序は必要ありませんSUM(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つのインデックス。あなたの場合、インデックスは約10万です。これはよく聞こえますが、各インデックスはサイズ(行数)と幅(b値のみを格納するため)の両方が非常に小さいことを考慮してください。ただし、他のすべての面(a,b)では、(b)インデックスのスペースを使用している間(100Kのインデックスを合わせて)Bツリーインデックスとして機能します。
    欠点は、新しい値がaテーブルに追加されるたびに、自分で作成して保守する必要があることです。テーブルはかなり安定しているため、多くの(または)挿入/更新が行われないため、問題のようには見えません。

  • サマリー表。テーブルはかなり安定しているため、必要な最も一般的な集計(sum(b), sum(c), sum(d), avg(b), count(distinct b)など)を使用して、いつでもサマリーテーブルを作成および設定できます。それは小さく(100K行のみ)、メインテーブルで行が挿入/更新/削除された場合にのみ、1回だけデータを挿入して更新するだけで済みます。

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


1は興味深いですが、指摘したように、pg 10はまだ出ていません。2 おかしく聞こえます(少なくとも「一般的な知恵」に反する)。あなたが指摘するように、それは私のほとんど書き込みのないワークフローで動作する可能性があるからです。3.私のために動作しない、私が使用しSUM、彼らがより多くのようにしている(例として、実際には、私のクエリは事前に計算することができませんselect ... from t where a = '?' and ??wjereは、??いくつかの他のユーザー定義の条件になります。
fooの

1
まあ、私たちが何であるかわからなければ仕方がありません??;)
ypercubeᵀᴹ17年

フィルターされたインデックスに言及します。テーブルのパーティション分割はどうですか?
jpmc26

@ jpmc26おもしろい、私は答えにフィルターされたインデックスの提案はある意味でパーティション分割の一種であると考えていました。ここでもパーティション分割が役立つ場合がありますが、わかりません。その結果、多くの小さなインデックス/テーブルが作成されます。
ypercubeᵀᴹ

2
ここでは、データがほとんど更新されないため、部分的にカバーするbtreeインデックスがパフォーマンスの王になると期待しています。それが10万個のインデックスを意味する場合でも。合計インデックスサイズは最小です(BRINインデックスを除きますが、Postgresはヒープページを追加で読み取り、フィルタリングする必要があります)。インデックスの生成は、動的SQLを使用して自動化できます。この関連する回答の記述DO
アーウィンブランドステッター
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.