pgRoutingでスタート地点とゴール地点が多対多の検索を1発で実行する

本記事は [RDBMS-GIS Advent Calendar 2018](https://qiita.com/advent-calendar/2018/rdbms_gis) の15日目です。

##はじめに
最初に書いておくと、Postgresで複合型の使い方知ってるなら「で？」って話です。
自分はそれでめちゃくちゃ悩んだので書き残しておく。
そして悩みを聞いてアドバイスをくれた皆様ありがとうございます。

今回の対象バージョンはpgRouting2.6.1になっております。
環境の構築方法は[Ubuntu18.04 LTSにpgRouting環境を構築してみる](https://qiita.com/ozo360/items/8516990f9b8e54283fde)で紹介しています。
また、古いバージョンのpgRoutingでは正常な結果が返らないのでやってはいけない。

##本題
あるときpgRoutingでスタート地点とゴール地点が多対多の検索をしたいと思った。

どんなときに利用するのかと言えば、多数の住民ポイントがあったとして複数の避難所ポイントまでの経路を検索して、どの避難所にも1km以内とか5分以内で到達できない住民を抽出したりするときに使ったらいいんじゃなかろうか。（適当）

普通にやろうとするとスタート地点毎に、到達圏域検索をおこなうか最短経路検索を行って到達したかどうかを判定することになると思う。
それはそれでいいんだけどSQLだけで処理できそうな気がしてならないのであがいている。

ちなみに、上の利用例では到達圏域検索の方が適切な処理だろうと考えられるがここでは最短経路検索を使用する。

##テストデータ作成
単純化するために以下のようなエッジを生成した。
エッジの両端が判りやすいようにQGISで矢線のスタイルとエッジIDの注記を表示している。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/240181/10717696-97a5-72e1-c6dd-cea61475a33f.png)

テーブルレイアウトは以下のようになっている。

``` sql
CREATE TABLE public.test_pl
(
    id integer NOT NULL DEFAULT nextval('test_pl_id_seq'::regclass),
    source integer,
    target integer,
    cost double precision,
    geom geometry(LineString,4237),
    CONSTRAINT test_pl_pkey PRIMARY KEY (id)
)
WITH (
    OIDS = FALSE
)
TABLESPACE pg_default;

CREATE INDEX test_pl_the_geom_gidx
    ON public.test_pl USING gist
    (geom)
    TABLESPACE pg_default;
```

##トポロジデータ作成
ここでいうトポロジデータはPostGISのトポロジデータとは異なる。
pgRoutingは地物データから論理的な木構造データを作るだけで地物の編集をしてもトポロジカルに移動したりしない。

pgAdminからSQLを実行してみよう。

``` sql
SELECT pgr_createTopology('test_pl', 0.0001, 'geom', 'id');
```

実行すると`test_pl`のsourceとtargetにノード番号が付与されるとともに、`test_pl_vertices_pgr`という名前のノードテーブルが生成されたと思う。
test_pl_vertices_pgrもQGISに読み込んで確認しよう。赤字がノードテーブルのノード番号になる。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/240181/239e0054-ece7-81db-b972-1fd327b48d1c.png)

最後にエッジのコストを一律「1」としておこう。
1分と解釈してもいいし1kmと解釈しても良いだろう。

``` sql
UPDATE test_pl SET cost = 1;
```


##まずは普通に検索
さて、データが出来上がったのでまずは普通に検索を行ってみよう。
1番のノードから7番への最短ルート検索をしてみよう。

``` sql
SELECT *
FROM pgr_kdijkstraPath(
  'SELECT id, source, target, cost::float8 FROM test_pl',
  1, -- source_node_id
  array[7], -- target_node_ids
  false, -- directed boolean
  false -- has_cost boolean
);
```
結果が返りました。
![pgr1.png](https://qiita-image-store.s3.amazonaws.com/0/240181/b281041d-f41d-3af4-f878-a60b8316cb68.png)

id1は対象target、id2は経由するノード番号、id3は経由するエッジ番号、costは対象エッジのコストですね。
以下のような経路を辿るのが最短だと判断されました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/240181/0f79bcdc-2f6f-0cf6-d547-2b952f15b8c5.png)

今度は1番のノードから7番と13番への最短ルート検索をしてみよう。

