「附近的人」功能通常分兩步:矩形邊界框初篩(利用經緯度範圍快速過濾)+ 精確球面距離計算(Haversine 公式)。這能在百萬級數據下保持毫秒級響應。
給定中心點 ($lat, $lng) 和半徑 $distance(千米),計算四個頂點座標:
define('EARTH_RADIUS', 6371); // 地球平均半徑 km
function getBoundingBox($lat, $lng, $distance) {
// 緯度跨度(度)
$dlat = rad2deg($distance / EARTH_RADIUS);
// 經度跨度(度),隨緯度變化
$dlng = rad2deg(2 * asin(sin($distance / (2 * EARTH_RADIUS)) / cos(deg2rad($lat))));
return [
'left_top' => ['lat' => $lat + $dlat, 'lng' => $lng - $dlng],
'right_top' => ['lat' => $lat + $dlat, 'lng' => $lng + $dlng],
'left_bottom' => ['lat' => $lat - $dlat, 'lng' => $lng - $dlng],
'right_bottom' => ['lat' => $lat - $dlat, 'lng' => $lng + $dlng],
];
}
假設表 users 有 lat, lng 字段(已建立聯合索引):
$box = getBoundingBox($centerLat, $centerLng, $radiusKm);
$sql = "SELECT * FROM `users`
WHERE `lat` BETWEEN {$box['left_bottom']['lat']} AND {$box['left_top']['lat']}
AND `lng` BETWEEN {$box['left_top']['lng']} AND {$box['right_top']['lng']}
AND `lat` != 0 AND `lng` != 0";
使用 Haversine 公式計算球面距離(單位:米):
function haversineDistance($lat1, $lng1, $lat2, $lng2) {
$lat1 = deg2rad($lat1); $lng1 = deg2rad($lng1);
$lat2 = deg2rad($lat2); $lng2 = deg2rad($lng2);
$dlat = $lat2 - $lat1;
$dlng = $lng2 - $lng1;
$a = sin($dlat/2)**2 + cos($lat1) * cos($lat2) * sin($dlng/2)**2;
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
return EARTH_RADIUS * $c * 1000; // 返回米
}
// 在 PHP 中對查詢結果二次排序
usort($results, function($a, $b) use ($centerLat, $centerLng) {
$da = haversineDistance($centerLat, $centerLng, $a['lat'], $a['lng']);
$db = haversineDistance($centerLat, $centerLng, $b['lat'], $b['lng']);
return $da <=> $db;
});
SPATIAL INDEX,配合 ST_Distance_Sphere() 可下推到數據庫層完成。GEOADD / GEORADIUS 實現毫秒級查詢,適合高併發場景。OFFSET 深分頁,改用游標式分頁(記住上次最大距離/ID)。class NearbySearch {
const EARTH_RADIUS = 6371000; // 米
public static function search(PDO $pdo, float $lat, float $lng, int $radius = 1000, int $limit = 20): array {
// 1. 邊界框
$dlat = rad2deg($radius / self::EARTH_RADIUS);
$dlng = rad2deg(2 * asin(sin($radius / (2 * self::EARTH_RADIUS)) / cos(deg2rad($lat))));
$minLat = $lat - $dlat; $maxLat = $lat + $dlat;
$minLng = $lng - $dlng; $maxLng = $lng + $dlng;
// 2. 粗篩 SQL
$sql = "SELECT id, lat, lng, name FROM users
WHERE lat BETWEEN ? AND ? AND lng BETWEEN ? AND ? AND lat != 0 LIMIT 500";
$stmt = $pdo->prepare($sql);
$stmt->execute([$minLat, $maxLat, $minLng, $maxLng]);
$candidates = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 3. 精確計算 + 排序
foreach ($candidates as &$user) {
$user['distance'] = self::haversine($lat, $lng, $user['lat'], $user['lng']);
}
usort($candidates, fn($a, $b) => $a['distance'] <=> $b['distance']);
return array_slice($candidates, 0, $limit);
}
private static function haversine($lat1, $lng1, $lat2, $lng2): float {
$lat1 = deg2rad($lat1); $lng1 = deg2rad($lng1);
$lat2 = deg2rad($lat2); $lng2 = deg2rad($lng2);
$dlat = $lat2 - $lat1; $dlng = $lng2 - $lng1;
$a = sin($dlat/2)**2 + cos($lat1)*cos($lat2)*sin($dlng/2)**2;
return 2 * atan2(sqrt($a), sqrt(1-$a)) * self::EARTH_RADIUS;
}
}