web-dev-qa-db-ja.com

2つの緯度/経度ポイント間の距離を見つける最速の方法

私は現在、すべての経度と緯度の情報を含むmysqlデータベースに100万個未満の場所があります。

クエリを使用して、1つのポイントと他の多くのポイント間の距離を見つけようとしています。特に1秒あたり100回以上のヒットでは、私が望むほど速くありません。

このために、mysql以外のより高速なクエリまたはおそらくより高速なシステムがありますか?私はこのクエリを使用しています:

SELECT 
  name, 
   ( 3959 * acos( cos( radians(42.290763) ) * cos( radians( locations.lat ) ) 
   * cos( radians(locations.lng) - radians(-71.35368)) + sin(radians(42.290763)) 
   * sin( radians(locations.lat)))) AS distance 
FROM locations 
WHERE active = 1 
HAVING distance < 10 
ORDER BY distance;

注:指定された距離はマイルです。 キロメートルが必要な場合は、6371ではなく3959を使用します。

212
Ryan Detzel

または、MySQL 5.1以降で:

    SELECT  *
    FROM    table
    WHERE   MBRContains
                    (
                    LineString
                            (
                            Point (
                                    @lon + 10 / ( 111.1 / COS(RADIANS(@lat))),
                                    @lat + 10 / 111.1
                                  ),
                            Point (
                                    @lon - 10 / ( 111.1 / COS(RADIANS(@lat))),
                                    @lat - 10 / 111.1
                                  ) 
                            ),
                    mypoint
                    )

これにより、おおよそ(@lat +/- 10 km, @lon +/- 10km)内のすべてのポイントが選択されます。

これは実際にはボックスではなく、球形の長方形、つまり球の緯度と経度の境界セグメントです。これは、フランツジョセフランドの平たい長方形とは異なる場合がありますが、ほとんどの居住地ではかなり近いです。

  • 追加のフィルタリングを適用して、円内のすべて(正方形ではなく)を選択します

  • 大きな円距離(長い距離の場合)を考慮して、追加の微フィルタリングを適用する可能性があります

111
Quassnoi

MySql固有の答えではありませんが、SQLステートメントのパフォーマンスが向上します。

あなたが効果的にしているのは、テーブル内のすべてのポイントまでの距離を計算し、それが与えられたポイントから10単位以内にあるかどうかを確認することです。

このsqlを実行する前にできることは、側面に20単位のボックスを描く4つのポイントを作成し、そのポイントを中心に(つまり、(x1、y1))することです。 。 。 (x4、y4)、ここで(x1、y1)は(givenlong + 10単位、givenLat + 10units)です。 。 。 (givenLong-10ユニット、givenLat -10ユニット)。 実際には、左上と右下の2つのポイントだけが必要です(X1、Y1)および(X2、Y2)

現在、SQLステートメントはこれらのポイントを使用して、特定のポイントから10u以上の行を確実に除外します。緯度と経度のインデックスを使用できるため、現在よりも桁違いに高速になります。

例えば.

select . . . 
where locations.lat between X1 and X2 
and   locations.Long between y1 and y2;

ボックスアプローチは誤検知を返す可能性があるため(ボックスのコーナーで特定のポイントから10u以上離れたポイントを選択できます)、各ポイントの距離を計算する必要があります。ただし、テストするポイントの数をボックス内のポイントに大幅に制限しているため、これもはるかに高速になります。

私はこのテクニックを「箱の中を考える」と呼んでいます:)

編集:これを1つのSQLステートメントに入れることはできますか?

MySqlまたはPhpが何をすることができるのかわかりません、ごめんなさい。 4つのポイントを構築するのに最適な場所がどこにあるのか、またはそれらをPhpのmySqlクエリに渡す方法がわからない。ただし、4つのポイントを取得したら、独自のSQLステートメントを自分のSQLステートメントと組み合わせるのを止めることはできません。

select name, 
       ( 3959 * acos( cos( radians(42.290763) ) 
              * cos( radians( locations.lat ) ) 
              * cos( radians( locations.lng ) - radians(-71.35368) ) 
              + sin( radians(42.290763) ) 
              * sin( radians( locations.lat ) ) ) ) AS distance 
from locations 
where active = 1 
and locations.lat between X1 and X2 
and locations.Long between y1 and y2
having distance < 10 ORDER BY distance;

