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

目次

5.1. 大きなジオメトリを持つ小さなテーブル
5.1.1. 問題の説明
5.1.2. 応急処置
5.2. ジオメトリインデックスでCLUSTERを実行する
5.3. 次元変換の回避

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

5.1.1. 問題の説明

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

(高解像度で全てのヨーロッパの国の境界を含むテーブルのような)大きなジオメトリがあるうえ、行がそう多くないテーブルを持つようになると、この問題が出てきます。テーブル自体は小さいのですが、多くの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)をご覧下さい。

5.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 = Envelope(Force_2d(the_geom));

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

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

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

5.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
HINT:  You may be able to work around this by marking column "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制限は使えません。

5.3. 次元変換の回避

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

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

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

In case of large tables, it may be wise to divide this UPDATE into smaller portions by constraining the UPDATE to a part of the table via a WHERE clause and your primary key or another feasible criteria, and running a simple "VACUUM;" between your UPDATEs. This drastically reduces the need for temporary disk space. Additionally, if you have mixed dimension geometries, restricting the UPDATE by "WHERE dimension(the_geom) >2" skips re-writing of geometries that already are in 2D.