これは、約3年前に作成されたアプリで使用する関数の大幅に簡略化されたバージョンです。手元の質問に適応しました。
-
ボックスを使用して、ポイントの周囲の場所を検索します 。より正確な結果を得るために円でこれを行うこともできますが、これは最初の概算にすぎません。
-
世界が平坦ではないという事実を無視します。私のアプリケーションは、直径数100kmのローカルリージョンのみを対象としていました。また、検索範囲は数キロメートルしかありません。この目的には、世界をフラットにするだけで十分です。 (Todo:地理的位置に応じて緯度/経度の比率をより適切に概算すると役立つ場合があります。)
-
Googleマップから取得するのと同じように地理コードで動作します。
-
拡張なしの標準PostgreSQLで動作します (PostGisは必要ありません)、PostgreSQL9.1および9.2でテスト済み。
インデックスがないと、ベーステーブルのすべての行の距離を計算し、最も近い行をフィルタリングする必要があります。大きなテーブルでは非常に高価です。
編集:
再確認したところ、現在の実装ではポイントのGisTインデックスが許可されています(Postgres 9.1以降)。それに応じてコードを簡略化しました。
主なトリック ボックスの機能的なGiSTインデックスを使用することです。 、列は単なるポイントですが。これにより、既存のGiST実装
を使用できるようになります。 。
このような(非常に高速な)検索を使用すると、ボックス内のすべての場所を取得できます。残りの問題:行数はわかっていますが、ボックスのサイズはわかりません。これは、答えの一部を知っているようなものですが、質問はわかりません。
同様の逆ルックアップを使用します dba.SEに関するこの関連する回答 。 (ただし、ここでは部分インデックスを使用していません。実際に機能する可能性もあります)。
非常に小さいものから「少なくとも十分な場所を保持するのに十分な大きさ」まで、事前定義された一連の検索ステップを繰り返します。つまり、検索ボックスのサイズに到達するには、いくつかの(非常に高速な)クエリを実行する必要があります。
次に、このボックスを使用してベーステーブルを検索し、インデックスから返された数行のみの実際の距離を計算します。少なくともを保持しているボックスが見つかったため、通常はある程度の余剰があります。 十分な場所。最も近いものを取ることによって、私たちは効果的に箱の角を丸めます。ボックスを1ノッチ大きくすることで、この効果を強制できます(radius
を乗算します)。 sqrt(2)による関数で、完全に正確を取得します。 結果ですが、これはそもそも概算であるため、私はすべてを尽くすことはしません。
これは、 SP GiST を使用すると、さらに高速で簡単になります。 PostgreSQLの最新バージョンで利用可能なインデックス。しかし、それが可能かどうかはまだわかりません。データ型の実際の実装が必要でしたが、それに飛び込む時間がありませんでした。方法を見つけたら、報告することを約束してください!
いくつかの値の例(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);
必要に応じて、ホームエリア、ステップ、スケーリング係数を調整する必要があります。ポイントの周囲数キロメートルのボックスを検索する限り、平らな地球で十分です。
これを使用するには、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
経度の長さ
から私の地域(オーストリア)向けに最適化されています および
これを使用して、スケーリングされた地理コードをベーステーブル、理想的には生成された列に追加します。 この回答で概説されているように:
年を無視する日付計算をどのように行いますか?
3を参照してください。黒魔術バージョン ここでプロセスを説明します。
次に、関数をさらに単純化できます。入力値を1回スケーリングし、冗長な計算を削除します。