MS SQLを使用して、4つのフロート(X1、Y1、X2、Y2)を宣言し、「メイン」選択ステートメントの前にそれらを計算するSQLステートメントを作成できることを知っています。 MySql。しかし、私はまだC#で​​4つのポイントを構築し、それらをパラメーターとしてSQLクエリに渡したいと思っています。

申し訳ありませんが、MySQLとこの特定の部分に回答できる人がいたら、この回答を自由に編集してください。

96
Binary Worrier

良い答えについては、このプレゼンテーションを確認してください。基本的には、コメントに示されている2つの異なるアプローチを示し、どちらか一方を使用する理由とタイミング、および「ボックス内」計算が非常に興味深い理由についての詳細な説明を示します。

MySQLによるジオディスタンス検索

16
eillarra

このブログ投稿 で、次のMySql関数が投稿されました。あまりテストしていませんが、投稿から収集したものから 緯度と経度のフィールドにインデックスが付けられている場合 、これはあなたのためにうまくいくかもしれません:

DELIMITER $$

DROP FUNCTION IF EXISTS `get_distance_in_miles_between_geo_locations` $$
CREATE FUNCTION get_distance_in_miles_between_geo_locations(geo1_latitude decimal(10,6), geo1_longitude decimal(10,6), geo2_latitude decimal(10,6), geo2_longitude decimal(10,6)) 
returns decimal(10,3) DETERMINISTIC
BEGIN
  return ((ACOS(SIN(geo1_latitude * PI() / 180) * SIN(geo2_latitude * PI() / 180) + COS(geo1_latitude * PI() / 180) * COS(geo2_latitude * PI() / 180) * COS((geo1_longitude - geo2_longitude) * PI() / 180)) * 180 / PI()) * 60 * 1.1515);
END $$

DELIMITER ;

使用例:緯度と経度のフィールドを持つ場所と呼ばれるテーブルを想定:

場所からのdistance_from_inputとしてget_distance_in_miles_between_geo_locations(-34.017330、22.809500、緯度、経度)を選択します。

すべて この投稿から削除

13
Brad Parks
SELECT * FROM (SELECT *,(((acos(sin((43.6980168*pi()/180)) * 
sin((latitude*pi()/180))+cos((43.6980168*pi()/180)) * 
cos((latitude*pi()/180)) * cos(((7.266903899999988- longitude)* 
pi()/180))))*180/pi())*60*1.1515 ) as distance 
FROM wp_users WHERE 1 GROUP BY ID limit 0,10) as X 
ORDER BY ID DESC

これは、MySQLのポイント間の距離計算クエリです。長いデータベースで使用しましたが、完璧に機能しています。注:要件に従って変更(データベース名、テーブル名、列など)を行ってください。

9
Sanni Poriya
set @latitude=53.754842;
set @longitude=-2.708077;
set @radius=20;

set @lng_min = @longitude - @radius/abs(cos(radians(@latitude))*69);
set @lng_max = @longitude + @radius/abs(cos(radians(@latitude))*69);
set @lat_min = @latitude - (@radius/69);
set @lat_max = @latitude + (@radius/69);

SELECT * FROM postcode
WHERE (longitude BETWEEN @lng_min AND @lng_max)
AND (latitude BETWEEN @lat_min and @lat_max);

ソース

8
Abhigyan

mySQL 5.7。*を使用している場合は、st_distance_sphere(POINT、POINT)を使用できます。

Select st_distance_sphere(POINT(-2.997065, 53.404146 ), POINT(58.615349, 23.56676 ))/1000  as distcance
6
alriyami
   select
   (((acos(sin(('$latitude'*pi()/180)) * sin((`lat`*pi()/180))+cos(('$latitude'*pi()/180)) 
    * cos((`lat`*pi()/180)) * cos((('$longitude'- `lng`)*pi()/180))))*180/pi())*60*1.1515) 
    AS distance
    from table having distance<22;
5
user3113927

MySQLプラグインとしてインストールする方法の詳細を含む完全なコードは次のとおりです。 https://github.com/lucasepe/lib_mysqludf_haversine

昨年コメントとして投稿しました。親切に@TylerCollierが回答として投稿することを提案してくれたので、ここにあります。

別の方法は、2点からのヘイバーサイン距離を返すカスタムUDF関数を記述することです。この関数は入力を取り込むことができます。

lat1 (real), lng1 (real), lat2 (real), lng2 (real), type (string - optinal - 'km', 'ft', 'mi')

したがって、次のように記述できます。

SELECT id, name FROM MY_PLACES WHERE haversine_distance(lat1, lng1, lat2, lng2) < 40;

