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


227

現在、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


31
あなたが与える公式は一定である要素がたくさんあるようです。データを事前に計算し、それらの値をDBに格納することは可能ですか?たとえば、3959 * acos(cos(radians(42.290763))は定数ですが、4つの主要な計算が含まれています。代わりに6696.7837を格納できますか?
Peter M

1
または、少なくともクエリの外で定数を事前計算しますか?これにより、実行する必要がある作業が削減されます。
Peter M

2
@Peter MそれなりのSQLデータベースは1回だけ計算されるように最適化されるようです。
mhenry1384 2012年

25
不思議に思う人のために、42.290763は緯度であり、-71.35368は距離を計算するポイントの経度です。
user276648 2013年

14
参考までに、この式で計算された距離はキロメートルではなくマイル単位です。結果をキロメートルで表示するには、3959を6371に置き換えてください
Sahil

回答:


115
  • テーブルPointGeometryデータ型の値を使用してポイントを作成しますMyISAMMysql 5.7.5以降、InnoDBテーブルもSPATIALインデックスをサポートするようになりました。

  • SPATIALこれらのポイントにインデックスを作成します

  • MBRContains()値を見つけるために使用します。

    SELECT  *
    FROM    table
    WHERE   MBRContains(LineFromText(CONCAT(
            '('
            , @lon + 10 / ( 111.1 / cos(RADIANS(@lon)))
            , ' '
            , @lat + 10 / 111.1
            , ','
            , @lon - 10 / ( 111.1 / cos(RADIANS(@lat)))
            , ' '
            , @lat - 10 / 111.1 
            , ')' )
            ,mypoint)

、または、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)

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

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

  • 大きな円の距離を説明するために、追加の細かいフィルタリングを適用する可能性があります(距離が長い場合)


15
@Quassnoi:いくつかの修正:座標の順序を緯度、経度に切り替えるとよいでしょう。また、経度距離は経度ではなく、緯度の余弦に比例します。そして、あなたはとして修正されるだろう、あなたが最初の座標ので、部門に乗算し、それを変更したいと思う@lon - 10 / ( 111.1 / cos(@lat))すべてが正しかった後に(とペアの2番目のこと。
M.デイヴAuayan

8
警告:回答の本文は、@ Mによる非常に有効なコメントに合わせて編集されていません。Dave Auayan。詳細注記:対象の円に(a)極が含まれている、または(b)経度の+/- 180度子午線が交差している場合、このメソッドは梨型になります。また、cos(lon)距離が短い場合にのみ正確に使用できます。janmatuschek.de/LatitudeLongitudeBoundingCoordinates
John Machinを

3
定数(10、111.11、@ lat、@ lon、mypoint)が何を表しているかを理解する方法はありますか?10はキロメートルの距離を表すと想定し、@ latと@lonは提供された緯度と経度を表しますが、この例では111.11とmypointは何を表していますか?
ashays、2011年

4
@ashays:111.(1)ある程度の緯度はおよそkmです。mypoint座標を格納するテーブルのフィールドです。
Quassnoi

1
別のエラー修正
-2

100

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

実際に行っているのは、テーブル内のすべてのポイントまでの距離を計算して、それが特定のポイントから10ユニット以内にあるかどうかを確認することです。

このsqlを実行する前にできることは、ポイントを中心にして、側面に20ユニットのボックスを描画する4つのポイントを作成することです。(x1、y1)。。。(x4、y4)、ここで(x1、y1)は(givenlong + 10 units、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)を宣言し、「メイン」のselectステートメントの前にそれらを計算するSQLステートメントを作成できることを知っています。 MySql。ただし、C#で4つのポイントを作成し、それらをパラメーターとしてSQLクエリに渡す傾向があります。

申し訳ありませんがこれ以上のサポートはできません。MySQLとPhpの特定の部分に誰かが回答できる場合は、この回答を編集して自由に回答してください。


4
このアプローチのmysqlプロシージャは、このプレゼンテーションにあります。scribd.com
Lucia

37
キロメートルの代わりに、マイルで検索するには、6371.で3959を置き換える
ErichBSchulz

4
+1、素晴らしいオプション。ボックスを追加すると、クエリが平均4秒から0.03秒に減少しました。
jvenema 2013

1
論理的すぎるようですが、このソリューションの賞を予約してください!200万件のレコードデータベースでは、クエリは16秒から0.06秒になりました。 注:クエリから距離の計算を切り取り、プログラムコードで距離の計算を行うと、より高速になります(大きなテーブルの場合)。
NLAnaconda 2014年

2
@Binary Worrier:したがって、X1、X2、Y1、Y2は、以下に示す例のように、経度の最小値と最大値、および緯度の最小値と最大値になります。blog.fedecarg.com / 2009/02/08 / アドバイスしてください。
Prabhat

14

次の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 ;

使用例:

placesフィールドlatitude&で呼び出されたテーブルを想定しますlongitude

SELECT get_distance_in_miles_between_geo_locations(-34.017330, 22.809500,
latitude, longitude) AS distance_from_input FROM places;

私はこれを試してみましたが完全に機能しますが、どういうわけか、distance_from_inputに基づいてWHEREステートメントを入れることができません。何か考えないのはなぜですか?
Chris Visser、2013年

あなたはそれを副選択として行うことができます:select * from(...)as t where distance_from_input> 5;
ブラッドパークス

2
または単に次のように直進します:get * distance_in_miles_between_geo_locations(-34.017330、22.809500、緯度、経度)> 5000である場所から*を選択します。
Brad Parks

2
リターンメーター:SELECT ROUND(((ACOS(SIN(lat1 * PI() / 180) * SIN(lat2 * PI() / 180) + COS(lat1 * PI() / 180) * COS(lat2 * PI() / 180) * COS((lnt1 - lnt2) * PI() / 180)) * 180 / PI()) * 60 * 1.1515) * 1.609344 * 1000) AS distance
Mohammad

13

同様の問題(単一ポイントからの距離で行をフィルタリングする)を解決する必要があり、元の質問と回答やコメントを組み合わせることで、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タイプのフィールドでPOINTSPATIALインデックス
6371があり、距離をキロメートル単位で計算します。
56.946285中心点の緯度は中心点の
24.105078経度です。
15はキロメートルで最大距離です

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

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

地図

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


これは十分に賛成できません。約500万レコードのテーブルとこの方法で空間インデックスを検索すると、古いA8プロセッサでの検索時間は0.005秒です。6371を3959に置き換えてマイル単位で結果を得ることができることは知っていますが、111.133と111.320の値を調整する必要がありますか、それとも普遍的に一定ですか?
Wranorn

素晴らしいソリューション。
SeaBiscuit

それはPOINT(LAT、LNG)またはPOINT(LNG、緯度)であるポイントを作成する方法
user606669

2
@ user606669それのPOINT(LNG、緯度)
マリスKiseļovs

現在、X()およびY()関数はST_YおよびST_Xである必要があります。
Andreas

11

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

1
これは非常によく、読みやすい代替手段です。POINT()へのパラメーターの順序は(lng、lat)であることに注意してください。そうしないと、 "close"になる可能性がありますが、他のメソッドとは結果が大きく異なります。参照:stackoverflow.com/questions/35939853/...
アンディP

9
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のポイント間の距離計算クエリです。長いデータベースで使用しましたが、完璧に機能します!注:必要に応じて変更(データベース名、テーブル名、列など)を実行します。


値1.1515は何を表していますか?以前に同様の数式を確認しましたが、1.1515ではなく1.75を使用しました。
TryHarder 2016

1
私自身の質問への回答では、答えはここにあると思いますstackoverflow.com/a/389251/691053
TryHarder

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

ソース


11
ソースを引用してください。これはからです:blog.fedecarg.com/2009/02/08/...
redburn

この場合69は何ですか?地球半径がある場合はどうすればいいですか?
CodeRunner 2017

2
1 Latittudeのキロメートルは111 KMです。1 Latittudeのマイルは69マイルです。69マイル= 111 KM。これが、変換でパラメーターを使用した理由です。
CodeRunner 2017

ずっと探していました。それがそんなに簡単なことかもしれないとは知りませんでした。どうもありがとうございます。
Vikas 2017年

lng_min / lng_maxは半径計算でlat_minとlat_maxを使用する必要があるため、これは正しくないでしょうか?
ベン、

6
   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

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`

4

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 km未満の距離ですべてのレコードをフェッチします。または:

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;
}

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

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

私はこれに2つの間違いに気づきました:

  1. absp8の選択ステートメントでの使用。省略absしただけでうまくいきました。

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


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

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/

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.