インデックスを適切に適用すると、クエリが非常に高速になります。
インデックスはポインタを使用して、データページに迅速にアクセスします。
PostgreSQL 11のインデックスに大きな変更が加えられ、待望のパッチが多数リリースされました。
このリリースの優れた機能のいくつかを見てみましょう。
並列B-TREEインデックスビルド
PostgreSQL 11は、並列インデックス作成を可能にするインフラストラクチャパッチを導入しました。
現在のところ、Bツリーインデックスでのみ使用できます。
並列Bツリーインデックスの構築は、並列作業(またはシリアルビルド)なしで同じことを行うよりも2〜3倍高速です。
PostgreSQL 11では、並列インデックスの作成はデフォルトでオンになっています。
2つの重要なパラメータがあります:
- max_parallel_workers-システムが並列クエリでサポートできるワーカーの最大数を設定します。
- max_parallel_maintenance_workers-インデックスの作成に使用できるワーカープロセスの最大数を制御します。
例で確認してみましょう:
severalnines=# CREATE TABLE test_btree AS SELECT generate_series(1,100000000) AS id;
SELECT 100000000
severalnines=# SET maintenance_work_mem = '1GB';
severalnines=# \timing
severalnines=# CREATE INDEX q ON test_btree (id);
TIME: 25294.185 ms (00:25.294)
8ウェイ並列作業で試してみましょう:
severalnines=# SET maintenance_work_mem = '2GB';
severalnines=# SET max_parallel_workers = 16;
severalnines=# SET max_parallel_maintenance_workers = 8;
severalnines=# \timing
severalnines=# CREATE INDEX q1 ON test_btree (id);
TIME: 11001.240 ms (00:11.001)
並列ワーカーとのパフォーマンスの違いを確認できます。わずかな変更で60%以上のパフォーマンスが得られます。パフォーマンスを向上させるために、maintenance_work_memを増やすこともできます。
ALTERテーブルは、並列ワーカーを増やすのにも役立ちます。以下の構文を使用して、max_parallel_maintenance_workersとともに並列ワーカーを増やすことができます。これにより、コストモデルが完全にバイパスされます。
ALTER TABLE test_btree SET (parallel_workers = 24);
ヒント:不利なクエリプランを防ぐために、インデックスの構築が完了したらデフォルトにリセットしてください。
CONCURRENTLYオプションを指定したCREATEINDEXは、特別な制限なしに並列ビルドをサポートします。実際には、最初のテーブルスキャンのみが並列で実行されます。
より詳細なパフォーマンステストはここにあります。
ハッシュ、要旨、およびGinインデックスの述語ロックを追加
PostgreSQL 11には、ハッシュインデックス、ginインデックス、およびgistインデックスの述語ロックサポートが付属しています。これらにより、SERIALIZABLEトランザクション分離がこれらのインデックスではるかに効率的になります。
利点:述語ロックは、不必要なシリアル化の失敗につながる誤検知の数を減らすことにより、シリアル化可能な分離レベルでのパフォーマンスを向上させることができます。
PostgreSQL 10では、ロック範囲は関係ですが、PostgreSQL 11では、ロックはページのみであることがわかります。
テストしてみましょう。
severalnines=# CREATE TABLE sv_predicate_lock1(c1 INT, c2 VARCHAR(10)) ;
CREATE TABLE
severalnines=# CREATE INDEX idx1_sv_predicate_lock1 ON sv_predicate_lock1 USING 'hash(c1) ;
CREATE INDEX
severalnines=# INSERT INTO sv_predicate_lock1 VALUES (generate_series(1, 100000), 'puja') ;
INSERT 0 100000
severalnines=# BEGIN ISOLATION LEVEL SERIALIZABLE ;
BEGIN
severalnines=# SELECT * FROM sv_predicate_lock1 WHERE c1=10000 FOR UPDATE ;
c1 | c2
-------+-------
10000 | puja
(1 row)
以下に示すように、ロックはリレーションではなくページレベルにあります。 PostgreSQL 10ではリレーションレベルであったため、PostgreSQL11での同時トランザクションには大きなメリットがあります。
severalnines=# SELECT locktype, relation::regclass, mode FROM pg_locks ;
locktype | relation | mode
---------------+-------------------------+-----------------
relation | pg_locks | AccessShareLock
relation | idx1_sv_predicate_lock1 | AccessShareLock
relation | sv_predicate_lock1 | RowShareLock
virtualxid | | ExclusiveLock
transactionid | | ExclusiveLock
page | idx1_sv_predicate_lock1 | SIReadLock
tuple | sv_predicate_lock1 | SIReadLock
(7 rows)
ヒント:順次スキャンには、常にリレーションレベルの述語ロックが必要です。これにより、シリアル化の失敗率が高くなる可能性があります。 random_page_costを減らしたり、cpu_tuple_costを増やしたりして、インデックススキャンの使用を促進すると役立つ場合があります。
一部の式インデックスのHOT更新を許可する
ヒープのみのタプル(HOT)機能は、冗長なインデックスエントリを排除し、テーブル全体のバキュームを実行せずに、DELETEされたまたは廃止されたUPDATEされたタプルによって使用されたスペースの再利用を可能にします。同じキーのインデックスエントリの作成を回避することで、インデックスサイズを削減します。
UPDATE後にインデックス式の値が変更されない場合は、以前PostgreSQLで許可されていなかった場所でHOT更新を許可し、そのような場合にパフォーマンスを大幅に向上させます。
これは、JSON値が変更されるがインデックス付けされた値は変更されないJSON->>フィールドなどのインデックスに特に役立ちます。
この機能は、パフォーマンスの低下(SimonによるAT Free BSDのみ)のために11.1でロールバックされました。詳細/ベンチマークは、ここにあります。これは将来のリリースで修正されるはずです。
ハッシュインデックスページ全体のスキャンを許可する
ハッシュインデックス:クエリプランナーは、インデックス付きの列が=演算子を使用した比較に含まれる場合は常に、ハッシュインデックスの使用を検討します。また、クラッシュセーフではなかったため(WALにログインしていなかったため)、DBがクラッシュした後に再構築する必要があり、ハッシュへの変更はストリーミングレプリケーションを介して書き込まれませんでした。
PostgreSQL 10では、ハッシュインデックスはWALログに記録されていました。つまり、クラッシュセーフであり、複製できます。ハッシュインデックスは、Bツリーに比べて使用するスペースがはるかに少ないため、メモリにうまく収まります。
PostgreSQL 11では、Btreeインデックスには「シングルページバキューム」と呼ばれる最適化があります。これは、インデックスページからデッドインデックスポインタを適切に削除し、そうでなければ発生する大量のインデックスの肥大化を防ぎます。同じロジックがハッシュインデックスに移植されています。スペースのリサイクルを促進し、膨張を減らします。
関数インデックスの統計
関数インデックス列にSTATISTICS値を指定できるようになりました。これは、特殊なアプリケーションの効率にとって非常に価値があります。式の列の統計を収集できるようになりました。これにより、プランナーはより正確な決定を下すことができます。
severalnines=# CREATE INDEX idx1_stats ON stat ((s1 + s2)) ;
CREATE INDEX
severalnines=# ALTER INDEX idx1_stats ALTER COLUMN 1 SET STATISTICS 1000 ;
ALTER INDEX
severalnines=# \d+ idx1_stats
Index "public.idx1_stats"
Column | Type | Definition | Storage | Stats target
--------+---------+------------+---------+--------------
expr | numeric | (c1 + c2) | main | 1000
btree, for table "public.stat1"
amcheck
新しいContribモジュールamcheckが追加されました。チェックできるのはBツリーインデックスのみです。
テストしてみましょう!
severalnines=# CREATE EXTENSION amcheck ;
CREATE EXTENSION
severalnines=# SELECT bt_index_check('idx1_stats') ;
ERROR: invalid page in block 0 of relation base/16385/16580
severalnines=#CREATE INDEX idx1_hash_data1 ON data1 USING hash (c1) ;
CREATE INDEX
severalnines=# SELECT bt_index_check('idx1_hash_data1') ;
ERROR: only B-Tree indexes are supported as targets for verification
DETAIL: Relation "idx1_hash_data1" is not a B-Tree index.
ローカルパーティションインデックスの可能性
PostgreSQL11より前は、子テーブルまたはパーティションテーブルにインデックスを作成することはできませんでした。
PostgreSQL 11では、CREATE INDEXをパーティションテーブル/親テーブルで実行すると、パーティションテーブルにインデックスのカタログエントリが作成され、カスケードして既存のパーティションに実際のインデックスが作成されます。将来のパーティションにも作成されます。
親テーブルを作成してパーティションを作成してみましょう:
severalnines=# create table test_part ( a int, list varchar(5) ) partition by list (list);
CREATE TABLE
severalnines=# create table part_1 partition of test_part for values in ('India');
CREATE TABLE
severalnines=# create table part_2 partition of test_part for values in ('USA');
CREATE TABLE
severalnines=#
severalnines=# \d+ test_part
Table "public.test_part"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
--------+----------------------+-----------+----------+---------+----------+--------------+-------------
a | integer | | | | plain | |
list | character varying(5) | | | | extended | |
Partition key: LIST (list)
Partitions: part_1 FOR VALUES IN ('India'),
part_2 FOR VALUES IN ('USA')
親テーブルにインデックスを作成してみましょう:
severalnines=# create index i_test on test_part (a);
CREATE INDEX
severalnines=# \d part_2
Table "public.part_2"
Column | Type | Collation | Nullable | Default
--------+----------------------+-----------+----------+---------
a | integer | | |
list | character varying(5) | | |
Partition of: test_part FOR VALUES IN ('USA')
Indexes:
"part_2_a_idx" btree (a)
severalnines=# \d part_1
Table "public.part_1"
Column | Type | Collation | Nullable | Default
--------+----------------------+-----------+----------+---------
a | integer | | |
list | character varying(5) | | |
Partition of: test_part FOR VALUES IN ('India')
Indexes:
"part_1_a_idx" btree (a)
インデックスはPostgreSQL11のすべてのパーティションにカスケードされます。これは、非常に優れた機能です。
カバーリングインデックス(インデックスのCLAUSEを含む)
インデックスに列を追加するINCLUDE句を指定できます。これは、一意のインデックスの一意の制約に関係のない列を追加する場合に効果的です。 INCLUDE列は、より多くのクエリがインデックスのみのスキャンの恩恵を受けることができるようにするためだけに存在します。現在のところ、BツリーインデックスのみがINCLUDE句をサポートしています。
INCLUDEなしで動作を確認しましょう。 SELECTに追加の列が表示された場合、インデックスのみのスキャンは使用されません。これは、INCLUDE句を使用して実現できます。
severalnines=# CREATE TABLE no_include (a int, b int, c int);
CREATE TABLE
severalnines=# INSERT INTO no_include SELECT 3 * val, 3 * val + 1, 3 * val + 2 FROM generate_series(0, 1000000) as val;
INSERT 0 1000001
severalnines=# CREATE UNIQUE INDEX old_unique_idx ON no_include(a, b);
CREATE INDEX
severalnines=# VACUUM ANALYZE;
VACUUM
EXPLAIN ANALYZE SELECT a, b FROM no_include WHERE a < 1000; - It will do index only scan
EXPLAIN ANALYZE SELECT a, b, c FROM no_include WHERE a < 1000; - It will not do index only scan as we have extra column in select.
severalnines=# CREATE INDEX old_idx ON no_include (a, b, c);
CREATE INDEX
severalnines=# VACUUM ANALYZE;
VACUUM
severalnines=# EXPLAIN ANALYZE SELECT a, b, c FROM no_include WHERE a < 1000; - It did index only scan as index on all three columns.
QUERY PLAN
-------------------------------------------------
Index Only Scan using old_idx on no_include
(cost=0.42..14.92 rows=371 width=12)
(actual time=0.086..0.291 rows=334 loops=1)
Index Cond: (a < 1000)
Heap Fetches: 0
Planning Time: 2.108 ms
Execution Time: 0.396 ms
(5 rows)
include句を試してみましょう。以下の例では、UNIQUE CONSTRAINTが列aとbに作成されていますが、インデックスにはc列が含まれています。
severalnines=# CREATE TABLE with_include (a int, b int, c int);
CREATE TABLE
severalnines=# INSERT INTO with_include SELECT 3 * val, 3 * val + 1, 3 * val + 2 FROM generate_series(0, 1000000) as val;
INSERT 0 1000001
severalnines=# CREATE UNIQUE INDEX new_unique_idx ON with_include(a, b) INCLUDE (c);
CREATE INDEX
severalnines=# VACUUM ANALYZE;
VACUUM
severalnines=# EXPLAIN ANALYZE SELECT a, b, c FROM with_include WHERE a < 10000;
QUERY PLAN
-----------------------------------------------------
Index Only Scan using new_unique_idx on with_include
(cost=0.42..116.06 rows=3408 width=12)
(actual time=0.085..2.348 rows=3334 loops=1)
Index Cond: (a < 10000)
Heap Fetches: 0
Planning Time: 1.851 ms
Execution Time: 2.840 ms
(5 rows)
メイン列リストの列とインクルードリストの列の間に重複があってはなりません
severalnines=# CREATE UNIQUE INDEX new_unique_idx ON with_include(a, b) INCLUDE (a);
ERROR: 42P17: included columns must not intersect with key columns
LOCATION: DefineIndex, indexcmds.c:373
メインリストの式で使用される列は機能します:
severalnines=# CREATE UNIQUE INDEX new_unique_idx_2 ON with_include(round(a), b) INCLUDE (a);
CREATE INDEX
式はインデックスのみのスキャンでは使用できないため、インクルードリストでは使用できません:
severalnines=# CREATE UNIQUE INDEX new_unique_idx_2 ON with_include(a, b) INCLUDE (round(c));
ERROR: 0A000: expressions are not supported in included columns
LOCATION: ComputeIndexAttrs, indexcmds.c:1446
結論
PostgreSQLの新機能は確かにDBAの生活を改善するので、オープンソースDBの強力な代替選択肢になることを目指しています。現在、インデックスのいくつかの機能がB-Treeに限定されていることを理解しています。これは、PostgreSQLの並列実行時代の素晴らしいスタートであり、詳しく調べるための優れたツールに向かっています。ありがとう!