sql >> Databasteknik >  >> RDS >> PostgreSQL

Hur kan jag få resultat från en JPA-enhet sorterad efter avstånd?

Detta är en till stor del förenklad version av en funktion som jag använder i en app som byggdes för cirka 3 år sedan. Anpassad till den aktuella frågan.

  • Hittar platser i omkretsen av en punkt med hjälp av en ruta . Man skulle kunna göra detta med en cirkel för att få mer exakta resultat, men det här är bara tänkt att vara en uppskattning till att börja med.

  • Struntar i att världen inte är platt. Min ansökan var endast avsedd för en lokal region, några 100 kilometer tvärs över. Och sökomkretsen sträcker sig bara över några kilometer. Att göra världen platt är tillräckligt bra för ändamålet. (Att göra:En bättre uppskattning av förhållandet lat/lon beroende på geografisk plats kan hjälpa.)

  • Fungerar med geokoder som du får från Google maps.

  • Fungerar med standard PostgreSQL utan tillägg (ingen PostGis krävs), testad på PostgreSQL 9.1 och 9.2.

Utan index skulle man behöva beräkna avståndet för varje rad i bastabellen och filtrera de närmaste. Extremt dyrt med stora bord.

Redigera:
Jag kontrollerade igen och den nuvarande implementeringen tillåter ett GisT-index på poäng (Postgres 9.1 eller senare). Förenklade koden i enlighet med detta.

Det stora tricket är att använda ett funktionellt GiST-index av boxar , även om kolumnen bara är en poäng. Detta gör det möjligt att använda den befintliga GiST-implementeringen .

Med en sådan (mycket snabb) sökning kan vi få alla platser i en ruta. Det återstående problemet:vi vet antalet rader, men vi vet inte storleken på lådan de är i. Det är som att veta en del av svaret, men inte frågan.

Jag använder en liknande omvänd sökning tillvägagångssätt till den som beskrivs mer i detalj i det här relaterade svaret på dba.SE . (Bara, jag använder inte partiella index här - kan faktiskt fungera också).

Iterera genom en rad fördefinierade söksteg, från mycket små upp till "precis tillräckligt stora för att hålla åtminstone tillräckligt med platser". Betyder att vi måste köra ett par (mycket snabba) frågor för att komma till storleken för sökrutan.

Sök sedan i bastabellen med denna ruta och beräkna det faktiska avståndet för endast de få rader som returneras från indexet. Det brukar bli lite överskott eftersom vi hittade lådan som rymmer minst tillräckligt många platser. Genom att ta de närmaste rundar vi effektivt lådans hörn. Du kan tvinga fram denna effekt genom att göra rutan ett snäpp större (multiplicera radius i funktionen av sqrt(2) för att bli helt exakt resultat, men jag skulle inte gå ut helt, eftersom detta är ungefärligt till att börja med).

Detta skulle vara ännu snabbare och enklare med en SP GiST index, tillgängligt i den senaste versionen av PostgreSQL. Men jag vet inte om det är möjligt än. Vi skulle behöva en faktisk implementering för datatypen och jag hade inte tid att dyka in i det. Om du hittar ett sätt, lova att rapportera tillbaka!

Med tanke på denna förenklade tabell med några exempelvärden (adr .. adress):

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

Indexet ser ut så här:

CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);

-> SQLfiddle

Du måste anpassa hemområdet, stegen och skalningsfaktorn efter dina behov. Så länge du söker i rutor på några kilometer runt en punkt, är en platt jord en tillräckligt bra uppskattning.

Du måste förstå plpgsql väl för att arbeta med detta. Jag känner att jag har gjort tillräckligt här.

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:

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

Returnerar en lista med $3 platser, om det finns tillräckligt många inom det definierade maximala sökområdet.
Sorterat efter faktiska avstånd.

Ytterligare förbättringar

Bygg 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 (bokstavligen) globala konstanterna 111200 och 111400 är optimerade för mitt område (Österrike) från Längden på en longitud och Längden på en latitudgrad , men i princip bara jobba över hela världen.

Använd den för att lägga till en skalad geokod till bastabellen, helst en genererad kolumn som beskrivs i det här svaret:
Hur gör du dateringsmatematik som ignorerar årtalet?
Se 3. Svart magisk version där jag leder dig genom processen.
Då kan du förenkla funktionen ytterligare:Skala ingångsvärden en gång och ta bort redundanta beräkningar.



  1. Fel vid körning av migrering:sqlalchemy.exc.CompileError:Postgresql ENUM-typ kräver ett namn

  2. Beräkna procent från SUM() i samma SELECT sql-fråga

  3. SQL Server Query timeout beroende på Where Clause

  4. Hur man får kortdagens namn från ett datum i MariaDB