你好,我是朱晓峰。今天,我来和你聊一聊怎么进行空间定位。

我们每天都会用到空间数据,比如你在网上购买一件商品,你手机上的App就能够算出你是不是需要负担运费,负担多少运费。这其实就是因为手机获取到了你的空间位置信息,发送到网购平台,然后根据你所在的位置是否属于偏远地区,来决定你是否需要负担运费,如果需要的话,应该负担多少。

而从应用开发者的角度出发,我们需要知道怎么进行空间定位,获取用户的空间位置信息,以及如何计算发货点与客户地址的距离,这些都要借助与空间数据相关的技术才能解决。

今天,我还是借助一个真实的项目,来给你介绍下空间数据类型、空间数据处理函数,以及如何为空间数据创建索引,帮助你提升开发基于空间数据应用的能力。

在我们超市项目的实施过程中,超市经营者提出了这样一个要求:要给距离门店5公里范围内的、从线上下单的客户提供送货上门的服务。要想解决这个问题,就需要用到空间数据了。

空间数据类型与空间函数

我先给你介绍下空间数据类型和空间函数。

MySQL支持的空间数据类型分为2类:

我简单说明一下这几种空间数据类型的特点。

几何类型是一个通用的空间数据类型,你可以把点类型、线类型和多边形类型数据的值赋予几何类型数据。但是点类型、线类型和多边形类型数据则不具备这种通用性,你只能赋予它们各自类型数据的值。

几何集类型数据可以保存点类型数据、线类型数据和多边形类型数据值的集合。多点类型、多线类型和多多边形类型则分别只能保存点类型数据、线类型数据和多边形类型数据值的集合。

下面我们重点介绍一下点类型,因为这种类型是最简单、最基础的空间类型,也最常用。

点类型(POINT)

点类型是最简单的空间数据类型,代表了坐标空间中的单个位置。在不同比例尺的坐标空间中,一个点可以有不同的含义。例如,在较大比例尺的世界地图中,一个点可能代表一座城市;而在较小比例尺的城市地图中,一个点可能只代表一个车站。

点类型数据的属性有2种:

点类型数据的维度是0,边界为空。

空间函数

MySQL支持的空间函数有一百多种,我们没有必要全部都掌握。所以,我给你重点介绍几个比较常用的空间函数ST_Distance_Sphere()、MBRContains()、MBRWithin()和ST_GeomFromText()。

1.ST_Distance_Sphere()函数

我们先从计算空间距离的函数ST_Distance_Sphere()说起,这个函数的语法结构和功能如下所示:

2.MBRContains()和MBRWithin()函数

在学习MBRContains()和MBRWithin()函数之前,我们要先了解一个概念,也就是最小边界矩形(MBR,Minimum Bounding Rectangle )。

最小边界矩形是指以二维坐标表示的若干二维形状(例如点、直线、多边形)的最大范围,即以给定的二维形状各顶点中的最大横坐标、最小横坐标、最大纵坐标、最小纵坐标决定的边界的矩形。

知道了这个概念,你就能更好地理解这两个函数了。

3.ST_GeomFromText()

这个函数的作用是通过WKT形式创建几何图形。而ST_GeomFromText(WKT,SRID)就表示,返回用WKT形式和SRID指定的参照系表达的几何图形。

这里的WKT是一种文本标记语言,用来表示几何对象。SRID(Spatial Reference Identifier)是空间参照标识符,默认是0,表示平面坐标系。我们平时常用的SRID是4326,是目前世界通用的以地球质心为原点的地心坐标系。

知道了这些基础知识,我们就可以着手解决超市经营者提出的需求了。

这家超市有很多门店,该怎么计算是否应该送货上门呢?如果应该送货上门,应该从哪家门店送货呢?我带你分析下具体的思路。

下面我们就来实际操作一下。

首先,我们创建一个门店表(demo.mybranch),包含门店编号、名称、位置等信息。

mysql> CREATE TABLE demo.mybranch
-> (
-> branchid SMALLINT PRIMARY KEY,
-> branchname VARCHAR(50) NOT NULL,
-> address GEOMETRY NOT NULL SRID 4326
-> );
Query OK, 0 rows affected (0.07 sec)

