MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

MongoDB索引与查询性能调优案例分享

2023-02-023.7k 阅读

MongoDB索引基础

在深入性能调优案例之前,我们先来回顾一下MongoDB索引的基础知识。

索引的概念

索引在MongoDB中就像是一本书的目录,它可以帮助数据库快速定位到满足查询条件的数据,而无需扫描整个集合。通过创建合适的索引,我们能够显著提升查询性能。

索引类型

  1. 单字段索引

    • 这是最基本的索引类型,针对单个字段创建。例如,在一个存储用户信息的集合中,如果经常根据email字段进行查询,就可以为email字段创建单字段索引。
    • 代码示例
    // 连接到MongoDB数据库
    const { MongoClient } = require('mongodb');
    const uri = "mongodb://localhost:27017";
    const client = new MongoClient(uri);
    
    async function createIndex() {
        try {
            await client.connect();
            const db = client.db('test');
            const users = db.collection('users');
            // 创建单字段索引
            await users.createIndex({ email: 1 });
            console.log('Index created successfully');
        } catch (e) {
            console.error(e);
        } finally {
            await client.close();
        }
    }
    
    createIndex();
    

    在上述代码中,{ email: 1 }表示按升序对email字段创建索引,若1换成-1则为降序索引。

  2. 复合索引

    • 复合索引是基于多个字段创建的索引。当查询条件涉及多个字段时,复合索引能发挥很大作用。假设我们有一个订单集合,经常根据customer_idorder_date查询订单,就可以创建复合索引。
    • 代码示例
    async function createCompoundIndex() {
        try {
            await client.connect();
            const db = client.db('test');
            const orders = db.collection('orders');
            // 创建复合索引
            await orders.createIndex({ customer_id: 1, order_date: -1 });
            console.log('Compound index created successfully');
        } catch (e) {
            console.error(e);
        } finally {
            await client.close();
        }
    }
    
    createCompoundIndex();
    

    这里的复合索引先按customer_id升序,再按order_date降序排列。

  3. 多键索引

    • 当文档中的字段是数组类型时,多键索引就派上用场了。例如,一个存储商品标签的集合,每个商品可能有多个标签,为tags数组字段创建多键索引,能快速查询包含特定标签的商品。
    • 代码示例
    async function createMultikeyIndex() {
        try {
            await client.connect();
            const db = client.db('test');
            const products = db.collection('products');
            // 创建多键索引
            await products.createIndex({ tags: 1 });
            console.log('Multikey index created successfully');
        } catch (e) {
            console.error(e);
        } finally {
            await client.close();
        }
    }
    
    createMultikeyIndex();
    
  4. 文本索引

    • 用于全文搜索场景,比如在博客文章集合中搜索包含特定关键词的文章。MongoDB的文本索引支持语言特定的词干分析和停用词处理。
    • 代码示例
    async function createTextIndex() {
        try {
            await client.connect();
            const db = client.db('test');
            const blogPosts = db.collection('blogPosts');
            // 创建文本索引
            await blogPosts.createIndex({ content: 'text' });
            console.log('Text index created successfully');
        } catch (e) {
            console.error(e);
        } finally {
            await client.close();
        }
    }
    
    createTextIndex();
    

性能调优案例分析

案例一:单字段索引优化简单查询

  1. 场景描述
    • 我们有一个employees集合,存储员工的信息,包括employee_idnamedepartmentsalary等字段。现在需要频繁根据employee_id查询单个员工的详细信息。
  2. 未优化前的查询与性能分析
    • 查询代码
    async function findEmployeeWithoutIndex() {
        try {
            await client.connect();
            const db = client.db('test');
            const employees = db.collection('employees');
            const result = await employees.findOne({ employee_id: 123 });
            console.log(result);
        } catch (e) {
            console.error(e);
        } finally {
            await client.close();
        }
    }
    
    findEmployeeWithoutIndex();
    
    • 在没有为employee_id字段创建索引时,MongoDB需要扫描整个employees集合来查找符合条件的文档。如果集合中的文档数量较多,查询性能会非常差。例如,当集合中有10万条记录时,查询可能需要数秒甚至更长时间。
  3. 优化措施
    • employee_id字段创建单字段索引。
    • 代码
    async function createEmployeeIdIndex() {
        try {
            await client.connect();
            const db = client.db('test');
            const employees = db.collection('employees');
            await employees.createIndex({ employee_id: 1 });
            console.log('Index for employee_id created');
        } catch (e) {
            console.error(e);
        } finally {
            await client.close();
        }
    }
    
    createEmployeeIdIndex();
    
  4. 优化后的性能提升
    • 再次执行相同的查询,由于索引的存在,MongoDB可以直接定位到employee_id为123的文档。查询时间大幅缩短,在同样10万条记录的集合中,查询可能只需几毫秒,性能提升了几个数量级。

