ヘルパーフィールドを座標テーブルに追加すると、クエリの応答時間を改善できます。
このような:
CREATE TABLE `Coordinates` (
`id` INT(10) UNSIGNED NOT NULL COMMENT 'id for the object',
`type` TINYINT(4) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'type',
`sin_lat` FLOAT NOT NULL COMMENT 'sin(lat) in radians',
`cos_cos` FLOAT NOT NULL COMMENT 'cos(lat)*cos(lon) in radians',
`cos_sin` FLOAT NOT NULL COMMENT 'cos(lat)*sin(lon) in radians',
`lat` FLOAT NOT NULL COMMENT 'latitude in degrees',
`lon` FLOAT NOT NULL COMMENT 'longitude in degrees',
INDEX `lat_lon_idx` (`lat`, `lon`)
)
TokuDBを使用している場合、たとえば次のように、どちらかの述語にクラスタリングインデックスを追加すると、パフォーマンスがさらに向上します。
alter table Coordinates add clustering index c_lat(lat);
alter table Coordinates add clustering index c_lon(lon);
各ポイントについて、基本的な緯度と経度(度単位)、およびsin(lat)(ラジアン単位)、cos(lat)* cos(lon)(ラジアン単位)およびcos(lat)* sin(lon)(ラジアン単位)が必要です。次に、次のようなmysql関数を作成します。
CREATE FUNCTION `geodistance`(`sin_lat1` FLOAT,
`cos_cos1` FLOAT, `cos_sin1` FLOAT,
`sin_lat2` FLOAT,
`cos_cos2` FLOAT, `cos_sin2` FLOAT)
RETURNS float
LANGUAGE SQL
DETERMINISTIC
CONTAINS SQL
SQL SECURITY INVOKER
BEGIN
RETURN acos(sin_lat1*sin_lat2 + cos_cos1*cos_cos2 + cos_sin1*cos_sin2);
END
これはあなたに距離を与えます。
lat / lonにインデックスを追加することを忘れないでください。そうすることで、境界ボクシングが検索を遅くするのではなく、検索に役立つようになります(インデックスは上記のCREATE TABLEクエリですでに追加されています)。
INDEX `lat_lon_idx` (`lat`, `lon`)
緯度/経度座標のみの古いテーブルがある場合、次のようにスクリプトを更新して更新できます:(phpはmeekrodbを使用)
$users = DB::query('SELECT id,lat,lon FROM Old_Coordinates');
foreach ($users as $user)
{
$lat_rad = deg2rad($user['lat']);
$lon_rad = deg2rad($user['lon']);
DB::replace('Coordinates', array(
'object_id' => $user['id'],
'object_type' => 0,
'sin_lat' => sin($lat_rad),
'cos_cos' => cos($lat_rad)*cos($lon_rad),
'cos_sin' => cos($lat_rad)*sin($lon_rad),
'lat' => $user['lat'],
'lon' => $user['lon']
));
}
次に、実際のクエリを最適化して、本当に必要なときにのみ距離計算を実行します。たとえば、内側と外側から円(楕円形)の境界を設定します。そのためには、クエリ自体のいくつかのメトリックを事前計算する必要があります。
// assuming the search center coordinates are $lat and $lon in degrees
// and radius in km is given in $distance
$lat_rad = deg2rad($lat);
$lon_rad = deg2rad($lon);
$R = 6371; // earth's radius, km
$distance_rad = $distance/$R;
$distance_rad_plus = $distance_rad * 1.06; // ovality error for outer bounding box
$dist_deg_lat = rad2deg($distance_rad_plus); //outer bounding box
$dist_deg_lon = rad2deg($distance_rad_plus/cos(deg2rad($lat)));
$dist_deg_lat_small = rad2deg($distance_rad/sqrt(2)); //inner bounding box
$dist_deg_lon_small = rad2deg($distance_rad/cos(deg2rad($lat))/sqrt(2));
これらの準備を考えると、クエリは次のようになります(php)。
$neighbors = DB::query("SELECT id, type, lat, lon,
geodistance(sin_lat,cos_cos,cos_sin,%d,%d,%d) as distance
FROM Coordinates WHERE
lat BETWEEN %d AND %d AND lon BETWEEN %d AND %d
HAVING (lat BETWEEN %d AND %d AND lon BETWEEN %d AND %d) OR distance <= %d",
// center radian values: sin_lat, cos_cos, cos_sin
sin($lat_rad),cos($lat_rad)*cos($lon_rad),cos($lat_rad)*sin($lon_rad),
// min_lat, max_lat, min_lon, max_lon for the outside box
$lat-$dist_deg_lat,$lat+$dist_deg_lat,
$lon-$dist_deg_lon,$lon+$dist_deg_lon,
// min_lat, max_lat, min_lon, max_lon for the inside box
$lat-$dist_deg_lat_small,$lat+$dist_deg_lat_small,
$lon-$dist_deg_lon_small,$lon+$dist_deg_lon_small,
// distance in radians
$distance_rad);
上記のクエリのEXPLAINは、それをトリガーするのに十分な結果がない限り、インデックスを使用していないと言っている可能性があります。インデックスは、座標テーブルに十分なデータがある場合に使用されます。SELECTにFORCE INDEX(lat_lon_idx)を追加して、テーブルサイズに関係なくインデックスを使用できるようにすることができるため、EXPLAINを使用して、正しく機能していることを確認できます。
上記のコードサンプルを使用すると、最小限のエラーで距離によるオブジェクト検索の実用的でスケーラブルな実装が必要です。