剖析 MySQL 空间索引回表性能瓶颈:从十几秒到百毫秒的优化实战

55 阅读2分钟

场景

最近在处理一个空间查询需求:判断 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;
    }
    // ... 其他逻辑
}