最後に、スキーマを完全に作成してサンプルデータを入力するスクリプトがあります。
次の2つのテーブルについて考えてみます。
ポリゴンのテーブル:
CREATE TABLE my_polygon (
my_polygon_id SERIAL PRIMARY KEY,
common_id INTEGER NOT NULL,
value1 NUMERIC NOT NULL,
value2 NUMERIC NOT NULL,
value3 NUMERIC NOT NULL,
geom GEOMETRY(Polygon) NOT NULL
)
;
CREATE INDEX ON my_polygon (common_id);
CREATE INDEX ON my_polygon USING Gist (common_id, geom);
ポリゴン内に含まれるポイントの表:
CREATE TABLE my_point (
my_point_id SERIAL PRIMARY KEY,
common_id INTEGER NOT NULL,
pointvalue NUMERIC NOT NULL,
geom GEOMETRY(Point) NOT NULL
);
CREATE INDEX ON my_point (common_id);
CREATE INDEX ON my_point USING Gist (common_id, geom);
私がジオメトリを使用しているという事実は、ここでの問題に厳密には関連していません。しかし、これにより、私がやろうとしていることの動機がより明確になると思います。
問題は、ポリゴン間にvery非常にわずかな重なりが存在することです。 (それらをクリーンアップしようとすることは実際にはオプションではありません。オーバーラップは、それらを生成する際のある種の浮動小数点エラーから発生します。しかし、一部のポイントはこれらの小さなオーバーラップの内側にあり、2つの行がJOIN
封じ込めに基づいてそれら。しかし、実際には、各ポイントは単一のポリゴンにのみ関連付けられる必要があります。 1つが2つに該当する場合、どちらに関連付けられるかは問題ではないため、次のように、クエリで1つだけを選択するようにしても問題ありません。
SELECT DISTINCT ON (my_point.my_point_id)
my_polygon.*,
my_point.my_point_id,
my_point.pointvalue,
my_point.geom AS pointgeom
FROM my_polygon
JOIN my_point ON my_point.common_id = my_polygon.common_id AND ST_Contains(my_polygon.geom, my_point.geom)
WHERE my_polygon.common_id = 1
ORDER BY my_point.my_point_id, my_polygon.my_polygon_id
上記のクエリのように、通常、common_id
に基づいてSELECT
を使用します。このクエリは正常に実行されます。クエリプランは次のようになります。
ただし、これは多くの異なるクエリで必要なロジックなので、ビューに入れたかったのです。その結果、クエリプランナーに関する限り、クエリは次のようになります。
SELECT *
FROM (
SELECT DISTINCT ON (my_point.my_point_id)
my_polygon.*,
my_point.my_point_id,
my_point.pointvalue,
my_point.geom AS pointgeom
FROM my_polygon
JOIN my_point ON my_point.common_id = my_polygon.common_id AND ST_Contains(my_polygon.geom, my_point.geom)
ORDER BY my_point.my_point_id, my_polygon.my_polygon_id
) point_with_polygon
WHERE common_id = 1
その結果、PostgreSQLはcommon_id
を実行した後にDISTINCT ON
afterでフィルタリングします。つまり、JOIN
全体を両方のテーブル。これがクエリプランです。
どうすればPostgreSQLがクエリの最初の部分にフィルターをプッシュしても、一般的なクエリをビューに配置できますか?
今はPG 9.3で立ち往生していますが、9.5へのアップグレードはオプションかもしれません。
PostGISが必要です。 (それがSQL Fiddleがない理由です。)
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS btree_Gist;
-- DROP FUNCTION ST_GeneratePoints(geometry, numeric);
DO $doblock$
BEGIN
IF NOT EXISTS(SELECT * FROM pg_proc WHERE UPPER(proname) = UPPER('ST_GeneratePoints')) THEN
-- Create naive ST_GeneratePoints if version of PostGIS is not new enough
CREATE FUNCTION ST_GeneratePoints(g geometry, npoints numeric)
RETURNS geometry
VOLATILE
RETURNS NULL ON NULL INPUT
LANGUAGE plpgsql
AS $$
DECLARE
num_to_generate INTEGER := npoints::INTEGER;
adjustment CONSTANT FLOAT := 0.00000000001;
x_min FLOAT := ST_XMin(g) + adjustment;
x_max FLOAT := ST_XMax(g) - adjustment;
y_min FLOAT := ST_YMin(g) + adjustment;
y_max FLOAT := ST_YMax(g) - adjustment;
temp_result GEOMETRY[];
result_array GEOMETRY[] := ARRAY[]::GEOMETRY[];
BEGIN
IF ST_IsEmpty(g) THEN
RAISE EXCEPTION 'Cannot generate points inside an empty geometry';
END IF;
IF ST_Dimension(g) < 2 THEN
RAISE EXCEPTION 'Only polygons supported';
END IF;
-- Reduce number of loops to reduce slow array_cat calls
WHILE num_to_generate > 0 LOOP
SELECT ARRAY_AGG(contained.point) INTO temp_result
FROM (
SELECT point
FROM (
SELECT ST_MakePoint(
x_min + random() * (x_max - x_min),
y_min + random() * (y_max - y_min)
) point
-- Generate extras to reduce number of loops
--
-- Each point has a probability of ST_Area(g) / ST_Area(ST_Envelope(g)) to fall within the polygon.
-- So on average, we expect ST_Area(g) / ST_Area(ST_Envelope(g)) of the points generated to fall within.
-- Generating ST_Area(ST_Envelope(g)) / ST_Area(g) * num_to_generate points means that on average, we'll
-- get
--
-- ST_Area(g) / ST_Area(ST_Envelope(g)) * ST_Area(ST_Envelope(g)) / ST_Area(g) * num_to_generate
-- = num_to_generate
--
-- points within the polygon. (Notice the numerators and denominators cancel out.) This means we'll
-- only run one loop about half the time without generating an excessive number of points.
--
-- Generate at least 20 to avoid a lot of loops for small numbers, though.
FROM generate_series(1, GREATEST(20, CEIL(ST_Area(ST_Envelope(g)) / ST_Area(g) * num_to_generate)::INTEGER))
) candidate
WHERE ST_Contains(g, candidate.point)
-- Filter out extras if we have too many matches
LIMIT num_to_generate
) contained
;
IF ARRAY_LENGTH(temp_result, 1) > 0 THEN
result_array := array_cat(result_array, temp_result);
num_to_generate := npoints - COALESCE(ARRAY_LENGTH(result_array, 1), 0);
END IF;
END LOOP;
RETURN (SELECT ST_Union(point) FROM UNNEST(result_array) result (point));
END;
$$;
RAISE NOTICE 'Created ST_GeneratePoints';
ELSE
RAISE NOTICE 'ST_GeneratePoints exists';
END IF;
END
$doblock$
;
DROP TABLE IF EXISTS my_polygon;
CREATE TABLE my_polygon (
my_polygon_id SERIAL PRIMARY KEY,
common_id INTEGER NOT NULL,
value1 NUMERIC NOT NULL,
value2 NUMERIC NOT NULL,
value3 NUMERIC NOT NULL,
geom GEOMETRY(Polygon) NOT NULL
)
;
CREATE INDEX ON my_polygon (common_id);
CREATE INDEX ON my_polygon USING Gist (common_id, geom);
WITH common AS (
SELECT
common_id,
random() * 5000 AS common_x_translate,
random() * 5000 AS common_y_translate
FROM (
SELECT TRUNC(random() * 1000) + 1 AS common_id
FROM generate_series(1, 100)
UNION
SELECT 1
) a
),
geom_set_with_small_overlaps AS (
SELECT
ST_MakeEnvelope(
x.translate,
y.translate,
x.translate + 1.1,
y.translate + 1.1
) AS geom
FROM
generate_series(0, 9) x (translate),
generate_series(0, 9) y (translate)
)
INSERT INTO my_polygon (common_id, value1, value2, value3, geom)
SELECT
common_id,
random() * 100,
random() * 100,
random() * 100,
ST_Translate(geom, common_x_translate, common_y_translate)
FROM common, geom_set_with_small_overlaps
;
DROP TABLE IF EXISTS my_point;
CREATE TABLE my_point (
my_point_id SERIAL PRIMARY KEY,
common_id INTEGER NOT NULL,
pointvalue NUMERIC NOT NULL,
geom GEOMETRY(Point) NOT NULL
);
INSERT INTO my_point (common_id, pointvalue, geom)
SELECT
common_id,
random() * 100,
(ST_Dump(ST_GeneratePoints(extent, FLOOR(5000 + random() * 15000)::NUMERIC))).geom
FROM (
SELECT
common_id,
-- Small negative buffer prevents lying on the outer Edge
ST_Buffer(ST_Extent(geom), - 0.0001) AS extent
FROM my_polygon
GROUP BY common_id
) common
UNION ALL
SELECT
common_id,
random() * 100,
(ST_Dump(ST_GeneratePoints(intersection, TRUNC(random() * 5)::NUMERIC))).geom
FROM (
SELECT
p1.common_id,
p1.my_polygon_id AS id1,
p2.my_polygon_id AS id2,
ST_Intersection(p1.geom, p2.geom) AS intersection
FROM my_polygon p1
JOIN my_polygon p2 ON (
p1.my_polygon_id < p2.my_polygon_id AND
p1.common_id = p2.common_id AND
ST_Intersects(p1.geom, p2.geom)
)
) a
;
CREATE INDEX ON my_point (common_id);
CREATE INDEX ON my_point USING Gist (common_id, geom);
あなたはおそらくその後にVACUUM ANALYZE
したいでしょう。
内部のWHERE
句(良好なパフォーマンス):
Unique (cost=1195.74..1207.74 rows=2400 width=216)
-> Sort (cost=1195.74..1201.74 rows=2400 width=216)
Sort Key: my_point.my_point_id, my_polygon.my_polygon_id
-> Nested Loop (cost=5.34..1060.99 rows=2400 width=216)
-> Bitmap Heap Scan on my_polygon (cost=4.93..191.74 rows=100 width=164)
Recheck Cond: (common_id = 1)
-> Bitmap Index Scan on my_polygon_common_id_geom_idx (cost=0.00..4.90 rows=100 width=0)
Index Cond: (common_id = 1)
-> Index Scan using my_point_common_id_geom_idx on my_point (cost=0.41..8.68 rows=1 width=52)
Index Cond: ((common_id = 1) AND (my_polygon.geom && geom))
Filter: _st_contains(my_polygon.geom, geom)
WHERE
句の外側(パフォーマンスが悪い):
Subquery Scan on a (cost=209447.85..215842.18 rows=1827 width=212)
Filter: (a.common_id = 1)
-> Unique (cost=209447.85..211274.80 rows=365390 width=212)
-> Sort (cost=209447.85..210361.33 rows=365390 width=212)
Sort Key: my_point.my_point_id, my_polygon.my_polygon_id
-> Nested Loop (cost=0.41..63285.00 rows=365390 width=212)
-> Seq Scan on my_polygon (cost=0.00..338.00 rows=9800 width=164)
-> Index Scan using my_point_common_id_geom_idx on my_point (cost=0.41..6.41 rows=1 width=52)
Index Cond: ((common_id = my_polygon.common_id) AND (my_polygon.geom && geom))
Filter: _st_contains(my_polygon.geom, geom)
これを回避してPGを正しく最適化する方法があることがわかりました:common_id
句にDISTINCT ON
を含める必要があります。
このような:
SELECT *
FROM (
SELECT DISTINCT ON (my_point.my_point_id, my_polygon.common_id)
my_polygon.*,
my_point.my_point_id,
my_point.pointvalue,
my_point.geom AS pointgeom
FROM my_polygon
JOIN my_point ON my_point.common_id = my_polygon.common_id AND ST_Contains(my_polygon.geom, my_point.geom)
ORDER BY my_point.my_point_id, my_polygon.common_id, my_polygon.my_polygon_id
) point_with_polygon
WHERE common_id = 1
これは、DISTINCT ON
の前にWHERE
句を含めるのと同じクエリプランになります。
Unique (cost=2307.77..2345.55 rows=5038 width=212)
-> Sort (cost=2307.77..2320.36 rows=5038 width=212)
Sort Key: my_point.my_point_id, my_polygon.my_polygon_id
-> Nested Loop (cost=9.36..1479.97 rows=5038 width=212)
-> Bitmap Heap Scan on my_polygon (cost=4.93..190.19 rows=100 width=164)
Recheck Cond: (common_id = 1)
-> Bitmap Index Scan on my_polygon_common_id_geom_idx (cost=0.00..4.90 rows=100 width=0)
Index Cond: (common_id = 1)
-> Bitmap Heap Scan on my_point (cost=4.43..12.89 rows=1 width=52)
Recheck Cond: ((common_id = 1) AND (my_polygon.geom && geom))
Filter: _st_contains(my_polygon.geom, geom)
-> Bitmap Index Scan on my_point_common_id_geom_idx (cost=0.00..4.43 rows=2 width=0)
Index Cond: ((common_id = 1) AND (my_polygon.geom && geom))
common_id
を含めることは、いずれにせよJOIN
条件の一部であるため冗長ですが、これはクエリの結果を変更しないことも意味します。
verySELECT
結果でであるcommon_id
を使用することが重要です。たとえば、次のクエリを見てください。
SELECT *
FROM (
SELECT DISTINCT ON (my_point.my_point_id, my_point.common_id)
my_polygon.*,
my_point.my_point_id,
my_point.pointvalue,
my_point.geom AS pointgeom
FROM my_polygon
JOIN my_point ON my_point.common_id = my_polygon.common_id AND ST_Contains(my_polygon.geom, my_point.geom)
ORDER BY my_point.my_point_id, my_point.common_id, my_polygon.my_polygon_id
) point_with_polygon
WHERE common_id = 1
このクエリでは、SELECT
句でmy_polygon.common_id
を使用していますが、my_point.common_id
句とORDER BY
句ではDISTINCT ON
を使用しています。この場合、PGはフィルタをしないフィルタをサブクエリにプッシュします。