Ini adalah versi yang sebagian besar disederhanakan dari fungsi yang saya gunakan dalam aplikasi yang dibuat sekitar 3 tahun yang lalu. Disesuaikan dengan pertanyaan yang ada.
-
Menemukan lokasi di keliling suatu titik menggunakan kotak . Seseorang dapat melakukan ini dengan lingkaran untuk mendapatkan hasil yang lebih akurat, tetapi ini hanya dimaksudkan sebagai perkiraan untuk memulai.
-
Mengabaikan fakta bahwa dunia tidak datar. Aplikasi saya hanya ditujukan untuk wilayah lokal, beberapa 100 kilometer. Dan perimeter pencarian hanya membentang beberapa kilometer. Membuat dunia datar sudah cukup baik untuk tujuan itu. (Yang harus dilakukan:Perkiraan yang lebih baik untuk rasio lintang/bujur tergantung pada geolokasi mungkin bisa membantu.)
-
Beroperasi dengan geocode seperti yang Anda dapatkan dari Google maps.
-
Bekerja dengan PostgreSQL standar tanpa ekstensi (tidak diperlukan PostGis), diuji pada PostgreSQL 9.1 dan 9.2.
Tanpa indeks, seseorang harus menghitung jarak untuk setiap baris di tabel dasar dan memfilter yang terdekat. Sangat mahal dengan meja besar.
Sunting:
Saya memeriksa ulang dan implementasi saat ini memungkinkan indeks GisT pada poin (Postgres 9.1 atau lebih baru). Sederhanakan kodenya.
Trik utama adalah menggunakan indeks GiST dari kotak functional fungsional , meskipun kolomnya hanya berupa titik. Hal ini memungkinkan untuk menggunakan implementasi GiST
yang ada .
Dengan pencarian (sangat cepat), kita bisa mendapatkan semua lokasi di dalam kotak. Masalah yang tersisa:kita tahu jumlah barisnya, tetapi kita tidak tahu ukuran kotaknya. Itu seperti mengetahui sebagian dari jawabannya, tetapi bukan pertanyaannya.
Saya menggunakan pencarian terbalik . yang serupa pendekatan yang dijelaskan secara lebih rinci di jawaban terkait ini di dba.SE . (Hanya saja, saya tidak menggunakan indeks parsial di sini - mungkin benar-benar berfungsi juga).
Ulangi melalui serangkaian langkah pencarian yang telah ditentukan sebelumnya, dari yang sangat kecil hingga "cukup besar untuk menampung setidaknya cukup lokasi". Berarti kita harus menjalankan beberapa kueri (sangat cepat) untuk mendapatkan ukuran kotak telusur.
Kemudian cari tabel dasar dengan kotak ini dan hitung jarak sebenarnya hanya untuk beberapa baris yang dikembalikan dari indeks. Biasanya akan ada kelebihan karena kami menemukan kotak itu berisi setidaknya lokasi yang cukup. Dengan mengambil yang terdekat, kami secara efektif membulatkan sudut kotak. Anda dapat memaksakan efek ini dengan membuat kotak sedikit lebih besar (kalikan radius
dalam fungsi dengan sqrt(2) untuk mendapatkan sepenuhnya akurat hasil, tapi saya tidak akan berusaha keras, karena ini adalah perkiraan untuk memulai).
Ini akan menjadi lebih cepat dan lebih sederhana dengan SP GiST index, tersedia di PostgreSQL versi terbaru. Tapi saya belum tahu apakah itu mungkin. Kami membutuhkan implementasi aktual untuk tipe data dan saya tidak punya waktu untuk menyelaminya. Jika Anda menemukan cara, berjanji untuk melaporkan kembali!
Diberikan tabel yang disederhanakan ini dengan beberapa nilai contoh (adr
.. alamat):
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)');
Indeks terlihat seperti ini:
CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);
Anda harus menyesuaikan area rumah, langkah-langkah dan faktor penskalaan dengan kebutuhan Anda. Selama Anda mencari di kotak beberapa kilometer di sekitar suatu titik, bumi datar adalah perkiraan yang cukup baik.
Anda perlu memahami plpgsql dengan baik untuk bekerja dengan ini. Saya merasa saya telah melakukan cukup banyak di sini.
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;
Telepon:
SELECT * FROM f_find_around (48.2, 16.3, 20);
Mengembalikan daftar $3
lokasi, jika ada cukup dalam area pencarian maksimum yang ditentukan.
Diurutkan berdasarkan jarak sebenarnya.
Peningkatan lebih lanjut
Bangun fungsi seperti:
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) --';
Konstanta global (secara harfiah) 111200
dan 111400
dioptimalkan untuk wilayah saya (Austria) dari Panjang derajat bujur
dan Panjang derajat lintang
, tetapi pada dasarnya hanya berfungsi di seluruh dunia.
Gunakan untuk menambahkan geocode berskala ke tabel dasar, idealnya kolom yang dibuat seperti diuraikan dalam jawaban ini:
Bagaimana caramu menghitung tanggal yang mengabaikan tahun?
Lihat 3. Versi ilmu hitam di mana saya memandu Anda melalui prosesnya.
Kemudian Anda dapat menyederhanakan fungsi lagi:Skalakan nilai input sekali dan hapus perhitungan yang berlebihan.