Postgres 部分インデックスの作成を高速化します。
-
26-09-2020 - |
質問
Postgres 9.4で大規模(1.2TB)の静的テーブルの部分インデックスを作成しようとしています。
データは完全に静的であるため、すべてのデータを挿入して、すべてのインデックスを作成できます。
この 1.2TB テーブルには、という名前の列があります。 run_id
データをきれいに分割します。さまざまな範囲をカバーするインデックスを作成することで、優れたパフォーマンスが得られました。 run_id
s.以下に例を示します。
CREATE INDEX perception_run_frame_idx_run_266_thru_270
ON run.perception
(run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;
これらの部分インデックスにより、必要なクエリ速度が得られます。残念ながら、各部分インデックスの作成には約 70 分かかります。
CPU に制限があるようです (top
プロセスでは 100% が表示されます)。
部分インデックスの作成を高速化するために何かできることはありますか?
システム仕様:
- 18コアXeon
- 192GB RAM
- RAID 内の 12 台の SSD
- 自動バキュームがオフになっています
- メンテナンス_ワーク_メモリ:64GB(高すぎる?)
テーブル仕様:
- サイズ:1.26TB
- 行の数:105億3,700万
- 一般的なインデックス サイズ:3.2GB (~.5GB の差異があります)
テーブル定義:
CREATE TABLE run.perception(
id bigint NOT NULL,
run_id bigint NOT NULL,
frame bigint NOT NULL,
by character varying(45) NOT NULL,
by_anyone bigint NOT NULL,
by_me bigint NOT NULL,
by_s_id integer,
owning_p_id bigint NOT NULL,
obj_type_set bigint,
seq integer,
subj_id bigint NOT NULL,
subj_state_frame bigint NOT NULL,
CONSTRAINT perception_pkey PRIMARY KEY (id))
(列名はあまり深く読みすぎないでください。多少難読化しています。)
背景情報:
- このデータを使用する別のチームがオンサイトにありますが、実際にはユーザーは 1 人か 2 人だけです。(このデータはすべてシミュレーションによって生成されます。) ユーザーは、挿入が完了し、インデックスが完全に構築された後にのみデータの分析を開始します。私たちの主な関心事は、使用可能なデータの生成に必要な時間を短縮することですが、現時点でのボトルネックはインデックスの作成時間です。
- パーシャルを使用する場合、クエリ速度は完全に適切です。実際、各インデックスがカバーする実行数を増やしても、十分なクエリ パフォーマンスを維持できると思います。
- 私の推測では、テーブルをパーティション分割する必要があると思います。私たちはその道を選択する前に、他のすべての選択肢を検討しているところです。
解決
BRIN指数
Postgres以降で利用可能 9.5 おそらくあなたが探しているものです。インデックス作成が大幅に高速化され、 多くの 小さいインデックス。ただし、クエリは通常、それほど高速ではありません。 マニュアル:
BRIN はブロック範囲インデックスの略です。Brinは、特定の列がテーブル内の物理的位置と何らかの自然な相関を持つ非常に大きなテーブルを処理するために設計されています。あ ブロック 範囲は、テーブルに物理的に隣接するページのグループです。各ブロック範囲について、いくつかの要約情報はインデックスによって保存されます。
続きを読んでください。
デペスは予備テストを実施した。
あなたのケースに最適なもの:行を書けるなら クラスター化された の上 run_id
, 、インデックスが非常に小さくなり、作成がはるかに安くなります。
CREATE INDEX foo ON run.perception USING brin (run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;
単にインデックスを付けるだけでもよいでしょう テーブル全体.
テーブルレイアウト
他に何をするにしても、次のように列を順序付けすることで、行ごとの位置合わせ要件によってパディングで失われる 8 バイトを節約できます。
CREATE TABLE run.perception(
id bigint NOT NULL PRIMARY KEY
, run_id bigint NOT NULL
, frame bigint NOT NULL
, by_anyone bigint NOT NULL
, by_me bigint NOT NULL
, owning_p_id bigint NOT NULL
, subj_id bigint NOT NULL
, subj_state_frame bigint NOT NULL
, obj_type_set bigint
, by_s_id integer
, seq integer
, by varchar(45) NOT NULL -- or just use type text
);
どの列にも NULL 値がない場合、テーブルは 79 GB 小さくなります。詳細:
また、NULL にできる列は 3 つだけです。NULL ビットマップは、9 ~ 72 列で 8 バイトを占めます。もし 唯一 整数 列が NULL の場合、ストレージのパラドックスの特殊なケースが存在します。代わりにダミー値を使用した方が安くなります。4 バイトが無駄になりましたが、行に NULL ビットマップが必要なくなったため、8 バイトが節約されました。詳細はこちら:
部分インデックス
実際のクエリによっては、上記の 5 つの部分インデックスの代わりに、次の 5 つの部分インデックスを使用した方が効率的な場合があります。
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 266;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 267;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 268;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 269;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 270;
それぞれに対して 1 つのトランザクションを実行します。
削除中 run_id
インデックス列としてこの方法を使用すると、インデックス エントリあたり 8 バイト、つまり行あたり 40 バイトではなく 32 バイトが節約されます。各インデックスの作成も低コストですが、大きすぎてキャッシュに保持できないテーブル (@Jürgen や @Chris のコメントのように) を 1 つだけ作成するのではなく 5 つ作成すると、大幅に時間がかかります。したがって、それはあなたにとって役に立つかもしれないし、役に立たないかもしれません。
パーティショニング
継承に基づく - Postgres 9.5 までの唯一のオプション。
(Postgres 11、またはできれば 12 の新しい宣言的パーティショニングは、よりスマートです。)
親テーブルのすべての子供のすべての制約は、制約除外中に調べられているため、多くのパーティションがクエリ計画時間を大幅に増やす可能性があります。したがって、レガシー継承ベースのパーティション化はうまく機能します おそらく最大 100 個のパーティション;何千ものパーティションを使用しようとしないでください。
太字で強調したのは私です。したがって、1000 個の異なる値を推定すると、 run_id
, の場合、それぞれ約 10 個の値にわたるパーティションを作成します。
maintenance_work_mem
あなたがすでに調整しているのを見逃していました maintenance_work_mem
私の最初の読書で。参考までに、私の回答に引用とアドバイスを残しておきます。ドキュメントごとに:
maintenance_work_mem
(整数)メンテナンス操作が使用するメモリの最大量を指定します。
VACUUM
,CREATE INDEX
, 、 そしてALTER TABLE ADD FOREIGN KEY
. 。デフォルトでは 64 メガバイト (64MB
)。これらの操作の1つだけがデータベースセッションで一度に実行できるため、通常はインストールが同時に実行されていないため、この値をより大きく設定することは安全です。work_mem
. 。設定が大きいほど、掃除機とデータベースダンプの復元のパフォーマンスが向上する可能性があります。ときは注意してください。
autovacuum
まで実行されますautovacuum_max_workers
このメモリが割り当てられる場合があるため、デフォルト値を高すぎないように注意してください。これを個別に制御すると便利な場合がありますsetting autovacuum_work_mem
.
必要なだけ大きく設定しますが、これは (私たちにとっては) 未知のインデックス サイズに依存します。実行中のセッションに対してのみローカルでのみ。引用文が説明しているように、高すぎる 一般的な そうしないと、自動バキュームがより多くの RAM を要求する可能性があるため、この設定ではサーバーが枯渇する可能性があります。また、実行セッション中であっても、空き RAM がデータのキャッシュに有効に活用される可能性があるため、必要以上に高く設定しないでください。
次のようになります。
BEGIN;
ローカルに設定 maintenance_work_mem = 10GB; -- depends on resulting index size
CREATE INDEX perception_run_frame_idx_run_266_thru_270 ON run.perception(run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;
COMMIT;
について SET LOCAL
:
の効果
SET LOCAL
コミットされているかどうかにかかわらず、現在のトランザクションの終わりまでのみ続きます。
オブジェクトのサイズを測定するには:
明らかに、サーバーは通常、それ以外の場合は合理的に構成する必要があります。
他のヒント
おそらくこれは過剰設計されているだけかもしれません。実際に単一の完全なインデックスを使用してみましたか?テーブル全体をカバーする部分インデックスは、インデックス検索にあまり利益をもたらしません。テキストから、すべての run_ids のインデックスがあると推測します。部分インデックスを使用したインデックス スキャンにはいくつかの利点があるかもしれませんが、それでも私は最初に単純な 1 つのインデックス ソリューションをベンチマークします。
インデックスを作成するたびに、テーブル全体の完全な IO バウンド スキャンが必要です。したがって、複数の部分インデックスを作成すると、単一のインデックスの場合よりもテーブルを読み取る IO がはるかに多く必要になりますが、単一の大きなインデックスでは並べ替えがディスクに溢れてしまいます。部分的なインデックスを必要とする場合は、(メモリが許す限り) すべて (または複数) のインデックスを同時に並行して構築してみてください。
すべての run_id (8 バイトの bigint ) をメモリ内で並べ替えるのに必要な maintenance_work_mem を大まかに見積もるには、10.5 * 8 GB + オーバーヘッドが必要になります。
デフォルト以外の表領域にインデックスを作成することもできます。これらのテーブルスペースは、冗長ではないディスク (障害が発生した場合にインデックスを再作成するだけ)、またはより高速なアレイ上にあるディスクを指す可能性があります。
部分インデックスと同じ基準を使用してテーブルをパーティション化することも検討してください。これにより、実際にインデックスをまったく作成しなくても、クエリ実行時にインデックスと同じ速度が可能になります。