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_440784116 小时前
【HandlerThread实现原理】
android·java·开发语言
leoZ2316 小时前
本地跑大模型实战(七):llama.cpp 性能调优,让推理更快更省
java·人工智能·spring·生成对抗网络·语言模型·自然语言处理·llama
mubei-1238 小时前
SpringDAO的用法
java·开发语言·数据库
happymagic8 小时前
java spring boot做的jar包程序,如何实现自动运行启动
java·运维·服务器·spring boot·jar
都叫我大帅哥10 小时前
给 Java 程序员的 CSRF 攻击详解:从原理到防御实战
java
江南十四行11 小时前
Spring框架核心(上)——IoC控制反转与DI依赖注入详解
java·后端·spring
fb_1234511 小时前
Linux磁盘分区从入门到实操:MBR_GPT全解析+分区工具实战指南
java·linux·gpt
码智社12 小时前
JSON 零基础到资深全体系教程
java·json
wgego13 小时前
基础的反序列化一些总结(php和java)
java·开发语言·笔记
IKUN家族13 小时前
Spring MVC(四)
java·spring·mvc