距離が40キロメートル未満のすべてのレコードを取得します。または:

SELECT id, name FROM MY_PLACES WHERE haversine_distance(lat1, lng1, lat2, lng2, 'ft') < 25;

距離が25フィート未満のすべてのレコードを取得します。

コア機能は次のとおりです。

double
haversine_distance( UDF_INIT* initid, UDF_ARGS* args, char* is_null, char *error ) {
    double result = *(double*) initid->ptr;
    /*Earth Radius in Kilometers.*/ 
    double R = 6372.797560856;
    double DEG_TO_RAD = M_PI/180.0;
    double RAD_TO_DEG = 180.0/M_PI;
    double lat1 = *(double*) args->args[0];
    double lon1 = *(double*) args->args[1];
    double lat2 = *(double*) args->args[2];
    double lon2 = *(double*) args->args[3];
    double dlon = (lon2 - lon1) * DEG_TO_RAD;
    double dlat = (lat2 - lat1) * DEG_TO_RAD;
    double a = pow(sin(dlat * 0.5),2) + 
        cos(lat1*DEG_TO_RAD) * cos(lat2*DEG_TO_RAD) * pow(sin(dlon * 0.5),2);
    double c = 2.0 * atan2(sqrt(a), sqrt(1-a));
    result = ( R * c );
    /*
     * If we have a 5th distance type argument...
     */
    if (args->arg_count == 5) {
        str_to_lowercase(args->args[4]);
        if (strcmp(args->args[4], "ft") == 0) result *= 3280.8399;
        if (strcmp(args->args[4], "mi") == 0) result *= 0.621371192;
    }

    return result;
}
4
Luca Sepe

同様の問題(単一ポイントからの距離で行をフィルタリング)を解決する必要があり、元の質問と回答およびコメントを組み合わせることで、MySQL 5.6と5.7の両方で完全に機能するソリューションを思い付きました。

SELECT 
    *,
    (6371 * ACOS(COS(RADIANS(56.946285)) * COS(RADIANS(Y(coordinates))) 
    * COS(RADIANS(X(coordinates)) - RADIANS(24.105078)) + SIN(RADIANS(56.946285))
    * SIN(RADIANS(Y(coordinates))))) AS distance
FROM places
WHERE MBRContains
    (
    LineString
        (
        Point (
            24.105078 + 15 / (111.320 * COS(RADIANS(56.946285))),
            56.946285 + 15 / 111.133
        ),
        Point (
            24.105078 - 15 / (111.320 * COS(RADIANS(56.946285))),
            56.946285 - 15 / 111.133
        )
    ),
    coordinates
    )
HAVING distance < 15
ORDER By distance

coordinatesはタイプPOINTのフィールドで、SPATIALインデックスがあります
6371は、キロメートル単位の距離を計算するためのものです
56.946285は中心点の緯度です
24.105078は中心点の経度です
10はキロメートル単位の最大距離です

私のテストでは、MySQLはcoordinatesフィールドでSPATIALインデックスを使用して、四角形内のすべての行をすばやく選択し、すべてのフィルターされた場所の実際の距離を計算して四角形の角から場所を除外し、円内の場所のみを残します。

これは私の結果の視覚化です:

map

灰色の星は地図上のすべてのポイントを視覚化し、黄色の星はMySQLクエリによって返されるものです。四角形の角の内側(ただし、円の外側)にある灰色の星は、MBRContains()によって選択され、HAVING句によって選択解除されました。

3

MySQLでのジオディスタンス検索 を読んでください。これは、MySQLへのHaversine Formulaの実装に基づくソリューションです。これは、理論、実装、およびパフォーマンスの最適化を含む完全なソリューションの説明です。私の場合、空間最適化部分は正しく機能しませんでしたが。

これに2つの間違いがあることに気付きました。

  1. p8のselectステートメントでのabsの使用。 absを省略しただけで機能しました。

  2. p27の空間検索距離関数は、ラジアンに変換したり、経度をcos(latitude)で乗算したりしません。ただし、空間データを考慮してロードしない限り(記事のコンテキストからはわかりません)、p26の例は、空間データPOINTがラジアンまたは度でロードされます。

3
Richard Sandoz

これは、MySQLへのHaversine Formulaの実装に基づくソリューションであるMySQLを使用したGeo Distance Searchの非常に詳細な説明です。理論、実装、およびパフォーマンスの最適化に関するソリューションの完全な説明。私の場合、空間最適化部分は正しく機能しませんでしたが。 http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL

