MyBatis-Plus + PostGIS 实战(二):空间查询与空间索引

目录

    • 一、开篇痛点:存进去了,能查了吗?
    • 二、矩形范围查询(BBox)
      • [2.1 业务场景](#2.1 业务场景)
      • [2.2 SQL 本质](#2.2 SQL 本质)
      • [2.3 Mapper 接口](#2.3 Mapper 接口)
    • [三、距离查询(附近 POI)](#三、距离查询(附近 POI))
      • [3.1 业务场景](#3.1 业务场景)
      • [3.2 关键坑位:Geometry 的距离单位不是米!](#3.2 关键坑位:Geometry 的距离单位不是米!)
      • [3.3 正确写法(推荐)](#3.3 正确写法(推荐))
      • [3.4 Mapper 接口](#3.4 Mapper 接口)
    • 四、代码
      • [4.1 PoiMapper.xml](#4.1 PoiMapper.xml)
      • [4.2 PoiMapper](#4.2 PoiMapper)
      • [4.3 PoiService](#4.3 PoiService)
      • [4.4 Controller](#4.4 Controller)
      • [4.5 接口测试](#4.5 接口测试)
    • [五、空间索引(GIST)------ 性能命门](#五、空间索引(GIST)—— 性能命门)
      • [5.1 创建索引](#5.1 创建索引)
      • [5.2 验证索引是否生效](#5.2 验证索引是否生效)
      • [5.3 常见失效原因](#5.3 常见失效原因)
    • [六、踩坑复盘:手写 SQL 导致 geom = null](#六、踩坑复盘:手写 SQL 导致 geom = null)
    • [七、SRID 一致性问题](#七、SRID 一致性问题)
    • 八、小结

一、开篇痛点:存进去了,能查了吗?

前几篇我们解决了 Geometry 字段的 CRUD、Swagger 展示、TypeHandler 生效规则以及 Geometry vs Geography 的选型问题。

"那我怎么查'我附近的 POI'?怎么查'某个矩形范围内的点'?"

这一篇,我们就来回答这个问题。

二、矩形范围查询(BBox)

2.1 业务场景

矩形/多边形范围查询是地图类应用 80% 的查询场景:

  • 地图缩放/拖拽时加载可见区域 POI
  • 列表页初次加载
  • 后台网格筛选

2.2 SQL 本质

sql 复制代码
SELECT *
FROM poi
WHERE ST_Intersects(
    geom,
    ST_MakeEnvelope(116.3, 39.8, 116.5, 40.0, 4326)
);

2.3 Mapper 接口

java 复制代码
List<PoiEntity> selectByBBox(
    @Param("minX") Double minX,
    @Param("minY") Double minY,
    @Param("maxX") Double maxX,
    @Param("maxY") Double maxY
);

三、距离查询(附近 POI)

3.1 业务场景

  • 周边的店
  • 外卖 / 打车

3.2 关键坑位:Geometry 的距离单位不是米!

如果你直接用 geometry 做距离查询,单位会是角度(度),不是米!

这也是我们在前几篇中选型的直接回报------必须理解 Geometry vs Geography 的区别。

3.3 正确写法(推荐)

sql 复制代码
SELECT *
FROM poi
WHERE ST_DWithin(
    geom::geography,
    ST_SetSRID(ST_MakePoint(116.397, 39.903), 4326)::geography,
    1000  -- 米
);

3.4 Mapper 接口

java 复制代码
List<PoiEntity> selectNearby(
    @Param("lon") Double lon,
    @Param("lat") Double lat,
    @Param("radius") Integer radius
);

四、代码

4.1 PoiMapper.xml

java 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.pgd.postgisdemo.mapper.PoiMapper">
    <resultMap id="BaseResultMap" type="org.pgd.postgisdemo.entity.PoiEntity">
        <id column="id" property="id"/>
        <result column="name" property="name"/>
        <result column="category" property="category"/>
        <result
                column="geom"
                property="geom"
                typeHandler="org.pgd.postgisdemo.handler.GeometryTypeHandler"/>
    </resultMap>

    <select id="selectByBBox" resultMap="BaseResultMap">
        SELECT id, name, category, geom
        FROM poi
        WHERE ST_Intersects(
        geom,
        ST_MakeEnvelope(#{minX}, #{minY}, #{maxX}, #{maxY}, 4326)
        )
    </select>
    <select id="selectNearby" resultMap="BaseResultMap">
        SELECT id, name, category, geom
        FROM poi
        WHERE ST_DWithin(
        geom::geography,
        ST_SetSRID(ST_MakePoint(#{lon}, #{lat}), 4326)::geography,
        #{radius}
        )
    </select>
</mapper>

4.2 PoiMapper

java 复制代码
public interface PoiMapper extends BaseMapper<PoiEntity> {
    List<PoiEntity> selectByBBox(
            @Param("minX") Double minX,
            @Param("minY") Double minY,
            @Param("maxX") Double maxX,
            @Param("maxY") Double maxY
    );
    List<PoiEntity> selectNearby(
            @Param("lon") Double lon,
            @Param("lat") Double lat,
            @Param("radius") Integer radius
    );
}

4.3 PoiService

java 复制代码
public interface PoiService extends IService<PoiEntity> {

    List<PoiEntity> selectByBBox(Double minX, Double minY, Double maxX, Double maxY);

    List<PoiEntity> selectNearby(Double lon, Double lat, Integer radius);
}
@Service
public class PoiServiceImpl
        extends ServiceImpl<PoiMapper, PoiEntity>
        implements PoiService {
 @Override
    public List<PoiEntity> selectByBBox(Double minX, Double minY, Double maxX, Double maxY) {
        return baseMapper.selectByBBox(minX, minY, maxX, maxY);
    }

    @Override
    public List<PoiEntity> selectNearby(Double lon, Double lat, Integer radius) {
        return baseMapper.selectNearby(lon, lat, radius);
    }
}    

4.4 Controller

java 复制代码
 @GetMapping("/bbox")
    public R<List<PoiEntity>> bbox(
            @RequestParam double minX,
            @RequestParam double minY,
            @RequestParam double maxX,
            @RequestParam double maxY) {
        return R.ok(poiService.selectByBBox(minX, minY, maxX, maxY));
    }

    @GetMapping("/nearBy")
    public R<List<PoiEntity>> bbox(
            @RequestParam double lon,
            @RequestParam double lat,
            @RequestParam Integer radius) {
        return R.ok(poiService.selectNearby(lon, lat, radius));
    }

4.5 接口测试

测试库的编的数据如下图

bbox接口测试结果

nearBy接口测试结果

五、空间索引(GIST)------ 性能命门

5.1 创建索引

sql 复制代码
CREATE INDEX idx_poi_geom
ON poi
USING GIST (geom);

5.2 验证索引是否生效

sql 复制代码
EXPLAIN ANALYZE
SELECT * FROM poi
WHERE ST_Intersects(
    geom,
    ST_MakeEnvelope(116.3, 39.8, 116.5, 40.0, 4326)
);
  • 看到 Bitmap Heap ScanIndex Scan → 索引生效
  • 看到 Seq Scan → 索引失效

5.3 常见失效原因

原因 说明 解决方案
函数套太多 ST_Buffer(geom, 1000) 简化 SQL,避免嵌套空间函数
SRID 不一致 混合使用 SRID 0 和 4326 统一使用 4326
数据量太小 少于 1000 条时优化器可能不走索引 正常,数据量大了自然走索引

六、踩坑复盘:手写 SQL 导致 geom = null

在开发 bbox 查询接口时,我遇到了奇怪的现象:

  • SQL 能查到数据
  • id / name / category 都有值
  • 唯独 geom 为 null ❌

排查后发现,原因是:手写 SQL + resultType 不会触发 @TableField(typeHandler=...)

这正是我们之前总结的那条规则:

TypeHandler 不是写了就生效,而是在正确的映射规则下才生效。

解决方案:使用 XML resultMap 显式指定 TypeHandler:

xml 复制代码
<resultMap id="BaseResultMap" type="...PoiEntity">
    <id column="id" property="id"/>
    <result column="geom" property="geom"
            typeHandler="...GeometryTypeHandler"/>
</resultMap>

<select id="selectByBBox" resultMap="BaseResultMap">
    SELECT id, name, category, geom
    FROM poi
    WHERE ST_Intersects(...)
</select>

七、SRID 一致性问题

空间查询中 SRID 不一致是最常见的错误之一:

复制代码
Operation on mixed SRID geometries
(Point, 4326) != (Polygon, 0)

解决方案

  • 数据库字段:geometry(Point, 4326)
  • 查询参数:ST_SetSRID(..., 4326)
  • 不要混用 SRID 0

八、小结

空间查询并不难,难的是:

  • 选对函数(ST_Intersects vs ST_DWithin
  • 选对类型(geometry vs geography)
  • 建对索引(GIST)
  • 保证 SRID 一致

这四点做对了,PostGIS 才能真正"快"起来。

相关推荐
weixin_3993806918 分钟前
TongWeb7.0.4.9M11一键安装脚本(by sy)
java·spring
CallFay云起未来24 分钟前
AI客服能不能减少人工回复?从重复咨询到人机协同的落地分析
java·大数据·人工智能·文心一言
我命由我1234528 分钟前
Android 开发 - 获取当前设备屏幕的旋转角度
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
Elastic 中国社区官方博客32 分钟前
OpenTelemetry Java 扩展:无需分叉 agent 即可自定义追踪
java·大数据·运维·开发语言·数据库·人工智能·elasticsearch
京东云开发者44 分钟前
上游给空、下游拿到 -1,我把故障一直追到了 commons-beanutils 的构造函数
java·ai编程
阿无,1 小时前
Java多线程面试题之AQS
java·开发语言
数据狐(Datafox)1 小时前
京东商品列表API技术解析与落地应用(含标准 JSON 示例)
java·大数据·前端·人工智能·python·数据分析·json
摇滚侠1 小时前
《SpringBoot 3:入门与应用实战》第 7 章 AOP 思想与实现 阅读笔记 15
java·spring boot·笔记
风尘小子1 小时前
java静态类使用@Value动态绑定环境变量
java
重生之后端学习2 小时前
239. 滑动窗口最大值[困难]✅
java·数据结构·算法·leetcode·职场和发展