MySQL 如何存储经纬度坐标:选型、建表、空间查询实战

前言

很多业务都需要存储地理位置:门店地址、用户位置、配送点位、设备上报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索引有约束:

  1. 空间字段不能为NULL
  2. 必须是NOT NULL
  3. 存储坐标顺序: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;

解释:

  1. ST_Buffer:以中心点为圆心,生成半径5000米圆形区域;
  2. ST_Contains:判断点位是否落在圆形内,可以走空间索引;
  3. ST_X() 获取经度,ST_Y() 获取纬度;
  4. 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;

✅ 优点:

  1. SPATIAL 空间索引,百万级点位附近查询性能远优于双字段;
  2. 原生空间函数,支持圆形、多边形范围查询(比如围栏区域判断 ST_Within);
  3. 内置球面计算,不用手写Haversine公式,减少人为bug。

❌ 缺点:

  1. 语法特殊,开发门槛略高;
  2. 空间字段无法为NULL;
  3. 其他数据库(如PostgreSQL)空间语法不一样,迁移成本高;
  4. 部分中间件、数据同步工具对POINT类型兼容性一般。

四、常见坑点(重点避坑)

  1. POINT顺序写反!POINT(经度,纬度)

    很多人习惯先纬度后经度,插入后点位直接跑到别的城市,调试半天找不到问题。记住:POINT(lng, lat)。

  2. 空间索引不能建立在允许NULL的POINT字段

    建SPATIAL索引时报错,90%是字段没有加 NOT NULL。

  3. DOUBLE vs DECIMAL

    GPS民用设备小数点后6位足够,DOUBLE完全够用,性能更好,优先DOUBLE。

  4. ST_Buffer半径单位

    ST_Buffer 在 ST_Distance_Sphere 配套场景下,单位是米;不要误传公里。

  5. 不要用空间类型做普通等值查询

    空间索引擅长范围、区域查询;如果业务只需要根据经纬度等值匹配,双字段B+树更合适。

  6. 备份与同步

    mysqldump可以导出POINT;但canal、DTS等同步工具,部分版本解析空间类型会出现异常,需要提前测试。

五、业务选型建议

  1. 简单业务,点位量<10万,附近查询少,需要兼容多数据库 :双字段 DOUBLE lng, lat,加B+树索引 + 矩形预过滤。
  2. 高频LBS业务,门店/骑手/设备点位,百万级别,大量附近查询、电子围栏 :POINT + SPATIAL索引,优先这个方案。
  3. 需要同时存储经纬度并且加密:推荐双字段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开发成本更低。

无论哪种方案,都要注意经纬度顺序、索引设计,避免上线后出现性能黑洞。

相关推荐
CJi0NG1 小时前
【自用】MySQL-存储引擎
数据库·mysql
高级程序源1 小时前
django房屋租赁系统28636-计算机课程设计、毕业设计
python·mysql·elasticsearch·小程序·django·flask·课程设计
JMchen1 小时前
集成第三方 C/C++ 库到 Android NDK 项目:OpenCV 与 FFmpeg 实战指南
android·opencv·cmake
雨落在了我的手上1 小时前
MySQL数据库基础(9):数据库设计
数据库·mysql
xuxigifxfh2 小时前
面试记录2026
java·mysql·面试·职场和发展
高级程序源2 小时前
springboot荆州旅游推介平台24175-计算机课程设计、毕业设计
vue.js·spring boot·python·mysql·django·课程设计
狼与自由2 小时前
Mysql 3种日志的区别
数据库·mysql
小米里的大麦2 小时前
19 C、C++ 操作 MySQL
c语言·c++·mysql
shuoshuohaohao2 小时前
Spring-boot-3 -注解 yaml配置 -日志
java·数据库·mysql