LBS-查找附近的人-MySQL实现

基于MySQL实现地理位置信息的处理,使用这种方式非常简单,只要项目中有使用到MySQL都可以快速的添加,没有任何的迁移运维成本。

在MySQL实现附近的人,只要一条SQL就可以搞定了,下面两条sql分别查找了10km内附近的人

开始前先创建一些测试数据,本文测试数据300w条,表结构如下

CREATE TABLE `t_address` (
  `addres_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `address_point` point NOT NULL,
  `lng` double(11,7) DEFAULT NULL,
  `lat` double(11,7) DEFAULT NULL,
  `name` char(80) COLLATE utf8_unicode_ci DEFAULT NULL,
  `geohash` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`addres_id`),
  SPATIAL KEY `address_point` (`address_point`)
) ENGINE=InnoDB AUTO_INCREMENT=3115414 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

SQL实现方案一

SELECT
    a.*,
    (
    6371 * acos(
    cos( radians( a.lat ) ) * cos( radians( 29.8039097230 ) ) * cos(
      radians( 121.5619236231 ) - radians( a.lng )
  ) + sin( radians( a.lat ) ) * sin( radians( 29.8039097230 ) )
    )
    ) AS distance
FROM
    t_address a
HAVING distance < 10
ORDER BY
    distance
    LIMIT 10

结果:

addres_id address_point lng lat name geohash distance
2481526 POINT(29.808 121.57) 121.5698029 29.8079889 633888 wtq3w6kkjez5 0.8852657850354427
627775 POINT(29.8049 121.574) 121.5736314 29.804911 2487639 wtq3w3z4nyjg 1.1351200210680374
911664 POINT(29.7964 121.572) 121.5720822 29.7964358 2203750 wtq3w2q0ff4y 1.2850983037800985
173640 POINT(29.7944 121.551) 121.5507155 29.7943927 2941774 wtq3mzysbvqe 1.5131126629455265
2409024 POINT(29.7911 121.569) 121.5687796 29.7911388 706390 wtq3qr79vyu1 1.5665993825391673
2400235 POINT(29.8185 121.56) 121.5603863 29.8184593 715179 wtq3whm9479w 1.6246238779751165
577146 POINT(29.8192 121.563) 121.5627771 29.8192162 2538268 wtq3whrmd0b3 1.703993324474751
2824745 POINT(29.7864 121.555) 121.5547026 29.786401 290669 wtq3qn3qmgee 2.067818698122141
421937 POINT(29.8256 121.561) 121.5609701 29.8255529 2693477 wtq3wjtfryj1 2.4083690561167903
352040 POINT(29.785 121.541) 121.5413481 29.7849754 2763374 wtq3mwpwn9vf 2.893922069882071

SQL实现方案二

SELECT
    a.*,
    (
        6378.138 * 2 * asin(
            sqrt(
                pow(
                    sin(
                        (
                            radians( a.lat ) - radians( 29.8039097230 )
                        ) / 2
                    ),
                    2
        ) + cos( radians( a.lat ) ) * cos( radians( 29.8039097230 ) ) * pow(
                    sin(
                        (
                            radians( a.lng ) - radians( 121.5619236231 )
                        ) / 2
                    ),
                    2
                )
            )
        )
    ) AS distance
FROM
    t_address a
HAVING distance < 10
ORDER BY
    distance
    LIMIT 10

结果:

addres_id address_point lng lat name geohash distance
627775 POINT(29.8049 121.574) 121.5736314 29.804911 2487639 wtq3w3z4nyjg 1.1363918007586489
173640 POINT(29.7944 121.551) 121.5507155 29.7943927 2941774 wtq3mzysbvqe 1.514807939036847
2409024 POINT(29.7911 121.569) 121.5687796 29.7911388 706390 wtq3qr79vyu1 1.5683545802038237
2400235 POINT(29.8185 121.56) 121.5603863 29.8184593 715179 wtq3whm9479w 1.6264440919817333
577146 POINT(29.8192 121.563) 121.5627771 29.8192162 2538268 wtq3whrmd0b3 1.7059024589764031
2824745 POINT(29.7864 121.555) 121.5547026 29.786401 290669 wtq3qn3qmgee 2.0701354612260814
421937 POINT(29.8256 121.561) 121.5609701 29.8255529 2693477 wtq3wjtfryj1 2.4110673677266843
352040 POINT(29.785 121.541) 121.5413481 29.7849754 2763374 wtq3mwpwn9vf 2.8971643888141014

SQL实现方案三

就精度而言,两个条sql的差别只有几米,所以使用如果你的应用数据量不大,sql查询使用方案就行,但是数据量不大这么小小的优化也没有太大的意义,所以就有了方案三

SELECT * FROM t_address WHERE ((lat BETWEEN 29.7 AND 29.8) AND (lng BETWEEN 121.5 AND 121.6)) LIMIT 10

结果:

addres_id address_point lng lat name geohash
16102 POINT(29.7055 121.576) 121.5758251 29.705483 2983899 wtq2yx8yfp3s
173640 POINT(29.7944 121.551) 121.5507155 29.7943927 2941774 wtq3mzysbvqe
197687 POINT(29.7584 121.598) 121.5978734 29.758412 2917727 wtq3r12g7f44
284988 POINT(29.7065 121.569) 121.5690565 29.7065188 2830426 wtq2yrgvh26x
333257 POINT(29.7791 121.594) 121.5939357 29.7790684 2782157 wtq3qvn58h9y
352040 POINT(29.785 121.541) 121.5413481 29.7849754 2763374 wtq3mwpwn9vf
404880 POINT(29.7668 121.563) 121.5627643 29.7667671 2710534 wtq3q4z7cxwf
603845 POINT(29.7924 121.504) 121.5037781 29.7923631 2511569 wtq3kzs355p8
620453 POINT(29.7581 121.582) 121.5822016 29.7580579 2494961 wtq3q9m3q8h3
657333 POINT(29.7486 121.573) 121.5734477 29.7485898 2458081 wtq3nrx44ehf

将原来精确计算距离的方式改成矩形的范围查询,这样就避免了复杂的数学计算。影响这条sql速度最关键的就是 LIMIT 关键字和范围了,查的越多速度越差,当然这样查询的数据就没有了排序和距离计算,在一些不在意距离的情况下还是可以采用的。

三个方案的比较如下:

测试环境:MACOS10.13.4+8G内存+2.6GHz+MySQL5.7(本地数据库) 测试工具 Navicat,测试时还有负载一些其他软件,应该影响不大。
实测300w条数据

|方案|距离精度|带排序速度|是否有距离排序|
|:---|:---|:---|
|方案一|略低(去除了部分计算)|大致1.83s|有|
|方案二|略高(完整计算公式)|大致2.55s|有|
|方案三|无|非常快|无|

最原始的方式介绍完了,上述的方案在数据量达到一定数量级的情况下再怎么优化都不明显了。接下来就是需要做一些有效果的方案了。

SQL+GeoHash方案

GeoHash编码是,就是通过算法把地理坐标装换成一个值,简单点来说就是把二维坐标装换成一个字符串,但是这个字符串并不是毫无意义。

Geohash有如下特点:

  • GeoHas将地理坐标装换成为字符串,保护了用户隐私,方便缓存(后面会提到用redis缓存)
  • 坐标值GeoHash值越接近,意味着坐标之间离的越近
  • GeoHash值越长,表示的范围越精确。如下表展示了GeoHash值的长度和相应表示范围的关系
geohash length lat bits lng bits lat error lng error km error
1 2 3 ±23 ±23 ±2500
2 5 5 ±2.8 ±5.6 ±630
3 7 8 ±0.70 ±0.70 ±78
5 12 13 ±0.022 ±0.022 ±2.4
6 15 15 ±0.0027 ±0.0055 ±0.61
7 17 18 ±0.00068 ±0.00068 ±0.076

从上表可以看出,当GeoHash值的长度为6的时候,对应坐标的距离大约是相距0.61km,查找附近的人这个需求平常也就是查找1km范围内,所以按照表数据查找GeoHash相似度为前6位就差不多了。

但是使用GeoHash查找附近的人需要注意如下图的例子

geoHash

{: .text-center}

在上图中查找A点附近的点时,由于A点和C点是在同一个区域内,根据GeoHash算法认为A点附近只有C点,没有B点。但是实际上B点离A点甚至要比C点还要近,为了取得更精确的结果,除了目标点的GeoHash值外,还需要使用A点周围8个区域值的GeoHash值。

基于以上知识,只要在数据插入的时候把经纬度进行GeoHash处理,查找附近坐标的时候用SQL中的模糊查询LIKE就可以实现。

比如西湖的雷峰塔经纬度为(30.2304462483, 120.1499176025),GeoHash后得到的值为 wtm7yp63pgxu ,那查询2km内范围的人,只要匹配 wtm7y% 就可以做到了。

select * from t_address where geohash like 'wtm7yp%'

添加索引

ALTER TABLE `t_address`
ADD INDEX `idx_geohash`(`geohash`) USING HASH;

在没有添加索引之前,300w条GeoHash值,查询前5位相似的值大约耗时 3.1s, 加 LIMIT 后提高到 1.3s

添加索引后,300w条GeoHash值,查询前5位相似的值大约耗时 0.001s, 这效率加不加 LIMIT 也没有什么关系了,所以这个方案的前提就是一定要加索引,切记。

总结来看,SQL+GeoHash方案这个方案还是不错的。想只用mysql直接解决查找附近的人,尤其是附近店铺这种店铺位置信息变化不大的情况下,这个方案还是非常值得推荐。

MySQL空间存储(MySQL Spatial Extensions)方案

MySQL的空间扩展(MySQL Spatial Extensions),它允许在MySQL中直接处理、保存和分析地理位置相关的信息,看起来这是使用MySQL处理地理位置信息的“官方解决方案”。

至于计算距离,官方指南的做法是这样的:

GLength(LineStringFromWKB(LineString(point1, point2)))

这条语句的处理逻辑是先通过两个点产生一个LineString的类型的数据,然后调用GLength得到这个LineString的实际长度。

这么做虽然有些复杂,貌似也解决了距离计算的问题,但需要注意的是:这种方法计算的是欧式空间的距离,简单来说,它给出的结果是两个点在三维空间中的直线距离,不是飞机在地球上飞的那条轨迹,而是笔直穿过地球的那条直线。

所以如果你的地理位置信息是用经纬度进行存储的,你就无法简单的直接使用这种方式进行距离计算。

下面是使用 MySQL Spatial Extensions 实现查找附近的人解决方案

对上面的数据表做空间索引:

ALTER TABLE t_address ADD SPATIAL INDEX(address_point);  

查找(30.620076,104.067221)附近 10 公里的点

注意:Point(纬度/latitude,经度/longitude),纬度再前,经度在后。

SELECT
    *,
    GLength (
    LineStringFromWKB (
    LineString ( address_point, Point ( 30.620076, 104.067221 ) )
    )
    ) * 111.1 AS distance
FROM t_address
WHERE
    MBRContains (
        LineString (
            Point ( 30.620076 + 10 / ( 111.1 / COS( RADIANS( 104.067221 ) ) ), 104.067221 + 10 / 111.1  ),
            Point ( 30.620076 - 10 / ( 111.1 / COS( RADIANS( 104.067221 ) ) ), 104.067221 - 10 / 111.1  )  
            ),
        address_point
    )
ORDER BY
    distance
    LIMIT 10

MBRContains函数,它属于最小边界矩形空间关系函数,查询效率也很高,源于它使用了R树索引,但是他不是圆形的范围,而是一个矩形的范围,和我们的 SQL实现方案三 有相似的思路。

300w条数据,全量查询耗时大约 0.001s,同样也达到了我们想要的效果,性能同 sql+GeoHash方案 差不多,而且这个方案还可以计算距离并排序。总结来看也是非常值得推荐的

以上的测试数据和运行环境都不一定完全相同,所以耗时仅供参考,也要结合实际去选择正确的方案。

到此,我们mysql的所有方案已经完成,接下来会介绍使用redis的解决方案。

文章同步发布在博客, LBS-查找附近的人-MySQL实现

你可能感兴趣的:(LBS-查找附近的人-MySQL实现)