场景
最近在处理一个空间查询需求:判断 MySQL 中大量带有空间索引的点数据,是否位于一个包含数百个顶点的复杂多边形内。结果集非常大,通常有几万个点
在这个场景下,我们遇到了一个奇怪的性能瓶颈:
- 使用 MySQL 的空间函数(如
ST_Within)进行查询,数据库查询很快(几十到几百毫秒),但回表(Fetch)阶段极慢,长达十几秒 - 如果绕过空间索引,查询又变得非常慢
原因:空间索引回表的独特特性
其实这是 MySQL 空间索引回表特性造成的
它将使用射线法确认点是否真的在多边形内的巨大计算开销,在 Workbench 等工具中统计为了回表耗时
传统 B+Tree 索引 vs. 空间 R-Tree 索引的回表对比
| 特性 | 传统 B+Tree 索引 | 空间 R-Tree 索引 |
|---|---|---|
| 索引作用 | 存储精确值。索引本身就能确定记录是否匹配 | 存储近似值(MBR)。索引只能确定记录可能匹配 |
| 回表目的 | 获取其他列的数据(SELECT *)。索引已经找到了精确的一行或多行数据 | 进行精确的几何计算。索引只找到了候选的一行或多行数据,需要进一步验证 |
| 回表主要开销 | I/O 密集型。成本在于从磁盘上读取那些完整的数据页 | CPU 密集型。成本在于对每个候选几何图形执行精确的数学计算 |
| 计算复杂度 | 几乎可以忽略不计 | 非常高。需要使用射线法等算法,计算点与复杂多边形每条边的位置关系。顶点数量越多,计算量呈线性甚至多项式增长 |
因此,一个复杂的空间查询慢,往往不是慢在索引扫描(找 MBR 候选集),而是慢在回表后对那几十甚至上百个候选几何目标进行 “验明正身” 的 CPU 计算上
性能优化解决方案
传递包裹四边形给 MySQL 做初筛,初筛结果返回后端,通过 JTS 库在内存中做并行计算,得出精确查询结果
@Override
public List<Point> getPoints(Object param) {
if (param.getRange() != null) {
// 1. 利用JTS库,根据查询多边形生成其最小外包矩形(MBR)
Envelope envelope = param.getRange().getEnvelopeInternal();
String geometryWkt = String.format("POLYGON((%.6f %.6f, %.6f %.6f, %.6f %.6f, %.6f %.6f, %.6f %.6f))",
envelope.getMinY(), envelope.getMinX(), // 左下角
envelope.getMaxY(), envelope.getMinX(), // 右下角
envelope.getMaxY(), envelope.getMaxX(), // 右上角
envelope.getMinY(), envelope.getMaxX(), // 左上角
envelope.getMinY(), envelope.getMinX() // 闭合回起点
);
// 2. 使用MBR在数据库层进行快速初筛(利用空间索引)
List<Point> entities = mapper.selectList(buildQueryWrapper(param, geometryWkt));
// 3. 在应用层使用JTS进行精确的包含关系计算(利用并行流提升速度)
// JTS(并发安全)细筛耗时不超过200ms
List<Point> result = entities.parallelStream()
.filter(point -> {
org.locationtech.jts.geom.Point pointJts = GeometryJTSUtil.createPoint(point.getLng(), point.getLat());
return param.getRange().contains(pointJts);
}).toList();
return result;
}
// ... 其他逻辑
}