基于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查找附近的人需要注意如下图的例子
{: .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实现