返回 筑基・数据元府藏真
地理空间数据处理
博主
大约 17 分钟
地理空间数据处理
一、问题引入:附近的人功能实现
1.1 真实案例:外卖配送范围计算
场景:外卖平台需要计算商家配送范围
需求:
- 用户下单时判断是否在配送范围内
- 搜索时只展示可配送的商家
- 配送费根据距离动态计算
传统方案的问题:
┌─────────────────────────────────────────────────────────────┐
│ 方案1:应用层计算 │
│ - 查询所有商家坐标 │
│ - 逐个计算与用户的距离 │
│ - 数据量大时性能极差 │
│ - 10000个商家需要计算10000次 │
├─────────────────────────────────────────────────────────────┤
│ 方案2:矩形范围过滤 │
│ - 根据距离估算经纬度范围 │
│ - 使用BETWEEN查询 │
│ - 不准确:地球是球形,矩形范围会漏掉或包含错误数据 │
├─────────────────────────────────────────────────────────────┤
│ 方案3:GeoHash编码 │
│ - 将坐标编码为字符串 │
│ - 查询相邻的GeoHash区域 │
│ - 需要额外维护GeoHash字段 │
├─────────────────────────────────────────────────────────────┤
│ 方案4:MySQL空间索引(推荐) │
│ - 使用POINT类型存储坐标 │
│ - 建立SPATIAL INDEX │
│ - 使用ST_Distance_Sphere计算球面距离 │
│ - 精度高,性能好 │
└─────────────────────────────────────────────────────────────┘
1.2 地理空间数据应用场景
| 场景 | 说明 | 技术方案 |
|---|---|---|
| 附近的人/商家 | 基于位置的服务(LBS) | MySQL空间索引 + Redis Geo |
| 配送范围判断 | 多边形区域包含检测 | MySQL POLYGON + ST_Contains |
| 轨迹存储分析 | 车辆/人员轨迹记录 | PostGIS / 时序数据库 |
| 地理围栏 | 电子围栏告警 | Redis GeoHash |
| 路径规划 | 最优路线计算 | 图数据库 / 专用算法 |
| 区域统计 | 热力图、分布分析 | 空间聚合查询 |
二、MySQL空间数据类型详解
2.1 空间数据类型介绍
MySQL 5.7+ 支持空间数据类型,8.0进一步优化:
┌──────────────────────────────────────────────────────────────┐
│ 数据类型 │ 说明 │ 示例 │
├───────────────────┼─────────────────────────┼────────────────┤
│ POINT │ 点(经纬度坐标) │ 商家位置 │
│ LINESTRING │ 线(路径、轨迹) │ 配送路线 │
│ POLYGON │ 多边形(区域) │ 配送范围 │
│ MULTIPOINT │ 多点集合 │ 多个门店 │
│ MULTILINESTRING │ 多线集合 │ 多条路线 │
│ MULTIPOLYGON │ 多边形集合 │ 多个配送区 │
│ GEOMETRY │ 通用几何类型 │ 混合存储 │
│ GEOMETRYCOLLECTION│ 几何集合 │ 复杂场景 │
└──────────────────────────────────────────────────────────────┘
坐标系(SRID):
- 0:默认平面坐标系
- 4326:WGS 84(GPS使用的经纬度坐标系)⭐ 推荐
- 3857:Web墨卡托投影(地图显示用)
2.2 基础DDL操作
-- 创建用户位置表
CREATE TABLE user_location (
user_id BIGINT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
-- 空间数据类型:POINT,SRID 4326表示WGS84坐标系
location POINT NOT NULL SRID 4326,
location_name VARCHAR(100) COMMENT '位置名称',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- 空间索引(必须)
SPATIAL INDEX idx_location (location)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户位置表';
-- 创建商家表(带配送范围)
CREATE TABLE merchant (
merchant_id BIGINT PRIMARY KEY AUTO_INCREMENT,
merchant_name VARCHAR(100) NOT NULL,
-- 商家位置
location POINT NOT NULL SRID 4326,
-- 配送范围(多边形)
delivery_area POLYGON SRID 4326,
delivery_radius INT COMMENT '配送半径(米)',
min_delivery_amount DECIMAL(10,2) COMMENT '起送金额',
status TINYINT DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 索引
SPATIAL INDEX idx_location (location),
SPATIAL INDEX idx_delivery_area (delivery_area),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入位置数据
INSERT INTO user_location (user_id, username, location, location_name) VALUES
(1, '张三', ST_GeomFromText('POINT(116.4074 39.9042)', 4326), '北京市中心'),
(2, '李四', ST_GeomFromText('POINT(121.4737 31.2304)', 4326), '上海市中心'),
(3, '王五', ST_GeomFromText('POINT(113.2644 23.1291)', 4326), '广州市中心'),
(4, '赵六', ST_GeomFromText('POINT(114.0579 22.5431)', 4326), '深圳市中心');
-- 插入商家数据(带配送范围)
INSERT INTO merchant (merchant_name, location, delivery_area, delivery_radius, min_delivery_amount) VALUES
('海底捞火锅(朝阳店)',
ST_GeomFromText('POINT(116.4474 39.9242)', 4326),
ST_GeomFromText('POLYGON((116.4374 39.9142, 116.4574 39.9142, 116.4574 39.9342, 116.4374 39.9342, 116.4374 39.9142))', 4326),
3000, 100.00);
2.3 空间查询操作
-- 1. 查询附近的人(5公里内)
SELECT
user_id,
username,
location_name,
-- 计算球面距离(米)
ST_Distance_Sphere(location, ST_GeomFromText('POINT(116.4074 39.9042)', 4326)) AS distance
FROM user_location
WHERE ST_Distance_Sphere(location, ST_GeomFromText('POINT(116.4074 39.9042)', 4326)) <= 5000
ORDER BY distance;
-- 2. 使用空间索引优化(MBR包含查询)
-- 先使用MBRIntersects快速过滤,再精确计算距离
SELECT
user_id,
username,
ST_Distance_Sphere(location, ST_GeomFromText('POINT(116.4074 39.9042)', 4326)) AS distance
FROM user_location
WHERE ST_Intersects(
location,
ST_Buffer(ST_GeomFromText('POINT(116.4074 39.9042)', 4326), 5000)
)
ORDER BY distance;
-- 3. 查询在配送范围内的商家
SELECT
merchant_id,
merchant_name,
ST_Distance_Sphere(location, ST_GeomFromText('POINT(116.4474 39.9242)', 4326)) AS distance
FROM merchant
WHERE ST_Contains(
delivery_area,
ST_GeomFromText('POINT(116.4474 39.9242)', 4326)
);
-- 4. 查询某个点是否在多边形内
SELECT merchant_name
FROM merchant
WHERE ST_Contains(
delivery_area,
ST_GeomFromText('POINT(116.4450 39.9200)', 4326)
);
-- 5. 查询两个商家的距离
SELECT
m1.merchant_name AS merchant1,
m2.merchant_name AS merchant2,
ST_Distance_Sphere(m1.location, m2.location) AS distance
FROM merchant m1, merchant m2
WHERE m1.merchant_id = 1 AND m2.merchant_id = 2;
-- 6. 查询商家的配送范围面积
SELECT
merchant_name,
ST_Area(delivery_area) AS area,
ST_Area(delivery_area, 'kilometer') AS area_km2
FROM merchant;
-- 7. 查询轨迹长度(LINESTRING)
SELECT
route_id,
ST_Length_Spheroid(route, 'SPHEROID[\"WGS 84\",6378137,298.257223563]') AS length_m
FROM delivery_route;
三、GeoHash方案详解
3.1 GeoHash原理
GeoHash原理:
- 将二维经纬度编码为一维字符串
- 基于Z-order曲线填充空间
- 字符串越长,精度越高
精度对照表:
┌──────────┬─────────────┬──────────────┐
│ GeoHash长度 │ 精度(km) │ 适用场景 │
├──────────┼─────────────┼──────────────┤
│ 1 │ 5000 │ 国家级别 │
│ 2 │ 630 │ 省份级别 │
│ 3 │ 78 │ 城市级别 │
│ 4 │ 20 │ 区县级别 │
│ 5 │ 2.4 │ 附近搜索 │
│ 6 │ 0.6 │ 精确位置 │
│ 7 │ 0.074 │ 建筑级别 │
│ 8 │ 0.019 │ 精确到米 │
└──────────┴─────────────┴──────────────┘
相邻区域特性:
- 前缀相同的GeoHash表示相邻区域
- 但相邻区域的GeoHash前缀不一定相同
- 需要查询中心点+周围8个区域
3.2 Java GeoHash实现
/**
* GeoHash服务
*/
@Service
@Slf4j
public class GeoHashService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 编码经纬度为GeoHash
*/
public String encode(double lat, double lon, int precision) {
return GeoHashUtils.encode(lat, lon, precision);
}
/**
* 解码GeoHash为经纬度
*/
public double[] decode(String geohash) {
return GeoHashUtils.decode(geohash);
}
/**
* 获取周围8个区域的GeoHash
*/
public List<String> getNeighbors(String geohash) {
return GeoHashUtils.getNeighbors(geohash);
}
/**
* 获取指定距离内的所有GeoHash
*/
public List<String> getGeoHashesInRadius(double lat, double lon,
double radiusKm, int precision) {
String center = encode(lat, lon, precision);
List<String> neighbors = getNeighbors(center);
neighbors.add(center);
return neighbors;
}
/**
* 使用Redis存储位置
*/
public void addLocation(String key, Long id, double lat, double lon) {
redisTemplate.opsForGeo().add(key, new Point(lon, lat), id.toString());
}
/**
* 查询附近的位置(Redis GeoRadius)
*/
public List<NearbyLocation> findNearby(String key, double lat, double lon,
double radiusKm, int limit) {
GeoResults<RedisGeoCommands.GeoLocation<String>> results =
redisTemplate.opsForGeo().radius(key,
new Circle(
new Point(lon, lat),
new Distance(radiusKm, Metrics.KILOMETERS)
),
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
.includeDistance()
.includeCoordinates()
.sortAscending()
.limit(limit)
);
List<NearbyLocation> locations = new ArrayList<>();
if (results != null) {
for (GeoResult<RedisGeoCommands.GeoLocation<String>> result : results) {
NearbyLocation loc = new NearbyLocation();
loc.setId(Long.valueOf(result.getContent().getName()));
loc.setLon(result.getContent().getPoint().getX());
loc.setLat(result.getContent().getPoint().getY());
loc.setDistance(result.getDistance().getValue());
locations.add(loc);
}
}
return locations;
}
/**
* 计算两点距离
*/
public double calculateDistance(String key, Long id1, Long id2) {
Distance distance = redisTemplate.opsForGeo()
.distance(key, id1.toString(), id2.toString(), Metrics.KILOMETERS);
return distance != null ? distance.getValue() : -1;
}
}
/**
* 基于GeoHash的附近搜索
*/
@Component
public class GeoHashSearchService {
@Autowired
private MerchantRepository merchantRepository;
/**
* GeoHash附近搜索
* 适用于:数据量不大,无需精确距离排序的场景
*/
public List<Merchant> searchNearbyByGeoHash(double lat, double lon,
double radiusKm) {
// 根据半径确定精度
int precision = getPrecisionByRadius(radiusKm);
// 获取中心点和周围8个区域的GeoHash
String center = GeoHashUtils.encode(lat, lon, precision);
List<String> neighbors = GeoHashUtils.getNeighbors(center);
neighbors.add(center);
// 查询这些GeoHash前缀匹配的记录
// 注意:这只是近似查询,需要在应用层进一步过滤
List<Merchant> candidates = merchantRepository
.findByGeoHashPrefixIn(neighbors.stream()
.map(h -> h.substring(0, precision))
.collect(Collectors.toList()));
// 精确计算距离并过滤
return candidates.stream()
.filter(m -> calculateDistance(lat, lon, m.getLat(), m.getLon()) <= radiusKm)
.sorted(Comparator.comparingDouble(m ->
calculateDistance(lat, lon, m.getLat(), m.getLon())))
.collect(Collectors.toList());
}
private int getPrecisionByRadius(double radiusKm) {
if (radiusKm >= 500) return 3;
if (radiusKm >= 100) return 4;
if (radiusKm >= 10) return 5;
if (radiusKm >= 1) return 6;
return 7;
}
private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
// Haversine公式计算球面距离
final int R = 6371; // 地球半径(km)
double latDistance = Math.toRadians(lat2 - lat1);
double lonDistance = Math.toRadians(lon2 - lon1);
double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2)
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
* Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
}
四、Java空间数据处理
4.1 JPA空间数据映射
/**
* 商家实体(带空间数据)
*/
@Entity
@Table(name = "merchant")
@Data
public class Merchant {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long merchantId;
private String merchantName;
// 空间数据需要使用自定义类型处理器
@Column(columnDefinition = "POINT SRID 4326")
@Convert(converter = PointConverter.class)
private Point location;
@Column(columnDefinition = "POLYGON SRID 4326")
@Convert(converter = PolygonConverter.class)
private Polygon deliveryArea;
private Integer deliveryRadius;
private BigDecimal minDeliveryAmount;
private Integer status;
@Transient
private Double distance; // 查询时动态计算的距离
}
/**
* Point类型转换器
*/
@Converter
public class PointConverter implements AttributeConverter<Point, byte[]> {
private static final WKBWriter wkbWriter = new WKBWriter();
private static final WKBReader wkbReader = new WKBReader();
private static final WKTWriter wktWriter = new WKTWriter();
private static final WKTReader wktReader = new WKTReader();
@Override
public byte[] convertToDatabaseColumn(Point attribute) {
if (attribute == null) return null;
return wkbWriter.write(attribute);
}
@Override
public Point convertToEntityAttribute(byte[] dbData) {
if (dbData == null) return null;
try {
return (Point) wkbReader.read(dbData);
} catch (ParseException e) {
throw new RuntimeException("解析空间数据失败", e);
}
}
/**
* 从经纬度创建Point
*/
public static Point fromLatLon(double lat, double lon) {
GeometryFactory factory = new GeometryFactory();
Point point = factory.createPoint(new Coordinate(lon, lat));
point.setSRID(4326);
return point;
}
}
4.2 MyBatis空间数据操作
/**
* MyBatis空间数据类型处理器
*/
@Component
public class PointTypeHandler extends BaseTypeHandler<Point> {
private static final WKBWriter wkbWriter = new WKBWriter();
private static final WKBReader wkbReader = new WKBReader();
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
Point parameter, JdbcType jdbcType) throws SQLException {
// 使用ST_GeomFromWKB函数
ps.setBytes(i, wkbWriter.write(parameter));
}
@Override
public Point getNullableResult(ResultSet rs, String columnName) throws SQLException {
byte[] bytes = rs.getBytes(columnName);
return parseWKB(bytes);
}
@Override
public Point getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
byte[] bytes = rs.getBytes(columnIndex);
return parseWKB(bytes);
}
@Override
public Point getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
byte[] bytes = cs.getBytes(columnIndex);
return parseWKB(bytes);
}
private Point parseWKB(byte[] bytes) {
if (bytes == null) return null;
try {
return (Point) wkbReader.read(bytes);
} catch (ParseException e) {
throw new RuntimeException("解析WKB失败", e);
}
}
}
/**
* Mapper接口
*/
@Mapper
public interface MerchantMapper {
/**
* 查询附近商家
*/
@Select("SELECT " +
" merchant_id, merchant_name, location, delivery_radius, " +
" ST_Distance_Sphere(location, ST_GeomFromText(#{pointWkt}, 4326)) AS distance " +
"FROM merchant " +
"WHERE ST_Distance_Sphere(location, ST_GeomFromText(#{pointWkt}, 4326)) <= #{radius} " +
"ORDER BY distance " +
"LIMIT #{limit}")
@Results({
@Result(property = "merchantId", column = "merchant_id"),
@Result(property = "merchantName", column = "merchant_name"),
@Result(property = "location", column = "location", typeHandler = PointTypeHandler.class),
@Result(property = "distance", column = "distance")
})
List<Merchant> findNearby(@Param("pointWkt") String pointWkt,
@Param("radius") double radius,
@Param("limit") int limit);
/**
* 查询在配送范围内的商家
*/
@Select("SELECT * FROM merchant " +
"WHERE ST_Contains(delivery_area, ST_GeomFromText(#{pointWkt}, 4326)) " +
"AND status = 1")
List<Merchant> findByDeliveryArea(@Param("pointWkt") String pointWkt);
}
4.3 配送服务实现
/**
* 配送服务
*/
@Service
@Slf4j
public class DeliveryService {
@Autowired
private MerchantMapper merchantMapper;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 检查地址是否在配送范围内
*/
public DeliveryCheckResult checkDeliveryRange(Long merchantId, double lat, double lon) {
Merchant merchant = merchantMapper.selectById(merchantId);
if (merchant == null) {
return DeliveryCheckResult.fail("商家不存在");
}
// 计算距离
double distance = calculateDistance(
merchant.getLocation().getY(), merchant.getLocation().getX(),
lat, lon
);
// 检查是否在配送半径内
if (distance > merchant.getDeliveryRadius()) {
return DeliveryCheckResult.fail(
String.format("超出配送范围,距离%.1fkm,最大配送%.1fkm",
distance / 1000, merchant.getDeliveryRadius() / 1000)
);
}
// 检查是否在多边形配送区域内(如果有)
if (merchant.getDeliveryArea() != null) {
GeometryFactory factory = new GeometryFactory();
Point userPoint = factory.createPoint(new Coordinate(lon, lat));
userPoint.setSRID(4326);
if (!merchant.getDeliveryArea().contains(userPoint)) {
return DeliveryCheckResult.fail("该地址不在配送区域内");
}
}
// 计算配送费
double deliveryFee = calculateDeliveryFee(distance);
return DeliveryCheckResult.success(distance, deliveryFee);
}
/**
* 搜索附近可配送的商家
*/
public List<MerchantVO> searchNearbyMerchants(double lat, double lon,
double radiusKm, String keyword) {
// 构建WKT格式点
String pointWkt = String.format("POINT(%f %f)", lon, lat);
// 查询附近商家
List<Merchant> merchants = merchantMapper.findNearby(
pointWkt, radiusKm * 1000, 50);
// 转换为VO
return merchants.stream()
.filter(m -> keyword == null || m.getMerchantName().contains(keyword))
.map(m -> {
MerchantVO vo = new MerchantVO();
BeanUtils.copyProperties(m, vo);
vo.setDistance(m.getDistance() / 1000); // 转换为km
vo.setDeliveryFee(calculateDeliveryFee(m.getDistance()));
return vo;
})
.collect(Collectors.toList());
}
/**
* 计算配送费
*/
private double calculateDeliveryFee(double distanceMeters) {
double distanceKm = distanceMeters / 1000;
if (distanceKm <= 3) {
return 0; // 3公里内免配送费
} else if (distanceKm <= 5) {
return 5; // 3-5公里5元
} else if (distanceKm <= 10) {
return 10; // 5-10公里10元
} else {
return 10 + (distanceKm - 10) * 2; // 超过10公里每公里2元
}
}
/**
* 计算球面距离(米)
*/
private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
// 使用Haversine公式
final double R = 6371000; // 地球半径(米)
double latDistance = Math.toRadians(lat2 - lat1);
double lonDistance = Math.toRadians(lon2 - lon1);
double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2)
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
* Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
}
五、方案对比与选型
5.1 技术方案对比
┌─────────────────────────────────────────────────────────────────────┐
│ 地理空间处理方案对比 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 方案 │ 精度 │ 性能 │ 复杂度 │ 适用场景 │
│ ──────────────────┼─────────┼─────────┼────────┼──────────────────│
│ MySQL空间索引 │ 高 │ 中 │ 低 │ 中小规模数据 │
│ Redis Geo │ 中 │ 高 │ 低 │ 附近搜索、缓存 │
│ GeoHash │ 中 │ 高 │ 中 │ 分布式系统 │
│ PostGIS │ 高 │ 高 │ 中 │ 专业GIS应用 │
│ Elasticsearch Geo │ 高 │ 高 │ 中 │ 全文+空间搜索 │
│ │
│ 选型建议: │
│ - 已有MySQL,数据量<1000万 → MySQL空间索引 │
│ - 纯附近搜索,高并发 → Redis Geo │
│ - 需要复杂空间分析 → PostGIS │
│ - 需要全文+空间联合搜索 → Elasticsearch │
│ │
└─────────────────────────────────────────────────────────────────────┘
5.2 混合架构方案
/**
* 混合空间数据服务
* MySQL存主数据,Redis做缓存,ES做搜索
*/
@Service
@Slf4j
public class HybridGeoService {
@Autowired
private MerchantRepository merchantRepository;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ElasticsearchRestTemplate elasticsearchTemplate;
/**
* 初始化商家位置到Redis
*/
@PostConstruct
public void initMerchantLocations() {
String key = "merchant:location";
// 清空旧数据
redisTemplate.delete(key);
// 加载所有商家位置
List<Merchant> merchants = merchantRepository.findAll();
for (Merchant m : merchants) {
redisTemplate.opsForGeo().add(key,
new Point(m.getLocation().getX(), m.getLocation().getY()),
m.getMerchantId().toString());
}
log.info("Initialized {} merchant locations to Redis", merchants.size());
}
/**
* 附近搜索(Redis + MySQL)
*/
public List<MerchantVO> searchNearby(double lat, double lon, double radiusKm) {
// 1. 从Redis获取附近商家ID
GeoResults<RedisGeoCommands.GeoLocation<String>> results =
redisTemplate.opsForGeo().radius("merchant:location",
new Circle(new Point(lon, lat), new Distance(radiusKm, Metrics.KILOMETERS)),
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
.includeDistance()
.sortAscending()
.limit(50)
);
if (results == null || results.getContent().isEmpty()) {
return Collections.emptyList();
}
// 2. 获取商家ID列表
List<Long> merchantIds = results.getContent().stream()
.map(r -> Long.valueOf(r.getContent().getName()))
.collect(Collectors.toList());
// 3. 从MySQL查询商家详情
List<Merchant> merchants = merchantRepository.findAllById(merchantIds);
Map<Long, Merchant> merchantMap = merchants.stream()
.collect(Collectors.toMap(Merchant::getMerchantId, m -> m));
// 4. 组装结果(保持Redis返回的顺序)
List<MerchantVO> vos = new ArrayList<>();
for (GeoResult<RedisGeoCommands.GeoLocation<String>> result : results) {
Long id = Long.valueOf(result.getContent().getName());
Merchant m = merchantMap.get(id);
if (m != null) {
MerchantVO vo = new MerchantVO();
BeanUtils.copyProperties(m, vo);
vo.setDistance(result.getDistance().getValue());
vos.add(vo);
}
}
return vos;
}
}
六、最佳实践与注意事项
6.1 最佳实践清单
┌─────────────────────────────────────────────────────────────────────┐
│ 地理空间数据处理最佳实践 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 【数据存储】 │
│ □ 1. 始终使用SRID 4326(WGS84)存储经纬度 │
│ □ 2. 经度在前,纬度在后(X, Y) │
│ □ 3. 建立SPATIAL INDEX索引 │
│ □ 4. 定期验证坐标数据有效性 │
│ │
│ 【查询优化】 │
│ □ 1. 先使用MBR过滤,再精确计算距离 │
│ □ 2. 限制返回结果数量,避免大数据量传输 │
│ □ 3. 缓存热门区域查询结果 │
│ □ 4. 分页查询时避免使用OFFSET,使用上次最大距离 │
│ │
│ 【精度处理】 │
│ □ 1. 距离计算使用ST_Distance_Sphere(球面距离) │
│ □ 2. 注意浮点数精度问题,比较时使用容差 │
│ □ 3. 边界情况处理(跨越180度经线) │
│ │
│ 【性能监控】 │
│ □ 1. 监控空间查询响应时间 │
│ □ 2. 定期分析空间索引使用情况 │
│ □ 3. 大数据量考虑分区或分表 │
│ │
└─────────────────────────────────────────────────────────────────────┘
6.2 常见错误
| 错误 | 问题 | 解决方案 |
|---|---|---|
| 坐标顺序错误 | 经纬度顺序颠倒 | 记住:经度(lon)是X,纬度(lat)是Y |
| 缺少SRID | 空间函数计算错误 | 始终指定SRID 4326 |
| 无空间索引 | 查询性能极差 | 为空间列建立SPATIAL INDEX |
| 平面距离计算 | 长距离误差大 | 使用ST_Distance_Sphere |
| 忽略边界 | 跨越180度经线计算错误 | 使用空间函数处理 |
系列上一篇:JSON与文档存储的高级应用
系列下一篇:时序数据处理与分析
知识点测试
读完文章了?来测试一下你对知识点的掌握程度吧!
评论区
使用 GitHub 账号登录后即可发表评论,支持 Markdown 格式。
如果评论系统无法加载,请确保:
- 您的网络可以访问 GitHub
- giscus GitHub App 已安装到仓库
- 仓库已启用 Discussions 功能