案例二:复合索引优化多条件查询

  1. 场景描述
    • 有一个sales集合,记录销售数据,每个文档包含product_idcustomer_idsale_datequantityprice等字段。现在需要查询特定product_idcustomer_id在某个时间段内的销售记录。
  2. 未优化前的查询与性能分析
    • 查询代码
    async function findSalesWithoutIndex() {
        try {
            await client.connect();
            const db = client.db('test');
            const sales = db.collection('sales');
            const startDate = new Date('2023 - 01 - 01');
            const endDate = new Date('2023 - 12 - 31');
            const result = await sales.find({
                product_id: 'P001',
                customer_id: 'C001',
                sale_date: { $gte: startDate, $lte: endDate }
            }).toArray();
            console.log(result);
        } catch (e) {
            console.error(e);
        } finally {
            await client.close();
        }
    }
    
    findSalesWithoutIndex();
    
    • 没有索引时,MongoDB需要全集合扫描,对每个文档逐一检查是否满足所有条件。随着集合规模增大,查询性能急剧下降。若集合中有100万条销售记录,查询可能需要数十秒。
  3. 优化措施
    • 创建复合索引,索引字段顺序要根据查询条件的选择性来确定。通常将选择性高的字段放在前面。这里product_idcustomer_id相对sale_date选择性更高,所以创建复合索引{ product_id: 1, customer_id: 1, sale_date: 1 }
    • 代码
    async function createSalesCompoundIndex() {
        try {
            await client.connect();
            const db = client.db('test');
            const sales = db.collection('sales');
            await sales.createIndex({ product_id: 1, customer_id: 1, sale_date: 1 });
            console.log('Compound index for sales created');
        } catch (e) {
            console.error(e);
        } finally {
            await client.close();
        }
    }
    
    createSalesCompoundIndex();
    
  4. 优化后的性能提升
    • 优化后,查询性能显著提高。因为复合索引可以快速定位到满足product_idcustomer_id条件的文档,再在这些文档中筛选出符合日期范围的记录。同样100万条记录的集合,查询时间可能缩短到几百毫秒。

案例三:多键索引优化数组字段查询

  1. 场景描述
    • 有一个recipes集合,存储各种食谱,每个食谱文档包含recipe_nameingredients(食材数组)、instructions等字段。现在需要查询包含特定食材的食谱。
  2. 未优化前的查询与性能分析
    • 查询代码
    async function findRecipesWithoutIndex() {
        try {
            await client.connect();
            const db = client.db('test');
            const recipes = db.collection('recipes');
            const result = await recipes.find({ ingredients: 'tomato' }).toArray();
            console.log(result);
        } catch (e) {
            console.error(e);
        } finally {
            await client.close();
        }
    }
    
    findRecipesWithoutIndex();
    
    • 未创建多键索引时,MongoDB需遍历整个集合,检查每个文档的ingredients数组是否包含tomato。当集合中有大量食谱文档时,查询会很慢,比如集合有5万条记录,查询可能需要数秒。
  3. 优化措施
    • ingredients数组字段创建多键索引。
    • 代码
    async function createIngredientsMultikeyIndex() {
        try {
            await client.connect();
            const db = client.db('test');
            const recipes = db.collection('recipes');
            await recipes.createIndex({ ingredients: 1 });
            console.log('Multikey index for ingredients created');
        } catch (e) {
            console.error(e);
        } finally {
            await client.close();
        }
    }
    
    createIngredientsMultikeyIndex();
    
  4. 优化后的性能提升
    • 多键索引创建后,查询速度大幅提升。MongoDB可以利用索引快速定位到包含tomato食材的食谱文档。同样5万条记录的集合,查询时间可能缩短到几十毫秒。

