mysql geohash函数_基于MySQL实现按距离排序、范围查找geoHash

简介 现在几乎所有的O2O应用中都会存在“按范围搜素、离我最近、显示距离”等等类似的功能,那这样的功能是怎么实现的呢?本文提供了基于MySQL的实现方式,同样适用于其它数据库。本文不分析,只讲怎么实现,有关分析的文章可以看参考链接。 实现 为了方便下面说明,先给出一个初始表结构: CREATE TABLE `customer` (     `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',     `name` VARCHAR(5) NOT NULL COMMENT '名称' COLLATE 'latin1_swedish_ci',     `lon` DOUBLE(9,6) NOT NULL COMMENT '经度',     `lat` DOUBLE(8,6) NOT NULL COMMENT '纬度',     PRIMARY KEY (`id`) ) COMMENT='商户表' CHARSET=utf8mb4 ENGINE=InnoDB; 实现过程主要分为四步:  1. 搜索  在数据库中搜索出接近指定范围内的商户,如:搜索出1公里范围内的。  2. 过滤  搜索出来的结果可能会存在超过1公里的,需要再次过滤。如果对精度没有严格要求,可以跳过。  3. 排序  距离由近到远排序。如果不需要,可以跳过。  4. 分页  如果需要2、3步,才需要对分页特殊处理。如果不需要,可以在第1步直接SQL分页。 第1步数据库完成,后3步应用程序完成。 step1 搜索 搜索可以用下面两种方式来实现。 区间查找 customer表中使用两个字段存储了经度和纬度,如果提前计算出经纬度的范围,然后在这两个字段上加上索引,那搜索性能会很不错。  那怎么计算出经纬度的范围呢?已知条件是移动设备所在的经纬度,还有满足业务要求的半径,这很像初中的一道平面几何题:给定圆心坐标和半径,求该圆外切正方形四个顶点的坐标。而我们面对的是一个球体,可以使用spatial4j来计算。     com.spatial4j     spatial4j     0.5 double lon = 116.312528, lat = 39.983733;// 移动设备经纬度 int radius = 1;// 千米 SpatialContext geo = SpatialContext.GEO; Rectangle rectangle = geo.getDistCalc().calcBoxByDistFromPt(         geo.makePoint(lon, lat), radius * DistanceUtils.KM_TO_DEG, geo, null); System.out.println(rectangle.getMinX() + "-" + rectangle.getMaxX());// 经度范围 System.out.println(rectangle.getMinY() + "-" + rectangle.getMaxY());// 纬度范围 计算出经纬度范围之后,SQL是这样: SELECT id, name FROM customer WHERE (lng BETWEEN ? AND ?) AND (lat BETWEEN ? AND ?); 需要给lon、lat两个字段建立联合索引: INDEX `idx_lon_lat` (`lon`, `lat`) geohash geohash的原理不讲了,详细可以看这篇文章,讲的很详细。geohash算法能把二维的经纬度编码成一维的字符串,它的特点是越相近的经纬度编码后越相似,所以可以通过前缀like的方式去匹配周围的商户。  customer表要增加一个字段,来存储每个商户的geohash编码,并且建立索引。 CREATE TABLE `customer` (     `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',     `name` VARCHAR(5) NOT NULL COMMENT '名称' COLLATE 'latin1_swedish_ci',     `lon` DOUBLE(9,6) NOT NULL COMMENT '经度',     `lat` DOUBLE(8,6) NOT NULL COMMENT '纬度',     `geo_code` CHAR(12) NOT NULL COMMENT 'geohash编码',     PRIMARY KEY (`id`),     INDEX `idx_geo_code` (`geo_code`) ) COMMENT='商户表' CHARSET=utf8mb4 ENGINE=InnoDB; 13 在新增或修改一个商户的时候,维护好geo_code,那geo_code怎么计算呢?spatial4j也提供了一个工具类GeohashUtils.encodeLatLon(lat, lon),默认精度是12位。  这个存储做好后,就可以通过geo_code去搜索了。拿到移动设备的经纬度,计算geo_code,这时可以指定精度计算,那指定多长呢?我们需要一个geo_code长度和距离的对照表: geohash length width height 1 5,009.4km 4,992.6km 2 1,252.3km 624.1km 3 156.5km 156km 4 39.1km 19.5km 5 4.9km 4.9km 6 1.2km 609.4m 7 152.9m 152.4m 8 38.2m 19m 9 4.8m 4.8m 10 1.2m 59.5cm 11 14.9cm 14.9cm 12 3.7cm 1.9cm https://en.wikipedia.org/wiki/Geohash#Cell_Dimensions 假设我们的需求是1公里范围内的商户,geo_code的长度设置为5就可以了,GeohashUtils.encodeLatLon(lat, lon, 5)。计算出移动设备经纬度的geo_code之后,SQL是这样: SELECT id, name FROM customer WHERE geo_code LIKE CONCAT(?, '%'); 这样会比区间查找快很多,并且得益于geo_code的相似性,可以对热点区域做缓存。 geohash边界和角的问题可以使用geohash-java来解决。 step2 过滤 上面两种搜索方式,都不是精确搜索,只是尽量缩小搜索范围,提升响应速度。所以需要在应用程序中做过滤,把距离大于1公里的商户过滤掉。计算距离同样使用spatial4j。 // 移动设备经纬度 double lon1 = 116.3125333347639, lat1 = 39.98355521792821; // 商户经纬度 double lon2 = 116.312528, lat2 = 39.983733; SpatialContext geo = SpatialContext.GEO; double distance = geo.calcDistance(geo.makePoint(lon1, lat1), geo.makePoint(lon2, lat2)) * DistanceUtils.DEG_TO_KM; System.out.println(distance);// KM 过滤代码就不写了,遍历一遍搜索结果即可。 step3 排序 同样,排序也需要在应用程序中处理。排序基于上面的过滤结果做就可以了Collections.sort(list, comparator)。 step4 分页 如果需要2、3步,只能在内存中分页,做法也很简单,可以参考这篇文章。 总结 全文的重点都在于搜索如何实现,更好的利用数据库的索引,两种搜索方式以百万数据量为分割线,第一种适用于百万以下,第二种适用于百万以上,未经过严格验证。可能有人会有疑问,过滤和排序都在应用层做,内存占用会不会很严重?这是个潜在问题,但大多数情况下不会。看我们大部分的应用场景,都是单一种类POI(Person Of Interest)的搜索,如酒店、美食、KTV、电影院等等,这种数据密度是很小,1公里内的酒店,能有多少家,50家都算多的,所以最终要看具体业务数据密度。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值