第7章 性能向上に関する技法

目次

7.1. 大きなジオメトリを持つ小さなテーブル
7.1.1. 問題の説明
7.1.2. 応急処置
7.2. ジオメトリインデクスでCLUSTERを実行する
7.3. 次元変換の回避
7.4. コンフィギュレーションのチューン
7.4.1. 起動時
7.4.2. 実行時

7.1. 大きなジオメトリを持つ小さなテーブル

7.1.1. 問題の説明

現版のPostgreSQL (8.0を含む)では、TOASTテーブルに従うクエリオプティマイザの弱さに苦しみます。 TOASTテーブルは、(長いテキスト、イメージ、多数の頂点を持つ複合ジオメトリといった)通常のデータページに適合しない、(データサイズという意味では)巨大な値を納めるための「拡張部屋」の一種です。詳細情報は the PostgreSQL Documentation for TOASTをご覧ください。

(高解像度で全てのヨーロッパの国の境界を含むテーブルのような)大きなジオメトリがあるうえ、行がそう多くないテーブルを持つようになると、この問題が出てきます。テーブル自体は小さいのですが、多くのTOASTスペースを使います。例として、テーブル自体は概ね80行で3データページしか使わなくてもTOASTテーブルで8225ページを使うとします。

ここで、ジオメトリ演算子の&&を使って、ほとんどマッチしないようなバウンダリボックスを検索するクエリを出してみます。クエリオプティマイザにはテーブルは3ページ80行しかないように見えます。オプティマイザは、小さなテーブルを順に走査する方がインデクスを使うよりも早いと見積もります。そして、GiSTインデクスは無視すると決めます。通常なら、この見積もりは正しいです。しかし、この場合は&&演算子が全てのジオメトリをディスクから呼び出しでバウンディングボックスと比較しなければならなくなり、ゆえに、全てのTOASTページもまた呼び出す必要があります。

