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

ab314e21fa0e

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的解决方案。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