案例四:文本索引优化全文搜索

  1. 场景描述
    • 有一个news_articles集合,存储新闻文章,每个文档包含titlecontentpublication_date等字段。现在需要实现一个功能,能够搜索文章标题和内容中包含特定关键词的新闻。
  2. 未优化前的查询与性能分析
    • 查询代码
    async function findArticlesWithoutIndex() {
        try {
            await client.connect();
            const db = client.db('test');
            const articles = db.collection('news_articles');
            const keyword = 'technology';
            const result = await articles.find({
                $or: [
                    { title: { $regex: keyword, $options: 'i' } },
                    { content: { $regex: keyword, $options: 'i' } }
                ]
            }).toArray();
            console.log(result);
        } catch (e) {
            console.error(e);
        } finally {
            await client.close();
        }
    }
    
    findArticlesWithoutIndex();
    
    • 这种基于正则表达式的查询没有利用索引,MongoDB需要扫描整个集合,对每个文档的titlecontent字段进行字符串匹配。当集合中有大量新闻文章时,查询性能极低,如集合有20万篇文章,查询可能需要数分钟。
  3. 优化措施
    • 创建文本索引,将titlecontent字段都包含在索引中。
    • 代码
    async function createNewsArticleTextIndex() {
        try {
            await client.connect();
            const db = client.db('test');
            const articles = db.collection('news_articles');
            await articles.createIndex({ title: 'text', content: 'text' });
            console.log('Text index for news articles created');
        } catch (e) {
            console.error(e);
        } finally {
            await client.close();
        }
    }
    
    createNewsArticleTextIndex();
    
  4. 优化后的性能提升
    • 使用文本索引后,查询性能得到极大改善。文本索引支持高效的全文搜索,能够快速定位到包含关键词的新闻文章。同样20万篇文章的集合,查询时间可能缩短到几秒甚至更短。

索引使用注意事项

  1. 索引维护成本
    • 创建索引会占用额外的磁盘空间,并且每次插入、更新或删除文档时,MongoDB都需要更新相关的索引。所以,不要过度创建索引,只创建那些对查询性能有显著提升的索引。例如,在一个很少查询的字段上创建索引,不仅浪费空间,还会降低写操作的性能。
  2. 索引顺序
    • 对于复合索引,字段顺序至关重要。要根据查询条件中字段的选择性和使用频率来确定顺序。一般将选择性高(能过滤出较少文档)的字段放在前面。如在案例二中,product_idcustomer_id相对sale_date选择性更高,所以放在复合索引的前面。
  3. 覆盖索引
    • 尽量使用覆盖索引,即查询所需的所有字段都包含在索引中。这样MongoDB无需再从文档中读取数据,直接从索引中获取结果,能大幅提升查询性能。例如,查询employees集合中employee_idname字段,若创建的索引包含这两个字段{ employee_id: 1, name: 1 },就可以利用覆盖索引。
    • 代码示例
    async function findEmployeeWithCoveringIndex() {
        try {
            await client.connect();
            const db = client.db('test');
            const employees = db.collection('employees');
            await employees.createIndex({ employee_id: 1, name: 1 });
            const result = await employees.find({ employee_id: 123 }, { _id: 0, employee_id: 1, name: 1 }).toArray();
            console.log(result);
        } catch (e) {
            console.error(e);
        } finally {
            await client.close();
        }
    }
    
    findEmployeeWithCoveringIndex();
    
  4. 索引失效情况
    • 某些查询操作可能导致索引失效。例如,使用$where子句的查询,因为$where中的JavaScript代码无法利用索引。另外,对索引字段进行函数操作,如$substr$toUpper等,也会使索引失效。例如:
    // 索引失效的查询
    async function findInvalidQuery() {
        try {
            await client.connect();
            const db = client.db('test');
            const employees = db.collection('employees');
            const result = await employees.find({ $where: "this.name.indexOf('John')!== -1" }).toArray();
            console.log(result);
        } catch (e) {
            console.error(e);
        } finally {
            await client.close();
        }
    }
    
    findInvalidQuery();
    
    这里的$where查询不能利用name字段的索引,应尽量避免这种写法,可改用$regex等能利用索引的操作符。

通过上述案例和注意事项,我们对MongoDB索引与查询性能调优有了更深入的理解。在实际应用中,需要根据具体的业务场景和数据特点,合理创建和使用索引,以实现最佳的查询性能。