これは、約 3 年前に作成したアプリで使用する機能を大幅に簡略化したものです。当面の質問に適応。
ボックスを使用して、ポイントの周囲の位置を検索します。より正確な結果を得るために円を使用してこれを行うこともできますが、これは最初は概算にすぎません。
世界が平らではないという事実を無視します。私のアプリケーションは、数 100 キロメートルのローカル リージョンのみを対象としていました。そして、捜索範囲はわずか数キロメートルしかありません。世界を平らにすることは、目的には十分です。(Todo: 位置情報に応じた緯度/経度比のより適切な概算が役立つ場合があります。)
Google マップから取得したようなジオコードで動作します。
PostgreSQL 9.1 および 9.2 でテストされた、拡張機能なし(PostGis は不要) の標準 PostgreSQLで動作します。
インデックスがなければ、ベース テーブルのすべての行の距離を計算し、最も近い行をフィルタリングする必要があります。大きなテーブルで非常に高価です。
編集:
再確認したところ、現在の実装ではポイントの GisT インデックスが許可されています (Postgres 9.1 以降)。それに応じてコードを簡素化しました。
主なトリックは、列が単なるポイントであっても、ボックスの機能的なGiST インデックスを使用することです。これにより、既存のGiST 実装を使用できるようになります。
このような (非常に高速な) 検索を使用すると、ボックス内のすべての場所を取得できます。残りの問題: 行の数はわかっていますが、それらが入っているボックスのサイズはわかりません。これは、答えの一部を知っているが、質問を知らないようなものです。
dba.SE に関するこの関連する回答で詳しく説明されている方法と同様の逆引き参照アプローチを使用します。(ただし、ここでは部分インデックスを使用していません-実際にも機能する可能性があります)。
非常に小さいものから「少なくとも十分な場所を保持するのに十分な大きさ」まで、定義済みの検索ステップの配列を繰り返します。つまり、検索ボックスのサイズを取得するには、いくつかの (非常に高速な) クエリを実行する必要があります。
次に、このボックスでベース テーブルを検索し、インデックスから返された数行のみの実際の距離を計算します。ボックスには少なくとも十分な場所が含まれていることがわかったので、通常はいくらかの余剰があります。最も近いものを取得することで、ボックスの角を効果的に丸めます。ボックスを 1 ノッチ大きくすることで、この効果を強制することができます(完全に正確なradius
結果を得るには、関数を sqrt(2) で乗算しますが、これは最初から概算であるため、全力を尽くすことはしません)。
これは、最新バージョンの PostgreSQL で利用可能なSP GiSTインデックスを使用すると、さらに高速かつ簡単になります。しかし、それが可能かどうかはまだわかりません。データ型を実際に実装する必要がありましたが、それについて詳しく説明する時間がありませんでした。方法を見つけたら、また報告することを約束してください!
adr
いくつかの値の例 ( .. アドレス)を含むこの簡略化された表を考えると、次のようになります。
CREATE TABLE adr(adr_id int, adr text, geocode point);
INSERT INTO adr (adr_id, adr, geocode) VALUES
(1, 'adr1', '(48.20117,16.294)'),
(2, 'adr2', '(48.19834,16.302)'),
(3, 'adr3', '(48.19755,16.299)'),
(4, 'adr4', '(48.19727,16.303)'),
(5, 'adr5', '(48.19796,16.304)'),
(6, 'adr6', '(48.19791,16.302)'),
(7, 'adr7', '(48.19813,16.304)'),
(8, 'adr8', '(48.19735,16.299)'),
(9, 'adr9', '(48.19746,16.297)');
インデックスは次のようになります。
CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);
-> SQLfiddle
必要に応じて、ホーム エリア、ステップ、倍率を調整する必要があります。ポイントの周囲数キロメートルのボックスで検索する限り、平らな地球は十分な近似値です。
これを使用するには、plpgsql をよく理解する必要があります。私はここで十分にやったと感じています。
CREATE OR REPLACE FUNCTION f_find_around(_lat double precision, _lon double precision, _limit bigint = 50)
RETURNS TABLE(adr_id int, adr text, distance int) AS
$func$
DECLARE
_homearea CONSTANT box := '(49.05,17.15),(46.35,9.45)'::box; -- box around legal area
-- 100m = 0.0008892 250m, 340m, 450m, 700m,1000m,1500m,2000m,3000m,4500m,7000m
_steps CONSTANT real[] := '{0.0022,0.003,0.004,0.006,0.009,0.013,0.018,0.027,0.040,0.062}'; -- find optimum _steps by experimenting
geo2m CONSTANT integer := 73500; -- ratio geocode(lon) to meter (found by trial & error with google maps)
lat2lon CONSTANT real := 1.53; -- ratio lon/lat (lat is worth more; found by trial & error with google maps in (Vienna)
_radius real; -- final search radius
_area box; -- box to search in
_count bigint := 0; -- count rows
_point point := point($1,$2); -- center of search
_scalepoint point := point($1 * lat2lon, $2); -- lat scaled to adjust
BEGIN
-- Optimize _radius
IF (_point <@ _homearea) THEN
FOREACH _radius IN ARRAY _steps LOOP
SELECT INTO _count count(*) FROM adr a
WHERE a.geocode <@ box(point($1 - _radius, $2 - _radius * lat2lon)
, point($1 + _radius, $2 + _radius * lat2lon));
EXIT WHEN _count >= _limit;
END LOOP;
END IF;
IF _count = 0 THEN -- nothing found or not in legal area
EXIT;
ELSE
IF _radius IS NULL THEN
_radius := _steps[array_upper(_steps,1)]; -- max. _radius
END IF;
_area := box(point($1 - _radius, $2 - _radius * lat2lon)
, point($1 + _radius, $2 + _radius * lat2lon));
END IF;
RETURN QUERY
SELECT a.adr_id
,a.adr
,((point (a.geocode[0] * lat2lon, a.geocode[1]) <-> _scalepoint) * geo2m)::int4 AS distance
FROM adr a
WHERE a.geocode <@ _area
ORDER BY distance, a.adr, a.adr_id
LIMIT _limit;
END
$func$ LANGUAGE plpgsql;
電話:
SELECT * FROM f_find_around (48.2, 16.3, 20);
$3
定義された最大検索領域に十分な数の場所がある場合、場所のリストを返します。
実際の距離で並べ替えます。
さらなる改善
次のような関数を作成します。
CREATE OR REPLACE FUNCTION f_geo2m(double precision, double precision)
RETURNS point AS
$BODY$
SELECT point($1 * 111200, $2 * 111400 * cos(radians($1)));
$BODY$
LANGUAGE sql IMMUTABLE;
COMMENT ON FUNCTION f_geo2m(double precision, double precision)
IS 'Project geocode to approximate metric coordinates.
SELECT f_geo2m(48.20872, 16.37263) --';
(文字通り)グローバル定数111200
とは、経度の長さと緯度の長さ111400
から私の地域(オーストリア)に最適化されていますが、基本的には世界中で機能します。
それを使用して、スケーリングされたジオコードをベース テーブルに追加します。理想的には、この回答で概説されているような生成された列です:
How do you do date math that ignores the year? プロセスを説明する3. ブラック マジック バージョン
を参照してください。
次に、関数をもう少し単純化できます。入力値を一度スケーリングし、冗長な計算を削除します。