这里需要注意一下,我这里的address字段,定义的空间数据类型是GEOMETRY,SRID是4326。因为GEOMETRY类型比较通用,可以赋予任何类型的空间数据值,而且方便后面创建索引。SRID值为4326,表示采用地心坐标系,这样计算出来的距离才比较准确。当然,你完全可以使用空间数据类型POINT,也能达到同样的效果。

现在,我们把门店位置信息录入表中:

mysql> INSERT INTO demo.mybranch VALUES
    -> (1,'西直门店',ST_GeomFromText('POINT(39.938099 116.350266)', 4326)),          -- 西直门店的经纬度信息
    -> (2,'东直门店',ST_GeomFromText('POINT(39.941143 116.433769)', 4326)),
    -> (3,'崇文门店',ST_GeomFromText('POINT(39.896877 116.416977)', 4326)),
    -> (4,'五道口店',ST_GeomFromText('POINT(39.9921 116.34584)', 4326)),
    -> (5,'清河店',ST_GeomFromText('POINT(39.743378 116.332878)', 4326));
Query OK, 5 rows affected (0.03 sec)
Records: 5  Duplicates: 0  Warnings: 0

结果显示,数据插入成功了。这里有2个问题需要你注意。

第一,我是用门店的经度和纬度值,来表示门店的地理位置。要获得门店的地理位置,你可以通过地图数据获得,但是这样做成本比较高。还有一种办法,就是通过大厂提供的免费的API接口获取,比如百度地图API,这样比较简单。

第二,WKT格式表达一个点的时候,在关键字POINT后面的括号中,要先写这个点的纬度,后写这个点的经度。这与一般的习惯相反,不要搞错。而且,经度值与纬度值之间用空格隔开,而不是用逗号。

准备好各门店的位置信息之后,我们就可以通过空间函数来计算距离了。

假设我们获取到客户所在位置的地理坐标为:纬度是39.994671,经度是116.330788,那么,我们就可以通过下面的SQL语句查询到这个位置与各个门店的距离:

mysql> SELECT branchid,branchname,st_distance_sphere(ST_GeomFromText('POINT(39.994671 116.330788)',4326),address) AS distance
    -> FROM demo.mybranch;
+----------+------------+--------------------+
| branchid | branchname | distance           |
+----------+------------+--------------------+
|        1 | 西直门店   |  6505.859589677078 |
|        2 | 东直门店   |  10604.07854447186 |
|        3 | 崇文门店   |  13123.76779555601 |
|        4 | 五道口店   |  1313.741752971374 |
|        5 | 清河店     | 27943.114458834025 |
+----------+------------+--------------------+
5 rows in set (0.00 sec)

结果显示,所有门店与客户位置之间的距离,都已经计算出来了。

需要注意的是,这个结果中查出来的距离是以米为单位的。根据这个查询的结果,五道口店的球面最短距离只有1313米,也就是1.3公里,满足送货上门的条件。其他门店的最短距离都在5公里以上。因此,应该从五道口店送货上门。到这里,超市经营者的要求就得到了满足。

好了,到这里,我们已经知道了如何定位一个空间位置,以及如何计算2个位置之间的距离。接下来,我们就再来了解下如何通过创建索引来提升空间数据的查询效率。

用空间数据创建索引

对于空间数据的查询,一般分为2种:一种是查询包括一个点的空间对象;另外一种是查询与某一个区域有交集的空间对象。

为了提高查询的速度,就可以用空间数据字段创建空间索引。MySQL支持使用InnoDB存储引擎,或者是MyISAM存储引擎的数据表,创建空间索引。

我们有三种创建空间索引的方式。

第一,我们可以在创建数据表时创建空间索引,语法结构是:

CREATE TABLE 表名 (字段名 GEOMETY NOT NULL SRID 4326,SPATIAL INDEX(空间数据字段名));

第二种是在修改表时创建空间索引,语法结构是:

