sql >> データベース >  >> RDS >> PostgreSQL

距離順に並べられたJPAエンティティから結果を取得するにはどうすればよいですか?

    これは、約3年前に作成されたアプリで使用する関数の大幅に簡略化されたバージョンです。手元の質問に適応しました。

    • ボックスを使用して、ポイントの周囲の場所を検索します 。より正確な結果を得るために円でこれを行うこともできますが、これは最初の概算にすぎません。

    • 世界が平坦ではないという事実を無視します。私のアプリケーションは、直径数100kmのローカルリージョンのみを対象としていました。また、検索範囲は数キロメートルしかありません。この目的には、世界をフラットにするだけで十分です。 (Todo:地理的位置に応じて緯度/経度の比率をより適切に概算すると役立つ場合があります。)

    • Googleマップから取得するのと同じように地理コードで動作します。

    • 拡張なしの標準PostgreSQLで動作します (PostGisは必要ありません)、PostgreSQL9.1および9.2でテスト済み。

    インデックスがないと、ベーステーブルのすべての行の距離を計算し、最も近い行をフィルタリングする必要があります。大きなテーブルでは非常に高価です。

    編集:
    再確認したところ、現在の実装ではポイントのGisTインデックスが許可されています(Postgres 9.1以降)。それに応じてコードを簡略化しました。

    主なトリック ボックスの機能的なGiSTインデックスを使用することです。 、列は単なるポイントですが。これにより、既存のGiST実装 を使用できるようになります。 。

    このような(非常に高速な)検索を使用すると、ボックス内のすべての場所を取得できます。残りの問題:行数はわかっていますが、ボックスのサイズはわかりません。これは、答えの一部を知っているようなものですが、質問はわかりません。

    同様の逆ルックアップを使用します dba.SEに関するこの関連する回答 。 (ただし、ここでは部分インデックスを使用していません。実際に機能する可能性もあります)。

    非常に小さいものから「少なくとも十分な場所を保持するのに十分な大きさ」まで、事前定義された一連の検索ステップを繰り返します。つまり、検索ボックスのサイズに到達するには、いくつかの(非常に高速な)クエリを実行する必要があります。

    次に、このボックスを使用してベーステーブルを検索し、インデックスから返された数行のみの実際の距離を計算します。少なくともを保持しているボックスが見つかったため、通常はある程度の余剰があります。 十分な場所。最も近いものを取ることによって、私たちは効果的に箱の角を丸めます。ボックスを1ノッチ大きくすることで、この効果を強制できます(radiusを乗算します)。 sqrt(2)による関数で、完全に正確を取得します。 結果ですが、これはそもそも概算であるため、私はすべてを尽くすことはしません。

    これは、 SP GiST を使用すると、さらに高速で簡単になります。 PostgreSQLの最新バージョンで利用可能なインデックス。しかし、それが可能かどうかはまだわかりません。データ型の実際の実装が必要でしたが、それに飛び込む時間がありませんでした。方法を見つけたら、報告することを約束してください!

    いくつかの値の例(adr ..アドレス):

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

    インデックスは次のようになります:

    CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);
    

    -> SQLfiddle

    必要に応じて、ホームエリア、ステップ、スケーリング係数を調整する必要があります。ポイントの周囲数キロメートルのボックスを検索する限り、平らな地球で十分です。

    これを使用するには、plpgsqlをよく理解する必要があります。ここでは十分にやり遂げたと思います。

    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;
    

    電話:

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

    $3のリストを返します 定義された最大検索領域に十分な場所がある場合は、場所。
    実際の距離で並べ替えられます。

    さらなる改善

    次のような関数を作成します:

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

    (文字通り)グローバル定数111200 および111400 経度の長さ から私の地域(オーストリア)向けに最適化されています および緯度の長さ 、しかし基本的には世界中で機能します。

    これを使用して、スケーリングされた地理コードをベーステーブル、理想的には生成された列に追加します。 この回答で概説されているように:
    年を無視する日付計算をどのように行いますか?
    3を参照してください。黒魔術バージョン ここでプロセスを説明します。
    次に、関数をさらに単純化できます。入力値を1回スケーリングし、冗長な計算を削除します。



    1. JPAを使用したSpringのカスタムクエリ

    2. DBに最後に挿入された行の値

    3. PDOを使用した複数のデータベース

    4. JSON_MODIFY()SQL Server(T-SQL)の例