<p>因为地球是球面,用简单的 ABS(lat - ?) + ABS(lon - ?) 计算曼哈顿距离会忽略曲率,导致高纬度区域经度差对应实际距离大幅缩短,且无法反映球面最短路径,从而漏掉真实邻近点。</p>

为什么直接用 WHERE 加经纬度范围会漏掉真实邻近点
因为地球是球面,用简单的 ABS(lat - ?) 划矩形框,会在高纬度地区严重低估距离(比如在哈尔滨和新加坡同样差 0.1 度,实际公里数差一倍),更糟的是——它根本不是圆形邻域,四个角上的点可能比框内中心点远得多。
真正靠谱的做法是先用球面距离公式粗筛,再用精确计算排序。但别急着写 ST_DistanceSphere——很多旧版 MySQL 或没启 GIS 扩展的实例根本不支持。
- 确认你的 MySQL 版本 ≥ 5.7.6 且启用了
spatial扩展(运行SELECT @@have_geometry;返回YES) - 如果用 PostgreSQL,优先选
ST_DWithin而非ST_Distance,前者能走空间索引 - 若数据库连
POINT类型都不支持(比如 SQLite 或老旧 MySQL),老老实实用 Haversine 公式手写嵌套
MySQL 8.0+ 下用 ST_DWithin 做高效邻近查询
必须确保坐标字段是 POINT 类型并建了空间索引,否则嵌套子查询照样慢成线性扫描。
ALTER TABLE locations ADD COLUMN geom POINT SRID 4326;
UPDATE locations SET geom = ST_POINTFROMTEXT(CONCAT('POINT(', lng, ' ', lat, ')'), 4326);
CREATE SPATIAL INDEX idx_geom ON locations(geom);然后主查询才能真正利用索引:
SELECT id, name, ST_Distance(geom, ST_PointFromText('POINT(116.3974 39.9093)', 4326)) AS dist_m
FROM locations
WHERE ST_DWithin(geom, ST_PointFromText('POINT(116.3974 39.9093)', 4326), 1000)
ORDER BY dist_m;-
ST_DWithin第三个参数单位是「米」(SRID 4326 下自动转为大地距离),不是度 - 嵌套子查询在这里没必要——
ST_DWithin本身已是最外层过滤条件,强行套(SELECT ...)反而让优化器放弃索引 - 如果要查“每个用户最近的 3 家店”,就得用窗口函数或
LATERAL,不是简单嵌套能解决的
没有空间扩展时,用 Haversine 公式硬写嵌套子查询
这是兼容性最强的 fallback 方案,但性能取决于是否能利用普通 B-tree 索引对经纬度做初步剪枝。
SELECT id, name,
6371 * ACOS(
COS(RADIANS(39.9093)) * COS(RADIANS(lat)) *
COS(RADIANS(lng) - RADIANS(116.3974)) +
SIN(RADIANS(39.9093)) * SIN(RADIANS(lat))
) AS distance_km
FROM locations
WHERE lat BETWEEN 39.9093 - 0.1 AND 39.9093 + 0.1
AND lng BETWEEN 116.3974 - 0.1 AND 116.3974 + 0.1
HAVING distance_km <= 10
ORDER BY distance_km;- 先用矩形范围(
BETWEEN)快速排除明显过远的行,这是关键提速点 -
HAVING不能换成WHERE,因为distance_km是计算字段 - 常数
6371是地球平均半径(km),若要米就换6371000 - 这个公式在两极附近误差略大,但对城市级应用足够用
PostgreSQL 中避免 ST_Distance 导致全表扫描
新手常犯的错:写 ORDER BY ST_Distance(geom, $1) LIMIT 10,结果每次都要算全表距离。正确做法是先用 ST_DWithin 缩小候选集,再排序。
SELECT id, name, ST_Distance(geom, ST_MakePoint(116.3974, 39.9093)::geography) AS dist_m FROM locations WHERE ST_DWithin(geom, ST_MakePoint(116.3974, 39.9093)::geography, 1000) ORDER BY dist_m LIMIT 10;
- 务必用
::geography强制走球面计算,用::geometry会按平面算,赤道附近误差小,高纬度完全不可用 -
ST_DWithin的第三个参数单位也是米,且能命中GIST索引 - 如果表里有百万级点,而你只要最近 1 个,加
LIMIT 1后 PostgreSQL 通常能用索引快速定位,不用全扫
地理坐标的邻近查询真正的坑不在公式本身,而在索引类型、SRID 一致性、以及是否把「过滤」和「排序」拆开——嵌套只是手段,别让它掩盖了底层数据结构和查询计划的问题。

















