mysql 5.7+ 应使用 st_distance_sphere() 计算经纬度距离,它基于 wgs84 球面模型返回精确米制大圆距离;需将坐标存为 srid 4326 的 point 类型并建 spatial 索引,但该函数本身不走索引,应配合 st_contains 或矩形边界先粗筛再精算。

MySQL 5.7+ 直接用 ST_Distance_Sphere() 算经纬度距离
MySQL 5.7 起原生支持地理空间函数,ST_Distance_Sphere() 是最推荐的方案:它基于球面模型(WGS84),返回单位为米的精确大圆距离。比手写 Haversine 公式更可靠,也避免浮点误差和 SQL 注入风险。
前提是你得把坐标存成 POINT 类型,并加 SPATIAL 索引:
ALTER TABLE locations ADD COLUMN coord POINT SRID 4326;
UPDATE locations SET coord = ST_PointFromText(CONCAT('POINT(', lng, ' ', lat, ')'), 4326);
ALTER TABLE locations ADD SPATIAL INDEX(coord);
查询时直接调用:
SELECT id, name,
ST_Distance_Sphere(coord, ST_PointFromText('POINT(116.397428 39.90923)', 4326)) AS distance_m
FROM locations
WHERE ST_Distance_Sphere(coord, ST_PointFromText('POINT(116.397428 39.90923)', 4326))
-
ST_PointFromText()的参数顺序是'POINT(longitude latitude)',别写反 - SRID 必须显式指定为
4326,否则ST_Distance_Sphere()会报错或返回 0 - 该函数不走
SPATIAL索引加速 —— 它本身不支持索引下推,但配合WHERE中的几何过滤(如ST_Within())可先粗筛再精算
MySQL 5.6 或低版本只能手写 Haversine 公式
老版本没 ST_Distance_Sphere(),只能用三角函数硬算。注意:必须用 RADIANS() 转角度,地球平均半径取 6371000 米(不是 6371!单位要统一)。
SELECT id, name,
6371000 * ACOS(
COS(RADIANS(39.90923)) * COS(RADIANS(lat)) *
COS(RADIANS(lng) - RADIANS(116.397428)) +
SIN(RADIANS(39.90923)) * SIN(RADIANS(lat))
) AS distance_m
FROM locations
WHERE distance_m
- 不能在
WHERE里直接用别名distance_m,得重复整个表达式,或套一层子查询 - 如果
lat/lng是DECIMAL类型,确保精度 ≥ 6 小数位,否则郊区定位偏差可能达百米 - 没有空间索引可用,全表扫描不可避免 —— 数据量超 10 万行就明显变慢
为什么不用 ST_Distance()?它返回的是平面距离
ST_Distance() 默认按平面欧氏距离算,单位取决于 SRID 的坐标系。对 WGS84(SRID 4326)来说,结果是“度”,不是米,数值毫无业务意义。例如两点相距 0.1 度,在赤道约 11km,在北纬 40° 只有 ~8.5km —— 完全不可比。
- 即使强制指定
ST_Distance(a, b, 4326),MySQL 仍按平面投影算,误差随距离增大而飙升 - 官方文档明确建议:地理坐标距离一律用
ST_Distance_Sphere() - 某些旧客户端驱动(如早期 MySQL Connector/J)可能不识别
ST_*函数,报错FUNCTION xxx does not exist,需升级驱动
性能瓶颈常卡在数据类型和索引误用上
很多人以为加了 SPATIAL 索引就能加速距离查询,其实不然:ST_Distance_Sphere() 本身无法利用空间索引做范围剪枝。真正有效的优化是“先框再算”:
SELECT * FROM (
SELECT id, name, coord,
ST_Distance_Sphere(coord, @center) AS dist_m
FROM locations
WHERE ST_Contains(
ST_Buffer(@center, 5000 / 111195), -- 粗略转成度(赤道近似)
coord
)
) t
WHERE dist_m
-
ST_Buffer()生成圆形区域,但 WGS84 下它是椭圆近似,且性能较差;更稳妥的做法是用矩形边界:lng BETWEEN ? AND ? AND lat BETWEEN ? AND ? - 经度每度约 111km × cos(纬度),纬度每度恒为 ~111km —— 所以边界计算必须动态适配中心点纬度
- 如果表里有大量无效坐标(如
lat=0, lng=0),务必在 WHERE 中提前过滤,避免无谓计算
实际部署时,坐标的精度、索引策略、是否缓存中间结果,比选哪个公式影响更大。别一上来就优化 SQL,先确认你的 lat/lng 到底有没有被当字符串存进去了。











