Postgresの世界では、テーブルデータストレージ(別名「ヒープ」)を効率的にナビゲートするためにインデックスが不可欠です。 Postgresはヒープのクラスタリングを維持せず、MVCCアーキテクチャは同じタプルリングの複数のバージョンにつながります。アプリケーションをサポートするための効果的かつ効率的なインデックスを作成して維持することは、不可欠なスキルです。
デプロイメントでのインデックスの使用を最適化および改善するためのヒントを確認するために読んでください。
注:以下に示すクエリは、変更されていないpagilaサンプルデータベースで実行されます。
非アクティブなすべての顧客の電子メールをフェッチするクエリについて考えてみます。 顧客 テーブルにはアクティブがあります 列であり、クエリは簡単です:
pagila=# EXPLAIN SELECT email FROM customer WHERE active=0;
QUERY PLAN
-----------------------------------------------------------
Seq Scan on customer (cost=0.00..16.49 rows=15 width=32)
Filter: (active = 0)
(2 rows)
クエリでは、顧客テーブルの完全な順次スキャンが必要です。アクティブな列にインデックスを作成しましょう:
pagila=# CREATE INDEX idx_cust1 ON customer(active);
CREATE INDEX
pagila=# EXPLAIN SELECT email FROM customer WHERE active=0;
QUERY PLAN
-----------------------------------------------------------------------------
Index Scan using idx_cust1 on customer (cost=0.28..12.29 rows=15 width=32)
Index Cond: (active = 0)
(2 rows)
これは役に立ち、シーケンシャルスキャンは「インデックススキャン」になりました。つまり、Postgresはインデックス「idx_cust1」をスキャンし、テーブルのヒープをさらに検索して、他の列の値(この場合は email )を読み取ります。 列)クエリに必要です。
PostgreSQL11はカバーインデックスを導入しました。この機能を使用すると、インデックス自体に1つ以上の追加の列を含めることができます。つまり、これらの追加の列の値は、インデックスデータストレージ内に格納されます。
この機能を使用し、インデックス内にメールの値を含める場合、Postgresはテーブルのヒープを調べてメールの値を取得する必要はありません。 。これが機能するかどうかを見てみましょう:
pagila=# CREATE INDEX idx_cust2 ON customer(active) INCLUDE (email);
CREATE INDEX
pagila=# EXPLAIN SELECT email FROM customer WHERE active=0;
QUERY PLAN
----------------------------------------------------------------------------------
Index Only Scan using idx_cust2 on customer (cost=0.28..12.29 rows=15 width=32)
Index Cond: (active = 0)
(2 rows)
「インデックスのみのスキャン」は、クエリがインデックス自体によって完全に満たされるようになったことを示しているため、テーブルのヒープを読み取るためのすべてのディスクI/Oを回避できる可能性があります。
現在、カバーインデックスはBツリーインデックスでのみ使用できます。また、カバーリングインデックスを維持するためのコストは、通常のインデックスよりも当然高くなります。
部分インデックスは、テーブル内の行のサブセットのみにインデックスを付けます。これにより、インデックスのサイズが小さくなり、スキャンが高速になります。
カリフォルニアにいる顧客の電子メールのリストを取得する必要があると仮定します。クエリは次のとおりです:
SELECT c.email FROM customer c
JOIN address a ON c.address_id = a.address_id
WHERE a.district = 'California';
これには、結合されている両方のテーブルのスキャンを含むクエリプランがあります:
pagila=# EXPLAIN SELECT c.email FROM customer c
pagila-# JOIN address a ON c.address_id = a.address_id
pagila-# WHERE a.district = 'California';
QUERY PLAN
----------------------------------------------------------------------
Hash Join (cost=15.65..32.22 rows=9 width=32)
Hash Cond: (c.address_id = a.address_id)
-> Seq Scan on customer c (cost=0.00..14.99 rows=599 width=34)
-> Hash (cost=15.54..15.54 rows=9 width=4)
-> Seq Scan on address a (cost=0.00..15.54 rows=9 width=4)
Filter: (district = 'California'::text)
(6 rows)
通常のインデックスで何が得られるか見てみましょう:
pagila=# CREATE INDEX idx_address1 ON address(district);
CREATE INDEX
pagila=# EXPLAIN SELECT c.email FROM customer c
pagila-# JOIN address a ON c.address_id = a.address_id
pagila-# WHERE a.district = 'California';
QUERY PLAN
---------------------------------------------------------------------------------------
Hash Join (cost=12.98..29.55 rows=9 width=32)
Hash Cond: (c.address_id = a.address_id)
-> Seq Scan on customer c (cost=0.00..14.99 rows=599 width=34)
-> Hash (cost=12.87..12.87 rows=9 width=4)
-> Bitmap Heap Scan on address a (cost=4.34..12.87 rows=9 width=4)
Recheck Cond: (district = 'California'::text)
-> Bitmap Index Scan on idx_address1 (cost=0.00..4.34 rows=9 width=0)
Index Cond: (district = 'California'::text)
(8 rows)
アドレスのスキャン idx_address1のインデックススキャンに置き換えられました 、およびアドレスのヒープのスキャン。
これは頻繁なクエリであり、最適化する必要があると仮定すると、地区が「カリフォルニア」である住所の行のみにインデックスを付ける部分インデックスを使用できます。
pagila=# CREATE INDEX idx_address2 ON address(address_id) WHERE district='California';
CREATE INDEX
pagila=# EXPLAIN SELECT c.email FROM customer c
pagila-# JOIN address a ON c.address_id = a.address_id
pagila-# WHERE a.district = 'California';
QUERY PLAN
------------------------------------------------------------------------------------------------
Hash Join (cost=12.38..28.96 rows=9 width=32)
Hash Cond: (c.address_id = a.address_id)
-> Seq Scan on customer c (cost=0.00..14.99 rows=599 width=34)
-> Hash (cost=12.27..12.27 rows=9 width=4)
-> Index Only Scan using idx_address2 on address a (cost=0.14..12.27 rows=9 width=4)
(5 rows)
クエリはインデックスidx_address2のみを読み取るようになりました テーブルに触れないアドレス 。
インデックス付けが必要な一部の列には、スカラーデータ型がない場合があります。 jsonbのような列タイプ 、配列 およびtsvector 複合値または複数の値があります。このような列にインデックスを付ける必要がある場合は、通常、それらの列の個々の値も検索する必要があります。
舞台裏のアウトテイクを含むすべての映画のタイトルを見つけてみましょう。 映画 テーブルには、 special_featuresというテキスト配列列があります 、テキスト配列要素舞台裏が含まれています 映画にその機能がある場合。このような映画をすべて見つけるには、の「舞台裏」が含まれるすべての行を選択する必要があります。 配列の値のspecial_features :
SELECT title FROM film WHERE special_features @> '{"Behind The Scenes"}';
封じ込め演算子@> 左側が右側のスーパーセットであるかどうかを確認します。
クエリプランは次のとおりです。
pagila=# EXPLAIN SELECT title FROM film
pagila-# WHERE special_features @> '{"Behind The Scenes"}';
QUERY PLAN
-----------------------------------------------------------------
Seq Scan on film (cost=0.00..67.50 rows=5 width=15)
Filter: (special_features @> '{"Behind The Scenes"}'::text[])
(2 rows)
これには、67のコストで、ヒープのフルスキャンが必要です。
通常のBツリーインデックスが役立つかどうかを見てみましょう:
pagila=# CREATE INDEX idx_film1 ON film(special_features);
CREATE INDEX
pagila=# EXPLAIN SELECT title FROM film
pagila-# WHERE special_features @> '{"Behind The Scenes"}';
QUERY PLAN
-----------------------------------------------------------------
Seq Scan on film (cost=0.00..67.50 rows=5 width=15)
Filter: (special_features @> '{"Behind The Scenes"}'::text[])
(2 rows)
インデックスも考慮されていません。 Bツリーインデックスは、インデックスを作成した値に個々の要素があることを認識していません。
必要なのはGINインデックスです。
pagila=# CREATE INDEX idx_film2 ON film USING GIN(special_features);
CREATE INDEX
pagila=# EXPLAIN SELECT title FROM film
pagila-# WHERE special_features @> '{"Behind The Scenes"}';
QUERY PLAN
---------------------------------------------------------------------------
Bitmap Heap Scan on film (cost=8.04..23.58 rows=5 width=15)
Recheck Cond: (special_features @> '{"Behind The Scenes"}'::text[])
-> Bitmap Index Scan on idx_film2 (cost=0.00..8.04 rows=5 width=0)
Index Cond: (special_features @> '{"Behind The Scenes"}'::text[])
(4 rows)
GINインデックスは、インデックス付けされた複合値に対する個々の値の照合をサポートできるため、元の値の半分未満のコストでクエリプランが作成されます。
時間の経過とともにインデックスが蓄積され、別のインデックスとまったく同じ定義を持つインデックスが追加されることがあります。カタログビューpg_indexes
を使用できます 人間が読める形式のインデックスのSQL定義を取得します。同一の定義を簡単に検出することもできます:
SELECT array_agg(indexname) AS indexes, replace(indexdef, indexname, '') AS defn
FROM pg_indexes
GROUP BY defn
HAVING count(*) > 1;
そして、ストックパギラデータベースで実行した場合の結果は次のとおりです。
pagila=# SELECT array_agg(indexname) AS indexes, replace(indexdef, indexname, '') AS defn
pagila-# FROM pg_indexes
pagila-# GROUP BY defn
pagila-# HAVING count(*) > 1;
indexes | defn
------------------------------------------------------------------------+------------------------------------------------------------------
{payment_p2017_01_customer_id_idx,idx_fk_payment_p2017_01_customer_id} | CREATE INDEX ON public.payment_p2017_01 USING btree (customer_id
{payment_p2017_02_customer_id_idx,idx_fk_payment_p2017_02_customer_id} | CREATE INDEX ON public.payment_p2017_02 USING btree (customer_id
{payment_p2017_03_customer_id_idx,idx_fk_payment_p2017_03_customer_id} | CREATE INDEX ON public.payment_p2017_03 USING btree (customer_id
{idx_fk_payment_p2017_04_customer_id,payment_p2017_04_customer_id_idx} | CREATE INDEX ON public.payment_p2017_04 USING btree (customer_id
{payment_p2017_05_customer_id_idx,idx_fk_payment_p2017_05_customer_id} | CREATE INDEX ON public.payment_p2017_05 USING btree (customer_id
{idx_fk_payment_p2017_06_customer_id,payment_p2017_06_customer_id_idx} | CREATE INDEX ON public.payment_p2017_06 USING btree (customer_id
(6 rows)
また、一方が他方の列のスーパーセットにインデックスを付ける複数のインデックスが作成される可能性もあります。これは望ましい場合と望ましくない場合があります。スーパーセットを使用すると、インデックスのみのスキャンが実行される可能性がありますが、これは良いことですが、スペースを取りすぎる可能性があります。または、本来最適化することを目的としていたクエリが使用されなくなった可能性があります。
このようなインデックスの検出を自動化したい場合は、pg_catalogtablepg_indexが出発点として適しています。
データベースを使用するアプリケーションが進化するにつれて、それらが使用するクエリも進化します。以前に追加されたインデックスは、クエリで使用できなくなります。インデックスがスキャンされるたびに、統計マネージャーによって記録され、システムカタログビューpg_stat_user_indexes
で累積カウントを確認できます。 値としてidx_scan
。この値を一定期間(たとえば1か月)にわたって監視すると、どのインデックスが未使用で削除できるかがわかります。
「パブリック」スキーマ内のすべてのインデックスの現在のスキャンカウントを取得するためのクエリは次のとおりです。
SELECT relname, indexrelname, idx_scan
FROM pg_catalog.pg_stat_user_indexes
WHERE schemaname = 'public';
次のような出力で:
pagila=# SELECT relname, indexrelname, idx_scan
pagila-# FROM pg_catalog.pg_stat_user_indexes
pagila-# WHERE schemaname = 'public'
pagila-# LIMIT 10;
relname | indexrelname | idx_scan
---------------+--------------------+----------
customer | customer_pkey | 32093
actor | actor_pkey | 5462
address | address_pkey | 660
category | category_pkey | 1000
city | city_pkey | 609
country | country_pkey | 604
film_actor | film_actor_pkey | 0
film_category | film_category_pkey | 0
film | film_pkey | 11043
inventory | inventory_pkey | 16048
(10 rows)
インデックスを再作成する必要があることは珍しくありません。インデックスも肥大化する可能性があり、インデックスを再作成するとそれが修正され、スキャンが高速化されます。インデックスも破損する可能性があります。インデックスパラメータを変更すると、インデックスの再作成も必要になる場合があります。
PostgreSQL 11では、Bツリーインデックスの作成は同時に行われます。複数の並列ワーカーを利用して、インデックスの作成を高速化できます。ただし、これらの構成エントリが適切に設定されていることを確認する必要があります。
SET max_parallel_workers = 32;
SET max_parallel_maintenance_workers = 16;
デフォルト値は不当に小さいです。理想的には、これらの数はCPUコアの数とともに増加するはずです。詳細については、ドキュメントを参照してください。
CONCURRENTLY を使用して、バックグラウンドでインデックスを作成することもできます。 CREATE INDEXのパラメータ コマンド:
pagila=# CREATE INDEX CONCURRENTLY idx_address1 ON address(district);
CREATE INDEX
これは、テーブルのロックを必要としないため、書き込みをロックアウトしないという点で、通常の作成インデックスを実行することとは異なります。欠点は、完了するまでに時間とリソースがかかることです。