はじめに
PostGISでは、複数のジオメトリを1つにまとめる操作を行う関数がいくつか用意されていますが、関数名からだけでは機能の違いが判然としない部分もあるため、結局どれを使うのが最適なのかわからないケースもあるかと思います。
本記事では、PostGISにおけるジオメトリ統合の主要な関数について、ポリゴンとラインそれぞれの場合に分けて検証し、用途に応じた最適な関数の選定基準を提示できればと思います。
テストデータの準備
検証は以下の環境で実施します。
- Windows PC(Windows 11 Pro、Intel N95 1.7GHz、メモリ16GB)
- PostgreSQL 16.11
まず、テストデータを用意します。
-- テスト用テーブルの作成
CREATE TABLE test_polygons (
id SERIAL PRIMARY KEY,
name VARCHAR(50),
geom GEOMETRY(Polygon, 4326)
);
CREATE TABLE test_lines (
id SERIAL PRIMARY KEY,
name VARCHAR(50),
geom GEOMETRY(LineString, 4326)
);
-- ポリゴンデータの挿入(隣接・重複する3つの四角形)
INSERT INTO test_polygons (name, geom) VALUES
('Polygon A', ST_GeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 4326)),
('Polygon B', ST_GeomFromText('POLYGON((2 0, 4 0, 4 2, 2 2, 2 0))', 4326)),
('Polygon C', ST_GeomFromText('POLYGON((1 1, 3 1, 3 3, 1 3, 1 1))', 4326));
-- ラインデータの挿入(接続可能な3つのライン)
INSERT INTO test_lines (name, geom) VALUES
('Line A', ST_GeomFromText('LINESTRING(0 0, 2 2)', 4326)),
('Line B', ST_GeomFromText('LINESTRING(2 2, 3 1)', 4326)),
('Line C', ST_GeomFromText('LINESTRING(0 2, 2 0)', 4326));
ジオメトリを表示すると以下のとおりです。
ポリゴンの結合
まず、ポリゴンの結合について取り上げます。
最初は、最もシンプルなST_Collectを紹介します。
ST_Collect - 集約してMultiPolygonを作成
ST_Collectは、複数のジオメトリをそのままグループ化して、マルチジオメトリにする関数です。
ポリゴンの場合はマルチポリゴンになります。
集約関数のため、複数データ行を入力とすることができます。
単にひとつにまとめるだけで、オーバーラップを併合して境界を溶かしたりはしません。
SELECT ST_AsText(ST_Collect(geom))
FROM test_polygons;
結果:
MULTIPOLYGON(((0 0,2 0,2 2,0 2,0 0)),((2 0,4 0,4 2,2 2,2 0)),((1 1,3 1,3 3,1 3,1 1)))
なお、ST_DumpはST_Collectの逆の操作になります。
SELECT ST_AsText((ST_Dump(ST_Collect(geom))).geom)
FROM test_polygons;
結果:
POLYGON((0 0,2 0,2 2,0 2,0 0))
POLYGON((2 0,4 0,4 2,2 2,2 0))
POLYGON((1 1,3 1,3 3,1 3,1 1))
マルチポリゴンにしたいだけであれば、ST_Collectでよいのですが、オーバーラップを併合して境界を溶かしたい場合は、ST_Union系の関数を使います。
ST_MemUnion - 境界を溶かして結合(逐次統合)
ST_Union系の関数のうち、内部のアルゴリズム的に最もシンプルな関数が、ST_MemUnionになります。
ST_MemUnionを実行すると、オーバーラップを併合して境界を溶かしたジオメトリを生成します。
ST_Collectと同様、複数データ行を入力とすることができる集約関数です。
SELECT ST_AsText(ST_MemUnion(geom))
FROM test_polygons;
結果:
POLYGON((0 2,1 2,1 3,3 3,3 2,4 2,4 0,2 0,0 0,0 2))
オーバーラップ部分の境界がなくなり、一つのポリゴンに統合されていることがわかるかと思います。
この統合操作を実行するためのアルゴリズムはいくつかありますが、ST_MemUnionでは、Iterated Union(逐次加算的結合) という方法が使われています。
これは、1つ1つ対象のジオメトリを読み込んで、逐次結合していく方法です。
メモリ使用量を小さく抑えられますが、処理時間は長くなります。
そこで、通常は、より高効率なアルゴリズムで処理するST_Unionを使用します。
ST_Union - 境界を溶かして結合(カスケード統合)
ST_Unionも、ST_MemUnionと同様にオーバーラップするジオメトリの境界を溶かして統合したジオメトリを作成する集約関数です。
SELECT ST_AsText(ST_Union(geom))
FROM test_polygons;
結果:
POLYGON((0 0,0 2,1 2,1 3,3 3,3 2,4 2,4 0,2 0,0 0))
ST_MemUnionの結果と始点が若干異なるものの、形状は同じです。
しかし、結合のアルゴリズムが異なります。
ST_Unionでは、Cascaded Unionと呼ばれるアルゴリズムが使われています。
これは、あらかじめすべての対象ジオメトリをメモリに読み込んで、STRツリーと呼ばれる静的なRツリーインデックス(Sort-Tile-Recursive packed R-Tree index)を作成し、このインデックスにしたがって、下位ノードから段階的に統合処理をすることで、近接するポリゴン同士から効率的に結合するというアプローチをとります。
これにより、高速な統合処理を実現しますが、事前にすべてのジオメトリをメモリ上に展開しSTRツリーを作成するため、メモリ使用量が大きくなる傾向があります。
そのため、使用できるメモリが限定的な環境では、ST_Unionではメモリ不足でエラーになることがあります。
その場合、ST_UnaryUnionとST_Collectを組み合わせてチャンク(一定のまとまり)ごとに結合処理をする方法が考えられます。
ST_UnaryUnion - 境界を溶かして結合(単一入力のカスケード統合)
ST_UnaryUnionは、単一入力のジオメトリ(マルチポリゴンやジオメトリコレクション)内のオーバーラップを併合する関数です。
ST_UnionやST_MemUnionと異なり、集約関数ではないため、複数のデータ行を入力にとることはできません。
したがって、複数データ行のジオメトリを統合する場合は、以下のとおり、ST_Collectを合わせて用いることになります。
SELECT ST_AsText(ST_UnaryUnion(ST_Collect(geom)))
FROM test_polygons;
結果:
POLYGON((0 0,0 2,1 2,1 3,3 3,3 2,4 2,4 0,2 0,0 0))
結合のアルゴリズムはST_Unionと同様、Cascaded Unionが使われるため、ST_MemUnionより高速に処理されます。
また、一定数量ずつ小分けにしてST_UnaryUnionとST_Collectを使って段階的にジオメトリを結合することで、メモリ使用量を抑えつつ、効率的に結合処理を実行することができます。
例えば、以下のようなSQLを実行します。
WITH numbered AS (
SELECT geom,
(row_number() OVER (ORDER BY ST_GeoHash(ST_Centroid(geom), 6))) / 100 AS grp
FROM t_union_test
),
chunked AS (
SELECT ST_UnaryUnion(ST_Collect(geom)) AS g
FROM numbered
GROUP BY grp
)
SELECT ST_UnaryUnion(ST_Collect(g)) FROM chunked;
ここでは、重心座標ベースでST_GeoHash順にソートした上で、100個ずつのチャンクを作って、チャンクごとに結合し、最後に、チャンクごとに結合したジオメトリを一つに統合しています。
ST_Union系関数の性能比較
結合処理の効率性を検証するため、ST_Union、ST_MemUnion、ST_UnaryUnionのそれぞれを使った場合の処理時間を比較してみます。
-- テスト用データ作成
CREATE TABLE t_union_test AS
SELECT
i AS id,
-- 15m間隔に隣同士が重なり合う半径10mのバッファ
ST_Transform(ST_Buffer(
ST_SetSRID(ST_MakePoint((i % 80) * 15, (i / 80) * 15), 3857),
10
), 4326) AS geom
FROM generate_series(1, 2000) AS i;
ANALYZE t_union_test;
-- (A) ST_Union 統合
EXPLAIN (ANALYZE, BUFFERS)
SELECT ST_Union(geom) FROM t_union_test;
"Aggregate (cost=229.50..229.51 rows=1 width=32) (actual time=566.683..566.686 rows=1 loops=1)"
" Buffers: shared hit=192"
" -> Seq Scan on t_union_test (cost=0.00..212.00 rows=2000 width=568) (actual time=0.046..0.702 rows=2000 loops=1)"
" Buffers: shared hit=192"
"Planning:"
" Buffers: shared hit=20"
"Planning Time: 2.408 ms"
"Execution Time: 567.317 ms"
-- (B) ST_MemUnion 統合
EXPLAIN (ANALYZE, BUFFERS)
SELECT ST_MemUnion(geom) FROM t_union_test;
"Aggregate (cost=25212.00..25212.01 rows=1 width=32) (actual time=38329.418..38329.419 rows=1 loops=1)"
" Buffers: shared hit=192"
" -> Seq Scan on t_union_test (cost=0.00..212.00 rows=2000 width=568) (actual time=0.035..8.795 rows=2000 loops=1)"
" Buffers: shared hit=192"
"Planning Time: 0.115 ms"
"Execution Time: 38329.766 ms"
-- (C) ST_UnaryUnion + ST_Collect チャンクごと統合
EXPLAIN (ANALYZE, BUFFERS)
-- 例:GeoHash順にソートした上で100件ずつ unary union
WITH numbered AS (
SELECT geom,
(row_number() OVER (ORDER BY ST_GeoHash(ST_Centroid(geom), 6))) / 100 AS grp
FROM t_union_test
),
chunked AS (
SELECT ST_UnaryUnion(ST_Collect(geom)) AS g
FROM numbered
GROUP BY grp
)
SELECT ST_UnaryUnion(ST_Collect(g)) FROM chunked;
"Aggregate (cost=6291.28..6303.79 rows=1 width=32) (actual time=722.121..722.125 rows=1 loops=1)"
" Buffers: shared hit=192"
" -> HashAggregate (cost=3636.66..6263.66 rows=200 width=40) (actual time=36.299..351.440 rows=21 loops=1)"
" Group Key: (row_number() OVER (?) / 100)"
" Batches: 1 Memory Usage: 3104kB"
" Buffers: shared hit=192"
" -> WindowAgg (cost=1821.66..3361.66 rows=2000 width=608) (actual time=14.338..15.571 rows=2000 loops=1)"
" Buffers: shared hit=192"
" -> Sort (cost=1821.66..1826.66 rows=2000 width=600) (actual time=14.310..14.488 rows=2000 loops=1)"
" Sort Key: (st_geohash(st_centroid(t_union_test.geom), 6))"
" Sort Method: quicksort Memory: 1220kB"
" Buffers: shared hit=192"
" -> Seq Scan on t_union_test (cost=0.00..1712.00 rows=2000 width=600) (actual time=0.086..12.351 rows=2000 loops=1)"
" Buffers: shared hit=192"
"Planning:"
" Buffers: shared hit=2"
"Planning Time: 1.598 ms"
"Execution Time: 723.638 ms"
処理時間は、
(A) ST_Union 統合 < (C) ST_UnaryUnion + ST_Collect チャンクごと統合 <<< (B) ST_MemUnion 統合
となります。
メモリ使用量は逆順になるため、実務では、(A) ST_Union 統合→(C) ST_UnaryUnion + ST_Collect チャンクごと統合→(B) ST_MemUnion 統合の順番に試行して最初にメモリ不足にならなかった方法を選定すればよいと考えられます。
ラインの結合
次にラインの結合について検討します。
ST_Collect - 集約してMultiLineStringを作成
ポリゴンと同様に、ST_Collectは複数のラインをそのままグループ化します。
SELECT ST_AsText(ST_Collect(geom))
FROM test_lines;
結果:
MULTILINESTRING((0 0,2 2),(2 2,3 1),(0 2,2 0))
ST_DumpがST_Collectと逆の挙動になるのも同様です。
SELECT ST_AsText((ST_Dump(ST_Collect(geom))).geom)
FROM test_lines;
結果:
LINESTRING(0 0,2 2)
LINESTRING(2 2,3 1)
LINESTRING(0 2,2 0)
端点が一致するライン同士を結合して、一つのラインにしたい場合は別の関数を使う必要があります。
まずは、ポリゴンの場合と同様にST_Unionを試してみます。
ST_Union - 集約してMultiLineStringを作成(交差する地点で分割)
実は、ST_Unionをラインに対して実施すると、接続するライン同士を結合したりはせずに、ST_Collectと同様に複数ラインをグループ(マルチ)化します。
ただし、ST_Collectと異なり、グループ化する際に、互いに交差するラインについては交差するところでラインを分割します。
SELECT ST_AsText(ST_Union(geom))
FROM test_lines;
結果:
MULTILINESTRING((0 0,1 1),(1 1,2 2),(2 2,3 1),(0 2,1 1),(1 1,2 0))
ST_Collectの結果と比較すると、ST_Unionでは(1,1)の交点でラインが分割されています。
そのため、ラインを結合するという目的でST_Unionを使用すると、想定していない処理結果になる可能性があり注意が必要です。
接続されたラインを結合したい場合は、ST_LineMergeを使う必要があります。
ST_LineMerge - 接続されたラインを1本に結合
ST_LineMergeは、端点が接続されているラインを1本の連続したラインに統合します。
ただし、集約関数ではないため、複数データ行のラインを結合したい場合は、ST_Collectと合わせて使います。
SELECT ST_AsText(
ST_LineMerge(
ST_Collect(geom)
)
)
FROM test_lines;
結果:
MULTILINESTRING((0 0,2 2,3 1),(0 2,2 0))
ライン(0 0,2 2)とライン(2 2,3 1)が一つのライン(0 0,2 2,3 1)に統合されています。
まとめ
以上をまとめると次のようになります。
ポリゴンの場合
- 単純にグループ化したい場合は
ST_Collectを使う - 境界を溶かしてオーバーラップを併合したい場合は
ST_Unionを使う - ただし、メモリ不足で落ちる場合は
ST_UnaryUnion+ST_CollectまたはST_MemUnionを使う
ラインの場合
- 単純にグループ化したい場合は
ST_Collectを使う - 接続されたラインを結合したい場合は
ST_LineMerge+ST_Collectを使う
データの性質と処理の目的に応じて関数を使い分けることで、効率的な地理空間データ処理が可能になるかと思いますので、その際の参考になれば幸いです。