PostgreSQL
 sql >> Cơ Sở Dữ Liệu >  >> RDS >> PostgreSQL

Làm cách nào để nhận kết quả từ một thực thể JPA được sắp xếp theo khoảng cách?

Đây là phiên bản được đơn giản hóa phần lớn của một chức năng mà tôi sử dụng trong một ứng dụng được xây dựng cách đây khoảng 3 năm. Thích ứng với câu hỏi hiện tại.

  • Tìm các vị trí trong chu vi của một điểm bằng cách sử dụng hộp . Người ta có thể làm điều này với một vòng tròn để có được kết quả chính xác hơn, nhưng điều này chỉ có nghĩa là gần đúng để bắt đầu.

  • Bỏ qua sự thật rằng thế giới không phẳng. Ứng dụng của tôi chỉ dành cho một vùng địa phương, rộng vài 100 km. Và chu vi tìm kiếm chỉ trải dài vài km. Làm cho thế giới phẳng là đủ tốt cho mục đích này. (Todo:Một ước lượng tốt hơn cho tỷ lệ vĩ độ / kinh độ tùy thuộc vào vị trí địa lý có thể hữu ích.)

  • Hoạt động với mã địa lý giống như bạn lấy từ bản đồ của Google.

  • Hoạt động với PostgreSQL tiêu chuẩn không có phần mở rộng (không yêu cầu PostGis), được thử nghiệm trên PostgreSQL 9.1 và 9.2.

Nếu không có chỉ mục, người ta sẽ phải tính toán khoảng cách cho mọi hàng trong bảng cơ sở và lọc những hàng gần nhất. Cực kỳ đắt với những chiếc bàn lớn.

Chỉnh sửa:
Tôi đã kiểm tra lại và việc triển khai hiện tại cho phép chỉ số GisT trên điểm (Postgres 9.1 trở lên). Đơn giản hóa mã cho phù hợp.

Thủ thuật chính là sử dụng chỉ mục GiST chức năng của hộp , mặc dù cột chỉ là một điểm. Điều này giúp bạn có thể sử dụng triển khai GiST hiện có .

Với tìm kiếm (rất nhanh) như vậy, chúng tôi có thể nhận được tất cả các vị trí bên trong một hộp. Vấn đề còn lại:chúng ta biết số lượng hàng, nhưng chúng ta không biết kích thước của hộp mà chúng đang ở trong. Điều đó giống như biết một phần của câu trả lời, nhưng không phải câu hỏi.

Tôi sử dụng tra cứu ngược tương tự cách tiếp cận được mô tả chi tiết hơn trong câu trả lời có liên quan này trên dba.SE . (Chỉ có điều, tôi không sử dụng các chỉ mục từng phần ở đây - cũng có thể thực sự hoạt động).

Lặp lại thông qua một loạt các bước tìm kiếm được xác định trước, từ rất nhỏ đến "chỉ đủ lớn để chứa ít nhất đủ vị trí". Có nghĩa là chúng tôi phải chạy một vài truy vấn (rất nhanh) để có được kích thước cho hộp tìm kiếm.

Sau đó, tìm kiếm bảng cơ sở với hộp này và tính toán khoảng cách thực tế chỉ cho một vài hàng được trả về từ chỉ mục. Thường sẽ có một số thặng dư vì chúng tôi tìm thấy hộp chứa ít nhất đủ các địa điểm. Bằng cách lấy những cái gần nhất, chúng tôi làm tròn các góc của hộp một cách hiệu quả. Bạn có thể tạo hiệu ứng này bằng cách làm cho hộp lớn hơn một khía (nhân radius trong hàm bởi sqrt (2) để hoàn toàn chính xác kết quả, nhưng tôi sẽ không nói hết, vì điều này gần như là bắt đầu).

Điều này thậm chí sẽ nhanh hơn và đơn giản hơn với SP GiST chỉ mục, có sẵn trong phiên bản PostgreSQL mới nhất. Nhưng tôi không biết liệu điều đó có khả thi không. Chúng tôi cần một triển khai thực tế cho loại dữ liệu và tôi không có thời gian để đi sâu vào nó. Nếu bạn tìm ra cách, xin hứa sẽ báo lại!

Cung cấp bảng đơn giản này với một số giá trị mẫu (adr .. địa chỉ):

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

Chỉ mục trông như thế này:

CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);

-> SQLfiddle

Bạn sẽ phải điều chỉnh khu vực nhà, các bước và hệ số tỷ lệ theo nhu cầu của bạn. Miễn là bạn tìm kiếm trong các ô có kích thước vài km xung quanh một điểm, thì trái đất phẳng là một giá trị gần đúng đủ tốt.

Bạn cần hiểu rõ về plpgsql để làm việc với điều này. Tôi cảm thấy mình đã làm khá đủ ở đây.

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;

Gọi:

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

Trả về danh sách $3 các vị trí, nếu có đủ trong khu vực tìm kiếm tối đa đã xác định.
Được sắp xếp theo khoảng cách thực tế.

Cải tiến hơn nữa

Xây dựng một chức năng như:

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

Hằng số toàn cầu (theo nghĩa đen) 111200111400 được tối ưu hóa cho khu vực của tôi (Áo) từ Độ dài của kinh độ Độ dài của vĩ độ , nhưng về cơ bản chỉ hoạt động trên toàn thế giới.

Sử dụng mã này để thêm mã địa lý được chia tỷ lệ vào bảng cơ sở, lý tưởng nhất là cột được tạo như được nêu trong câu trả lời này:
Bạn thực hiện phép toán ngày tháng bỏ qua năm như thế nào?
Tham khảo 3. Phiên bản ma thuật đen nơi tôi sẽ hướng dẫn bạn toàn bộ quy trình.
Sau đó, bạn có thể đơn giản hóa hàm một số chi tiết nữa:Chia tỷ lệ các giá trị đầu vào một lần và loại bỏ các phép tính thừa.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Tổng hợp các hàm trên nhiều bảng đã kết hợp

  2. F # Kết nối SQLProvider với Postgres

  3. Chế độ xem PostgreSQL:Tham chiếu một trường được tính toán trong một trường được tính toán khác

  4. Truy xuất N bản ghi đầu tiên của mảng JSON bằng truy vấn Postgresql

  5. Nhiều bản ghi không mong muốn trong Nhóm theo mệnh đề trong Hậu kỳ