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

相关推荐
从此以后自律3 小时前
Java 并发编程完整讲解
java·开发语言
Sylvia33.3 小时前
足球数据接口开发实战:如何用火星数据API盘活赛事应用
java·服务器·开发语言·数据库·python
小小放舟、3 小时前
PaiCLI-Demo:从零实现 ReAct Agent + Tool Call
java·后端·intellij-idea·springboot·agent·react·tool call
ch.ju4 小时前
Java程序设计(第3版)第四章——匿名内部类
java·开发语言
ruleslol4 小时前
Mybatis11-不同的数据库连接池的配置
数据库·mybatis
C++、Java和Python的菜鸟5 小时前
第2章 前端Web基础(js、vue+Ajax)
java
harmful_sheep5 小时前
idea相关设置
java·ide·intellij-idea
奋发向前wcx5 小时前
y1,y2总复习笔记2 2026.7.15
java·笔记·算法
han_hanker5 小时前
mybatis-plus 常用方法
java·tomcat·mybatis
李少兄6 小时前
MyBatis、MyBatis-Plus 与全自动 ORM 的本质区别
mybatis