顺序 | 类型 | 说明 | 查询示例 |
E | Equality(等值) | 索引最左位置,精确匹配可快速缩小扫描范围 | { status: "paid" } |
S | Sort(排序) | 紧跟 E 之后,利用索引有序性,避免内存排序 | .sort({ createTime: -1 }) |
R | Range(范围) | 索引最右位置,范围扫描后索引有序性中断,后续字段退化为逐条过滤 | { createTime: { $gte: ISODate("2024-01-01") } } |
// 业务查询语句db.t_orders.find({userId: 10086, // E: 等值status: "paid", // E: 等值createTime: { $gte: ISODate("2024-02-01") } // R: 范围}).sort({ amount: -1 }) // S: 排序// 错误:Range 在 Sort 之前,索引在 createTime 发散后无法支撑 amount 排序,触发内存排序db.t_orders.createIndex({ userId: 1, status: 1, createTime: 1, amount: -1 },{ background: true })// 正确:ESR 顺序,等值过滤 → 索引排序 → 范围截断db.t_orders.createIndex({ userId: 1, status: 1, amount: -1, createTime: 1 },{ background: true })
查询条件 | 索引利用率 | 执行说明 |
{ a: 1 } | 完全利用 | 命中最左前缀 a。 |
{ a: 1, b: 1 } | 完全利用 | 命中最左前缀 a, b。 |
{ a: 1, b: 1, c: 1 } | 完全利用 | 完整匹配全部索引字段 a, b, c。 |
{ a: 1, c: 1 } | 部分利用 | 仅能利用最左字段 a 进行索引分支定位,因缺失中间字段 b,导致 c 字段的索引匹配被阻断。 |
{ b: 1 } | 无法使用 | 缺失最左字段 a,失去搜索基准,触发全表扫描。 |
{ b: 1, c: 1 } | 无法使用 | 缺失最左字段 a,触发全表扫描。 |
// 已有复合索引db.t_orders.createIndex({ userId: 1, status: 1, createTime: 1 },{ background: true })// 正确:包含最左字段 userId,索引完全命中db.t_orders.find({ userId: 10086, status: "paid" })// 部分命中:包含最左字段 userId,但跳过 status,仅利用 userId 定位db.t_orders.find({ userId: 10086, createTime: { $gte: ISODate("2024-01-01") } })// 错误:缺失最左字段 userId,触发全表扫描db.t_orders.find({ status: "pending" })// 解决方案:为独立查询字段单独建立索引db.t_orders.createIndex({ status: 1 },{ background: true })
区分度级别 | 数据特征 | 字段示例 | 索引排位策略 |
高(High) | 具有唯一性或极少重复 | 用户 ID、订单号 | 应优先作为复合索引的最前置字段。 |
中(Medium) | 存在一定重复,但分类较多 | 城市、商品分类 | 有效。通常排在复合索引的中段,需联合使用。 |
低(Low) | 值域极小,海量重复 | 性别、布尔值 | 差。若必须纳入复合索引,应严格置于最末端。 |
{userId, status}:随着业务的需求变化,产生以下两种查询需求:db.orders.find({ userId: "U123" })db.orders.find({ status: "completed" }){status, userId}索引匹配最左边的 status 字段,其性能也会很差。 // 错误:未指定 background,可能阻塞所有操作db.t_orders.createIndex({ customerId: 1 })// 单个创建:开启 background 模式,避免阻塞业务db.t_orders.createIndex({ customerId: 1, createTime: -1 },{ background: true })
资源维度 | 性能损耗说明 |
写入延迟(Write I/O) | 每次执行 Insert、Update 或 Delete 时,数据库必须同步遍历并更新所有相关索引的 B-Tree 结构。索引越多,单次写入的 I/O 开销呈线性增长。 |
存储占用(Disk) | 每一个索引均是一棵独立的数据树,会占用大量的物理磁盘空间。 |
// 第一步:查看集合当前所有索引db.t_orders.getIndexes()// 第二步:通过 $indexStats 排查每个索引的实际使用情况db.t_orders.aggregate([{ $indexStats: {} }]).forEach(function(idx) {print("索引名称:", idx.name,"| 调用次数:", idx.accesses.ops,"| 最近使用:", idx.accesses.since)})// 第三步:查看索引与数据的存储占比var stats = db.t_orders.stats()print("数据大小:", (stats.size / 1024 / 1024).toFixed(2), "MB")print("索引大小:", (stats.totalIndexSize / 1024 / 1024).toFixed(2), "MB")print("索引/数据比:", (stats.totalIndexSize / stats.size * 100).toFixed(1), "%")// 第四步:删除调用次数为 0 的冗余索引// 操作前务必确认该索引确实无业务依赖db.t_orders.dropIndex("idx_legacy_report_1")db.t_orders.dropIndex("idx_legacy_report_2")// 注意:禁止删除 _id 默认索引// db.t_orders.dropIndex("_id") // 此操作会报错,_id 索引不可删除
// 第一步:执行统计聚合,识别无用索引db.t_orders.aggregate([{ $indexStats: {} }])/* 输出示例分析:{"name": "idx_old_field","accesses": {"ops": NumberLong(0), // 关键指标:调用次数为 0"since": ISODate("2024-01-01T00:00:00Z")}}*/// 第二步:逻辑隐藏(支持快速回滚,MongoDB 4.4+)db.t_orders.hideIndex("idx_old_field")// 观察期:持续监控 1-7 天,若业务受损可立即执行 db.t_orders.unhideIndex("idx_old_field") 恢复。// 第三步:物理删除(确认无损后执行)db.t_orders.dropIndex("idx_old_field")
特性 / 限制 | 规范说明 |
单键约束 | TTL 索引必须是单键索引(Single Field Index)。若 TTL 字段被放入复合索引中,TTL 自动回收特性将直接失效。 |
数据类型 | 目标字段的数据类型必须是 Date 类型(或包含 Date 类型的数组),否则引擎不会执行清理。 |
清理延迟 | 并非到期精确秒级删除。默认情况下,TTLMonitor 线程每60秒轮询一次,因此数据的物理删除存在分钟级的延迟。 |
引擎参数控制 | 线上若存在性能波动,DBA 可通过动态调整 ttlMonitorSleepSecs(控制轮询休眠间隔)和 ttlDeleteBatch(控制单批次删除量)来进一步平抑 I/O 资源消耗。具体操作,请参见 参数配置。 |
// 模式一:固定时长淘汰(例如:按 createTime 字段,30 天后过期)db.t_sessions.createIndex({ createTime: 1 },{ expireAfterSeconds: 2592000, background: true })// 模式二:精准时间戳淘汰(推荐,由业务层动态决定存活时间)// 将 expireAfterSeconds 设为 0,引擎将在 expireAt 时间点到达时进行清理db.t_sessions.createIndex({ expireAt: 1 },{ expireAfterSeconds: 0, background: true })// 业务层插入时,直接指定具体的死亡时间db.t_sessions.insertOne({sessionId: "sess_001",userId: "user_001",expireAt: new Date(Date.now() + 3600000) // 动态指定 1 小时后过期})
特性 / 限制 | 规范说明 |
必选参数 timeField | 创建时序集合时必须指定 timeField,该字段的值必须是 Date 类型,用于标识每条测量值的时间戳。MongoDB 据此自动分桶存储。 |
推荐参数 metaField | metaField 用于标识数据源(如设备 ID、传感器编号),MongoDB 据此对数据进行分区。建议选择很少变化的标识符,避免使用数组类型。 |
粒度设置 granularity | 建议根据同一数据源相邻测量值的时间间隔选择粒度: seconds(桶跨度1小时)、minutes(桶跨度24小时)、hours(桶跨度30天)。粒度过粗导致单桶数据量过大查询变慢,粒度过细导致桶数量激增浪费存储。 |
自动索引 | MongoDB 6.0+自动在 timeField 和 metaField 上创建二级索引,低版本需手动创建。 |
不支持的操作 | 时序集合不支持 distinct() 高效执行(建议用 $group 聚合替代);8.0以下版本不支持区域分片(Zone Sharding);metaField 一旦定义不可更换为其他字段。 |
metaField(设备 ID)分区、按 timeField 分桶压缩,存储空间减少约70%,相同查询延迟降至200ms以内,同时无需额外维护数据归档脚本。// 推荐:创建时序集合(传感器每 5 分钟上报一次)db.createCollection("t_sensor_data", {timeseries: {timeField: "timestamp", // 必选:时间戳字段metaField: "sensorId", // 推荐:数据源标识granularity: "minutes" // 粒度匹配采集频率(5 分钟 → minutes)}})// 推荐:创建时序集合(系统监控每秒采集)db.createCollection("t_metrics", {timeseries: {timeField: "ts",metaField: "metadata", // metadata 可以是对象,如 { host: "web01", region: "bj" }granularity: "seconds" // 粒度匹配采集频率(秒级 → seconds)}})// 插入时序数据db.t_sensor_data.insertMany([{ sensorId: "sensor_001", timestamp: new Date(), temperature: 23.5, humidity: 65 },{ sensorId: "sensor_001", timestamp: new Date(), temperature: 23.6, humidity: 64 },{ sensorId: "sensor_002", timestamp: new Date(), temperature: 18.2, humidity: 72 }], { ordered: false }) // ordered: false 提升批量写入性能// 错误:在普通集合上手动管理时序数据db.createCollection("t_sensor_data_old")db.t_sensor_data_old.createIndex({ sensorId: 1, timestamp: -1 })// 需要自行处理数据归档、压缩、清理,维护成本高// 查询最近 24 小时某设备的数据(自动利用时序索引)db.t_sensor_data.find({sensorId: "sensor_001",timestamp: { $gte: new Date(Date.now() - 86400000) }}).sort({ timestamp: -1 })// 用 $group 聚合替代 distinct()(时序集合不支持高效 distinct)db.t_sensor_data.aggregate([{ $match: { timestamp: { $gte: new Date(Date.now() - 86400000) } } },{ $group: { _id: "$sensorId" } }])
MongoDB 版本 | 默认内存限制 | 超限后果 |
4.2及以前 | 32MB | 报错,查询失败 |
4.4+ | 100MB | 报错,查询失败 |
db.t_orders.find({ status: "paid" }).sort({ amount: -1 }),涉及千万级订单数据。Sort operation used more than the maximum 33554432 bytes of RAM,报表功能不可用,影响运营决策2天。explain("executionStats") 确认执行计划存在 SORT 阶段,排序字段 amount 缺少索引,触发内存排序。建立 { status: 1, amount: -1 } 复合索引,status 用于过滤、amount 利用索引有序性完成排序,SORT 阶段消除,响应时间从超时降至200ms。// 错误:排序字段未建索引,可能触发内存排序db.t_orders.find({ status: "paid" }).sort({ createTime: -1 })// 正确:排序字段包含在索引中db.t_orders.createIndex({ status: 1, createTime: -1 }, { background: true })db.t_orders.find({ status: "paid" }).sort({ createTime: -1 }) // 索引排序,无内存限制// 验证是否使用索引排序db.t_orders.find({ status: "paid" }).sort({ createTime: -1 }).explain("executionStats")// 检查是否有 SORT 阶段,无 SORT 表示使用了索引排序
$ne、$nin、无前缀 $regex、$where 等低效操作符。操作符 | 问题 | 优化方案 |
$ne | 需扫描所有非匹配值 | 改用 $in 列出有效值 |
$nin | 需扫描所有不在列表中的值 | 改用 $in 正向匹配 |
$not | 通常无法利用索引 | 改用正向条件 |
$regex(无前缀) | 前缀无锚定无法利用索引 | 使用前缀锚定 /^prefix/ |
$where | JavaScript 执行,极慢 | 改用标准查询操作符 |
$exists: false | 需扫描所有文档 | 使用稀疏索引或重新设计 |
// 低效:$ne 无法有效利用索引db.t_orders.find({ status: { $ne: "cancelled" } })// 优化:$in 列出所有有效状态db.t_orders.find({ status: { $in: ["pending", "paid", "shipped", "completed"] } })// 低效:无前缀正则,全表扫描db.t_users.find({ name: /张/ })// 优化:前缀锚定正则,可利用索引db.t_users.find({ name: /^张/ })
检查项 | 验证方法 | 通过标准 |
所有查询都命中索引 | explain("executionStats") | stage 为 IXSCAN,无 COLLSCAN |
无内存排序 | explain("executionStats") | 无 SORT 阶段 |
扫描效率合理 | totalDocsExamined / nReturned | 比值接近1 |
复合索引遵循 ESR | 审查索引字段顺序 | Equality → Sort → Range |
索引数量可控 | db.collection.getIndexes() | ≤ 10个 |
建索引使用 background | 审查建索引命令 | 包含 background: true |
检查项 | 验证方法 | 操作建议 |
无用索引清理 | $indexStats | ops: 0 的索引评估后删除 |
索引使用率 | $indexStats | 低使用率索引考虑删除 |
索引大小 | db.collection.stats().indexSizes | 异常大的索引需要分析 |
db.t_orders.find({ status: "paid" }).explain("executionStats")
// 关键指标,接近 1:索引高效,扫描即命中totalDocsExamined / nReturned ≈ 1 // 理想值
// 在 explain 结果中定位排序阶段executionStats.executionStages.stage// 或嵌套在 inputStage / inputStages 中
stage 值 | 含义 | 是否需要优化 |
SORT | 内存排序,未利用索引有序性 | 需优化 |
SORT_KEY_GENERATOR | 正在提取排序键,配合 SORT 出现 | 需优化 |
无 SORT 阶段 | 排序由索引天然有序性完成 | 无需优化 |
// 不符合 ESR:范围字段在前,排序字段在后db.t_orders.createIndex({ createTime: 1, status: 1 })db.t_orders.find({ createTime: { $gte: ISODate("2024-01-01") }, status: "paid" }).sort({ amount: -1 })// 符合 ESR:等值 → 排序 → 范围db.t_orders.createIndex({ status: 1, amount: -1, createTime: 1 })
对比维度 | 多个单字段索引 | 复合索引 |
索引方案 | { status: 1 } + { userId: 1 } + { createTime: 1 } | { status: 1, userId: 1, createTime: -1 } |
查询过程 | 查询一般只有一个字段走索引 | 单次 B-Tree 查找直接定位 |
排序 | 索引无法覆盖排序,可能触发内存 SORT | 索引天然有序,无需内存排序 |
覆盖查询 | 无法实现,必须回表取完整文档 | 若查询字段全部包含在索引中,可直接返回结果,无需回表 |
内存开销 | 整体内存开销更大 | 无额外开销 |
// 多个单字段索引:MongoDB 尝试索引交集,效率不稳定db.t_orders.createIndex({ status: 1 })db.t_orders.createIndex({ userId: 1 })db.t_orders.createIndex({ createTime: 1 })// 复合索引:一个索引覆盖查询 + 排序,遵循 ESR 原则db.t_orders.createIndex({ status: 1, userId: 1, createTime: -1 })// 当查询模式不固定、字段组合多变时,单字段索引更灵活:// 场景:运营后台的动态筛选,用户可能按任意字段组合查询,此时为每种组合建复合索引不现实,单字段索引 + 索引交集是合适的选择db.t_orders.find({ status: "paid" }) // 只按状态db.t_orders.find({ userId: "u_10001" }) // 只按用户db.t_orders.find({ createTime: { $gte: ISODate("...") } }) // 只按时间db.t_orders.find({ status: "paid", userId: "u_10001" }) // 状态 + 用户
sh.stopBalancer()
db.orders.createIndex({ userId: 1, createTime: -1 }, { background: true })
// 查看当前正在执行的索引构建任务db.currentOp({ "command.createIndexes": { $exists: true } })
文档反馈