このバグに苦しむかどうかを見るには、PostgreSQLの"EXPLAIN ANALYZE"コマンドを使います。詳しい情報と技術に関する詳細については、postgres performance mailing list のスレッド(http://archives.postgresql.org/pgsql-performance/2005-02/msg00030.php)をご覧下さい。

7.1.2. 応急処置

PostgreSQLコミュニティでは、TOASTを意識したクエリ見積もりを作ることで、この問題を解決しようとしています。今のところは、二つの応急処置があります。

一つは、クエリプランナにインデクスの使用を強制することです。クエリを発行する前に"SET enable_seqscan TO off;"をサーバに送信します。これは基本的にクエリプランナに対して可能な限り順に走査することを避けるよう強制します。そのためGiSTインデクスを通常使うようになります。しかし、このフラグは接続するたびに設定しなければならず、他のケースにおいてはクエリプランナに誤った見積もりをさせることになるので、 "SET enable_seqscan TO on;"をクエリの後に送信すべきです。

もう一つは、順に走査することをクエリプランナが考える程度に早くすることです。これは、バウンダリボックスの「キャッシュ」を行う追加カラムを作成し、このカラムにマッチさせるようにすることで達成することができます。ここでの例では次のようになります。

SELECT AddGeometryColumn('myschema','mytable','bbox','4326','GEOMETRY','2');
UPDATE mytable SET bbox = ST_Envelope(ST_Force2D(the_geom));

そして、次のように、&&演算子をgeom_columnに対して行っていたものをbboxに変更します。

SELECT geom_column
FROM mytable
WHERE bbox && ST_SetSRID('BOX3D(0 0,1 1)'::box3d,4326);

もちろん、mytableの行を変更または追加したら、bboxを「同期」するようにしなければなりません。最もすっきりした方法はトリガです。もしくは、アプリケーションを変更してbboxカラムの現状を保持するか、テーブル更新後にいつもUPDATEクエリを実行するかでも対応できます。

7.2. ジオメトリインデクスでCLUSTERを実行する

読み込むことがほとんどで、かつほとんどのクエリでひとつのインデクスを使うようなテーブルのために、PostgreSQLはCLUSTERコマンドを提供しています。このコマンドは、全てのデータ行を、インデクス基準にあわせて物理的に再整理するので、二つの性能の利点を生みます。一つは、インデクスの範囲走査のために、データテーブルのシーク回数が劇的に減少することです。もう一つは、いくつかの小さなインデクス間隔に集中する場合には、データ行が分布するデータページがより少なくなるので、より効率的なキャッシュを持つことです (この点で、PostgreSQLマニュアルのCLUSTERコマンドのドキュメントを読むように仕向けられていると感じて下さい)。

しかし、GiSTインデクスは単純にNULL値を無視するため現在のところPostGISのGiSTインデクスのクラスタリングはできず、次のようなエラーメッセージを得ます。

lwgeom=# CLUSTER my_geom_index ON my_table;
ERROR: cannot cluster when index access method does not handle null values
(エラー: インデクスアクセスメソッドがNULL値を扱わない場合クラスタ化できません)
HINT: You may be able to work around this by marking column "the_geom" NOT NULL.
(ヒント: 列"the_geom"をNOT NULLとすることで、これを回避できるかもしれません)

ヒントメッセージにある通り、テーブルに"not null"制限を追加することで、この欠陥にとりあえず対応できます。例を示します。

lwgeom=# ALTER TABLE my_table ALTER COLUMN the_geom SET not null;
ALTER TABLE

もちろん、ジオメトリカラムで実際にNULL値が必要な場合、この対応はできません。さらには、制限を追加するには上の方法を使わなければならず、"ALTER TABLE blubb ADD CHECK (geometry is not null);"のようなCHECK制限は使えません。

7.3. 次元変換の回避

ときどき、テーブルで3次元、4次元のデータを持つのに、常にOpenGIS準拠のST_AsText()またはST_AsBinary()関数を使ってアクセスして 2次元ジオメトリを出力させるようなことが起きます。内部でST_Force_2d()関数を呼んでいるために発生しますが、これは、大きなジオメトリでは重大なオーバヘッドを誘引することになります。このオーバヘッドを回避するには、一度追加された次元を前もって落とし、かつこれを永続化するのが適当かも知れません。

UPDATE mytable SET the_geom = ST_Force2D(the_geom);
VACUUM FULL ANALYZE mytable;

AddGeometryColumn()を使ってジオメトリカラムを追加した場合、ジオメトリの次元に関する制限があることに注意してください。この制限を迂回するには、制限の削除が必要になります。geometry_columnsテーブル内のエントリを更新して、その後で制限を再作成することを忘れないで下さい。

大きなテーブルの場合、WHERE節、およびプライマリキー若しくは他の適切な基準によってテーブルの一部へのUPDATEを制限させて、UPDATEの実行の間に単に"VACUUM;"と実行することで、UPDATEをより小さい塊に分割するのが賢いやり方かもしれません。これにより、テンポラリディスクスペースが劇的に減少します。さらに、次元混合のジオメトリを持つ場合、"WHERE dimension(the_geom)>2"によってUPDATEを制限することで、2次元で書かれているジオメトリの再書き込みをスキップさせることができます。

7.4. コンフィギュレーションのチューン

PostGISの調整はPostgreSQLの作業量の調整と非常に似ています。ジオメトリとラスタは重く、メモリ関連の最適化は他のPostgreSQLクエリと比べて影響が大きい点だけは留意して下さい。

PostgreSQLの最適化に関する一般的な詳細は、Tuning your PostgreSQL Serverをご覧ください。

PostgreSQL 9.4以上では、ALTER SYSTEM..を使うことで、postgresql.confやpostgresql.auto.confを触ることなくサーバレベルで設定できます。

ALTER SYSTEM SET work_mem = '256MB';
-- 起動時設定でない設定を強制します。新規接続に影響を与えます。
SELECT pg_reload_conf();
-- 現在の設定値を表示
-- 全ての設定を見るにはSHOW ALLを使います
SHOW work_mem;

この設定に追加して、PostGISには、「PostGIS GUC (Grand Unified Custom)変数」で示している、いくつかの独特の設定があります。

7.4.1. 起動時

これらの設定はpostgresql.conf内にあります。

constraint_exclusion

  • デフォルト: partition

  • 一般的にテーブルのパーティショニングに使われます。デフォルトは"partition"になっています。この場合、制約やテーブルが継承階層の中にあって、クエリプランナに他のペナルティを与えない場合に、制約を考慮に入れたテーブルの解析だけを行わせます。PostgreSQL 8.4以上ではこれが理想的です。

shared_buffers

  • デフォルト: ~32MB

  • 有効RAMの1/3から3/4程度にします。

max_worker_processes これは、PostgreSQL 9.4以上で有効です。PostgreSQL 9.6以上では、並列クエリ処理に使うプロセス数の最大値の制御で、さらに重要なものとなっています。

  • デフォルト: 8

  • システムが対応できるバックグラウンドプロセスの最大値を設定します。このパラメータはサーバ起動時のみ設定できます。

7.4.2. 実行時

work_mem (並べ替えや複雑なクエリに使われるメモリ)

  • デフォルト: 1-4MB

  • 大きなデータベースの場合や、複雑なクエリの場合、RAMが多い場合は値を大きくするように調整します。

  • 同時接続ユーザ数が多い場合や、RAMが少ない場合には値を小さくするように調整します。

  • たくさんのRAMを持ち、少数の開発者しかいない場合は次のようにします。

    SET work_mem TO '256MB';;
                    

maintenance_work_mem (VACUUM, CREATE INDEX等で使われるメモリ)

  • デフォルト: 16-64MB

  • 一般的には低すぎます - メモリスワップの間、入出力が拘束され、オブジェクトがロックされます。

  • 本番サーバでは32MBから1GBが推奨ですが、同時接続ユーザ数に依存します。たくさんのRAMを持ち、少数の開発者しかいない場合は次のようにします。

    SET maintenance_work_mem TO '1GB';
                    

max_parallel_workers_per_gather PostgreSQL 9.6以上で有効です。また、並列クエリに対応しているPostGIS 2.3以上でのみ影響が出ます。0より大きい値にすると、ST_Intersects等の関係関数が複数のプロセスを使えるようになり、2倍以上高速になります。多数のプロセッサを予備に持っているなら、この値を、持っているプロセッサ数にあわせるべきです。また、max_worker_processesを少なくともこの値より大きくするようにして下さい。

  • デフォルト: 0

  • 単一のGatherノードが開始できるワーカの最大数を設定します。並列ワーカは、max_worker_processesで確立されたプロセスのプールから取得されます。要求したワーカ数は、実際には実行可能になっていない場合があることに注意して下さい。これが発生する場合には、想定より少ないワーカでプランが実行され、非効率になります。これの値を0 (デフォルト値)にすると、並列クエリ実行が無効になります。