bigywater.tech
MongoDB · 40

2dsphere 地理索引:$near 怎么查

$near 到底怎么查?结果对不对?这篇用 5 万个点对比 Python 的 haversine 暴力计算,看 explain 的逐圈搜索、多边形的索引键数,以及经纬度顺序写反会怎样。

272 / 272
$near 2000 米内返回的点数与暴力计算的点数,顺序也一致
452 个键
$near 返回 272 个结果时检查的索引键数
5 / 20 / 1
约 1 km² 多边形、约 2500 km² 多边形、点分别生成的 2dsphere 索引条目数

点的索引

实测 · 本机执行输出
  test> db.places.createIndex({loc: "2dsphere"})
  loc_2dsphere
  test> db.places.getIndexes().filter(i => i.name != "_id_")
  [
    {
      v: 2,
      key: { loc: '2dsphere' },
      name: 'loc_2dsphere',
      '2dsphereIndexVersion': 4,
      collation: { locale: 'simple' }
    }
  ]
  validate 的索引条目数: {'_id_': 50000, 'loc_2dsphere': 50000}

2dsphere 索引为每个点生成一个条目,版本 4;GeoJSON 坐标顺序是 [经度, 纬度]。

$near 的正确性

实测 · 本机执行输出
  $near(中心点, 2000 米) 返回 272 个;暴力计算 2000 米内有 272 个;_id 序列完全一致: True
  最近的 5 个(_id, 暴力算出的距离米): [(17981, 88.4), (18246, 136.0), (44372, 273.2), (31564, 279.6), (31867, 288.2)]
  test> mongosh 里同样的 $near 查询的前 5 个 _id
  [ 17981, 18246, 44372, 31564, 31867 ]

2000 米内 $near 返回的 272 个点,与用 haversine 公式在 Python 里暴力计算的集合、顺序完全一致。

怎么扫描

实测 · 本机执行输出
  test> db.places.find({loc: {$near: ..., $maxDistance: 2000}})
  {
    plan: 'GEO_NEAR_2DSPHERE(loc_2dsphere)[FETCH > IXSCAN(loc_2dsphere) | FETCH > IXSCAN(loc_2dsphere) | FETCH > IXSCAN(loc_2dsphere)]',
    nReturned: 272,
    keysExamined: 452,
    docsExamined: 381,
    ms: 1
  }
  $near 2000 米 1.2 ms;$geoWithin/$centerSphere(用 2dsphere 索引)2000 米 0.8 ms;同样的 $geoWithin 强制全表 24 ms

计划是 GEO_NEAR_2DSPHERE 加几段 FETCH + IXSCAN,由近到远逐圈扩展;检查 452 个键、381 个文档返回 272 个。强制全表的 $geoWithin 要 24 ms,用索引不到 1 ms。

$geoNear

实测 · 本机执行输出
  test> db.places.aggregate([{$geoNear: {near: {type: "Point", coordinates: [121.47, 31.23]}, distanceField: "dist", spherical: true, maxDistance: 1000}}, {$limit: 3}, {$project: {name: 1, dist: {$round: ["$dist", 1]}}}])
  [
    { _id: 17981, name: 'p17981', dist: 88.5 },
    { _id: 18246, name: 'p18246', dist: 136.1 },
    { _id: 44372, name: 'p44372', dist: 273.5 }
  ]

聚合的 $geoNear 在结果里给出 distanceField,单位米。

多边形

实测 · 本机执行输出
  small 单独放进一个集合,2dsphere 索引条目数 5
  big   单独放进一个集合,2dsphere 索引条目数 20
  point 单独放进一个集合,2dsphere 索引条目数 1
  三个放在一起的索引条目数: 26
  test> 哪些区域包含点 [121.405, 31.205]
  [ 'small', 'big' ]
  test> 哪些区域包含点 [121.5, 31.4]
  [ 'big' ]

多边形被 S2 单元覆盖,产生多个索引键:约 1 km² 的 5 条,约 2500 km² 的 20 条;点只有 1 条。

经纬度顺序

实测 · 本机执行输出
  test> 数据在 [经度 10, 纬度 45] 附近;按 [10, 45] 查 5 km 内
  125
  test> 写反成 [45, 10](纬度 10、经度 45)—— 仍然合法,只是在另一个地方
  0
  test> 上海的 [纬度 31.23, 经度 121.47] 写反:纬度 121.47 越界,直接报错
  invalid point in geo near query $geometry argument: { type: "Point", coordinates: [ 31.23, 121.47 ] }  Longitude/latitude is out of bounds, lng: 31.23 lat: 121.
  test> 插入纬度 95 的点
  Could not extract geo keys at path 'loc' for document with _id ObjectId('6acc46c1aa3177504c05e95d'): Longitude/latitude is out of bounds, lng: 121.47 lat: 95

写反了不一定报错:在合法范围内只是查不到(0 个),越界才报错。

分步演示

2dsphere 地理索引
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MongoDB 9.0.2(Homebrew 的 mongod、mongos)/ mongosh 2.12.0 / pymongo 4.18.3;每个实验在临时目录里起全新的 mongod(独立 dbpath,端口 9900 以上,只监听 127.0.0.1,WiredTiger 缓存 0.25 GB),需要副本集或分片集群时在本机起多个进程;命令输出来自真实的 mongosh,批量写入、计时与多线程用 pymongo;计时用 time.perf_counter,数字每次运行会有小幅抖动,ObjectId、时间戳等每次运行都不同。haversine 暴力计算用地球半径 6371008.8 米;随机点由固定种子生成
    • 没有验证:2d(平面)索引与 legacy 坐标对;$geoWithin 的 $geometry 多边形与跨 180 度经线;2dsphere 复合索引(与非地理字段);S2 覆盖精度参数(2dsphereCoarsestIndexedLevel);$near 在分片集群上的行为;大规模数据下的性能
    京ICP备2026067502号-1京公网安备11010502063487号