MongoDB索引优化实战:让查询飞起来

写在前面:索引是数据库查询性能的关键,MongoDB提供了丰富的索引类型来满足不同场景的需求。本篇将详细介绍MongoDB索引的创建、使用、管理和优化技巧,帮助您打造高效的MongoDB查询。


文章目录

    • 一、索引基础概念
      • [1.1 什么是索引?](#1.1 什么是索引?)
      • [1.2 索引结构](#1.2 索引结构)
      • [1.3 索引类型](#1.3 索引类型)
    • 二、单字段索引
      • [2.1 创建单字段索引](#2.1 创建单字段索引)
      • [2.2 查看索引](#2.2 查看索引)
      • [2.3 删除索引](#2.3 删除索引)
    • 三、复合索引
      • [3.1 创建复合索引](#3.1 创建复合索引)
      • [3.2 复合索引原理](#3.2 复合索引原理)
      • [3.3 最左前缀原则](#3.3 最左前缀原则)
      • [3.4 索引顺序选择](#3.4 索引顺序选择)
    • 四、多键索引
      • [4.1 数组字段索引](#4.1 数组字段索引)
      • [4.2 嵌套数组索引](#4.2 嵌套数组索引)
    • 五、文本索引
      • [5.1 创建文本索引](#5.1 创建文本索引)
      • [5.2 文本搜索](#5.2 文本搜索)
      • [5.3 文本搜索排序](#5.3 文本搜索排序)
    • 六、唯一索引
      • [6.1 创建唯一索引](#6.1 创建唯一索引)
      • [6.2 唯一索引与稀疏索引](#6.2 唯一索引与稀疏索引)
    • 七、地理空间索引
      • [7.1 创建地理空间索引](#7.1 创建地理空间索引)
      • [7.2 地理位置查询](#7.2 地理位置查询)
    • 八、索引管理
      • [8.1 查看索引统计](#8.1 查看索引统计)
      • [8.2 重建索引](#8.2 重建索引)
      • [8.3 索引属性](#8.3 索引属性)
    • 九、查询分析与优化
      • [9.1 使用 explain](#9.1 使用 explain)
      • [9.2 性能指标解读](#9.2 性能指标解读)
      • [9.3 慢查询日志](#9.3 慢查询日志)
    • 十、实战:百万级数据优化
      • [10.1 创建测试数据](#10.1 创建测试数据)
      • [10.2 优化前后对比](#10.2 优化前后对比)
      • [10.3 索引设计原则](#10.3 索引设计原则)
    • 十一、总结

一、索引基础概念

1.1 什么是索引?

复制代码
📚 索引概念:

索引就像书籍的目录,
可以快速定位到需要的内容,
而不需要逐页翻找。

无索引:全表扫描 → 慢
有索引:直接定位 → 快

1.2 索引结构

复制代码
📊 B-Tree索引结构:

                    [中间节点]
                   /    |    \
            [叶子节点1] [叶子节点2] [叶子节点3]
               ↓          ↓          ↓
            数据页      数据页      数据页

特点:
- 平衡树结构,查询复杂度 O(log n)
- 叶子节点按顺序存储
- 支持范围查询和排序

1.3 索引类型

复制代码
🗂️ MongoDB索引类型:

1. 单字段索引 - 最常用
2. 复合索引 - 多字段组合
3. 多键索引 - 数组字段
4. 文本索引 - 全文搜索
5. 哈希索引 - 等值查询
6. 地理空间索引 - 位置查询
7. 唯一索引 - 约束
8. 稀疏索引 - 稀疏字段

二、单字段索引

2.1 创建单字段索引

javascript 复制代码
// 为 age 字段创建索引
db.users.createIndex({ age: 1 })

// 1 表示升序,-1 表示降序
db.users.createIndex({ age: -1 })

// 为嵌套字段创建索引
db.orders.createIndex({ "customer.address.city": 1 })

// 指定索引名称
db.users.createIndex(
    { age: 1 },
    { name: "idx_user_age" }
)

2.2 查看索引

javascript 复制代码
// 查看集合的所有索引
db.users.getIndexes()

// 返回结果示例:
[
    {
        "v": 2,
        "key": { "_id": 1 },
        "name": "_id_"
    },
    {
        "v": 2,
        "key": { "age": 1 },
        "name": "age_1"
    }
]

2.3 删除索引

javascript 复制代码
// 根据索引名称删除
db.users.dropIndex("age_1")

// 根据索引键删除
db.users.dropIndex({ age: 1 })

// 删除所有非默认索引
db.users.dropIndexes()

// 注意:不能删除 _id 索引

三、复合索引

3.1 创建复合索引

javascript 复制代码
// 为 users 集合创建复合索引:先按 city 排序,再按 age 排序
db.users.createIndex({ city: 1, age: -1 })

// 索引顺序:city(升序) + age(降序)

3.2 复合索引原理

复制代码
🔍 复合索引工作原理:

假设索引:{ city: 1, age: -1 }

数据示例:
{ city: "北京", age: 25 }
{ city: "北京", age: 30 }
{ city: "上海", age: 20 }
{ city: "上海", age: 28 }
{ city: "深圳", age: 25 }

索引存储顺序:
1. 北京-30
2. 北京-25
3. 深圳-25
4. 上海-28
5. 上海-20

3.3 最左前缀原则

复制代码
💡 最左前缀原则:

复合索引 { city: 1, age: -1 } 可以支持:

✅ { city: "北京" }              - 完全使用
✅ { city: "北京", age: 25 }     - 完全使用
✅ { city: "北京", age: { $gt: 20 } } - 完全使用

❌ { age: 25 }                   - 无法使用
❌ { age: { $gt: 20 } }          - 无法使用

⚠️ { age: { $gt: 20 }, city: "北京" } - 可以使用(MongoDB会自动优化)

3.4 索引顺序选择

javascript 复制代码
// 场景:经常查询某个城市的用户,按年龄排序

// 方案1:city在前
db.users.createIndex({ city: 1, age: -1 })

// 查询:
db.users.find({ city: "北京" }).sort({ age: -1 })  // ✅ 使用索引
db.users.find({ city: "北京" })                     // ✅ 使用索引
db.users.find({ age: { $gt: 20 } })                 // ❌ 不使用索引

// 方案2:age在前
db.users.createIndex({ age: -1, city: 1 })

// 查询:
db.users.find({ age: { $gt: 20 } })                 // ✅ 使用索引
db.users.find({ age: { $gt: 20 } }).sort({ city: 1 }) // ✅ 使用索引
db.users.find({ city: "北京" })                     // ❌ 不使用索引

四、多键索引

4.1 数组字段索引

javascript 复制代码
// 为数组字段创建索引
db.products.createIndex({ tags: 1 })

// 插入测试数据
db.products.insertMany([
    { name: "iPhone", tags: ["手机", "电子产品", "苹果"] },
    { name: "MacBook", tags: ["电脑", "电子产品", "苹果"] },
    { name: "小米手机", tags: ["手机", "电子产品", "国产"] }
])

// 查询使用索引
db.products.find({ tags: "手机" })  // ✅ 使用多键索引

4.2 嵌套数组索引

javascript 复制代码
// 订单中包含items数组,每个item有product字段
db.orders.insertMany([
    { 
        orderId: "O001",
        items: [
            { product: "iPhone", quantity: 1 },
            { product: "AirPods", quantity: 2 }
        ]
    },
    { 
        orderId: "O002", 
        items: [
            { product: "MacBook", quantity: 1 }
        ]
    }
])

// 为嵌套数组字段创建索引
db.orders.createIndex({ "items.product": 1 })

// 查询
db.orders.find({ "items.product": "iPhone" })  // ✅ 使用索引

五、文本索引

5.1 创建文本索引

javascript 复制代码
// 为文章标题和内容创建文本索引
db.articles.createIndex({ title: "text", content: "text" })

// 或者指定权重
db.articles.createIndex(
    { title: "text", content: "text" },
    { weights: { title: 10, content: 1 } }
)

// 单一字段文本索引
db.products.createIndex({ description: "text" })

5.2 文本搜索

javascript 复制代码
// 插入测试数据
db.articles.insertMany([
    { title: "MongoDB入门教程", content: "学习MongoDB基础" },
    { title: "Redis缓存实战", content: "Redis高性能缓存" },
    { title: "MongoDB索引优化", content: "MongoDB查询优化技巧" }
])

// 文本搜索
db.articles.find({ $text: { $search: "MongoDB" } })

// 搜索多个词(OR)
db.articles.find({ $text: { $search: "MongoDB 教程" } })

// 搜索短语(AND)
db.articles.find({ $text: { $search: "\"MongoDB 入门\"" } })

// 排除某个词
db.articles.find({ $text: { $search: "MongoDB -Redis" } })

5.3 文本搜索排序

javascript 复制代码
// 按相关性排序
db.articles.find(
    { $text: { $search: "MongoDB" } },
    { score: { $meta: "textScore" } }
).sort(
    { score: { $meta: "textScore" } }
)

// 结果会按相关性得分排序

六、唯一索引

6.1 创建唯一索引

javascript 复制代码
// 为邮箱创建唯一索引
db.users.createIndex({ email: 1 }, { unique: true })

// 复合唯一索引
db.users.createIndex(
    { email: 1, status: 1 },
    { unique: true }
)

// 尝试插入重复邮箱会报错
db.users.insertOne({ email: "test@example.com" })
// Error: E11000 duplicate key error

6.2 唯一索引与稀疏索引

javascript 复制代码
// 稀疏索引:只索引非空值
db.users.createIndex(
    { phone: 1 },
    { unique: true, sparse: true }
)

// 场景:phone字段不是必须的,但如果有值必须唯一
// sparse: true 表示不索引 null 或不存在的字段

七、地理空间索引

7.1 创建地理空间索引

javascript 复制代码
// 2dsphere 索引:用于地球表面的坐标
db.stores.createIndex({ location: "2dsphere" })

// 2d 索引:用于平面坐标
db.points.createIndex({ location: "2d" })

7.2 地理位置查询

javascript 复制代码
// 插入带地理位置的店铺数据
db.stores.insertMany([
    { 
        name: "店铺A", 
        location: { type: "Point", coordinates: [116.4074, 39.9042] }  // 北京
    },
    { 
        name: "店铺B", 
        location: { type: "Point", coordinates: [121.4737, 31.2304] }  // 上海
    },
    { 
        name: "店铺C", 
        location: { type: "Point", coordinates: [114.0579, 22.5431] }  // 深圳
    }
])

// 查询附近1km内的店铺
db.stores.find({
    location: {
        $near: {
            $geometry: { type: "Point", coordinates: [116.4074, 39.9042] },
            $maxDistance: 1000  // 1000米
        }
    }
})

八、索引管理

8.1 查看索引统计

javascript 复制代码
// 查看集合索引信息
db.users.getIndexStats()

// 查看索引大小
db.users.stats().indexSizes

8.2 重建索引

javascript 复制代码
// 重建集合的所有索引
db.users.reIndex()

// 效果:删除并重新创建所有索引
// 适用于:数据大量删除后,索引文件过大

8.3 索引属性

javascript 复制代码
// 创建带属性的索引
db.users.createIndex(
    { email: 1 },
    { 
        unique: true,           // 唯一索引
        sparse: true,           // 稀疏索引
        expireAfterSeconds: 3600,  // TTL索引,3600秒后自动删除
        background: true        // 后台创建,不阻塞业务
    }
)

九、查询分析与优化

9.1 使用 explain

javascript 复制代码
// 分析查询计划
db.users.find({ age: { $gt: 25 }, city: "北京" })
    .explain("executionStats")

// 返回结果关键字段:
{
    "queryPlanner": {
        "plannerVersion": 1,
        "namespace": "myapp.users",
        "indexFilterSet": false,
        "winningPlan": {
            "stage": "FETCH",
            "inputStage": {
                "stage": "IXSCAN",  // 使用索引扫描
                "indexName": "age_1_city_1",
                ...
            }
        }
    },
    "executionStats": {
        "executionTimeMillis": 2,    // 执行时间(毫秒)
        "totalDocsExamined": 100,    // 扫描的文档数
        "totalKeysExamined": 50,     // 扫描的索引键数
        "nReturned": 10              // 返回的文档数
    }
}

9.2 性能指标解读

复制代码
📊 性能分析指标:

✅ 好的查询:
- stage: "IXSCAN" - 使用索引
- totalDocsExamined: 10
- totalKeysExamined: 10
- executionTimeMillis: 1

❌ 差的查询:
- stage: "COLLSCAN" - 全表扫描
- totalDocsExamined: 1000000
- executionTimeMillis: 5000

9.3 慢查询日志

javascript 复制代码
// 查看慢查询(默认 > 100ms)
db.system.profile.find().pretty()

// 设置慢查询阈值(毫秒)
db.setProfilingLevel(1, 100)  // 记录超过100ms的查询

// 查看当前配置
db.getProfilingStatus()

十、实战:百万级数据优化

10.1 创建测试数据

javascript 复制代码
// 插入100万条测试数据
const bulk = [];
for (let i = 0; i < 1000000; i++) {
    bulk.push({
        userId: i,
        username: "user" + i,
        age: Math.floor(Math.random() * 100),
        city: ["北京", "上海", "深圳", "广州"][Math.floor(Math.random() * 4)],
        status: ["active", "inactive"][Math.floor(Math.random() * 2)],
        createdAt: new Date()
    });
    
    // 每1000条插入一次
    if (bulk.length === 1000) {
        db.users.insertMany(bulk);
        bulk = [];
    }
}

10.2 优化前后对比

javascript 复制代码
// ❌ 优化前:无索引,查询慢
db.users.find({ city: "北京", age: { $gt: 25 } })
    .explain("executionStats")
// 结果:COLLSCAN,扫描100万条,耗时 2000ms

// ✅ 优化后:创建复合索引
db.users.createIndex({ city: 1, age: 1 })

// 再次查询
db.users.find({ city: "北京", age: { $gt: 25 } })
    .explain("executionStats")
// 结果:IXSCAN,扫描1000条,耗时 5ms

10.3 索引设计原则

复制代码
📋 索引设计最佳实践:

1. 优先为 WHERE 子句中的字段创建索引

2. 考虑查询的选择性
   - 选择性高的字段放前面
   - 尽量使用唯一索引

3. 避免创建过多索引
   - 每个索引都会占用空间
   - 写入时需要维护所有索引

4. 使用复合索引替代多个单字段索引
   - 减少索引数量
   - 利用最左前缀原则

5. 定期检查并删除无用索引
   - db.users.getIndexes() 查看
   - dropIndex() 删除

十一、总结

复制代码
📊 本篇总结:

✅ 掌握内容:
- 索引基础概念和类型
- 单字段索引创建和使用
- 复合索引与最左前缀原则
- 多键索引(数组字段)
- 文本索引(全文搜索)
- 唯一索引与稀疏索引
- 地理空间索引
- 索引管理与属性
- 查询分析与优化
- 百万级数据优化实战

作者 :刘~浪地球
更新时间 :2026-05-07
本文声明:原创不易,转载需授权!

相关推荐
瀚高PG实验室1 小时前
debezium在LANG=zh_CN.UTF-8下,无法解析timestamp类型的列值为BC的字段
服务器·数据库·postgresql·瀚高数据库
AstartesEternal1 小时前
REDIS下载及安装教程
数据库·redis·缓存
Allen_LVyingbo1 小时前
面向医疗群体智能的协同诊疗与群体决策支持系统(上)
数据结构·数据库·人工智能·git·python·动态规划
东南门吹雪2 小时前
Spring事务传播机制深度解析
java·数据库·spring
不甘先生2 小时前
PostgreSQL 中的 JSONB 详解:从入门到实战
数据库·postgresql
Irene19912 小时前
PL/SQL:异常处理补充
数据库·sql
dishugj2 小时前
SAP HANA数据库文件目录说明
服务器·数据库·oracle
l1t2 小时前
DeepSeek总结的使用 eBPF 和硬件断点跟踪 PostgreSQL
数据库·驱动开发·postgresql
薪火铺子2 小时前
MySQL InnoDB 索引底层:B+树深度解析
数据库·b树·mysql