必须将经纬度转为严格geojson point结构{"type":"point","coordinates":[lng,lat]}才能创建2dsphere索引,顺序错误会导致空间查询结果错误;轨迹数据应采用时间序列集合或按天分片存储,避免单文档超16mb;查询最新轨迹需冗余last_timestamp字段并建索引;$geonear是唯一支持距离返回与后续过滤的空间操作符。

用 2dsphere 索引前必须把经纬度转成 GeoJSON Point 结构
直接存 [lng, lat] 数组或 {lon: 116.4, lat: 39.9} 对象,createIndex({"loc": "2dsphere"}) 会失败并报错 can't create 2dsphere index on non-GeoJSON geometry。MongoDB 只认严格符合 GeoJSON 规范的嵌套结构。
正确写法是:{"loc": {"type": "Point", "coordinates": [116.4, 39.9]}} —— 注意顺序必须是 经度在前、纬度在后,写反了(比如 [39.9, 116.4])能插入、能建索引,但所有 $near、$geoWithin 查询结果全错,且不报任何警告。
- 旧数据迁移可用聚合更新:
db.tracking.updateMany({}, [{$set: {"loc": {type: "Point", coordinates: "$loc"}}}])(前提是原loc是合法[lng, lat]数组) - 应用层插入前务必校验:
Array.isArray(doc.loc) && doc.loc.length === 2 && isFinite(doc.loc[0]) && isFinite(doc.loc[1]) - 字段路径必须精确匹配索引定义,例如索引建在
"position.loc",查询时就得用position.loc,不能只写loc
轨迹数组别硬塞进主文档——16MB 限制不是靠压缩能绕过的
单条物流记录若存数月每分钟 GPS 点,events 数组轻松突破 16MB 文档上限。这不是配置问题,是模型级硬伤。试图用 $push + $slice 控制长度(如 $slice: -500)只能延缓爆仓,无法支撑长期轨迹回溯。
真正可行的紧凑方案只有两个:
-
时间切分:每个文档只存「最近 7 天」轨迹,旧数据归档到
tracking_history集合,用{_id: {tracking_id: "SF123", date: "2026-09-01"}}作复合主键 -
改用时间序列集合(推荐):MongoDB 5.0+ 原生支持,自动分桶、高压缩、带 TTL 过期:
db.createCollection("gps_timeseries", {timeseries: {timeField: "timestamp", metaField: "meta", granularity: "minutes"}});每条轨迹单独文档,meta存{tracking_id: "SF123"},查起来比数组还快
find().sort({"events.timestamp": -1}) 查不到最新轨迹?那是你误解了数组排序语义
"events.timestamp" 是多键索引展开后的隐式字段,sort 行为是对整个文档按「数组中任意一个 timestamp」排序,不是提取数组里最大的那个值。所以它根本不能保证返回的文档里 events[0] 就是最新事件。
要拿到某单最新轨迹,有两条路:
- 聚合取最大值:
{$sortArray: {input: "$events", sortBy: {timestamp: -1}}}配合{$arrayElemAt: [..., 0]},但性能差,不适合高频查询 - 业务层维护冗余字段:
$push同时$set: {last_location: newPoint, last_timestamp: newDate()},再对last_timestamp建普通索引——简单、快、可靠
顺带一提:events.timestamp 上必须建多键索引:db.tracking.createIndex({"events.timestamp": 1}),否则聚合阶段无法高效过滤。
查“配送中车辆离客户最近的那几辆”——别用 $centerSphere,用 $geoNear 配合 distanceField
$centerSphere 只能画圆,真实配送区域是 Polygon 或不规则地理围栏,它完全不适用。而 $geoNear 是唯一能返回带距离字段、支持后续 $match 和 $sort 的空间操作符。
典型写法:
db.gps_timeseries.aggregate([
{
$geoNear: {
near: {type: "Point", coordinates: [116.4, 39.9]},
distanceField: "dist_m",
spherical: true,
maxDistance: 5000,
query: {tracking_id: {$in: ["SF123", "SF456"]}}
}
},
{$match: {"dist_m": {$lt: 1000}}},
{$sort: {"dist_m": 1}}
])
关键点:
-
spherical: true必须显式开启,否则默认平面计算,5km 以上误差可达百米级 -
query参数可提前过滤 tracking_id,避免全量扫描 - 返回的
dist_m单位是米,不用再除以 1000 或乘以 6371
复杂点在于:$geoNear 必须是聚合管道第一个阶段,且一个管道只能有一个。如果业务需要先 $match 再算距离,得把条件挪进 query 里,或者拆成两次查询。