``` sql
SELECT *
FROM pgr_kdijkstraPath(
  'SELECT id, source, target, cost::float8 FROM test_pl',
  1, -- source_node_id
  array[7,13], -- target_node_ids
  false, -- directed boolean
  false -- has_cost boolean
);
```
![pgr2.png](https://qiita-image-store.s3.amazonaws.com/0/240181/2badf744-251b-5245-b174-74bf0b26a3c1.png)

ちゃんと結果が返ってますね。
図は省略。

##普通のSQL使いには違和感ありまくり
はい、普通のSQL使いの皆さんはFROM句に関数て！となると思います。
そして結果がテーブル！変態！

今回やりたいことはsource_node_idに他のテーブルの結果の値と結合すれば多対多の検索が出来るのは判るんですが、どうやって実現すればよいのか。
考えれば考えるほど分けが判らなくなるわけですが、そもそもこの関数の戻り値が何かを把握することが必要でした。

##普通の関数みたく使ってみる
普通にSELECT句に関数を置いて実行してみよう。

``` sql
SELECT pgr_kdijkstraPath(
  'SELECT id, source, target, cost::float8 FROM test_pl',
  1, -- source_node_id
  array[7,13], -- target_node_ids
  false, -- directed boolean
  false -- has_cost boolean
);
```

はい、なんとなく想像してたような値が返りました。
![pgr3.png](https://qiita-image-store.s3.amazonaws.com/0/240181/dfe0f297-2e6f-71f5-5073-9bc520a71d0b.png)

ここで注目しないといけないのが戻り値の型です！
pgr_costresult3型となっていますね。なんぞこれ。

というわけでこれが複合型ですね。
https://www.postgresql.jp/document/10/html/rowtypes.html

pgAdmin4で[Schemas]-[public]-[Types]を覗くと入っていました`pgr_costresult3`
定義は以下のようになってます。

``` sql
CREATE TYPE public.pgr_costresult3 AS
(
	seq integer,
	id1 integer,
	id2 integer,
	id3 integer,
	cost double precision
);

ALTER TYPE public.pgr_costresult3
    OWNER TO postgres;
```

複合型は正しくキャストすればレコードのような扱いができます。
先ほどのSQL実行結果からid2のフィールドだけ抜き出してみましょう。

``` sql
SELECT (pgr_kdijkstraPath(
  'SELECT id, source, target, cost::float8 FROM test_pl',
  1, -- source_node_id
  array[7,13], -- target_node_ids
  false, -- directed boolean
  false -- has_cost boolean
)::pgr_costresult3).id2
```
![pgr5.png](https://qiita-image-store.s3.amazonaws.com/0/240181/8a58b1fb-8258-1af3-54f7-f671e0290338.png)

良い感じ。
SELECT句で呼べたらもうこっちのもんです。

##多対多の検索を実行する
ノードテーブル`test_pl_vertices_pgr`とくっつけて1番と8番のノードから7番と13番への最短ルート検索をしてみよう。
pgr_kdijkstraPathの第2引数として`test_pl_vertices_pgr`のidを使用しています。
idはbigintなのでintにキャストしないといけません。

``` sql
SELECT test_pl_vertices_pgr.id, pgr_kdijkstraPath(
  'SELECT id, source, target, cost::float8 FROM test_pl',
  id::int,
  array[7,13], -- target_node_id
  false,
  false
) prg_costresult3
FROM test_pl_vertices_pgr
WHERE id in (1,8) -- source_node_id;
```

多対多の検索結果が返っています。
![pgr4.png](https://qiita-image-store.s3.amazonaws.com/0/240181/6e841c6b-028e-d6b5-984c-aa55d387b3a2.png)

ここまで来ればもう一息
サブクエリ化してから普通の表形式で返します。

``` sql
SELECT 
  id source_node, 
  (pgr_costresult3::pgr_costresult3).id1 target_node,
  (pgr_costresult3::pgr_costresult3).id2 node_id,
  (pgr_costresult3::pgr_costresult3).id3 edge_id,
  (pgr_costresult3::pgr_costresult3).cost 
FROM (
  SELECT test_pl_vertices_pgr.id, pgr_kdijkstraPath(
    'SELECT id, source, target, cost::float8 FROM test_pl',
    id::int,
    array[7,13], -- target_node_id
    false,
    false
  ) pgr_costresult3
  FROM test_pl_vertices_pgr
  WHERE id in (1,8) -- source_node_id;
) prg_result;
```

どーん
![pgr6.png](https://qiita-image-store.s3.amazonaws.com/0/240181/3a79588d-c18f-565c-c446-c4ebdb1d7a3b.png)

これであとは如何様にも料理してくださいというデータになりました。
念のためルートを示した図も張り付けておきます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/240181/36d90b50-f11a-ee7f-c058-483efa9f4203.png)


##更にもう一押し
target_node_idもテーブルから参照しましょう。
integer[]型を渡せばいいのでarrayとサブクエリ使うだけですね。

``` sql
SELECT 
  id source_node, 
  (pgr_costresult3::pgr_costresult3).id1 target_node,
  (pgr_costresult3::pgr_costresult3).id2 node_id,
  (pgr_costresult3::pgr_costresult3).id3 edge_id,
  (pgr_costresult3::pgr_costresult3).cost 
FROM (
  SELECT test_pl_vertices_pgr.id, pgr_kdijkstraPath(
    'SELECT id, source, target, cost::float8 FROM test_pl',
    id::int,
    array(
      SELECT id::int 
      FROM test_pl_vertices_pgr
      WHERE id in (7,13) -- target_node_id
    ),
    false,
    false
  ) pgr_costresult3
  FROM test_pl_vertices_pgr
  WHERE id in (1,8) -- source_node_id;
) prg_result;
```

こんな感じでいけます。
今回は`test_pl_vertices_pgr`を使ってますけど`tbl_source_node`と`tbl_target_node`みたいなテーブルを用意して事前にデータをINSERTしておけばもっと素直に検索が可能になりますね。

``` sql
SELECT 
  id source_node, 
  (pgr_costresult3::pgr_costresult3).id1 target_node,
  (pgr_costresult3::pgr_costresult3).id2 node_id,
  (pgr_costresult3::pgr_costresult3).id3 edge_id,
  (pgr_costresult3::pgr_costresult3).cost 
FROM (
  SELECT test_pl_vertices_pgr.id, pgr_kdijkstraPath(
    'SELECT id, source, target, cost::float8 FROM test_pl',
    id,
    array(
      SELECT id FROM tbl_target_node
    ),
    false,
    false
  ) pgr_costresult3
  FROM tbl_source_node
) prg_result;
```

うん、なんか使えそうな形にまとまりました。

以上です。

# 本記事のライセンス

<a rel="license" href="http://creativecommons.org/licenses/by/4.0/">![クリエイティブ・コモンズ・ライセンス](https://i.creativecommons.org/l/by/4.0/88x31.png)</a>
この記事は <a rel="license" href="http://creativecommons.org/licenses/by/4.0/">クリエイティブ・コモンズ 表示 4.0 国際 ライセンス</a>の下に提供されています。