经纬度不能直接相减计算距离,需转为平面坐标系(如Web Mercator)再网格化;推荐用偏移取整或放大为BIGINT生成整数网格ID,避免浮点误差;PostGIS应先ST_Transform再ST_SnapToGrid,MySQL需预计算并建联合索引。

经纬度直接相减会出大问题,必须先转成平面距离单位
地球是球面,ABS(lat1 - lat2) 或 ABS(lng1 - lng2) 的差值没有实际距离意义——赤道上经度1°约111km,到了北纬60°就只剩约55km。直接用原始经纬度做范围筛选(比如 WHERE lat BETWEEN x-0.1 AND x+0.1)会导致高纬度区域网格严重变形、面积失真,密度统计结果不可比。
实操建议:
- 若精度要求不高(城市级,误差容忍±5%),可用「Haversine 近似公式」快速算公里级距离,但不适合做网格划分
- 真正用于网格化分组,推荐把经纬度转为局部平面坐标系,例如 Web Mercator(EPSG:3857)——它保持形状和局部比例,适合中低纬度快速网格切分
- PostgreSQL + PostGIS 用户直接用
ST_Transform(ST_Point(lng, lat), 3857);MySQL 8.0+ 可用ST_SRID(ST_Point(lng, lat), 4326)配合自定义投影函数,但原生支持弱,建议预计算 - 纯 SQL(无空间扩展)场景:对纬度做动态缩放校正,例如
lng * COS(RADIANS(lat))再参与网格计算,能显著改善中纬度偏差
网格 ID 要用整数生成,避免浮点误差导致同格不同 ID
常见错误是写 FLOOR(lat / 0.01) 和 FLOOR(lng / 0.01) 拼接字符串,看似简单,但浮点计算在不同数据库或版本中可能产生 -0.0 或微小舍入误差(如 39.99999999999999 → FLOOR 得 39 而非 40),造成同一物理网格被拆成两个 group。
实操建议:
- 统一用「偏移取整法」:先加一个极小正偏移再取整,例如
FLOOR((lat + 0.0000001) / 0.01),规避负零和截断边界问题 - 更稳妥的是转成大整数:把经纬度放大 10⁶ 倍转
BIGINT,再整除网格步长(如 10000 对应 0.01° 网格),完全规避浮点 - 网格 ID 推荐拼成单字段,如
(lat_grid * 1000000 + lng_grid),而不是字符串拼接CONCAT(lat_grid, '_', lng_grid)——后者无法走索引,且大小写/空格隐患多 - 注意:0.01° 网格在赤道约 1.1km×1.1km,在北京约 1.1km×0.8km,若需严格等距,应按目标米数反推经纬度步长(例如 500m 网格需动态算
0.0045左右的 lng_step)
PostgreSQL + PostGIS 是最省心的组合,但要注意 SRID 和索引顺序
用 ST_GeomFromText('POINT(lng lat)', 4326) 创建点后,若不做 ST_Transform(..., 3857) 就直接 ST_SnapToGrid,结果仍是球面网格,密度单位仍是“点数/经纬度平方”,毫无地理意义。
实操建议:
- 建表时明确指定几何列 SRID:
geom GEOMETRY(POINT, 4326),插入前用ST_SetSRID(ST_MakePoint(lng, lat), 4326) - 网格聚合必须在投影坐标系下进行:
ST_SnapToGrid(ST_Transform(geom, 3857), 100)表示 100 米网格(单位是米) - 记得给转换后的几何字段建索引:
CREATE INDEX idx_geom_3857 ON tbl USING GIST (ST_Transform(geom, 3857)),否则ST_SnapToGrid无法高效分组 - 别用
ST_Collect聚合点再算面积——网格面积是固定的,密度 = 计数 / 单网格面积(如 100×100=10000 平方米),直接COUNT(*) / 10000.0更快更准
MySQL 用户得手动模拟网格逻辑,小心 NULL 和时区隐式转换
MySQL 8.0 虽有 ST_PointFromText,但 ST_SnapToGrid 不支持投影变换,ST_DistanceSphere 只能两两点算,没法直接网格分组。硬要用纯 SQL 实现,容易掉进隐式类型转换坑里。
实操建议:
- 预计算两个字段:
lat_grid = FLOOR((lat + 0.0000001) / 0.005),lng_grid = FLOOR((lng + 0.0000001) / 0.005)(0.005° ≈ 550m 赤道宽度) - WHERE 条件必须显式排除 NULL:
WHERE lat IS NOT NULL AND lng IS NOT NULL,否则FLOOR(NULL)得 NULL,整个 GROUP BY 出乱序 - 避免在 WHERE 中用函数包裹字段(如
FLOOR(lat / 0.005)),会导致索引失效;应提前存好lat_grid字段并建联合索引(lat_grid, lng_grid) - 注意 MySQL 默认时区可能影响 NOW() 在分区或时间过滤中的行为,但本场景不涉及时序,重点盯住
lat/lng列是否被意外 cast 成 float(丢失精度),建议用DECIMAL(10,8)存储原始坐标
网格化密度统计真正的难点不在公式,而在每一步坐标系转换的意图是否清晰、每处取整是否抗边界误差、每个索引是否落在实际执行路径上。少一次隐式转换,就少一个线上查不出密度突变点的深夜排查。

















