sql >> Database teknologi >  >> RDS >> PostgreSQL

Hvordan kan jeg få resultater fra en JPA-enhed ordnet efter afstand?

Dette er en stort set forenklet version af en funktion, jeg bruger i en app, der blev bygget for omkring 3 år siden. Tilpasset det aktuelle spørgsmål.

  • Finder steder i omkredsen af ​​et punkt ved hjælp af en boks . Man kunne gøre dette med en cirkel for at få mere præcise resultater, men dette er kun ment som en tilnærmelse til at begynde med.

  • Ignorerer det faktum, at verden ikke er flad. Min ansøgning var kun beregnet til en lokal region, et par 100 kilometer på tværs. Og søgeomkredsen strækker sig kun over et par kilometer på tværs. At gøre verden flad er godt nok til formålet. (Todo:En bedre tilnærmelse af forholdet lat/lon afhængigt af geoplaceringen kan hjælpe.)

  • Fungerer med geokoder, som du får fra Google maps.

  • Virker med standard PostgreSQL uden udvidelse (ingen PostGis påkrævet), testet på PostgreSQL 9.1 og 9.2.

Uden indeks ville man skulle beregne afstanden for hver række i basistabellen og filtrere de nærmeste. Ekstremt dyrt med store borde.

Rediger:
Jeg tjekkede igen, og den nuværende implementering tillader et GisT-indeks på point (Postgres 9.1 eller nyere). Forenklet koden i overensstemmelse hermed.

Det store trick er at bruge et funktionelt GiST-indeks over kasser , selvom klummen kun er en pointe. Dette gør det muligt at bruge den eksisterende GiST-implementering .

Med sådan en (meget hurtig) søgning kan vi få alle lokationer inde i en boks. Det resterende problem:vi kender antallet af rækker, men vi kender ikke størrelsen på den kasse, de er i. Det er som at kende en del af svaret, men ikke spørgsmålet.

Jeg bruger et lignende omvendt opslag tilgang til den, der er beskrevet mere detaljeret i dette relaterede svar på dba.SE . (Kun, jeg bruger ikke delvise indekser her - måske virker det også).

Gentag gennem en række foruddefinerede søgetrin, fra meget små op til "lige store nok til at rumme mindst nok placeringer". Betyder, at vi skal køre et par (meget hurtige) forespørgsler for at komme til størrelsen på søgefeltet.

Søg derefter i basistabellen med denne boks, og beregn den faktiske afstand for kun de få rækker, der returneres fra indekset. Der vil normalt være noget overskud, da vi fandt boksen med mindst nok steder. Ved at tage de nærmeste runder vi effektivt boksens hjørner. Du kan fremtvinge denne effekt ved at gøre boksen et hak større (multiplicer radius i funktionen af ​​sqrt(2) for at blive fuldstændig præcis resultater, men jeg vil ikke gå helt ud, da dette er omtrentligt til at begynde med).

Dette ville være endnu hurtigere og enklere med en SP GiST indeks, tilgængelig i den seneste version af PostgreSQL. Men jeg ved ikke om det er muligt endnu. Vi skulle have en faktisk implementering af datatypen, og jeg havde ikke tid til at dykke ned i det. Hvis du finder en måde, lover du at rapportere tilbage!

Givet denne forenklede tabel med nogle eksempelværdier (adr .. adresse):

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

Indekset ser således ud:

CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);

-> SQLfiddle

Du bliver nødt til at justere hjemmeområdet, trinene og skaleringsfaktoren til dine behov. Så længe du søger i kasser på et par kilometer omkring et punkt, er en flad jord en god nok tilnærmelse.

Du skal forstå plpgsql godt for at arbejde med dette. Jeg føler, jeg har gjort nok her.

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;

Ring til:

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

Returnerer en liste med $3 steder, hvis der er nok i det definerede maksimale søgeområde.
Sorteret efter faktisk afstand.

Yderligere forbedringer

Byg en funktion som:

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

De (bogstaveligt talt) globale konstanter 111200 og 111400 er optimeret til mit område (Østrig) fra Længde af en længdegrad og Længden af ​​en breddegrad , men i bund og grund bare arbejde over hele verden.

Brug den til at tilføje en skaleret geokode til basistabellen, ideelt set en genereret kolonne som beskrevet i dette svar:
Hvordan laver du dateringsmatematik, der ignorerer året?
Se 3. Sort magisk version hvor jeg leder dig gennem processen.
Så kan du forenkle funktionen noget mere:Skaler inputværdier én gang og fjern overflødige beregninger.



  1. Sådan nulstiller du AUTO_INCREMENT i MySQL

  2. mysql rækkefølge efter max matcher feltværdi

  3. SQL-forespørgsel - Slet dubletter, hvis der er mere end 3 duplikater?

  4. Hvordan ekko hver række fra mySQLi-forespørgsel ved hjælp af PHP?