MySQL 空间数据类型
1. 空间数据类型
1.1 Point
- 特点:表示二维空间中的一个点。
- 作用: 存储坐标点,通常用于表示地理位置或位置信息。
- 场景: 适用于需要存储和查询特定地理坐标的场景,如地图应用。
CREATE TABLE locations (
id INT PRIMARY KEY,
name VARCHAR(255),
location POINT
);
1.2 LINESTRING
- 特点: 表示由一组连接的线段构成的线。
- 作用: 存储路径或线段,例如道路、河流等。
- 场景: 适用于需要存储和查询路径或线段的场景,如导航系统。
CREATE TABLE routes (
id INT PRIMARY KEY,
name VARCHAR(255),
path LINESTRING
);
1.3 POLYGON
- 特点: 表示由一组连接的线段构成的封闭多边形区域。
- 作用: 存储区域或多边形,例如国家、城市的边界。
- 场景: 适用于需要存储和查询多边形区域的场景,如地理信息系统。
CREATE TABLE regions (
id INT PRIMARY KEY,
name VARCHAR(255),
boundary POLYGON
);
1.4 GEOMETRY
- 特点: 通用的几何类型,可以表示点、线、面等。
- 作用: 用于存储任意几何形状。
- 场景: 适用于需要灵活处理各种几何形状的场景。
CREATE TABLE shapes (
id INT PRIMARY KEY,
name VARCHAR(255),
geometry GEOMETRY
);
2. 索引
为了提高对空间数据类型的查询性能,可以使用空间索引。MySQL支持对空间数据类型创建空间索引,其中最常用的是R-tree索引。
2.1 创建和删除空间索引
对于InnoDB
和MyISAM
表,MySQL 可以使用与创建常规索引类似的语法创建空间索引,但使用关键字 SPATIAL
。必须声明空间索引中的列NOT NULL
。以下示例演示了如何创建空间索引:
CREATE TABLE geom (g GEOMETRY NOT NULL SRID 4326, SPATIAL INDEX(g));
CREATE TABLE geom (g GEOMETRY NOT NULL SRID 4326); ALTER TABLE geom ADD SPATIAL INDEX(g);
CREATE TABLE geom (g GEOMETRY NOT NULL SRID 4326); CREATE SPATIAL INDEX g ON geom (g);
SPATIAL INDEX
创建 R 树索引。对于支持空间列的非空间索引的存储引擎,引擎会创建 B 树索引。空间值的 B 树索引对于精确值查找很有用,但对于范围扫描则不起作用。
优化器可以使用在 SRID 限制的列上定义的空间索引。有关更多信息,请参阅 第 11.4.1 节“空间数据类型”和 第 8.3.3 节“空间索引优化”。
有关索引空间列的更多信息,请参阅 第 13.1.15 节 “CREATE INDEX 语句”。
要删除空间索引,请使用ALTER TABLE
或DROP INDEX
:
ALTER TABLE geom DROP INDEX g;
DROP INDEX g ON geom;
示例:假设一个表geom
包含超过 32,000 个几何图形,这些几何图形存储在 g
类型为 的列中GEOMETRY
。该表还有一AUTO_INCREMENT
列 fid
用于存储对象 ID 值。
mysql> DESCRIBE geom;
+-------+----------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+----------+------+-----+---------+----------------+
| fid | int(11) | | PRI | NULL | auto_increment |
| g | geometry | | | | |
+-------+----------+------+-----+---------+----------------+
2 rows in set (0.00 sec)
mysql> SELECT COUNT(*) FROM geom;
+----------+
| count(*) |
+----------+
| 32376 |
+----------+
1 row in set (0.00 sec)
要在列上添加空间索引g
,请使用以下语句:
mysql> ALTER TABLE geom ADD SPATIAL INDEX(g);
Query OK, 32376 rows affected (4.05 sec)
Records: 32376 Duplicates: 0 Warnings: 0
2.2 使用空间索引
优化器会调查可用的空间索引是否可以参与对在 子句中使用诸如MBRContains()
or 之类的函数的查询的搜索。以下查询查找给定矩形中的所有对象: MBRWithin()
WHERE
mysql> SET @poly =
-> 'Polygon((30000 15000,
31000 15000,
31000 16000,
30000 16000,
30000 15000))';
mysql> SELECT fid,ST_AsText(g) FROM geom WHERE
-> MBRContains(ST_GeomFromText(@poly),g);
+-----+---------------------------------------------------------------+
| fid | ST_AsText(g) |
+-----+---------------------------------------------------------------+
| 21 | LINESTRING(30350.4 15828.8,30350.6 15845,30333.8 15845,30 ... |
| 22 | LINESTRING(30350.6 15871.4,30350.6 15887.8,30334 15887.8, ... |
| 23 | LINESTRING(30350.6 15914.2,30350.6 15930.4,30334 15930.4, ... |
| 24 | LINESTRING(30290.2 15823,30290.2 15839.4,30273.4 15839.4, ... |
| 25 | LINESTRING(30291.4 15866.2,30291.6 15882.4,30274.8 15882. ... |
| 26 | LINESTRING(30291.6 15918.2,30291.6 15934.4,30275 15934.4, ... |
| 249 | LINESTRING(30337.8 15938.6,30337.8 15946.8,30320.4 15946. ... |
| 1 | LINESTRING(30250.4 15129.2,30248.8 15138.4,30238.2 15136. ... |
| 2 | LINESTRING(30220.2 15122.8,30217.2 15137.8,30207.6 15136, ... |
| 3 | LINESTRING(30179 15114.4,30176.6 15129.4,30167 15128,3016 ... |
| 4 | LINESTRING(30155.2 15121.4,30140.4 15118.6,30142 15109,30 ... |
| 5 | LINESTRING(30192.4 15085,30177.6 15082.2,30179.2 15072.4, ... |
| 6 | LINESTRING(30244 15087,30229 15086.2,30229.4 15076.4,3024 ... |
| 7 | LINESTRING(30200.6 15059.4,30185.6 15058.6,30186 15048.8, ... |
| 10 | LINESTRING(30179.6 15017.8,30181 15002.8,30190.8 15003.6, ... |
| 11 | LINESTRING(30154.2 15000.4,30168.6 15004.8,30166 15014.2, ... |
| 13 | LINESTRING(30105 15065.8,30108.4 15050.8,30118 15053,3011 ... |
| 154 | LINESTRING(30276.2 15143.8,30261.4 15141,30263 15131.4,30 ... |
| 155 | LINESTRING(30269.8 15084,30269.4 15093.4,30258.6 15093,30 ... |
| 157 | LINESTRING(30128.2 15011,30113.2 15010.2,30113.6 15000.4, ... |
+-----+---------------------------------------------------------------+
20 rows in set (0.00 sec)
用于EXPLAIN
检查此查询的执行方式:
mysql> SET @poly =
-> 'Polygon((30000 15000,
31000 15000,
31000 16000,
30000 16000,
30000 15000))';
mysql> EXPLAIN SELECT fid,ST_AsText(g) FROM geom WHERE
-> MBRContains(ST_GeomFromText(@poly),g)\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: geom
type: range
possible_keys: g
key: g
key_len: 32
ref: NULL
rows: 50
Extra: Using where
1 row in set (0.00 sec)
检查如果没有空间索引会发生什么:
mysql> SET @poly =
-> 'Polygon((30000 15000,
31000 15000,
31000 16000,
30000 16000,
30000 15000))';
mysql> EXPLAIN SELECT fid,ST_AsText(g) FROM g IGNORE INDEX (g) WHERE
-> MBRContains(ST_GeomFromText(@poly),g)\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: geom
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 32376
Extra: Using where
1 row in set (0.00 sec)
执行SELECT
不带空间索引的语句会产生相同的结果,但会导致执行时间从 0.00 秒增加到 0.46 秒:
mysql> SET @poly =
-> 'Polygon((30000 15000,
31000 15000,
31000 16000,
30000 16000,
30000 15000))';
mysql> SELECT fid,ST_AsText(g) FROM geom IGNORE INDEX (g) WHERE
-> MBRContains(ST_GeomFromText(@poly),g);
+-----+---------------------------------------------------------------+
| fid | ST_AsText(g) |
+-----+---------------------------------------------------------------+
| 1 | LINESTRING(30250.4 15129.2,30248.8 15138.4,30238.2 15136. ... |
| 2 | LINESTRING(30220.2 15122.8,30217.2 15137.8,30207.6 15136, ... |
| 3 | LINESTRING(30179 15114.4,30176.6 15129.4,30167 15128,3016 ... |
| 4 | LINESTRING(30155.2 15121.4,30140.4 15118.6,30142 15109,30 ... |
| 5 | LINESTRING(30192.4 15085,30177.6 15082.2,30179.2 15072.4, ... |
| 6 | LINESTRING(30244 15087,30229 15086.2,30229.4 15076.4,3024 ... |
| 7 | LINESTRING(30200.6 15059.4,30185.6 15058.6,30186 15048.8, ... |
| 10 | LINESTRING(30179.6 15017.8,30181 15002.8,30190.8 15003.6, ... |
| 11 | LINESTRING(30154.2 15000.4,30168.6 15004.8,30166 15014.2, ... |
| 13 | LINESTRING(30105 15065.8,30108.4 15050.8,30118 15053,3011 ... |
| 21 | LINESTRING(30350.4 15828.8,30350.6 15845,30333.8 15845,30 ... |
| 22 | LINESTRING(30350.6 15871.4,30350.6 15887.8,30334 15887.8, ... |
| 23 | LINESTRING(30350.6 15914.2,30350.6 15930.4,30334 15930.4, ... |
| 24 | LINESTRING(30290.2 15823,30290.2 15839.4,30273.4 15839.4, ... |
| 25 | LINESTRING(30291.4 15866.2,30291.6 15882.4,30274.8 15882. ... |
| 26 | LINESTRING(30291.6 15918.2,30291.6 15934.4,30275 15934.4, ... |
| 154 | LINESTRING(30276.2 15143.8,30261.4 15141,30263 15131.4,30 ... |
| 155 | LINESTRING(30269.8 15084,30269.4 15093.4,30258.6 15093,30 ... |
| 157 | LINESTRING(30128.2 15011,30113.2 15010.2,30113.6 15000.4, ... |
| 249 | LINESTRING(30337.8 15938.6,30337.8 15946.8,30320.4 15946. ... |
+-----+---------------------------------------------------------------+
20 rows in set (0.46 sec)
3. 空间函数
空间函数
4. 实际项目和练习(Point)
4.1 MYSQL创建空间列
参考1.1
4.2 Java创建GeoPoint对象
/**
* @author THF
* @date 2023/11/22 10:58
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GeoPoint implements Serializable {
/**
* 经度
*/
private BigDecimal longitude;
/**
* 纬度
*/
private BigDecimal latitude;
}
4.3 在Java实体中使用
package com.pqkj.admin.service.modular.entity;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.pqkj.admin.service.core.easyExcel.RootDir;
import com.pqkj.admin.service.geo.GeoPoint;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import com.pqkj.admin.service.core.auth.vo.req.PageReqVO;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.Date;
import java.util.List;
/**
* 空间资产主表
*
* @author PQKJ
* @since 2023-11-15
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("space_assets")
public class SpaceAssets extends PageReqVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@Schema(description = "主键", defaultValue = "主键")
@TableId(value = "id")
private String id;
/**
* 名称
*/
@Schema(description = "名称", defaultValue = "名称")
private String name;
/**
* 距离
*/
@TableField(exist = false)
private String distance;
/**
* 经纬度信息
*/
private GeoPoint location;
/**
* 将单位(M)转换成 KM
*/
public void covertDistance() {
if (ObjectUtil.isNotEmpty(this.distance)) {
//转换成BigDecimal类型
BigDecimal distance = new BigDecimal(this.distance);
if (distance.compareTo(new BigDecimal("1000")) > 0) {
this.distance = distance.divide(new BigDecimal("1000"), 2, RoundingMode.HALF_UP) + "km";
} else {
this.distance = distance.setScale(2, RoundingMode.HALF_UP) + "m";
}
}
}
/**
* 处理返回数据
*/
public void returnDataHandle() {
this.covertDistance();
}
}
4.4 编写GeoPoint转换器
引入依赖
<dependency>
<groupId>com.vividsolutions</groupId>
<artifactId>jts</artifactId>
<version>1.13</version>
</dependency>
编写转换器
package com.pqkj.admin.service.covert;
import com.pqkj.admin.service.geo.GeoPoint;
import com.vividsolutions.jts.geom.*;
import com.vividsolutions.jts.geom.impl.CoordinateArraySequence;
import com.vividsolutions.jts.geom.impl.CoordinateArraySequenceFactory;
import com.vividsolutions.jts.io.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
/**
* @author THF
* @date 2023/11/22 11:01
*/
public class GeoPointConverter {
/**
* Little endian or Big endian
*/
private int byteOrder = ByteOrderValues.LITTLE_ENDIAN;
/**
* Precision model
*/
private PrecisionModel precisionModel = new PrecisionModel();
/**
* Coordinate sequence factory
*/
private CoordinateSequenceFactory coordinateSequenceFactory = CoordinateArraySequenceFactory.instance();
/**
* Output dimension
*/
private int outputDimension = 2;
/**
* Convert byte array containing SRID + WKB Geometry into Geometry object
*/
public GeoPoint from(byte[] bytes) {
if (bytes == null) {
return null;
}
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes)) {
// Read SRID
byte[] sridBytes = new byte[4];
inputStream.read(sridBytes);
int srid = ByteOrderValues.getInt(sridBytes, byteOrder);
// Prepare Geometry factory
GeometryFactory geometryFactory = new GeometryFactory(precisionModel, srid, coordinateSequenceFactory);
// Read Geometry
WKBReader wkbReader = new WKBReader(geometryFactory);
Geometry geometry = wkbReader.read(new InputStreamInStream(inputStream));
Point point = (Point) geometry;
// convert to GeoPoint
return new GeoPoint(BigDecimal.valueOf(point.getX()), BigDecimal.valueOf(point.getY()));
} catch (IOException | ParseException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Convert Geometry object into byte array containing SRID + WKB Geometry
*/
public byte[] to(GeoPoint geoPoint) {
if (geoPoint == null) {
return null;
}
Coordinate coordinate = new Coordinate(geoPoint.getLongitude().doubleValue(), geoPoint.getLatitude().doubleValue());
CoordinateArraySequence coordinateArraySequence = new CoordinateArraySequence(new Coordinate[]{coordinate}, 2);
Point point = new Point(coordinateArraySequence, new GeometryFactory());
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
// Write SRID
byte[] sridBytes = new byte[4];
ByteOrderValues.putInt(point.getSRID(), sridBytes, byteOrder);
outputStream.write(sridBytes);
// Write Geometry
WKBWriter wkbWriter = new WKBWriter(outputDimension, byteOrder);
wkbWriter.write(point, new OutputStreamOutStream(outputStream));
return outputStream.toByteArray();
} catch (IOException ioe) {
throw new IllegalArgumentException(ioe);
}
}
}
4.5 编写Mybatis类型处理器
package com.pqkj.admin.service.conf.mybatis.handler;
import com.pqkj.admin.service.covert.GeoPointConverter;
import com.pqkj.admin.service.geo.GeoPoint;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* @author THF
* @date 2023/11/22 10:48
*/
@MappedTypes({GeoPoint.class})
public class PointTypeHandler extends BaseTypeHandler<GeoPoint> {
GeoPointConverter converter = new GeoPointConverter();
@Override
public void setNonNullParameter(PreparedStatement ps, int i, GeoPoint parameter, JdbcType jdbcType) throws SQLException {
ps.setBytes(i, converter.to(parameter));
}
@Override
public GeoPoint getNullableResult(ResultSet rs, String columnName) throws SQLException {
return converter.from(rs.getBytes(columnName));
}
@Override
public GeoPoint getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return converter.from(rs.getBytes(columnIndex));
}
@Override
public GeoPoint getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return converter.from(cs.getBytes(columnIndex));
}
}
4.6 Mybatis Plus扫描类型处理器
# Mybatis-Plus配置
mybatis-plus:
type-handlers-package: com.pqkj.admin.service.conf.mybatis.handler
新增和修改时只要补充实体中的GeoPoint对象属性值,就可以正常插入和修改
4.7 查询某个点位附近几公里的数据
SQL示例:查询某点位附近5公里的数据
SELECT
sa.id,
sa.NAME,
ST_DISTANCE_SPHERE (
sa.location,
ST_GEOMFROMTEXT ( 'POINT(113.556818 22.222774)' )) AS distance
FROM
space_assets AS sa
WHERE ST_DISTANCE_SPHERE (
sa.location,
ST_GEOMFROMTEXT ( 'POINT(113.556818 22.222774)' )) <= 5000
ORDER BY
distance desc
SQL中的5000
为5千米,也就是5公里的意思,1000
则为1公里。
在Java使用Mybatis查询时,需要将POINT(113.556818 22.222774)
作为一个参数传进来
例如:
SELECT
sa.id,
sa.NAME,
ST_DISTANCE_SPHERE (
sa.location,
ST_GEOMFROMTEXT ( #{lonLat} )) AS distance
FROM
space_assets AS sa
WHERE ST_DISTANCE_SPHERE (
sa.location,
ST_GEOMFROMTEXT ( #{lonLat} )) <= #{distance}
ORDER BY
distance desc