sql >> Databáze >  >> RDS >> PostgreSQL

Jak mohu získat výsledky z entity JPA seřazené podle vzdálenosti?

Toto je do značné míry zjednodušená verze funkce, kterou používám v aplikaci vytvořené asi před 3 lety. Přizpůsobeno aktuální otázce.

  • Vyhledá umístění v obvodu bodu pomocí boxu . Dalo by se to udělat pomocí kruhu, abyste získali přesnější výsledky, ale to je pro začátek pouze přibližné.

  • Ignoruje skutečnost, že svět není plochý. Moje žádost byla určena pouze pro místní region o průměru několika 100 kilometrů. A hledaný obvod má jen několik kilometrů napříč. Udělat svět plochým je pro tento účel dost dobré. (Úkol:Může pomoci lepší přiblížení poměru šířky/délky v závislosti na geolokaci.)

  • Funguje s geokódy, které získáte z map Google.

  • Funguje se standardním PostgreSQL bez rozšíření (nevyžaduje se PostGis), testováno na PostgreSQL 9.1 a 9.2.

Bez indexu bychom museli vypočítat vzdálenost pro každý řádek v základní tabulce a filtrovat ty nejbližší. Extrémně drahé s velkými stoly.

Upravit:
Znovu jsem to zkontroloval a současná implementace umožňuje index GisT v bodech (Postgres 9.1 nebo novější). Zjednodušte odpovídajícím způsobem kód.

hlavní trik je použít funkční GiST index boxů , i když sloup je jen bod. Díky tomu je možné použít stávající implementaci GiST .

S takovým (velmi rychlým) vyhledáváním můžeme dostat všechna místa do krabice. Zbývající problém:známe počet řádků, ale neznáme velikost krabice, ve které jsou. To je jako znát část odpovědi, ale ne otázku.

Používám podobné zpětné vyhledávání přístup k tomu, který je podrobněji popsán v tato související odpověď na dba.SE . (Pouze zde nepoužívám částečné indexy – ve skutečnosti by také mohly fungovat).

Procházejte řadou předdefinovaných vyhledávacích kroků, od velmi malých až po „tak akorát velké, aby obsahovaly alespoň dostatek míst“. Znamená to, že musíme spustit několik (velmi rychlých) dotazů, abychom se dostali k velikosti vyhledávacího pole.

Poté pomocí tohoto pole prohledejte základní tabulku a vypočítejte skutečnou vzdálenost pouze pro několik řádků vrácených z indexu. Obvykle tam bude nějaký přebytek, protože jsme našli krabici obsahující nejméně dostatek míst. Tím, že vezmeme ty nejbližší, efektně zakulatíme rohy krabice. Tento efekt můžete vynutit tak, že rámeček uděláte o stupeň větší (vynásobte radius ve funkci pomocí sqrt(2), abyste byli zcela přesní výsledky, ale nešel bych do toho, protože to je pro začátek přibližné).

To by bylo ještě rychlejší a jednodušší s SP GiST index, dostupný v nejnovější verzi PostgreSQL. Ale nevím, jestli je to ještě možné. Potřebovali bychom skutečnou implementaci pro datový typ a neměl jsem čas se do toho ponořit. Pokud najdete způsob, slibte, že se ohlásíte!

Vzhledem k této zjednodušené tabulce s některými příklady hodnot (adr .. adresa):

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

Index vypadá takto:

CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);

-> SQLfiddle

Budete muset upravit oblast domova, kroky a měřítko podle svých potřeb. Dokud budete hledat v rámečcích o několika kilometrech kolem bodu, rovná země je dostatečně dobrá aproximace.

Abyste s tím mohli pracovat, musíte dobře rozumět plpgsql. Mám pocit, že jsem toho tady udělal docela dost.

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;

Volejte:

SELECT * FROM f_find_around (48.2, 16.3, 20);

Vrátí seznam $3 místa, pokud je jich dostatek v definované maximální prohledávané oblasti.
Seřazeno podle skutečné vzdálenosti.

Další vylepšení

Vytvořte funkci jako:

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

(doslova) globální konstanty 111200 a 111400 jsou optimalizovány pro mou oblast (Rakousko) z délky stupně zeměpisné délky a Délka stupně zeměpisné šířky , ale v podstatě jen pracovat po celém světě.

Použijte jej k přidání změněného geokódu do základní tabulky, ideálně vygenerovaného sloupce jak je uvedeno v této odpovědi:
Jak se dělá matematika data, která ignoruje rok?
Viz 3. Verze černé magie kde vás provedu celým procesem.
Pak můžete funkci ještě zjednodušit:Změňte jednou měřítko vstupních hodnot a odstraňte nadbytečné výpočty.



  1. Vložte ckeditor html kód do databáze

  2. Naplňte databázové tabulky velkým množstvím testovacích dat

  3. Počet řádků v Oracle SQL Select?

  4. Yii2:Kartik Gridview součet sloupce v zápatí