データベースインデックスは、データページへの直接アクセスを実現することにより、パフォーマンスの向上を目的とした特別なデータ構造の使用です。データベースインデックスは、印刷された本のインデックスセクションのように機能します。インデックスセクションを調べることで、関心のある用語を含むページをすばやく特定できます。ページを簡単に見つけて、直接アクセスできます。 。これは、探している用語が見つかるまで、本のページを順番にスキャンする代わりです。
インデックスは、DBAにとって不可欠なツールです。インデックスを使用すると、さまざまなデータドメインのパフォーマンスを大幅に向上させることができます。 PostgreSQLは、その優れた拡張性と、コアアドオンとサードパーティアドオンの両方の豊富なコレクションで知られており、インデックス作成もこのルールの例外ではありません。 PostgreSQLインデックスは、スカラー型の最も単純なbツリーインデックスから地理空間GiSTインデックス、fts、json、または配列GINインデックスまで、さまざまなケースをカバーしています。
ただし、インデックスは見た目ほど素晴らしい(そして実際にはそうです!)無料ではありません。インデックス付きテーブルへの書き込みには、一定のペナルティがあります。したがって、DBAは、特定のインデックスを作成するためのオプションを検討する前に、まずそのインデックスが意味をなすものであることを確認する必要があります。つまり、DBAの作成による利益は、書き込みによるパフォーマンスの低下を上回ります。
PostgreSQLの基本的なインデックス用語
PostgreSQLのインデックスの種類とその使用法を説明する前に、ドキュメントを読むときにDBAが遅かれ早かれ出くわすいくつかの用語を見てみましょう。
- インデックスアクセス方法 (アクセス方法とも呼ばれます ):インデックスタイプ(Bツリー、GiST、GINなど)
- タイプ: インデックス付き列のデータ型
- オペレーター: 2つのデータ型間の関数
- オペレーターファミリー: クロスデータ型演算子、同様の動作を持つ型の演算子をグループ化することにより
- オペレータークラス (インデックス戦略とも呼ばれます ):列のインデックスで使用される演算子を定義します
PostgreSQLのシステムカタログでは、アクセスメソッドはpg_amに、演算子クラスはpg_opclassに、演算子ファミリはpg_opfamilyに格納されています。上記の依存関係を次の図に示します。
PostgreSQLのインデックスの種類
PostgreSQLは次のインデックスタイプを提供します:
- Bツリー: ソート可能なタイプに適用可能なデフォルトのインデックス
- ハッシュ: 平等のみを処理します
- GiST: 非スカラーデータ型(幾何学的形状、フィート、配列など)に適しています
- SP-GiST: スペースパーティション化されたGIST、不均衡な構造(四分木、k-d木、基数木)を処理するためのGiSTの進化
- GIN: 複雑なタイプ(jsonb、fts、arraysなど)に適しています
- ブリン: 各ブロックに最小/最大値を格納することで並べ替えることができるデータをサポートする比較的新しいタイプのインデックス
低めに、実際の例をいくつか使って手を汚そうとします。示されているすべての例は、FreeBSD11.1上のPostgreSQL10.0(10と9の両方のpsqlクライアントを使用)で実行されています。
Bツリーインデックス
次の表があるとします。
create table part (
id serial primary key,
partno varchar(20) NOT NULL UNIQUE,
partname varchar(80) NOT NULL,
partdescr text,
machine_id int NOT NULL
);
testdb=# \d part
Table "public.part"
Column | Type | Modifiers
------------+-----------------------+---------------------------------------------------
id | integer | not null default nextval('part_id_seq'::regclass)
partno | character varying(20)| not null
partname | character varying(80)| not null
partdescr | text |
machine_id | integer | not null
Indexes:
"part_pkey" PRIMARY KEY, btree (id)
"part_partno_key" UNIQUE CONSTRAINT, btree (partno)
このかなり一般的なテーブルを定義すると、PostgreSQLは舞台裏で2つの一意のBツリーインデックス(part_pkeyとpart_partno_key)を作成します。したがって、PostgreSQLのすべての一意の制約は、一意のINDEXを使用して実装されます。テーブルに100万行のデータを入力しましょう:
testdb=# with populate_qry as (select gs from generate_series(1,1000000) as gs )
insert into part (partno, partname,machine_id) SELECT 'PNo:'||gs, 'Part '||gs,0 from populate_qry;
INSERT 0 1000000
それでは、テーブルに対していくつかのクエリを実行してみましょう。まず、\ timing:
と入力して、クエリ時間を報告するようにpsqlクライアントに指示します。testdb=# select * from part where id=100000;
id | partno | partname | partdescr | machine_id
--------+------------+-------------+-----------+------------
100000 | PNo:100000 | Part 100000 | | 0
(1 row)
Time: 0,284 ms
testdb=# select * from part where partno='PNo:100000';
id | partno | partname | partdescr | machine_id
--------+------------+-------------+-----------+------------
100000 | PNo:100000 | Part 100000 | | 0
(1 row)
Time: 0,319 ms
結果を得るのにほんの数ミリ秒しかかからないことがわかります。上記のクエリで使用された両方の列について、適切なインデックスがすでに定義されているため、これを予期していました。次に、インデックスが存在しない列のパーツ名をクエリしてみましょう。
testdb=# select * from part where partname='Part 100000';
id | partno | partname | partdescr | machine_id
--------+------------+-------------+-----------+------------
100000 | PNo:100000 | Part 100000 | | 0
(1 row)
Time: 89,173 ms
ここでは、インデックス付けされていない列の場合、パフォーマンスが大幅に低下することがはっきりとわかります。次に、その列にインデックスを作成して、クエリを繰り返します。
testdb=# create index part_partname_idx ON part(partname);
CREATE INDEX
Time: 15734,829 ms (00:15,735)
testdb=# select * from part where partname='Part 100000';
id | partno | partname | partdescr | machine_id
--------+------------+-------------+-----------+------------
100000 | PNo:100000 | Part 100000 | | 0
(1 row)
Time: 0,525 ms
新しいインデックスpart_partname_idxもBツリーインデックスです(デフォルト)。まず、100万行のテーブルでのインデックスの作成には、約16秒というかなりの時間がかかったことに注意してください。次に、クエリ速度が89ミリ秒から0.525ミリ秒に向上したことを確認します。 Bツリーインデックスは、等しいかどうかをチェックするだけでなく、<、<=、> =、>などの順序付けされたタイプの他の演算子を含むクエリにも役立ちます。 <=と>=
で試してみましょうtestdb=# select count(*) from part where partname>='Part 9999900';
count
-------
9
(1 row)
Time: 0,359 ms
testdb=# select count(*) from part where partname<='Part 9999900';
count
--------
999991
(1 row)
Time: 355,618 ms
最初のクエリは2番目のクエリよりもはるかに高速です。EXPLAIN(またはEXPLAIN ANALYZE)キーワードを使用することで、実際のインデックスが使用されているかどうかを確認できます。
testdb=# explain select count(*) from part where partname>='Part 9999900';
QUERY PLAN
-----------------------------------------------------------------------------------------
Aggregate (cost=8.45..8.46 rows=1 width=8)
-> Index Only Scan using part_partname_idx on part (cost=0.42..8.44 rows=1 width=0)
Index Cond: (partname >= 'Part 9999900'::text)
(3 rows)
Time: 0,671 ms
testdb=# explain select count(*) from part where partname<='Part 9999900';
QUERY PLAN
----------------------------------------------------------------------------------------
Finalize Aggregate (cost=14603.22..14603.23 rows=1 width=8)
-> Gather (cost=14603.00..14603.21 rows=2 width=8)
Workers Planned: 2
-> Partial Aggregate (cost=13603.00..13603.01 rows=1 width=8)
-> Parallel Seq Scan on part (cost=0.00..12561.33 rows=416667 width=0)
Filter: ((partname)::text <= 'Part 9999900'::text)
(6 rows)
Time: 0,461 ms
最初のケースでは、クエリプランナーはpart_partname_idxインデックスを使用することを選択します。また、これによりインデックスのみのスキャンが発生し、データテーブルにまったくアクセスできないこともわかります。 2番目のケースでは、返される結果がテーブルの大部分であるため、プランナーはインデックスを使用しても意味がないと判断します。この場合、シーケンシャルスキャンの方が高速であると考えられます。
ハッシュインデックス
PgSQL 9.6までのハッシュインデックスの使用は、WAL書き込みの欠如に関係する理由のために推奨されませんでした。 PgSQL 10.0の時点で、これらの問題は修正されましたが、それでもハッシュインデックスを使用する意味はほとんどありませんでした。 PgSQL 11には、ハッシュインデックスをその兄弟(Bツリー、GiST、GIN)とともにファーストクラスのインデックスメソッドにするための取り組みがあります。そこで、これを念頭に置いて、実際にハッシュインデックスを実際に試してみましょう。
パーツテーブルを新しい列parttypeで強化し、等しい分布の値を入力してから、「Steering」に等しいparttypeをテストするクエリを実行します。
testdb=# alter table part add parttype varchar(100) CHECK (parttype in ('Engine','Suspension','Driveline','Brakes','Steering','General')) NOT NULL DEFAULT 'General';
ALTER TABLE
Time: 42690,557 ms (00:42,691)
testdb=# with catqry as (select id,(random()*6)::int % 6 as cat from part)
update part SET parttype = CASE WHEN cat=1 THEN 'Engine' WHEN cat=2 THEN 'Suspension' WHEN cat=3 THEN 'Driveline' WHEN cat=4 THEN 'Brakes' WHEN cat=5 THEN 'Steering' ELSE 'General' END FROM catqry WHERE part.id=catqry.id;
UPDATE 1000000
Time: 46345,386 ms (00:46,345)
testdb=# select count(*) from part where id % 500 = 0 AND parttype = 'Steering';
count
-------
322
(1 row)
Time: 93,361 ms
次に、この新しい列のハッシュインデックスを作成し、前のクエリを再試行します。
testdb=# create index part_parttype_idx ON part USING hash(parttype);
CREATE INDEX
Time: 95525,395 ms (01:35,525)
testdb=# analyze ;
ANALYZE
Time: 1986,642 ms (00:01,987)
testdb=# select count(*) from part where id % 500 = 0 AND parttype = 'Steering';
count
-------
322
(1 row)
Time: 63,634 ms
ハッシュインデックスを使用した後の改善に注目します。次に、整数のハッシュインデックスのパフォーマンスを同等のbツリーインデックスと比較します。
testdb=# update part set machine_id = id;
UPDATE 1000000
Time: 392548,917 ms (06:32,549)
testdb=# select * from part where id=500000;
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------+------------+------------
500000 | PNo:500000 | Part 500000 | | 500000 | Suspension
(1 row)
Time: 0,316 ms
testdb=# select * from part where machine_id=500000;
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------+------------+------------
500000 | PNo:500000 | Part 500000 | | 500000 | Suspension
(1 row)
Time: 97,037 ms
testdb=# create index part_machine_id_idx ON part USING hash(machine_id);
CREATE INDEX
Time: 4756,249 ms (00:04,756)
testdb=#
testdb=# select * from part where machine_id=500000;
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------+------------+------------
500000 | PNo:500000 | Part 500000 | | 500000 | Suspension
(1 row)
Time: 0,297 ms
ご覧のとおり、ハッシュインデックスを使用すると、同等性をチェックするクエリの速度は、Bツリーインデックスの速度に非常に近くなります。ハッシュインデックスは、Bツリーよりも同等の方がわずかに速いと言われています。実際、ハッシュインデックスがBツリーの同等のものよりも良い結果をもたらすまで、各クエリを2、3回試行する必要がありました。
今日のホワイトペーパーをダウンロードするClusterControlを使用したPostgreSQLの管理と自動化PostgreSQLの導入、監視、管理、スケーリングを行うために知っておくべきことについて学ぶホワイトペーパーをダウンロードするGiSTインデックス
GiST(汎用検索ツリー)は、単一の種類のインデックスではなく、多くのインデックス戦略を構築するためのインフラストラクチャです。デフォルトのPostgreSQLディストリビューションは、幾何学的データ型、tsqueryおよびtsvectorのサポートを提供します。貢献して、他の多くの演算子クラスの実装があります。ドキュメントとcontribdirを読むことで、読者はGiSTとGINのユースケースの間にかなり大きな重複があることに気付くでしょう:int配列、メインケースに名前を付けるための全文検索。そのような場合、GINの方が高速であり、公式ドキュメントには明示的に記載されています。ただし、GiSTは広範な幾何学的データ型のサポートを提供します。また、この記事の執筆時点で、GiST(およびSP-GiST)は、除外制約で使用できる唯一の意味のある方法です。これに関する例を見ていきます。 (機械工学の分野にとどまり、)特定のマシンタイプに対して特定の期間有効なマシンタイプのバリエーションを定義する必要があると仮定します。また、特定のバリエーションの場合、同じマシンタイプに対して、特定のバリエーション期間と期間が重複(競合)する他のバリエーションは存在できません。
create table machine_type (
id SERIAL PRIMARY KEY,
mtname varchar(50) not null,
mtvar varchar(20) not null,
start_date date not null,
end_date date,
CONSTRAINT machine_type_uk UNIQUE (mtname,mtvar)
);
上記では、すべてのマシンタイプ名(mtname)に対して1つのバリエーション(mtvar)しか存在できないことをPostgreSQLに伝えています。 Start_dateは、このマシンタイプのバリエーションが有効である期間の開始日を示し、end_dateは、この期間の終了日を示します。 Null end_dateは、マシンタイプのバリエーションが現在有効であることを意味します。ここで、重複しない要件を制約付きで表現したいと思います。これを行う方法は、除外制約を使用することです:
testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);
EXCLUDE PostgreSQL構文を使用すると、さまざまなタイプの多くの列を、それぞれに異なる演算子を使用して指定できます。 &&は日付範囲の重複演算子であり、=はvarcharの一般的な等式演算子です。しかし、Enterキーを押す限り、PostgreSQLは次のメッセージで文句を言います:
ERROR: data type character varying has no default operator class for access method "gist"
HINT: You must specify an operator class for the index or define a default operator class for the data type.
ここで欠けているのは、varcharに対するGiSTopclassのサポートです。 btree_gist拡張機能の構築とインストールが正常に行われた場合、拡張機能の作成に進むことができます。
testdb=# create extension btree_gist ;
CREATE EXTENSION
次に、制約の作成とテストを再試行します。
testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);
ALTER TABLE
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SH','2008-01-01','2013-01-01');
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SG','2002-01-01','2009-01-01');
ERROR: conflicting key value violates exclusion constraint "machine_type_per"
DETAIL: Key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2002-01-01,2009-01-01)) conflicts with existing key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2008-01-01,2013-01-01)).
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SG','2002-01-01','2008-01-01');
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SJ','2013-01-01',null);
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SJ2','2018-01-01',null);
ERROR: conflicting key value violates exclusion constraint "machine_type_per"
DETAIL: Key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2018-01-01,)) conflicts with existing key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2013-01-01,)).
SP-GiSTインデックス
スペースパーティション化されたGiSTの略であるSP-GiSTは、GiSTのように、不均衡なディスクベースのデータ構造のドメインで多くの異なる戦略の開発を可能にするインフラストラクチャです。デフォルトのPgSQLディストリビューションは、2次元のポイント、(任意のタイプの)範囲、テキスト、およびinetタイプのサポートを提供します。 GiSTと同様に、SP-GiSTは、前の章で示した例と同様に、除外制約で使用できます。
GINインデックス
GiSTやSP-GiSTのようなGIN(一般化転置インデックス)は、多くのインデックス戦略を提供できます。 GINは、複合タイプの列にインデックスを付ける場合に適しています。デフォルトのPostgreSQLディストリビューションは、任意の配列タイプ、jsonb、および全文検索(tsvector)のサポートを提供します。貢献して、他の多くの演算子クラスの実装があります。 PostgreSQLの高く評価されている機能(および比較的最近の(9.4以降)の開発)であるJsonbは、インデックスのサポートをGINに依存しています。 GINのもう1つの一般的な使用法は、全文検索の索引付けです。 PgSQLでの全文検索はそれ自体で記事に値するので、ここでは索引付けの部分のみを取り上げます。まず、partdescr列にnull以外の値を指定し、意味のある値で1つの行を更新することにより、テーブルの準備をします。
testdb=# update part set partdescr ='';
UPDATE 1000000
Time: 383407,114 ms (06:23,407)
testdb=# update part set partdescr = 'thermostat for the cooling system' where id=500000;
UPDATE 1
Time: 2,405 ms
次に、新しく更新された列でテキスト検索を実行します。
testdb=# select * from part where partdescr @@ 'thermostat';
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------------------------------+------------+------------
500000 | PNo:500000 | Part 500000 | thermostat for the cooling system | 500000 | Suspension
(1 row)
Time: 2015,690 ms (00:02,016)
これは非常に遅く、結果を出すのにほぼ2秒かかります。次に、tsvector型でGINインデックスを作成し、インデックスに適した構文を使用してクエリを繰り返します。
testdb=# CREATE INDEX part_partdescr_idx ON part USING gin(to_tsvector('english',partdescr));
CREATE INDEX
Time: 1431,550 ms (00:01,432)
testdb=# select * from part where to_tsvector('english',partdescr) @@ to_tsquery('thermostat');
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------------------------------+------------+------------
500000 | PNo:500000 | Part 500000 | thermostat for the cooling system | 500000 | Suspension
(1 row)
Time: 0,952 ms
そして、2000倍のスピードアップが得られます。また、インデックスの作成にかかった時間が比較的短いことに気付くかもしれません。上記の例では、GINの代わりにGiSTを使用して実験し、両方のアクセス方法の読み取り、書き込み、およびインデックス作成のパフォーマンスを測定できます。
BRINインデックス
BRIN(Block Range Index)は、PostgreSQL 9.5で導入されて以来、PostgreSQLの一連のインデックスタイプに新しく追加されたものであり、標準のコア機能として数年しかありません。 BRINは、「ブロック範囲」と呼ばれる一連のページの要約情報を格納することにより、非常に大きなテーブルで機能します。 BRINインデックスは(GiSTのように)不可逆であり、これにはPostgreSQLのクエリエグゼキュータの追加ロジックと追加のメンテナンスの両方が必要です。 BRINの動作を見てみましょう:
testdb=# select count(*) from part where machine_id BETWEEN 5000 AND 10000;
count
-------
5001
(1 row)
Time: 100,376 ms
testdb=# create index part_machine_id_idx_brin ON part USING BRIN(machine_id);
CREATE INDEX
Time: 569,318 ms
testdb=# select count(*) from part where machine_id BETWEEN 5000 AND 10000;
count
-------
5001
(1 row)
Time: 5,461 ms
ここでは、BRINインデックスを使用することで、平均して約18倍のスピードアップが見られます。ただし、BRINの実際の家はビッグデータの領域にあるため、将来、この比較的新しいテクノロジーを実際のシナリオでテストしたいと考えています。