前言
很多业务都需要存储地理位置:门店地址、用户位置、配送点位、设备上报GPS坐标。最常见需求:保存经纬度 + 查询指定中心点附近多少公里范围内的数据。
不少新手直接用两个 DECIMAL 字段存经度、纬度,简单够用,但遇到"附近N公里"这类范围查询时,自己写球面距离公式,性能差,还容易踩精度坑。
MySQL 从5.7开始完善了空间索引,支持 POINT 空间类型,搭配空间函数,专门处理地理位置。
本文对比两种主流存储方案,附带建表、查询、索引示例,帮你根据业务选型。
版本说明:示例基于 MySQL 5.7 / 8.0,InnoDB引擎。
一、两种存储方案对比
方案1:双字段存储(lat 纬度,lng 经度),DECIMAL / DOUBLE
方案2:空间类型 POINT,MySQL原生地理类型
| 方案 | 存储结构 | 附近查询 | 索引 | 优点 | 缺点 |
|---|---|---|---|---|---|
| 双字段 DOUBLE | lng 经度,lat 纬度两个独立字段 |
需要手写Haversine公式计算距离 | 普通B+树索引,范围查询性能差 | 简单易懂,跨数据库兼容,便于调试 | 大范围距离查询慢,无法利用空间索引,计算距离公式容易写错 |
| POINT空间类型 | 单个字段存储经纬度点 | ST_Distance 空间函数 | SPATIAL空间索引 | 空间查询性能强,原生球面距离计算,支持复杂地理运算 | 仅MySQL支持,开发上手略难,插入语法特殊 |
小提示:经度 lng 范围 -180,180,纬度 lat 范围 -90,90,不要写反!写反坐标点位会跑到地球另一边。
二、方案1:DOUBLE双字段存储(简单业务首选)
适合:点位数量不多,附近查询频次低,或者需要兼容其他数据库,业务只做少量距离计算。
建表语句
sql
CREATE TABLE shop (
id INT PRIMARY KEY AUTO_INCREMENT,
shop_name VARCHAR(100) NOT NULL,
lng DOUBLE COMMENT '经度',
lat DOUBLE COMMENT '纬度',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
字段类型选择说明:
DOUBLE:推荐,精度足够GPS经纬度(小数点后6位,误差约0.1米)DECIMAL(10,6)也可以,但是计算会比DOUBLE略慢,没必要;不要用FLOAT,精度不足。
插入数据
sql
INSERT INTO shop(shop_name,lng,lat)
VALUES('重庆解放碑店',106.5869,29.5630);
查询:查找中心点附近5公里门店(Haversine公式)
中心点:lng=106.58, lat=29.56,半径5km
sql
SELECT
id,
shop_name,
lng,
lat,
-- 哈弗辛公式,返回距离单位:公里
6371 * ACOS(
COS(RADIANS(29.56)) * COS(RADIANS(lat))
* COS(RADIANS(lng) - RADIANS(106.58))
+ SIN(RADIANS(29.56)) * SIN(RADIANS(lat))
) AS distance
FROM shop
HAVING distance < 5
ORDER BY distance ASC;
⚠️ 缺点:
HAVING是查完全表再过滤 ,数据量大(几十万+)会全表扫描,性能很差。优化思路:先用经纬度做矩形粗筛,缩小数据范围,再计算精确距离。
增加矩形范围预过滤优化版:
sql
SELECT
id,
shop_name,
lng,
lat,
6371 * ACOS(
COS(RADIANS(29.56)) * COS(RADIANS(lat))
* COS(RADIANS(lng) - RADIANS(106.58))
+ SIN(RADIANS(29.56)) * SIN(RADIANS(lat))
) AS distance
FROM shop
WHERE
lat BETWEEN 29.56 - 5/111 AND 29.56 + 5/111
AND lng BETWEEN 106.58 - 5/(111*COS(RADIANS(29.56))) AND 106.58 + 5/(111*COS(RADIANS(29.56)))
HAVING distance < 5
ORDER BY distance ASC;
原理:1纬度≈111km;经度距离随纬度变化,所以需要根据纬度修正经度区间。
先通过B+树索引,框出一个矩形,把大量无关数据过滤掉,再精确计算球面距离。
索引
sql
CREATE INDEX idx_shop_lat_lng ON shop(lat,lng);
✅ 适用场景:点位数量不大,简单业务,需要迁移其他数据库,开发简单。
❌ 缺点:百万级点位,高频附近查询性能吃力。
三、方案2:POINT空间类型 + SPATIAL索引(地理查询推荐)
MySQL 原生空间类型,百万级点位,高频"附近N公里"查询首选,空间索引可以大幅加速范围检索。
注意:InnoDB支持SPATIAL索引有约束:
- 空间字段不能为NULL
- 必须是NOT NULL
- 存储坐标顺序:
POINT(经度,纬度),先lng,后lat,很容易写反!
建表语句
sql
CREATE TABLE shop_geo (
id INT PRIMARY KEY AUTO_INCREMENT,
shop_name VARCHAR(100) NOT NULL,
geo POINT NOT NULL COMMENT '经纬度点 POINT(lng, lat)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
SPATIAL INDEX idx_shop_geo (geo) -- 创建空间索引
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
插入数据
sql
INSERT INTO shop_geo(shop_name, geo)
VALUES ('重庆解放碑店', ST_GeomFromText('POINT(106.5869 29.5630)'));
查询:查询中心点5公里范围内门店
MySQL 8.0 推荐 ST_Distance_Sphere,直接计算球面距离,返回单位:米
中心点:106.58, 29.56,半径5000米
sql
SELECT
id,
shop_name,
ST_X(geo) AS lng, -- 获取经度
ST_Y(geo) AS lat, -- 获取纬度
ST_Distance_Sphere(geo, ST_GeomFromText('POINT(106.58 29.56)')) AS distance_meter
FROM shop_geo
WHERE
ST_Contains(
ST_Buffer(ST_GeomFromText('POINT(106.58 29.56)'), 5000),
geo
)
ORDER BY distance_meter ASC;
解释:
ST_Buffer:以中心点为圆心,生成半径5000米圆形区域;ST_Contains:判断点位是否落在圆形内,可以走空间索引;ST_X()获取经度,ST_Y()获取纬度;ST_Distance_Sphere:球面距离,单位米。
MySQL5.7 没有
ST_Distance_Sphere,需要改用ST_Distance搭配地球半径计算。
读取POINT字段
sql
SELECT ST_X(geo) AS lng, ST_Y(geo) AS lat FROM shop_geo;
✅ 优点:
SPATIAL空间索引,百万级点位附近查询性能远优于双字段;- 原生空间函数,支持圆形、多边形范围查询(比如围栏区域判断 ST_Within);
- 内置球面计算,不用手写Haversine公式,减少人为bug。
❌ 缺点:
- 语法特殊,开发门槛略高;
- 空间字段无法为NULL;
- 其他数据库(如PostgreSQL)空间语法不一样,迁移成本高;
- 部分中间件、数据同步工具对POINT类型兼容性一般。
四、常见坑点(重点避坑)
-
POINT顺序写反!POINT(经度,纬度)
很多人习惯先纬度后经度,插入后点位直接跑到别的城市,调试半天找不到问题。记住:POINT(lng, lat)。
-
空间索引不能建立在允许NULL的POINT字段
建SPATIAL索引时报错,90%是字段没有加
NOT NULL。 -
DOUBLE vs DECIMAL
GPS民用设备小数点后6位足够,DOUBLE完全够用,性能更好,优先DOUBLE。
-
ST_Buffer半径单位
ST_Buffer在ST_Distance_Sphere配套场景下,单位是米;不要误传公里。 -
不要用空间类型做普通等值查询
空间索引擅长范围、区域查询;如果业务只需要根据经纬度等值匹配,双字段B+树更合适。
-
备份与同步
mysqldump可以导出POINT;但canal、DTS等同步工具,部分版本解析空间类型会出现异常,需要提前测试。
五、业务选型建议
- 简单业务,点位量<10万,附近查询少,需要兼容多数据库 :双字段
DOUBLE lng, lat,加B+树索引 + 矩形预过滤。 - 高频LBS业务,门店/骑手/设备点位,百万级别,大量附近查询、电子围栏 :
POINT+ SPATIAL索引,优先这个方案。 - 需要同时存储经纬度并且加密:推荐双字段DOUBLE,应用层单独加密lng/lat;MySQL空间类型无法直接加密字段,使用空间索引后,密文无法使用空间函数。
六、扩展:电子围栏判断(多边形区域)
如果业务需要判断点位是否落在自定义多边形围栏内,POINT空间类型原生支持:
sql
SELECT * FROM shop_geo
WHERE ST_Within(geo, ST_GeomFromText('POLYGON((106.5 29.5,106.6 29.5,106.6 29.6,106.5 29.6,106.5 29.5))'));
ST_Within 判断点是否在多边形内部,适合区域配送、地理围栏校验。
总结
存储经纬度有两条路线:双字段DOUBLE简单通用,POINT空间类型擅长地理范围检索。
如果你的核心场景是**"查附近点位"**,优先选用POINT+空间索引;如果只是保存坐标、查询量不大,双字段DOUBLE开发成本更低。
无论哪种方案,都要注意经纬度顺序、索引设计,避免上线后出现性能黑洞。