PostgreSQL
 sql >> Teknologi Basis Data >  >> RDS >> PostgreSQL

Bagaimana saya bisa mendapatkan hasil dari entitas JPA yang dipesan berdasarkan jarak?

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);

-> SQLfiddle

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.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. (var)char sebagai tipe kolom untuk kinerja?

  2. Aplikasi Boot Musim Semi macet di Hikari-Pool-1 - Memulai...

  3. Dialek perlu diberikan secara eksplisit pada v4.0.0

  4. fungsi yang berbeda () (tidak memilih kualifikasi) di postgres

  5. Beberapa bergabung ke tabel yang sama