sql >> Base de Datos >  >> RDS >> PostgreSQL

¿Cómo puedo obtener resultados de una entidad JPA ordenados por distancia?

Esta es una versión muy simplificada de una función que uso en una aplicación que se creó hace unos 3 años. Adaptado a la pregunta en cuestión.

  • Encuentra ubicaciones en el perímetro de un punto usando un cuadro . Se podría hacer esto con un círculo para obtener resultados más precisos, pero para empezar solo pretende ser una aproximación.

  • Ignora el hecho de que el mundo no es plano. Mi aplicación solo estaba destinada a una región local, de unos 100 kilómetros de ancho. Y el perímetro de búsqueda solo se extiende por unos pocos kilómetros. Hacer que el mundo sea plano es lo suficientemente bueno para este propósito. (Todo:una mejor aproximación para la relación latitud/longitud según la geolocalización podría ayudar).

  • Funciona con códigos geográficos como los que obtienes de Google Maps.

  • Funciona con PostgreSQL estándar sin extensión (no se requiere PostGis), probado en PostgreSQL 9.1 y 9.2.

Sin índice, habría que calcular la distancia para cada fila de la tabla base y filtrar las más cercanas. Extremadamente caro con mesas grandes.

Editar:
Volví a verificar y la implementación actual permite un índice GisT en puntos (Postgres 9.1 o posterior). Simplificó el código en consecuencia.

El truco principal es usar un índice GiST funcional de cajas , aunque la columna es solo un punto. Esto hace posible usar la implementación de GiST existente. .

Con una búsqueda tan (muy rápida), podemos obtener todas las ubicaciones dentro de un cuadro. El problema restante:sabemos el número de filas, pero no sabemos el tamaño del cuadro en el que se encuentran. Eso es como saber parte de la respuesta, pero no la pregunta.

Yo uso una búsqueda inversa similar enfoque al descrito con más detalle en esta respuesta relacionada en dba.SE . (Solo que no estoy usando índices parciales aquí, en realidad también podría funcionar).

Iterar a través de una serie de pasos de búsqueda predefinidos, desde muy pequeños hasta "lo suficientemente grandes para contener al menos suficientes ubicaciones". Significa que tenemos que ejecutar un par de consultas (muy rápidas) para llegar al tamaño del cuadro de búsqueda.

Luego busque en la tabla base con este cuadro y calcule la distancia real solo para las pocas filas devueltas del índice. Por lo general, habrá algún excedente ya que encontramos que la caja contiene al menos suficientes ubicaciones. Al tomar los más cercanos, efectivamente redondeamos las esquinas de la caja. Puede forzar este efecto haciendo que la caja sea un poco más grande (multiplique radius en la función de sqrt(2) para ser completamente preciso resultados, pero no lo haría todo, ya que esto es aproximado para empezar).

Esto sería aún más rápido y sencillo con un SP GiST index, disponible en la última versión de PostgreSQL. Pero no sé si eso es posible todavía. Necesitaríamos una implementación real para el tipo de datos y no tuve tiempo de profundizar en ello. Si encuentras la manera, ¡prométeme volver a informar!

Dada esta tabla simplificada con algunos valores de ejemplo (adr .. dirección):

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

El índice se ve así:

CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);

-> SQLfiddle

Tendrás que ajustar el área de la casa, los pasos y el factor de escala a tus necesidades. Mientras busque en cuadros de unos pocos kilómetros alrededor de un punto, una tierra plana es una aproximación lo suficientemente buena.

Necesita comprender bien plpgsql para trabajar con esto. Siento que ya he hecho bastante aquí.

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;

Llamar:

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

Devuelve una lista de $3 ubicaciones, si hay suficientes en el área de búsqueda máxima definida.
Ordenados por distancia real.

Más mejoras

Cree una función como:

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

Las (literalmente) constantes globales 111200 y 111400 están optimizados para mi área (Austria) desde la Longitud de un grado de longitud y La longitud de un grado de latitud , pero básicamente solo funciona en todo el mundo.

Úselo para agregar un código geográfico escalado a la tabla base, idealmente una columna generada como se describe en esta respuesta:
¿Cómo se hacen las matemáticas de fechas que ignoran el año?
Consulte 3. Versión de magia negra donde lo guío a través del proceso.
Luego puede simplificar la función un poco más:Escale los valores de entrada una vez y elimine los cálculos redundantes.