第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_Force_2d(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_Force_2d(the_geom); 
VACUUM FULL ANALYZE mytable;

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

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

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

この技法は、FOSS4G 2007カンファレンスでのKevin Neufeldさんのプレゼンテーション「PostGISパワーユーザのための技法(Tips for the PostGIS Power User)」から得たものです。 PostGISの使用(たとえば、「静的データと複雑な解析」対「よくアップデートされるデータと多数のユーザ」など)に依存して、これらの変更によって、クエリがはっきり速度向上するようになります。

詳細情報(およびよりよい書式)については、オリジナルのプレゼンテーションがhttp://2007.foss4g.org/presentations/view.php?abstract_id=117にあります。

7.4.1. 起動時

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

checkpoint_segments

  • 自動WALチェックポイント間のログファイルのセグメント(1セグメントは通常16MB)の数です。デフォルトは3です。

  • 書き込み処理が多いデータベースごとに少なくとも10から30を設定します。大きいデータベース負荷がかかる場合は増やします。この話題に関する別の記事として、Greg Smith: Checkpoint and Background writerは読む価値があります。

  • できればxlogは別のディスク装置に保存します。

constraint_exclusion

  • デフォルト: off (PostgreSQL 8.4より前の場合。PostgreSQL 8.4以上はpartitionに設定)

  • 一般的にテーブルのパーティショニングに使われます。PostgreSQL 8.4より前を使用している場合、"on"にして、クエリプランナに対して求めるような最適化をさせます。 PostgreSQL 8.4からは、デフォルトは"partition"になっています。この場合、制約やテーブルが継承階層の中にあって、クエリプランナに他のペナルティを与えない場合に、制約を考慮に入れたテーブルの解析を強制します。PostgreSQL 8.4以上ではこれが理想的です。

shared_buffers

  • デフォルト: ~32MB

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

7.4.2. 実行時

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

  • デフォルトt: 1MB

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

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

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

                        SET work_mem TO 1200000;
                    

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

  • デフォルト: 16MB

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

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

                        SET maintainence_work_mem TO 1200000;