目录
-
- 一、开篇痛点:存进去了,能查了吗?
- 二、矩形范围查询(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 Scan 或 Index 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_IntersectsvsST_DWithin) - 选对类型(geometry vs geography)
- 建对索引(GIST)
- 保证 SRID 一致
这四点做对了,PostGIS 才能真正"快"起来。