3

球面投影 を使用すると、高速で単純かつ正確な(より短い距離の)近似を行うことができます。少なくとも私のルーティングアルゴリズムでは、正しい計算と比較して20%向上します。 Javaコードでは、次のようになります。

public double approxDistKm(double fromLat, double fromLon, double toLat, double toLon) {
    double dLat = Math.toRadians(toLat - fromLat);
    double dLon = Math.toRadians(toLon - fromLon);
    double tmp = Math.cos(Math.toRadians((fromLat + toLat) / 2)) * dLon;
    double d = dLat * dLat + tmp * tmp;
    return R * Math.sqrt(d);
}

MySQLについてはわかりません(ごめんなさい!)。

制限について必ず確認してください(assertEqualsの3番目のパラメーターは、キロメートル単位の精度を意味します)。

    float lat = 24.235f;
    float lon = 47.234f;
    CalcDistance dist = new CalcDistance();
    double res = 15.051;
    assertEquals(res, dist.calcDistKm(lat, lon, lat - 0.1, lon + 0.1), 1e-3);
    assertEquals(res, dist.approxDistKm(lat, lon, lat - 0.1, lon + 0.1), 1e-3);

    res = 150.748;
    assertEquals(res, dist.calcDistKm(lat, lon, lat - 1, lon + 1), 1e-3);
    assertEquals(res, dist.approxDistKm(lat, lon, lat - 1, lon + 1), 1e-2);

    res = 1527.919;
    assertEquals(res, dist.calcDistKm(lat, lon, lat - 10, lon + 10), 1e-3);
    assertEquals(res, dist.approxDistKm(lat, lon, lat - 10, lon + 10), 10);
3
Karussell

2つの座標間のメートル数を返すMySQL関数:

CREATE FUNCTION DISTANCE_BETWEEN (lat1 DOUBLE, lon1 DOUBLE, lat2 DOUBLE, lon2 DOUBLE)
RETURNS DOUBLE DETERMINISTIC
RETURN ACOS( SIN(lat1*PI()/180)*SIN(lat2*PI()/180) + COS(lat1*PI()/180)*COS(lat2*PI()/180)*COS(lon2*PI()/180-lon1*PI()/180) ) * 6371000

別の形式で値を返すには、関数の6371000を、選択した単位の地球の半径に置き換えます。たとえば、キロメートルは6371、マイルは3959になります。

関数を使用するには、MySQLの他の関数と同じように呼び出します。たとえば、テーブルcityがある場合、すべての都市から他のすべての都市までの距離を見つけることができます。

SELECT
    `city1`.`name`,
    `city2`.`name`,
    ROUND(DISTANCE_BETWEEN(`city1`.`latitude`, `city1`.`longitude`, `city2`.`latitude`, `city2`.`longitude`)) AS `distance`
FROM
    `city` AS `city1`
JOIN
    `city` AS `city2`
3
Robert

Mysqlを使用する

SET @orig_lon = 1.027125;
SET @dest_lon = 1.027125;

SET @orig_lat = 2.398441;
SET @dest_lat = 2.398441;

SET @kmormiles = 6371;-- for distance in miles set to : 3956

SELECT @kmormiles * ACOS(LEAST(COS(RADIANS(@orig_lat)) * 
 COS(RADIANS(@dest_lat)) * COS(RADIANS(@orig_lon - @dest_lon)) + 
 SIN(RADIANS(@orig_lat)) * SIN(RADIANS(@dest_lat)),1.0)) as distance;

参照: https://andrew.hedges.name/experiments/haversine/

参照: https://stackoverflow.com/a/24372831/5155484

参照: http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/

注:LEASTは、 https://stackoverflow.com/a/24372831/5155484 で提案されるコメントとしてnull値を避けるために使用されます

0
$objectQuery = "SELECT table_master.*, ((acos(sin((" . $latitude . "*pi()/180)) * sin((`latitude`*pi()/180))+cos((" . $latitude . "*pi()/180)) * cos((`latitude`*pi()/180)) * cos(((" . $longitude . "- `longtude`)* pi()/180))))*180/pi())*60*1.1515  as distance FROM `table_post_broadcasts` JOIN table_master ON table_post_broadcasts.master_id = table_master.id WHERE table_master.type_of_post ='type' HAVING distance <='" . $Radius . "' ORDER BY distance asc";
0
Neeraj Sharma