在地理位置数据分析中,我们常常需要将散落的经纬度坐标按照一定空间粒度进行聚合统计,例如统计每个一平方公里网格内的订单量或用户数。如果直接对经纬度字段做分组,由于浮点数精度问题会产生极多无意义的分组。通过经纬度换算函数将坐标映射到网格编号,再基于编号分组,是一种简单高效的方案。

为什么需要网格分组统计
原始经纬度是连续型数值,两条记录哪怕只相差零点零零零一度,也会被数据库视为不同值。当数据量达到几十万行时,直接按经纬度字段GROUP BY几乎等同于逐行输出,既慢又无业务价值。业务方通常关心的是某个区域整体的密度,而不是某一个精确点。
网格分组的核心思想是:把地球表面切分成大小相近的方块,每个方块赋予一个唯一编号。同一方块内的所有坐标都归到同一个编号下,统计时只需对编号计数即可。这种方式既能压缩分组数量,又能保留空间分布特征,非常适合热力图、基站负荷、配送区划等场景。
利用自定义换算函数生成网格ID
如果不想引入扩展模块,可以用简单的数学换算把经纬度转成网格行列号。以每格零点零一度为例,将经度加一百八十、纬度加九十后除以粒度并取整,即可得到全局统一的网格坐标。
-- MySQL 自定义函数:将经纬度转换为网格ID(粒度由参数控制) DELIMITER $$ CREATE FUNCTION lat_lng_to_grid( p_lng DOUBLE, p_lat DOUBLE, p_step DOUBLE ) RETURNS BIGINT BEGIN DECLARE grid_x BIGINT; DECLARE grid_y BIGINT; -- 经度范围约 -180~180,纬度范围约 -90~90 SET grid_x = FLOOR((p_lng + 180.0) / p_step); SET grid_y = FLOOR((p_lat + 90.0) / p_step); -- 组合为唯一ID,防止不同行列冲突 RETURN grid_x * 100000 + grid_y; END$$ DELIMITER ;
上述函数把二维坐标压缩成一维整数,查询时直接调用即可。例如统计每个网格的打车订单数:
SELECT lat_lng_to_grid(lng, lat, 0.1) AS grid_id, COUNT(*) AS order_cnt FROM taxi_orders WHERE create_time >= '2023-01-01' GROUP BY grid_id ORDER BY order_cnt DESC LIMIT 20;
这种写法的优点是纯SQL原生、不依赖插件,任何支持自定义函数的数据库都能用。缺点是需要自己把控粒度与编号溢出问题,当步长过小时grid_x乘以倍数可能超出BIGINT范围,此时可改为拼接字符串或使用两个字段分别存行列。
使用GeoHash函数简化实现
PostgreSQL配合PostGIS扩展提供了现成的ST_GeoHash函数,可以把几何点转成字符串哈希,前缀相同的点即位于相近区域。取哈希前若干位即为网格层级。
-- PostgreSQL + PostGIS 按geohash前6位分组 SELECT SUBSTRING(ST_GeoHash(ST_MakePoint(lng, lat), 6) FROM 1 FOR 6) AS geo_grid, COUNT(*) AS user_cnt FROM app_users GROUP BY geo_grid HAVING COUNT(*) > 50;
GeoHash的层级与精度对应关系明确:第6级约对应一点二公里见方,第7级约零点三公里。相比自定义函数,它考虑了地球曲率且社区通用,便于和前端可视化库打通。但要注意,GeoHash存在跨经线或跨赤道时相邻格子编码突变的问题,严格邻接分析时需配合邻居函数补充。
性能与索引建议
无论使用哪种换算方式,都建议在表中冗余一个网格ID字段,并在该字段上建普通B树索引,而不是每次查询都实时计算函数。可通过触发器或写入时计算来维护。
| 方案 | 精度控制 | 跨库兼容 | 适用场景 |
|---|---|---|---|
| 自定义换算函数 | 灵活,步长自定 | 高,纯SQL | 轻量统计、无扩展环境 |
| GeoHash | 固定层级 | 需PostGIS等 | 空间邻近、可视化对接 |
对于超大规模数据,还可将网格ID作为分区键做范围分区,使统计查询只扫描相关分区。如果业务只需近似结果,采样或预聚合表也能进一步降低响应时间。
小结
通过经纬度换算函数把坐标转成网格编号,是SQL中实现位置聚合统计的实用路径。自定义函数适合简单可控的场晧,GeoHash则在与空间生态集成时更省心。核心原则是把连续空间离散化,再用常规分组手段产出可解读的区域指标。