ALTER TABLE 表名 ADD SPATIAL INDEX (空间数据字段名);

第三种是单独创建空间索引,语法结构是:

CREATE SPATIAL INDEX 索引名 ON 表名(空间数据字段名);

这里要提醒你注意的是:空间索引与普通索引不同,必须要用关键字SPATIAL,而且,创建空间索引的空间数据字段不能为空。空间索引创建一个R树索引,支持区域扫描,对提升空间数据查询的效率很有帮助。

我还是以刚才的超市门店位置数据为例,来简单说明一下如何用空间类型字段创建空间索引。我我们先用下面的代码,单独创建一下空间索引:

mysql> CREATE SPATIAL INDEX index_address ON demo.mybranch(address);
Query OK, 0 rows affected, 1 warning (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 1

结果显示,创建成功了。现在我们来确认一下,刚才创建的空间索引能不能起到优化查询的作用。

在MySQL中,只有在WHERE条件筛选语句中包含类似MBRContains()和MBRWithin()这样的函数,空间索引才会起作用。

现在,我们来借助一个小例子验证一下,我们创建的空间索引能不能对空间数据的查询起到优化的作用。

假设我们创建了一个多边形的地理区域,代码如下所示:

mysql> SET @poly =
    -> 'Polygon((
    '> 40.016712 116.319618,
    '> 40.016712 116.412773,
    '> 39.907024 116.412773,
    '> 39.907024 116.319618,
    '> 40.016712 116.319618))';
Query OK, 0 rows affected (0.00 sec)

这里有个坑,你一定要注意,多边形的区域起点和终点一定要一致,否则就不是一个封闭的区域,MySQL就会提示非法的地理位置数据。

然后,我们查询下有多少门店在这个区域中。你可以用下面的代码来实现:

mysql> SELECT branchid,branchname FROM demo.mybranch
    -> WHERE MBRContains(ST_GeomFromText(@poly,4326),address);
+----------+------------+
| branchid | branchname |
+----------+------------+
|        1 | 西直门店   |
|        4 | 五道口店   |
+----------+------------+
2 rows in set (0.00 sec)

结果显示,有2个门店在这个地理区域范围内。下面我们用查询分析语句来分析一下这个查询,看看有没有用到空间索引:

mysql> EXPLAIN SELECT * FROM demo.mybranch
    -> WHERE MBRContains(ST_GeomFromText(@poly,4326),address);
+----+-------------+----------+------------+-------+---------------+---------------+---------+------+------+----------+-------------+
| id | select_type | table    | partitions | type  | possible_keys | key           | key_len | ref  | rows | filtered | Extra       |
+----+-------------+----------+------------+-------+---------------+---------------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | mybranch | NULL       | range | index_address | index_address | 34      | NULL |    5 |   100.00 | Using where |
+----+-------------+----------+------------+-------+---------------+---------------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

结果显示,我们创建的索引起了作用,MySQL优化器使用空间索引进行了区域扫描,提高了查询的效率。

总之,MySQL为空间数据提供了一套完整的解决方案。从空间数据类型到空间函数,再到空间索引,可以让我们像处理普通数据那样,来存储、处理和查询空间数据。这样一来,开发基于空间数据的应用就十分方便了。

总结

这节课,我给你介绍了MySQL的空间数据,包括空间数据类型POINT,空间数据处理函数ST_Distance_Sphere()、MBRContains()、MBRWithin()和ST_GeomFromText(),以及创建空间索引的方法。

MySQL的空间数据是非常有用的数据类型,通过各种空间数据处理函数,可以开发出路径规划、线路导航、自动驾驶等各种应用。虽然现在还存在数据量大、查询效率比较低等问题,但是通过不断使用新的技术,比如空间索引中引入R树索引等,进步是非常明显的。

如果你在实际工作中,需要开发基于空间数据的应用,课下可以再参考下链接中的内容。

思考题

在这节课中,我定义的门店表(demo.mybranch)中,地址的空间数据类型是几何类型GEOMETRY,请你改用点类型POINT,完成从创建表到查询最近门店的全部操作。

欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。

评论