MySQL 计算经纬度半径内最近商家,核心用 Haversine 公式(球面距离计算),结合索引优化提升查询效率,以下是完整实现方案:

一、核心公式(Haversine)

计算两点(用户经纬度 vs 商家经纬度)的球面距离(单位:公里):

6371 * 2 * ASIN(
    SQRT(
        POWER(SIN((用户纬度 - 商家纬度) * PI() / 180 / 2), 2) +
        COS(用户纬度 * PI() / 180) * COS(商家纬度 * PI() / 180) *
        POWER(SIN((用户经度 - 商家经度) * PI() / 180 / 2), 2)
    )
) AS distance
  • 6371 是地球平均半径(公里),需米则换为 6371000。
  • 经纬度需先转弧度( * PI() / 180 ),避免计算误差。

二、完整查询示例(返回半径 5 公里内最近商家)

  1. 表结构假设(商家表  merchant )

    CREATE TABLE merchant (
     id INT PRIMARY KEY AUTO_INCREMENT,
     name VARCHAR(50) COMMENT '商家名称',
     lat DECIMAL(10,6) NOT NULL COMMENT '纬度(如:39.9042)',
     lng DECIMAL(10,6) NOT NULL COMMENT '经度(如:116.4074)',
     address VARCHAR(200) COMMENT '地址'
    );
  2. 查询 SQL(用户经纬度:39.9042, 116.4074)

    SELECT 
     id, name, lat, lng, address,
     -- 计算距离(公里)
     6371 * 2 * ASIN(
         SQRT(
             POWER(SIN((39.9042 - lat) * PI() / 180 / 2), 2) +
             COS(39.9042 * PI() / 180) * COS(lat * PI() / 180) *
             POWER(SIN((116.4074 - lng) * PI() / 180 / 2), 2)
         )
     ) AS distance
    FROM merchant
    -- 筛选 5 公里内(先粗筛,提升效率)
    WHERE 
     lat BETWEEN 39.9042 - 5 / 111.12 AND 39.9042 + 5 / 111.12  -- 纬度每度约 111.12 公里
     AND lng BETWEEN 116.4074 - 5 / (111.12 * COS(39.9042 * PI() / 180)) AND 116.4074 + 5 / (111.12 * COS(39.9042 * PI() / 180))
    -- 按距离排序(最近在前)
    ORDER BY distance ASC
    -- 限制返回数量
    LIMIT 10;

三、性能优化(大数据量必备)

  1. 添加空间索引(MySQL 5.7+ 支持)

    -- 方式 1:创建经纬度联合索引(基础优化)
    CREATE INDEX idx_merchant_lat_lng ON merchant(lat, lng);
    
    -- 方式 2:创建空间索引(高效,支持 GIS 函数)
    ALTER TABLE merchant ADD COLUMN location POINT NOT NULL COMMENT '经纬度空间点' AFTER lng;
    -- 更新空间点数据(lat 纬度,lng 经度,注意顺序)
    UPDATE merchant SET location = POINT(lng, lat);
    -- 创建空间索引
    CREATE SPATIAL INDEX idx_merchant_location ON merchant(location);
  2. 空间索引查询(更高效)

    SELECT 
     id, name, lat, lng, address,
     -- ST_Distance_Sphere 直接计算球面距离(米),MySQL 8.0+ 支持
     ST_Distance_Sphere(POINT(116.4074, 39.9042), location) / 1000 AS distance  -- 转公里
    FROM merchant
    -- 筛选 5 公里内(空间范围查询)
    WHERE ST_DWithin(
     location,
     POINT(116.4074, 39.9042),
     5000  -- 半径(米)
    )
    ORDER BY distance ASC
    LIMIT 10;

四、关键注意事项

1. 经纬度格式:确保存储为  DECIMAL(10,6) (保留 6 位小数,精度约 10 厘米),避免用  FLOAT (精度丢失)。
2. 粗筛逻辑:未用空间索引时,先通过  BETWEEN  筛选大致经纬度范围,减少计算量。
3. 版本支持: ST_Distance_Sphere  是 MySQL 8.0+ 函数,5.7 及以下用 Haversine 公式。

五、应用层整合(如 PHP)

将用户经纬度(前端获取)传入 SQL,示例(Laravel):

$userLat = 39.9042; // 用户纬度(前端定位获取)
$userLng = 116.4074; // 用户经度
$radius = 5; // 搜索半径(公里)

$merchants = DB::table('merchant')
    ->selectRaw("
        id, name, lat, lng, address,
        6371 * 2 * ASIN(
            SQRT(
                POWER(SIN(($userLat - lat) * PI() / 180 / 2), 2) +
                COS($userLat * PI() / 180) * COS(lat * PI() / 180) *
                POWER(SIN(($userLng - lng) * PI() / 180 / 2), 2)
            )
        ) AS distance
    ")
    ->whereRaw("
        lat BETWEEN ? - ? / 111.12 AND ? + ? / 111.12
        AND lng BETWEEN ? - ? / (111.12 * COS(? * PI() / 180)) AND ? + ? / (111.12 * COS(? * PI() / 180))
    ", [$userLat, $radius, $userLat, $radius, $userLng, $radius, $userLat, $userLng, $radius, $userLat])
    ->orderBy('distance')
    ->limit(10)
    ->get();
 

标签: none

添加新评论