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 才能真正"快"起来。

相关推荐
mldong1 小时前
一个 App,十三套后端:手机审批端 uni-jeeflow-app 开源了
java·架构
tqs_123452 小时前
MySQL RR隔离级别死锁|Gap间隙锁、临键锁,订单并发范围查询死锁根因方案
java
逆境不可逃2 小时前
Pi Agent 学习笔记:多个工具怎样并行执行
java
Flynt3 小时前
Java 27 升级实测:默认值动得比新特性多,有个老参数会让 JVM 直接起不来
java·jvm·后端
Bs_MoneyMagnet3 小时前
基于springboot+vue的个人健康管理系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·vue3·springboot3·计算机毕业设计
星云API技术支持3 小时前
企业微信二次开发:群权限设置、成员管理与群资料维护的接口组合实践
java·前端·企业微信
niucloud-admin4 小时前
JAVA V6 多商户商城 开发文档——job 计划任务开发
java·python·github
海宇AI5 小时前
零信任架构实战:基于海宇柠檬查出险-登记证构建自动化车抵贷核保网关
java·人工智能·架构·自动化
cfm_29146 小时前
单例模式详解
java
我叫张土豆6 小时前
本体论、DDD、SDD、TDD:AI 编程时代的四层认知栈
java·人工智能