不能直接 GROUP BY latitude, longitude,因浮点精度、坐标系变形和孤立点干扰会导致语义错误;必须先通过ST_SnapToGrid、ST_ClusterDBSCAN或H3等地理感知方法进行空间语义对齐。

直接用 GROUP BY 对原始经纬度字段(如 latitude, longitude)做分组,几乎必然导致语义错误——浮点精度、坐标系变形、孤立点干扰会让结果不可信。真正可用的聚合必须先做空间语义对齐。
为什么不能直接 GROUP BY latitude, longitude
原始坐标是连续值,GROUP BY 会把任意微小差异(如 39.9042 和 39.9042000001)视为不同组;GPS 采集误差、WKT 导入截断、FLOAT 存储隐式转换都会放大这个问题。更关键的是:地理上相距百米的两个点,在 WGS84 下可能因浮点舍入被分到相邻网格,而真实空间邻近性完全丢失。
- MySQL/PostgreSQL 中
ROUND(lat, 5)看似合理,但高纬度地区经度 0.00001° ≈ 0.5m,赤道则 ≈ 1.1m,同一“网格”东西跨度不一致 - 未显式
CAST到DECIMAL(9,6)时,FLOAT类型参与ROUND可能触发 IEEE 754 截断,例如-0.0005四舍五入成0.000 - 直接
GROUP BY ROUND(lat,3), ROUND(lng,3)不过滤COUNT(*) = 1的组,结果里塞满噪声点
用 ST_SnapToGrid 实现米级可控网格聚合
ST_SnapToGrid 是 PostGIS 原生支持的地理感知网格化函数,它基于 geography 类型将球面距离映射为平面米制网格,自动处理 WGS84 非线性变形。比手工 ROUND 更可靠,且可走 GIST 索引加速预过滤。
- 必须先确保字段是
geography类型,否则ST_SnapToGrid默认按度计算,结果无意义 - 语法:
ST_SnapToGrid(location::GEOGRAPHY, 500)表示按 500 米边长六边形(实际是正方形投影栅格)对齐,返回GEOMETRY类型,需再用ST_AsText或哈希提取唯一键 - 推荐组合写法:
MD5(ST_AsText(ST_SnapToGrid(location::GEOGRAPHY, 500)))生成稳定分组键,避免浮点比较歧义 - 若要保留中心坐标供下游使用,可加
ST_Centroid(ST_Collect(...))聚合后计算每个网格质心
用 ST_ClusterDBSCAN 做密度自适应聚类
当业务需要识别“自然聚集区”(如外卖骑手扎堆区域、共享单车潮汐停放点),而非固定尺寸网格时,ST_ClusterDBSCAN 是唯一生产级选择。它不预设形状或大小,只依据空间密度和最小邻域半径发现簇。
-
eps参数单位是度(不是米!),0.000179 ≈ 20 米(赤道附近),高纬度需按cos(latitude)缩放校正,否则北方城市簇半径严重缩水 -
minpoints设为 2 表示两个点即可成簇,设为 5 则要求局部至少 5 个点才触发聚类,避免噪声点干扰 - 该函数是窗口函数,必须配合
OVER()使用,不能直接用于GROUP BY;需先生成cluster_id列,再按该列分组 - 后续取簇中心必须用
ST_Centroid(ST_Collect(pt)),不能对原始点AVG(lat)—— 球面几何不满足线性平均假设
用 H3 网格 ID 做全局一致整数分组
如果系统已接入 Uber H3(如通过 h3-pg 扩展),H3_FROMGEO 生成的 64 位整数是目前最高效、跨语言、无歧义的空间分组键。它天然支持层级聚合(分辨率 7 → 6 自动合并)、边界无撕裂、且支持索引点查。
- 调用前必须确认输入是
POINT(longitude, latitude)顺序,反了会导致 H3 ID 完全错乱 - 分辨率选型关键:res 8 ≈ 0.7 km²(适合城市级热力),res 9 ≈ 0.1 km²(适合小区级),过高(res 12)会导致单网格数据过少,分组失效
- H3 不是 SQL 原生函数,需提前安装扩展:
CREATE EXTENSION h3;,否则报错function h3_fromgeo does not exist - 整数 ID 可直接
GROUP BY,但注意:H3 网格是六边形,其“中心点”需用H3_TOGEO反查,不能用ST_Centroid计算
真正难的不是选哪个函数,而是理解每种聚合背后的地理假设:ST_SnapToGrid 假设均匀网格,ST_ClusterDBSCAN 假设密度驱动,H3 假设全球离散一致性。用错前提,结果再快也没意义。

















