MySQL存储过程封装地理近邻搜索可行,但ST_Distance_Sphere()在WHERE中无法走空间索引,须先用经纬度范围粗筛(如lat BETWEEN @lat±0.05)再精算距离,且POINT字段须建SPATIAL INDEX、经度在前,避免用户变量破坏查询优化。

直接用存储过程封装地理近邻搜索,本质是把「距离计算 + 条件过滤 + 排序限制」逻辑固化下来,但必须注意:MySQL 存储过程中无法直接使用空间索引加速 ST_Distance_Sphere() 或 ST_DWithin() 的 WHERE 条件——索引只在查询执行层生效,而存储过程本身不改变执行计划。所以关键不是“能不能写”,而是“怎么写才不拖慢性能”。
MySQL 存储过程中调用 ST_Distance_Sphere() 的正确姿势
这是最常用也最容易翻车的方式。很多人直接把 Haversine 公式塞进存储过程,结果一查 10 万条数据就卡死。
-
ST_Distance_Sphere(POINT(lng, lat), POINT(@lon, @lat))返回单位是米,比手写 Haversine 更准、更简,且 MySQL 8.0.16+ 原生支持,无需自定义函数 - 但
WHERE ST_Distance_Sphere(...) <= 5000会强制全表扫描——即使你建了SPATIAL INDEX,这个函数也无法命中索引 - 真正能走索引的是
ST_DWithin(),但它在 MySQL 中**不存在**(那是 PostGIS 的函数),别被文档名误导 - 补救办法:先用经纬度范围做粗筛(比如
lat BETWEEN @lat-0.05 AND @lat+0.05),再用ST_Distance_Sphere()精筛。0.05° ≈ 5.5km(赤道附近),足够覆盖常见半径
为什么不要在存储过程中用 SET @var = ... 反复算距离
看起来方便,实则破坏查询优化器对常量传播的判断,还容易触发隐式类型转换。
- 例如:
SET @dist = ST_Distance_Sphere(location, POINT(@lon, @lat)); SELECT * FROM t WHERE @dist <= 5000;—— 这种写法会让 MySQL 无法下推条件,变成先算全表距离再过滤 - 正确写法是把距离计算直接写在 WHERE 和 SELECT 中,让优化器有机会合并或提前终止
- 如果真要复用距离值(比如同时用于排序和 HAVING),用子查询或 CTE(MySQL 8.0+)更安全,例如:
SELECT id, name, dist FROM (SELECT id, name, ST_Distance_Sphere(location, POINT(@lon, @lat)) AS dist FROM places) t WHERE dist <= 5000 ORDER BY dist LIMIT 20;
存储过程里加空间索引的两个硬性前提
没索引的地理查询,不管包几层存储过程,都是线性扫描。
- 表字段必须是
POINT类型,且插入时用ST_Point(longitude, latitude)或POINT(longitude, latitude),顺序不能反(经度在前!) -
SPATIAL INDEX必须建在该POINT字段上,且仅对MBRContains()、MBRWithin()等最小外接矩形函数有效;ST_Distance_Sphere()本身不走这个索引 - 如果你的查询半径固定(如总是查 3km 内),可预先建好地理网格列(如 Geohash 前缀或 S2 cell ID),再建普通 B-tree 索引,这时存储过程里用
WHERE geohash LIKE 'wx4g0%'就能秒出
真正容易被忽略的一点:存储过程里的 SQL 一旦涉及用户变量(@var)或动态拼接,就无法被 MySQL 查询缓存(如果还开着的话)或 Prepared Statement 缓存复用。哪怕逻辑完全一样,每次调用都当全新语句处理——这对高频 LBS 查询是隐形性能杀手。


















