<audio id="audio" title="特别放送（四）| 位置信息：如何进行空间定位？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/75/db/7584e459a5404f7b86e93147e08273db.mp3"></audio>

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

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

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

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

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

## 空间数据类型与空间函数

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

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

- 一类是包含单个值的几何类型（GEOMETRY）、点类型（POINT）、线类型（LINESTRINIG）和多边形类型（POLYGON）；
- 另一类是包含多个值的多点类型（MULTIPOINT）、多线类型（MULTILINESTRING）、多多边形类型（MULTIPOLYGON）和几何集类型（GEOMETRYCOLLECTION）。

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

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

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

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

### 点类型（POINT）

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

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

- 坐标空间中的X轴的值（比如地理坐标中的经度值）；
- 坐标空间中的Y轴的值（比如地理坐标中的纬度值）。

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

### 空间函数

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

**1.ST_Distance_Sphere()函数**

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

- ST_Distance_Sphere(g1,g2)：g1与g2为2个点，函数返回球体上2个点g1与g2之间的最小球面距离。

**2.MBRContains()和MBRWithin()函数**

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

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

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

- MBRContains(g1,g2)：如果几何图形g1的最小边界矩形包含了几何图形g2的最小边界矩形，则返回1，否则返回0。
- MBRWithin(g1,g2)：与MBRContains(g1,g2)函数正好相反，MBRWithin(g1,g2)表示，如果几何图形g1的最小边界矩形，包含在几何图形g2的最小边界矩形之内，则返回1，否则返回0。

**3.ST_GeomFromText()**

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

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

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

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

- 第一步，把门店的位置信息录入数据表中；
- 第二步，根据下单客户的送货地址，获取到地理位置信息；
- 第三步，计算各门店位置与送货地址的距离，找出最近的门店安排送货，如果没有一家门店与客户的距离在5公里以内，则提示不能送货。

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

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

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

```

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

现在，我们把门店位置信息录入表中：

```
mysql&gt; INSERT INTO demo.mybranch VALUES
    -&gt; (1,'西直门店',ST_GeomFromText('POINT(39.938099 116.350266)', 4326)),          -- 西直门店的经纬度信息
    -&gt; (2,'东直门店',ST_GeomFromText('POINT(39.941143 116.433769)', 4326)),
    -&gt; (3,'崇文门店',ST_GeomFromText('POINT(39.896877 116.416977)', 4326)),
    -&gt; (4,'五道口店',ST_GeomFromText('POINT(39.9921 116.34584)', 4326)),
    -&gt; (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&gt; SELECT branchid,branchname,st_distance_sphere(ST_GeomFromText('POINT(39.994671 116.330788)',4326),address) AS distance
    -&gt; 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&gt; 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&gt; SET @poly =
    -&gt; 'Polygon((
    '&gt; 40.016712 116.319618,
    '&gt; 40.016712 116.412773,
    '&gt; 39.907024 116.412773,
    '&gt; 39.907024 116.319618,
    '&gt; 40.016712 116.319618))';
Query OK, 0 rows affected (0.00 sec)

```

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

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

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

```

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

```
mysql&gt; EXPLAIN SELECT * FROM demo.mybranch
    -&gt; 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树索引等，进步是非常明显的。

如果你在实际工作中，需要开发基于空间数据的应用，课下可以再参考下[链接](https://dev.mysql.com/doc/refman/8.0/en/spatial-types.html)中的内容。

## 思考题

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

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