目次
PostGISでサポートされるGISオブジェクトは、OpenGIS Consortium (OGC)が定義する"Simple Features" のスーパーセットです。 PostGIS 0.9版からOGCの"Simple Features for SQL"仕様で定められた全てのオブジェクトと関数をサポートしています。
PostGISは標準から拡張して 3DZ, 3DM, 4D 座標 (訳注: それぞれXYZ, XYM, XYZM)をサポートしています。
OpenGIS仕様は空間オブジェクトの表現について二つの標準を定義しています。Well-Knownテキスト (WKT)形式とWell-Knownバイナリ (WKB)形式です。WKTもWKBも、オブジェクトの型とオブジェクトを形成する座標に関する情報を持っています。
フィーチャーの空間オブジェクトのテキスト表現 (WKT)の例は、次の通りです。
POINT(0 0)
LINESTRING(0 0,1 1,1 2)
POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1))
MULTIPOINT(0 0,1 2)
MULTILINESTRING((0 0,1 1,1 2),(2 3,3 2,5 4))
MULTIPOLYGON(((0 0,4 0,4 4,0 4,0 0),(1 1,2 1,2 2,1 2,1 1)), ((-1 -1,-1 -2,-2 -2,-2 -1,-1 -1)))
GEOMETRYCOLLECTION(POINT(2 3),LINESTRING((2 3,3 4)))
OpenGIS仕様では、空間オブジェクトの内部保存書式は空間参照系識別子 (Spatial Referencing System IDentifier, SRID)を含むことも求められます。SRIDはデータベースへの挿入のために空間オブジェクトが生成される時に求められます。
これらの書式の入出力は次のインタフェースを用いて実現できます。
bytea WKB = asBinary(geometry); text WKT = asText(geometry); geometry = GeomFromWKB(bytea WKB, SRID); geometry = GeometryFromText(text WKT, SRID);
たとえば、OGC空間オブジェクトを生成して挿入する妥当なINSERTステートメントは次の通りです。
INSERT INTO SPATIALTABLE ( THE_GEOM, THE_NAME ) VALUES ( GeomFromText('POINT(-126.4 45.32)', 312), 'A Place' )
OGC書式は2次元ジオメトリしかサポートされておらず、また、入出力の表現においてSRIDは*決して*埋め込まれません。
PostGIS拡張書式は現在のところOGC書式のスーパーセットとなっています (全ての妥当なWKB/WKTは妥当なEWKB/EWKTです)。しかし、特にもしOGCがPostGIS拡張と矛盾する新しい書式を出すことがあるなら、これは将来変更されるかも知れません。ゆえにこの機能に頼るべきではありません。
PostGIS EWKB/EWKT では 3dm, 3dz, 4d の座標サポートが追加され、SRID情報が埋め込まれます。
拡張された空間オブジェクトのテキスト表現 (EWKT)の例は、次の通りです。
POINT(0 0 0) -- XYZ
SRID=32632;POINT(0 0) -- XY with SRID
POINTM(0 0 0) -- XYM
POINT(0 0 0 0) -- XYZM
SRID=4326;MULTIPOINTM(0 0 0,1 2 1) -- SRID付きXYM
MULTILINESTRING((0 0 0,1 1 0,1 2 1),(2 3 1,3 2 1,5 4 1))
POLYGON((0 0 0,4 0 0,4 4 0,0 4 0,0 0 0),(1 1 0,2 1 0,2 2 0,1 2 0,1 1 0))
MULTIPOLYGON(((0 0 0,4 0 0,4 4 0,0 4 0,0 0 0),(1 1 0,2 1 0,2 2 0,1 2 0,1 1 0)),((-1 -1 0,-1 -2 0,-2 -2 0,-2 -1 0,-1 -1 0)))
GEOMETRYCOLLECTIONM(POINTM(2 3 9),LINESTRINGM((2 3 4,3 4 5)))
これらの書式の入出力は次のインタフェースを用いて実現できます。
bytea EWKB = asEWKB(geometry); text EWKT = asEWKT(geometry); geometry = GeomFromEWKB(bytea EWKB); geometry = GeomFromEWKT(text EWKT);
たとえば、PostGISの空間オブジェクトを作成し挿入する妥当なINSERTステートメントは次の通りです。
INSERT INTO SPATIALTABLE ( THE_GEOM, THE_NAME ) VALUES ( GeomFromEWKT('SRID=312;POINTM(-126.4 45.32 15)'), 'A Place' )
The "canonical forms" of a PostgreSQL type are the representations you get with a simple query (without any function call) and the one which is guaranteed to be accepted with a simple insert, update or copy. For the postgis 'geometry' type these are:
- Output - binary: EWKB ascii: HEXEWKB (EWKB in hex form) - Input - binary: EWKB ascii: HEXEWKB|EWKT
たとえば、次のステートメントは、標準的なASCII文字列による入出力の処理でEWKTを読み、HEXEWKBを返すものです。
=# SELECT 'SRID=4;POINT(0 0)'::geometry; geometry ---------------------------------------------------- 01010000200400000000000000000000000000000000000000 (1 row)
OpenGISの「SQL用シンプルフィーチャー仕様」では、標準GISオブジェクト型とこれらを操作するために必要な関数、メタデータテーブルのセットが定義されています。メタデータが一貫性を維持していることを保証するために、空間カラムの生成、消去といった操作はOpenGISで定義されている空間プロシージャを通して実行されます。
OpenGISメタデータテーブルにはSPATIAL_REF_SYS
とGEOMETRY_COLUMNS
の二つがあります。SPATIAL_REF_SYS
テーブルは空間データベースで用いられる座標系の、数字によるIDと文字による説明を持っています。
SPATIAL_REF_SYS
テーブル定義は次の通りです。
CREATE TABLE SPATIAL_REF_SYS ( SRID INTEGER NOT NULL PRIMARY KEY, AUTH_NAME VARCHAR(256), AUTH_SRID INTEGER, SRTEXT VARCHAR(2048), PROJ4TEXT VARCHAR(2048) )
SPATIAL_REF_SYS
のカラムは次の通りです。
一意に定められた整数値で、データベースで空間参照系 (SRS)を識別するものです。
その参照系の引用元である標準の名前です。たとえば「EPSG」は妥当なAUTH_NAME
です。
AUTH_NAME
で引用される団体によって定義された空間参照系のIDです。EPSGの場合、EPSG投影コードが入ります。
空間参照系のWell-Knownテキスト表現です。たとえば、WKT SRSの表現は、次のようになります。
PROJCS["NAD83 / UTM Zone 10N", GEOGCS["NAD83", DATUM["North_American_Datum_1983", SPHEROID["GRS 1980",6378137,298.257222101] ], PRIMEM["Greenwich",0], UNIT["degree",0.0174532925199433] ], PROJECTION["Transverse_Mercator"], PARAMETER["latitude_of_origin",0], PARAMETER["central_meridian",-123], PARAMETER["scale_factor",0.9996], PARAMETER["false_easting",500000], PARAMETER["false_northing",0], UNIT["metre",1] ]
EPSG投影コードと対応するWKT表現の一覧についてはhttp://www.opengis.org/techno/interop/EPSG2WKT.TXTをご覧下さい。WKTの一般的な議論については、OpenGISの「座標変換サービス実装仕様」http://www.opengis.org/techno/specs.htmをご覧下さい。欧州石油調査グループ (European Petroleum Survey Group, EPSG)と EPSG空間参照系のデータベースに関する情報は、http://epsg.orgをご覧下さい。
PostGISは座標変換機能を提供するためにProj4ライブラリを用いています。 PROJ4TEXT
カラムには、特定のSRIDを示すProj4座標定義文字列が入ります。たとえば次のようになります。
+proj=utm +zone=10 +ellps=clrk66 +datum=NAD27 +units=m
詳細情報については、Proj4ウェブサイhttp://www.remotesensing.org/projをご覧下さい。spatial_ref_sys.sql
は、全てのEPSG投影のためのSRTEXT
とPROJ4TEXT
を持っています。
GEOMETRY_COLUMNS
テーブルは、次のように定義されています。
CREATE TABLE GEOMETRY_COLUMNS ( F_TABLE_CATALOG VARCHAR(256) NOT NULL, F_TABLE_SCHEMA VARCHAR(256) NOT NULL, F_TABLE_NAME VARCHAR(256) NOT NULL, F_GEOMETRY_COLUMN VARCHAR(256) NOT NULL, COORD_DIMENSION INTEGER NOT NULL, SRID INTEGER NOT NULL, TYPE VARCHAR(30) NOT NULL )
カラムは次のとおりです。
ジオメトリカラムを持つフィーチャーテーブルの完全修飾名。"catalog"および"schema"の語はOracle風であることに注意して下さい。"catalog"に類似するものはPostgreSQLになく、このカラムは空白にされます。"schema"についてはPostgreSQLスキーマ名が使われています (public
がデフォルトです)。
フィーチャーテーブル内のジオメトリカラムの名前。
そのカラムの空間の次元 (2, 3 または 4)。
このテーブルの座標ジオメトリのために使われる空間参照系のID。SPATIAL_REF_SYS
への外部キーになっています。
空間オブジェクトのタイプ。 空間カラムを単一型に制限するには、 POINT, LINESTRING, POLYGON, MULTIPOINT, MULTILINESTRING, MULTIPOLYGON, GEOMETRYCOLLECTION のうちのいずれかを、また、XYMで使う場合には LINESTRINGM, POLYGONM, MULTIPOINTM, MULTILINESTRINGM, MULTIPOLYGONM, GEOMETRYCOLLECTIONM のうちのいずれかを使います。複数の型が混合するコレクションの場合は "GEOMETRY"タイプとすることができます。
この属性は (おそらく)OpenGIS仕様に入っていませんが、型の同一性を保証するために必要です。
空間データを持つテーブルを生成するには、次の通り二段階で行います。
通常の非空間テーブルを生成します。
たとえば、次のようにします。CREATE TABLE ROADS_GEOM ( ID int4, NAME varchar(25) )
OpenGISの"AddGeometryColumn"関数によって空間カラムをテーブルに追加します。
The syntax is:
AddGeometryColumn(<schema_name>, <table_name>, <column_name>, <srid>, <type>, <dimension>)
Or, using current schema:
AddGeometryColumn(<テーブル名>, <カラム名>, <SRID>, <タイプ>, <次元>)
例1: SELECT AddGeometryColumn('public', 'roads_geom', 'geom', 423, 'LINESTRING', 2)
例2: SELECT AddGeometryColumn( 'roads_geom', 'geom', 423, 'LINESTRING', 2)
次はテーブルを作成して空間カラムを作る例です (128というSRIDがあると仮定します)。
CREATE TABLE parks ( PARK_ID int4, PARK_NAME varchar(128), PARK_DATE date, PARK_TYPE varchar(2) ); SELECT AddGeometryColumn('parks', 'park_geom', 128, 'MULTIPOLYGON', 2 );
もうひとつ、ジェネリックな「ジオメトリ」型とSRID不明を示す-1を使った例を挙げます。
CREATE TABLE roads ( ROAD_ID int4, ROAD_NAME varchar(128) ); SELECT AddGeometryColumn( 'roads', 'roads_geom', -1, 'GEOMETRY', 3 );
GEOSライブラリで実装される関数のほとんどが、ジオメトリがOpenGIS単純フィーチャー仕様の定義から見て妥当であることが仮定されています。ジオメトリの妥当性のチェックには、IsValid()関数を使います。例を次に示します。
gisdb=# select isvalid('LINESTRING(0 0, 1 1)'), isvalid('LINESTRING(0 0,0 0)'); isvalid | isvalid ---------+--------- t | f
デフォルトでは、PostGISはジオメトリ入力に関するこの妥当性チェックを適用しません。複雑なジオメトリの妥当性のチェックはCPU時間を多く必要とするためです。データソースが信用できない場合は、手動でこのチェックを強制するための制約を付けることができます。
ALTER TABLE mytable ADD CONSTRAINT geometry_valid_check CHECK (isvalid(the_geom));
妥当な入力ジオメトリでPostGIS関数を呼んだのに「GEOS Intersection()がエラーを投げました!」や「JTS Intersection()がエラーを投げました!」というようなメッセージに遭遇したら、それはたぶん、PostGISまたは使用しているライブラリの中のエラーを発見しました。PostGIS開発者に連絡するべきです。PostGIS関数が妥当である入力ジオメトリから妥当でないジオメトリが返る場合も同じです。
厳格にOGCジオメトリに準拠すると、Z値やM値を持てません。ST_IsValid()は高次を考慮に入れません。AddGeometryColumn()を実行するとジオメトリの次元をチェックする制約が加わるので、そこで2を指定すれば十分です。
空間テーブルを作成したら、これでGISデータをデータベースにアップロードする準備ができたことになります。現在、PostGIS/PostgreSQLデータベースにデータをロードするには、SQLステートメントを使う、またはシェープファイルのローダ/ダンパを使う、二つの方法があります。
データをテキスト表現に変換できるなら、フォーマットされたSQLを使うのがデータをPostGISに入れる最も簡単な方法です。Oracleや他のSQLデータベースを使うように、SQL端末モニタにSQLの"INSERT"ステートメントで一杯になった大きなテキストファイルをパイプで送ることで、大量のデータをロードできます。
データアップロードファイル (たとえばroads.sql
)は次のようになるでしょう。
BEGIN; INSERT INTO ROADS_GEOM (ID,GEOM,NAME ) VALUES (1,GeomFromText('LINESTRING(191232 243118,191108 243242)',-1),'Jeff Rd'); INSERT INTO ROADS_GEOM (ID,GEOM,NAME ) VALUES (2,GeomFromText('LINESTRING(189141 244158,189265 244817)',-1),'Geordie Rd'); INSERT INTO ROADS_GEOM (ID,GEOM,NAME ) VALUES (3,GeomFromText('LINESTRING(192783 228138,192612 229814)',-1),'Paul St'); INSERT INTO ROADS_GEOM (ID,GEOM,NAME ) VALUES (4,GeomFromText('LINESTRING(189412 252431,189631 259122)',-1),'Graeme Ave'); INSERT INTO ROADS_GEOM (ID,GEOM,NAME ) VALUES (5,GeomFromText('LINESTRING(190131 224148,190871 228134)',-1),'Phil Tce'); INSERT INTO ROADS_GEOM (ID,GEOM,NAME ) VALUES (6,GeomFromText('LINESTRING(198231 263418,198213 268322)',-1),'Dave Cres'); COMMIT;
データファイルは、次に示す"psql"というSQL端末モニタを使って、簡単にPostgreSQLにパイプで送ることができます。
psql -d [データベース名] -f roads.sql
shp2pgsql
データローダは、ESRIシェープファイルをPostGIS/PostgreSQLデータベースに挿入するための適切なSQLに変換します。ローダには、次に示すコマンドラインフラグによって識別される、いくつかの操作モードがあります。
シェープファイルにあるデータを持つ新しいテーブルを作成する前にデータベーステーブルを削除します。
シェープファイルからデータベーステーブルにデータを追加します。複数のファイルをロードするためにこのオプションを使う場合は、これらのファイルは同じ属性と同じデータ型を持つ必要があります。
新しいテーブルの作成とシェープファイルからのデータの読み込みを行います。これがデフォルトモードです。
テーブル作成のSQLコードを生成するだけで、実際のデータは追加しません。このモードは、テーブル作成とデータロードとを完全に分けたい場合に使用します。
出力データにPostgreSQLのダンプ書式を用います。このモードは-a, -c, -dと組み合わせて利用します。デフォルトの"insert"によるSQL書式よりも、大変早くロードできます。大きなデータセットではこちらを使用して下さい。
指定したSRIDでジオメトリデーブルの作成とデータの読み込みを行います。
識別子 (カラム、スキーマおよび属性)の大文字小文字を保持します。シェープファイルの属性は全て大文字であることに注意して下さい。
全ての整数を標準の32ビット整数に強制します。DBFヘッダではそれが正当であったとしても、64ビットのbigintを生成しません。
古い版 (0.x版)のPostGISのためにWKT書式を出力します。このオプションを使うと、座標変動が発生したり、M値が削除されることに注意して下さい。
-a, -c, -dおよび-pは互いに排他的であることに注意して下さい。
ローダを使って入力ファイルを生成してアップロードするセッション例は次の通りです。
# shp2pgsql shaperoads myschema.roadstable > roads.sql # psql -d roadsdb -f roads.sql
変換とアップロードはUNIXのパイプを使うと一回で実行できます。
# shp2pgsql shaperoads myschema.roadstable | psql -d roadsdb
データは、SQLまたはシェープファイルローダ/ダンパを使ってデータベースから抜き出すことができます。SQLに関する節において、空間テーブルでの比較とクエリを行うために用いることができる演算子のいくつかを議論します。
データベースからデータを引き出す最もストレートな手段は、次のように、SQLのSELECTクエリを使ってカラムを可読なテキストファイルとして出力することです。
db=# SELECT id, AsText(geom) AS geom, name FROM ROADS_GEOM; id | geom | name ---+-----------------------------------------+----------- 1 | LINESTRING(191232 243118,191108 243242) | Jeff Rd 2 | LINESTRING(189141 244158,189265 244817) | Geordie Rd 3 | LINESTRING(192783 228138,192612 229814) | Paul St 4 | LINESTRING(189412 252431,189631 259122) | Graeme Ave 5 | LINESTRING(190131 224148,190871 228134) | Phil Tce 6 | LINESTRING(198231 263418,198213 268322) | Dave Cres 7 | LINESTRING(218421 284121,224123 241231) | Chris Way (6 rows)
しかし、返ってくる結果の数を削るために、なんらかの制限をかけることが重要となるときがあるでしょう。属性ベースの制限の場合、非空間テーブルで使う通常の文法と同じSQLを使うだけです。空間ベースの制限の場合、次の演算子が使用可能であり、便利です。
この演算子で、一つのジオメトリのバウンディングボックスが他のバウンディングボックスとインタセクトするかを問い合わせることができます。
この演算子で、二つのジオメトリが幾何的に同一であるかを見ることができます。 たとえば、'POLYGON((0 0,1 1,1 0,0 0))' は 'POLYGON((0 0,1 1,1 0,0 0))' と同じかを見ることができます (これは同じとなります)。
この演算子は他より若干素朴なもので、二つのジオメトリのバウンディングボックスが同じかを見るだけです。
次に、これらの演算子をクエリで使うことができます。SQLコマンドラインからジオメトリとボックスの特定を行うときは、"GeomFromText()"関数で、明示的に文字列表現をジオメトリに変換しなければならないことに注意して下さい。たとえば、次のようになります。
SELECT ID, NAME FROM ROADS_GEOM WHERE GEOM ~= GeomFromText('LINESTRING(191232 243118,191108 243242)',-1);
上のクエリは"ROADS_GEOM"テーブルから、その値と等価である単一のレコードを返します。
"&&"演算子を使うとき、比較フィーチャーをBOX3DかGEOMETRYかに指定することができます。ただし、GEOMETRYを指定すると、それのバウンディングボックスが比較に使われます。
SELECT ID, NAME FROM ROADS_GEOM WHERE GEOM && GeomFromText('POLYGON((191232 243117,191232 243119,191234 243117,191232 243117))',-1);
上のクエリでは、比較するためにポリゴンのバウンディングボックスを用いています。
最も一般的な空間クエリは「フレームベース」のクエリでしょう。これは、表示するためのデータの価値のある「マップフレーム」を取得するために、データブラウザやウェブマッパのようなクライアントソフトウェアに使われます。このフレームで"BOX3D"オブジェクトを使う場合は、次のようなクエリになります。
SELECT AsText(GEOM) AS GEOM FROM ROADS_GEOM WHERE GEOM && GeomFromText('BOX3D(191232 243117,191232 243119)'::box3d,-1);
ここで、BOX3Dの投影を指定するためにSRIDを使っていることに注意して下さい。SRIDを-1に設定しているのは、SRIDを指定しないということを示しています。
pgsql2shp
テーブルダンパは、データベースに直接接続して、テーブル (あるいはクエリによって定義されたもの)をシェープファイルに変換するものです。基本的な文法は次の通りです。
pgsql2shp [<オプション>] <データベース> [<スキーマ>.]<テーブル>
pgsql2shp [<オプション>] <データベース> <クエリ>
コマンドラインオプションは次の通りです。
特定のファイル名に出力を書きこみます。
接続先データベースのホスト名。
接続先データベースのポート。
データベースに接続するためのパスワード。
データベースに接続する際のユーザ名。
複数のジオメトリカラムを持つテーブルの場合の、シェープファイルの出力に使用するジオメトリカラム。
バイナリカーソルを使います。これは、実行時間を短くしますが、テーブルの非ジオメトリ属性がテキストへのキャストを持っていない場合には、動作しません。
Rawモード。gid
フィールドを落としたり、カラム名をエスケープしてはいけません。
後方互換: 古い (1.0.0より前)のPostGISデータベースからダンプする際に3次元のシェープファイルを出力します (デフォルトでは2次元になります)。 PostGIS 1.0.0以上では、次元は完全に反映されます。
インデックスは大きなデータセットを持つ空間データベースの利用を可能にするものです。インデックスなしでは、フィーチャーの検索でデータベースの全レコードを「シーケンシャルスキャン」する必要があります。インデックスをつけることで、データを検索木に組織化して、特定のレコードを発見するための検索をより早くすることができます。 PostgreSQLは、B木、R木、GiSTの3種類のインデックスをデフォルトでサポートしています。
B木は、数字、文字、日付といった、一つの軸に沿ってソートできるデータに使用します。 GISデータは合理的に一つの軸に沿ったソートはできません ((0,0)と(0,1)と(1,0)で大きいのはどれでしょう?)ので、B木インデックスは、ここでは使えません。
R木はデータを長方形に分割して、さらにその長方形を小さい長方形に分割していったものです。R木はいくつかの空間データベースでGISデータのインデックスに使われますが、PostgreSQLのR木実装は、GiST実装ほどにロバストではありません。
GiST (Generalized Search Trees)インデックスはデータを「一方へのもの」 (訳注: 「左側にあるもの」「上側にあるもの」など)、「オーバラップするもの」、「中にあるもの」に分割して、GISデータを含む幅広いデータ型で使えるようにしたものです。PostGISではGISデータにインデックスを付けるためにGiSTの上でR木インデックス実装を使用しています。
GiSTは「汎用的な検索木 (Generalized Search Tree)」の意味で、インデックスの一般化された形式です。GISインデックスに加えて、GiSTは通常のB木インデックスに従わない全ての種類の不規則なデータ構造 (整数配列, スペクトラルデータ等)の検索速度を向上させるために使います。
ひとたびGISデータテーブルが数千行を超えたら、空間検索の速度向上のためインデックスを構築したくなるでしょう (これは属性検索でない場合です。属性でしたら通常のインデックスを属性フィールドに追加します)。
GiSTインデックスをジオメトリカラムに追加するための文は次の通りです。
CREATE INDEX [indexname] ON [tablename] USING GIST ( [geometryfield] GIST_GEOMETRY_OPS );
空間インデックスの構築は、計算量を集中させて行われます。100万行のテーブルで、300MHzのSolaris機ではGiSTインデックスの構築に概ね1時間かかりました。インデックスを構築したあとは、クエリプランの最適化に使うため、次のようにPostgreSQLにテーブル統計情報の収集をさせることが重要です。
VACUUM ANALYZE [テーブル名] [カラム名]; -- 次のクエリはPostgreSQL 7.4以前でのみ必要です SELECT UPDATE_GEOMETRY_STATS( [テーブル名], [カラム名] );
GiSTインデックスはPostgreSQLのR木インデックスと比べて二つの利点を持っています。まず、GiSTインデックスは「NULLセーフ」、すなわちNULL値を含むインデックスカラムで利用できることです。次に、GiSTインデックスはGISオブジェクトがPostgreSQLで8Kのページサイズを超えるサイズを扱う際に重要な「不可逆」の概念を持っていることです。不可逆にすることによって、PostgreSQLは、インデックスにおけるオブジェクトの「重要な」部分、GISオブジェクトの場合にはバウンディングボックスになりますが、これのみを納めることができます。 R木インデックスで8Kを超えるGISオブジェクトのインデックスを構築しようとすると、失敗します。
通常、インデックスは見えないところでデータアクセスの速度向上を行います。すなわち、ひとたびインデックスが構築されたら、クエリプランナは透過的に、クエリプランの速度を向上させるためにインデックス情報を使うべき時を判断します。残念なことに、PostgreSQLクエリプランナは、GiSTインデックスの使用について十分に最適化できず、時々、検索で空間インデックスを使用すべきなのに、テーブル全体を順に走査することがあります。
空間インデックスが使用されていない (または属性インデックスがその問題のために使用されていない)場合、次の二つのことができます。
まず、クエリプランナにインデックス使用まわりの判断に利用するためのより良い情報を提供するために、値の数量と分散に関する統計情報が収集されたかを確認してください。PostgreSQL 7.4以前では、update_geometry_stats([テーブル名], [カラム名]) (分散計算)とVACUUM ANALYZE [テーブル名] [カラム名] (値の数量の計算)とを実行します。PostgreSQL 8.0については、VACUUM ANALYZEを実行することで同じ動作になります。常に定期的なデータベースへのvacuumを実行すべきです。多くのPostgreSQLのデータベースエージェントは、閑散時のcronジョブとして定期的にVACUUMを実行します。
vacuumが働かないなら、SET ENABLE_SEQSCAN=OFFコマンドで、プランナにインデックス情報を強制的に使わせることができます。このコマンドは控え目に実行すべきで、かつ、空間インデックスがあるクエリ上でのみ使うべきです。一般的に言うと、通常のB木インデックスを使うべき時に関してあなたが知っていることよりも、プランナはより良く知っています。クエリを実行したら、ENABLE_SEQSCAN
設定を戻して、他のクエリでは通常通りプランナを使用することを考えるべきです。
0.6版では、ENABLE_SEQSCAN
でプランナにインデックスを強制的に使わせることは重要ではありません。
順に走査する際のコストとインデックスを使う際のコストとを比較してプランナが間違っていることに気付いたら、postgresql.confでrandom_page_costの値を減らしてみるか"SET random_page_cost=#"を使ってみてください。このパラメータのデフォルト値は4ですが、1か2にしてみて下さい。値を減らすことでプランナがよりインデックス検索を行う傾向になります。
空間データベース機能のレゾンデートルは、通常はデスクトップGISに求める機能を、データベース内部のクエリで実現することです。PostGISを効果的に使用するには、どの空間機能が有効かを知り、また、良い性能を提供する所に適切にインデックスがあることが保証されていることが求められます。
クエリを作成するとき、 &&のようなバウンディングボックスを基準とした演算子によってのみGiST空間インデックスの利点が出てくることだけは覚えておくことが重要です。distance()
のような関数では演算の最適化を行うためにインデックスを使うことができません。 たとえば、次のクエリでは、大きなテーブルでは本当に遅くなります。
SELECT the_geom FROM geom_table WHERE distance( the_geom, GeomFromText( 'POINT(100000 200000)', -1 ) ) < 100
このクエリは、geom_tableにおける (100000, 200000) の点から距離が100単位以内にある全てのジオメトリを選択します。これは、テーブル内にあるそれぞれの点と指定した点との距離を計算する、すなわち、それぞれの行でひとつのdistance()
計算を行うため、遅くなるのです。&&演算子を使うと、求められる距離計算の量を減らすことで回避できます。次のようにします。
SELECT the_geom FROM geom_table WHERE the_geom && 'BOX3D(90900 190900, 100100 200100)'::box3d AND distance( the_geom, GeomFromText( 'POINT(100000 200000)', -1 ) ) < 100
このクエリは、同じジオメトリを選択しますが、より効果的な方法で行われます。the_geomにGiSTインデックスがあると仮定すると、クエリプランナは、distance()
関数の結果を計算する前に行を減らすためにインデックスを使うことができると認識します。&&演算子で使われているBOX3D
ジオメトリは、指定位置を中心とした一辺200単位の正方形です。これが「クエリボックス」です。&&演算子は 結果セットを「クエリボックス」にオーバラップするバウンディングボックスを持つジオメトリだけに素早く減らすためにインデックスを使います。「クエリボックス」がジオメトリテーブル全体の範囲より十分に小さいと仮定すると、行われなければならない距離計算の量は劇的に減少します。
本節の例では、線型の道、ポリゴンの自治体境界、の二つのテーブルを使います。テーブルの定義をしまします。bc_roads
については次の通りです。
Column | Type | Description ------------+-------------------+------------------- gid | integer | Unique ID name | character varying | Road Name the_geom | geometry | Location Geometry (Linestring)
bc_municipality
テーブルの定義については次の通りです。
Column | Type | Description -----------+-------------------+------------------- gid | integer | Unique ID code | integer | Unique ID name | character varying | City / Town Name the_geom | geometry | Location Geometry (Polygon)
| |
4.6.2.1.1. | 道路の総延長はkm表記でいくらになるでしょう? |
この問題は、次のようなとても単純なSQLで答を得ることができます。 postgis=# SELECT sum(length(the_geom))/1000 AS km_roads FROM bc_roads; km_roads ------------------ 70842.1243039643 (1 row) | |
4.6.2.1.2. | プリンスジョージ市の大きさはha表記でいくらになるでしょう? |
このクエリでは、属性条件 (municipality name, 自治体名)に空間計算 (面積)を併用しています。 postgis=# SELECT area(the_geom)/10000 AS hectares FROM bc_municipality WHERE name = 'PRINCE GEORGE'; hectares ------------------ 32657.9103824927 (1 row) | |
4.6.2.1.3. | 県内で最も大きな面積となる自治体はどこでしょう? |
このクエリは、空間計測をクエリ条件に持ってきています。この問題へのアプローチの方法はいくつかありますが、最も効率的なのは次の通りです。 postgis=# SELECT name, area(the_geom)/10000 AS hectares FROM bc_municipality ORDER BY hectares DESC LIMIT 1; name | hectares ---------------+----------------- TUMBLER RIDGE | 155020.02556131 (1 row) このクエリの答を出すためには、全てのポリゴンの面積を求める必要があることに注意して下さい。このクエリを多く実行する場合、性能向上のためにテーブルにareaカラムを追加して、別のインデックスを追加することができるようにするのは、意義のあることです。結果を距離について降順に並べ替え、PostgreSQLの"LIMIT"コマンドを用いることで、max()のような集約関数を使わずに、簡単に最も大きい値を得ることができます。 | |
4.6.2.1.4. | 各自治体内に含まれる道路の総延長はいくらでしょう? |
これは、二つのテーブルからデータを持ち込んで (結合して)いるので「空間結合」の例です。しかし、結合の条件として共通キーの上で接続するという普通のリレーションのやり方でなく空間インタラクション条件 (「含む」)を使っています。 postgis=# SELECT m.name, sum(length(r.the_geom))/1000 as roads_km FROM bc_roads AS r,bc_municipality AS m WHERE r.the_geom && m.the_geom AND contains(m.the_geom,r.the_geom) GROUP BY m.name ORDER BY roads_km; name | roads_km ----------------------------+------------------ SURREY | 1539.47553551242 VANCOUVER | 1450.33093486576 LANGLEY DISTRICT | 833.793392535662 BURNABY | 773.769091404338 PRINCE GEORGE | 694.37554369147 ... このクエリは、テーブル内の全ての道路の合計を最終結果 (この例での話ですが約250Kmの道です)にまとめられるので、少し時間がかかります。より小さいオーバレイ (数百の道路で数千のレコード)の場合、応答はもっと早くなりえます。 | |
4.6.2.1.5. | プリンスジョージ市内の全ての道路からなるテーブルを作る |
これは「オーバレイ」の例です。つまり、二つのテーブルを取得して、空間的に切り取られた結果からなる新しいテーブルを出力します。上で示した「空間結合」と違い、このクエリは実際に新しいジオメトリを生成します。生成されたオーバレイはターボのかかった空間結合みたいなもので、より確かな解析作業に便利です。 postgis=# CREATE TABLE pg_roads as SELECT intersection(r.the_geom, m.the_geom) AS intersection_geom, length(r.the_geom) AS rd_orig_length, r.* FROM bc_roads AS r, bc_municipality AS m WHERE r.the_geom && m.the_geom AND intersects(r.the_geom, m.the_geom) AND m.name = 'PRINCE GEORGE'; | |
4.6.2.1.6. | ビクトリア州の「ダグラス通り」の長さはkm表記でいくらになるでしょう? |
postgis=# SELECT sum(length(r.the_geom))/1000 AS kilometers FROM bc_roads r, bc_municipality m WHERE r.the_geom && m.the_geom AND r.name = 'Douglas St' AND m.name = 'VICTORIA'; kilometers ------------------ 4.89151904172838 (1 row) | |
4.6.2.1.7. | 穴を持つ自治体ポリゴンのうち最も大きいのはどれでしょう? |
postgis=# SELECT gid, name, area(the_geom) AS area FROM bc_municipality WHERE nrings(the_geom) > 1 ORDER BY area DESC LIMIT 1; gid | name | area -----+--------------+------------------ 12 | SPALLUMCHEEN | 257374619.430216 (1 row) |
Minnesota MapServerはOpenGIS Web Mapping Server仕様を満たすウェブマッピングサーバです。
MapServerのウェブページはhttp://mapserver.gis.umn.eduにあります (訳注: 現在は http://mapserver.org/にあります)。
OpenGISウェブマップ仕様はhttp://www.opengis.org/techno/specs/01-047r2.pdfにあります。
MapServerでPostGISを使うには、MapServerのコンフィギュレーション方法についての知識が必要ですが、この文書の範囲外です。この節では、PostGIS特有の問題とコンフィギュレーション詳細について記載します。
MapServerでPostGISを使うには、次のものが必要です。
PostGIS 0.6以上
MapServer 3.5以上
MapServerは、他のPostgreSQLクライアントのように、libpq
を使ってPostGIS/PostgreSQLデータにアクセスします。これは、システムがPostgreSQLクライアントライブラリであるlibpq
を持っている限りは、PostGISサーバへのネットワークアクセスを持つあらゆる機械にMapServerをインストールできるということを示しています。
"--with-postgis"と好きなconfigureオプションを付けてMpaserverのコンパイルとインストールを行います。
MapServerのmapファイルの中に、PostGISレイヤを追加します。たとえば次のようになります。
LAYER CONNECTIONTYPE postgis NAME "widehighways" # リモートの空間データベースに接続します CONNECTION "user=dbuser dbname=gisdatabase host=bigserver" # 'roads'テーブルの'geom'カラムからラインを取得します DATA "geom from roads" STATUS ON TYPE LINE # 範囲内のラインのうち、広い高速道路のみ描画します FILTER "type = 'highway' and numlanes >= 4" CLASS # 非常に広い高速道路はより明るい色かつ2ピクセル幅にします EXPRESSION ([numlanes] >= 6) COLOR 255 22 22 SYMBOL "solid" SIZE 2 END CLASS # 残りは、暗い色かつ1ピクセル幅です EXPRESSION ([numlanes] < 6) COLOR 205 92 82 END END
上の例におけるPostGIS特有のディレクティブは次の通りです。
PostGISレイヤでは常に"postgis"とします。
データベース接続は「接続文字列」によって制御されます。接続文字列は、次に示すような標準的なキーと値からなります(<>内はデフォルト値)。
user=<ユーザ名> password=<パスワード> dbname=<ユーザ名> hostname=<サーバ> port=<5432>
空の接続文字列も妥当とされますし、あらゆるキーと値のペアは省略できます。接続するためには一般的にはdbnameとusernameとが最少で与えるものとなります。
このパラメータの形式は "<カラム名> from <テーブル名>"となります。ここで、カラム名は地図に描画したい空間カラムを指します。
フィルタは、妥当なSQL文字列でなければなりません。この文字列は、通常はSQLクエリにおける"WHERE"に続く論理式に対応します。たとえば、6レーン以上の道路だけを描画する場合には、"num_lanes >= 6"というフィルタを使います。
空間データベースにおいては、空間 (GiST)インデックスを、マップに描かれるレイヤ全てに構築していることを保証して下さい。
CREATE INDEX [インデックス名] ON [テーブル名] USING GIST ( [ジオメトリカラム] GIST_GEOMETRY_OPS );
MapServerを使ってレイヤにクエリを発行する場合は、「oidインデックス」も必要です (訳注: PostgreSQL 8.1以降は、oidはデフォルトでは追加されなくなりました。替わりにSERIAL型フィールドを生成して使うべきです。テーブル生成時に"WITH OID"を付けるとoid付きテーブルが生成されます)。
MapServerからは、クエリを実行するときに、それぞれの空間レコードを識別する一意な識別子が求められ、MapServerのPostGISモジュールはPostgreSQLのoid
値を一意な識別子に使います。これの副作用はクエリ内のレコードのランダムアクセスを早く行うのにoid
インデックスが必要となることです。
「oidインデックス」を構築するには、次のようなSQLを実行します。
CREATE INDEX [インデックス名] ON [テーブル名] ( oid );
USING
疑似SQL節を使ってMapServerがより複雑なクエリの結果を理解できるようにするための情報を追加します。より詳しく言うと、ビューまたは副問い合わせが元テーブル (DATA
定義で"FROM"の右にあるもの)として使われる時、MapServerが自動的に一意な識別子がそれぞれの行にあるか、また、SRIDがテーブルにあるかを判別するのは困難です。USING
節によって、MapServerがこれらの情報を得ることができます。例を次に挙げます。
DATA "the_geom FROM (SELECT table1.the_geom AS the_geom, table1.oid AS oid, table2.data AS data FROM table1 LEFT JOIN table2 ON table1.id = table2.id) AS new_table USING UNIQUE oid USING SRID=-1"
MapServerは、マップクエリを実行する際、行識別のために、それぞれの行に一意な識別子を求めます。 通常なら、oidを一意な識別子として使えますが、ビューや副問い合わせでは、自動的にoidを持つことができません。MapServerのクエリ機能を使いたいなら、一意性のあるカラムをビューまたは副問い合わせに追加する必要があり、USING UNIQUE
宣言を付ける必要があります。たとえば、この目的のための主キー値のテーブルでのカラム名や、結果セットで一意性が保障されたカラムを明示的にSELECTに入れることができます。
マップクエリを実行する場合には、USING
ステートメントは単純なDATA
ステートメントに対しても便利なものになります。マップクエリの速度性能向上のために、クエリ実行可能なレイヤで使われるテーブルのoidカラムにインデックスを追加することを、前に推奨しました。しかしながら、USINGを使うと、MapServerがテーブルのプライマリキーをマップクエリの識別子に使うことができ、インデックスを追加する必要はもうありません。
「マップクエリ」はマップ上でクリックして、その場所におけるフィーチャーに関する情報を問い合わせる動作です。「マップクエリ」とDATA
定義におけるSQLクエリと混同しないで下さい。
PostGISは、MapServerに正しいデータを返すために、ジオメトリがどの空間参照系を使っているかを知る必要があります。通常は、この情報はPostGISデータベースの"geometry_columns"テーブルから得ることができます。しかし、副問い合わせやビューのような一時テーブルでは、この方法は不可能です。そこで、USING SRID=
オプションを使って、正しいSRIDがDATA
定義で使われるように指定します。
MapserverのPostGISレイヤのパーサは、かなり原始的で、大文字小文字を区別するところが2,3あります。全てのSQLキーワードと全てのUSING
節が大文字であることを保証し、USING UNIQUE
節がUSING SRID
節より前に来るようにして下さい。
簡単な例から始めて、ステップアップしていきましょう。次のMapServerレイヤ定義を考えて下さい。
LAYER CONNECTIONTYPE postgis NAME "roads" CONNECTION "user=theuser password=thepass dbname=thedb host=theserver" DATA "the_geom FROM roads" STATUS ON TYPE LINE CLASS COLOR 0 0 0 END END
このレイヤは"roads"テーブルにある道路ジオメトリの全部を黒線で表示するものです。
では、少なくとも1:100000にズームするまでは高速道路だけを表示したい、としましょう。次の二つのレイヤで、その効果が実現できます。
LAYER CONNECTION "user=theuser password=thepass dbname=thedb host=theserver" DATA "the_geom FROM roads" MINSCALE 100000 STATUS ON TYPE LINE FILTER "road_type = 'highway'" CLASS COLOR 0 0 0 END END LAYER CONNECTION "user=theuser password=thepass dbname=thedb host=theserver" DATA "the_geom FROM roads" MAXSCALE 100000 STATUS ON TYPE LINE CLASSITEM road_type CLASS EXPRESSION "highway" SIZE 2 COLOR 255 0 0 END CLASS COLOR 0 0 0 END END
一つ目のレイヤはスケールが1:100000以上であるときに使われ、道路タイプが"highway"である道路のみ黒線で表示されます。FILTER
オプションによって、道路タイプが"highway"の場合のみ表示することになります。
二つ目のレイヤはスケールが1:100000未満である時に使われ、"highway"は赤い二重細線で表示され、他の道路は黒線で表示されます。
さて、MapServerの機能を使うだけで、二つのおもしろいことを実行しました。しかし、DATA
のSQLステートメントは、単純なままです。道路名が (どういう理由かは知りませんが)他のテーブルに収められていて、それのデータを取得するためにテーブルを連結して、道路のラベルを取る必要がある、とします。
LAYER CONNECTION "user=theuser password=thepass dbname=thedb host=theserver" DATA "the_geom FROM (SELECT roads.oid AS oid, roads.the_geom AS the_geom, road_names.name as name FROM roads LEFT JOIN road_names ON roads.road_name_id = road_names.road_name_id) AS named_roads USING UNIQUE oid USING SRID=-1" MAXSCALE 20000 STATUS ON TYPE ANNOTATION LABELITEM name CLASS LABEL ANGLE auto SIZE 8 COLOR 0 192 0 TYPE truetype FONT arial END END END
このANNOTAIONレイヤでは、縮尺が1:20000以下のときに、全ての道路に緑色のラベルを表示します。また、この例は、DATA
定義で、SQLのJOINを使用する方法も示しています。
Javaクライアントは、直接的にテキスト表現として、またはPostGISに同梱されているJDBC拡張オブジェクトを使用して、PostgreSQLデータベース内にある、PostGISの"geometry"オブジェクトにアクセスできます。JDBC拡張オブジェクトを使うためには、"postgis.jar"ファイルを、JDBCドライバパッケージの"postgresql.jar"とともに、 CLASSPATHに置く必要があります。
import java.sql.*; import java.util.*; import java.lang.*; import org.postgis.*; public class JavaGIS { public static void main(String[] args) { java.sql.Connection conn; try { /* * JDBCドライバをロードして接続を確立します。 */ Class.forName("org.postgresql.Driver"); String url = "jdbc:postgresql://localhost:5432/database"; conn = DriverManager.getConnection(url, "postgres", ""); /* * ジオメトリ型を接続に追加します。 * ご注意 : addDateType()を呼ぶ前に * 接続をpgsql特有の接続実装にキャストしなければなりません。 */ ((org.postgresql.Connection)conn).addDataType("geometry","org.postgis.PGgeometry"); ((org.postgresql.Connection)conn).addDataType("box3d","org.postgis.PGbox3d"); /* * ステートメントの生成とSELECTクエリの実行を行います。 */ Statement s = conn.createStatement(); ResultSet r = s.executeQuery("select AsText(geom) as geom,id from geomtable"); while( r.next() ) { /* * ジオメトリをオブジェクトとして検索してジオメトリ型にキャストします。 * オブジェクトを印字します */ PGgeometry geom = (PGgeometry)r.getObject(1); int id = r.getInt(2); System.out.println("Row " + id + ":"); System.out.println(geom.toString()); } s.close(); conn.close(); } catch( Exception e ) { e.printStackTrace(); } } }
"PGeometry"オブジェクトは、Point、LineString、Polygon、MultiPoint、MultiLineString、MultiPolygonの各型に依存する、特定のトポロジカルジオメトリオブジェクト ("Geometory"抽象クラスの子クラス)を持つラッパオブジェクトです。
PGgeometry geom = (PGgeometry)r.getObject(1); if( geom.getType() = Geometry.POLYGON ) { Polygon pl = (Polygon)geom.getGeometry(); for( int r = 0; r < pl.numRings(); r++ ) { LinearRing rng = pl.getRing(r); System.out.println("Ring: " + r); for( int p = 0; p < rng.numPoints(); p++ ) { Point pt = rng.getPoint(p); System.out.println("Point: " + p); System.out.println(pt.toString()); } } }
幾何オブジェクトのさまざまなデータアクセサ関数に関する参照情報については、拡張オブジェクトのJavaDocをご覧下さい。