MongoDB索引与查询性能调优案例分享
2023-02-023.7k 阅读
MongoDB索引基础
在深入性能调优案例之前,我们先来回顾一下MongoDB索引的基础知识。
索引的概念
索引在MongoDB中就像是一本书的目录,它可以帮助数据库快速定位到满足查询条件的数据,而无需扫描整个集合。通过创建合适的索引,我们能够显著提升查询性能。
索引类型
-
单字段索引
- 这是最基本的索引类型,针对单个字段创建。例如,在一个存储用户信息的集合中,如果经常根据
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
则为降序索引。 - 这是最基本的索引类型,针对单个字段创建。例如,在一个存储用户信息的集合中,如果经常根据
-
复合索引
- 复合索引是基于多个字段创建的索引。当查询条件涉及多个字段时,复合索引能发挥很大作用。假设我们有一个订单集合,经常根据
customer_id
和order_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
降序排列。 - 复合索引是基于多个字段创建的索引。当查询条件涉及多个字段时,复合索引能发挥很大作用。假设我们有一个订单集合,经常根据
-
多键索引
- 当文档中的字段是数组类型时,多键索引就派上用场了。例如,一个存储商品标签的集合,每个商品可能有多个标签,为
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();
- 当文档中的字段是数组类型时,多键索引就派上用场了。例如,一个存储商品标签的集合,每个商品可能有多个标签,为
-
文本索引
- 用于全文搜索场景,比如在博客文章集合中搜索包含特定关键词的文章。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();
性能调优案例分析
案例一:单字段索引优化简单查询
- 场景描述
- 我们有一个
employees
集合,存储员工的信息,包括employee_id
、name
、department
、salary
等字段。现在需要频繁根据employee_id
查询单个员工的详细信息。
- 我们有一个
- 未优化前的查询与性能分析
- 查询代码:
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万条记录时,查询可能需要数秒甚至更长时间。
- 优化措施
- 为
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();
- 为
- 优化后的性能提升
- 再次执行相同的查询,由于索引的存在,MongoDB可以直接定位到
employee_id
为123的文档。查询时间大幅缩短,在同样10万条记录的集合中,查询可能只需几毫秒,性能提升了几个数量级。
- 再次执行相同的查询,由于索引的存在,MongoDB可以直接定位到
案例二:复合索引优化多条件查询
- 场景描述
- 有一个
sales
集合,记录销售数据,每个文档包含product_id
、customer_id
、sale_date
、quantity
、price
等字段。现在需要查询特定product_id
和customer_id
在某个时间段内的销售记录。
- 有一个
- 未优化前的查询与性能分析
- 查询代码:
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万条销售记录,查询可能需要数十秒。
- 优化措施
- 创建复合索引,索引字段顺序要根据查询条件的选择性来确定。通常将选择性高的字段放在前面。这里
product_id
和customer_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();
- 创建复合索引,索引字段顺序要根据查询条件的选择性来确定。通常将选择性高的字段放在前面。这里
- 优化后的性能提升
- 优化后,查询性能显著提高。因为复合索引可以快速定位到满足
product_id
和customer_id
条件的文档,再在这些文档中筛选出符合日期范围的记录。同样100万条记录的集合,查询时间可能缩短到几百毫秒。
- 优化后,查询性能显著提高。因为复合索引可以快速定位到满足
案例三:多键索引优化数组字段查询
- 场景描述
- 有一个
recipes
集合,存储各种食谱,每个食谱文档包含recipe_name
、ingredients
(食材数组)、instructions
等字段。现在需要查询包含特定食材的食谱。
- 有一个
- 未优化前的查询与性能分析
- 查询代码:
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万条记录,查询可能需要数秒。
- 优化措施
- 为
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();
- 为
- 优化后的性能提升
- 多键索引创建后,查询速度大幅提升。MongoDB可以利用索引快速定位到包含
tomato
食材的食谱文档。同样5万条记录的集合,查询时间可能缩短到几十毫秒。
- 多键索引创建后,查询速度大幅提升。MongoDB可以利用索引快速定位到包含
案例四:文本索引优化全文搜索
- 场景描述
- 有一个
news_articles
集合,存储新闻文章,每个文档包含title
、content
、publication_date
等字段。现在需要实现一个功能,能够搜索文章标题和内容中包含特定关键词的新闻。
- 有一个
- 未优化前的查询与性能分析
- 查询代码:
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需要扫描整个集合,对每个文档的
title
和content
字段进行字符串匹配。当集合中有大量新闻文章时,查询性能极低,如集合有20万篇文章,查询可能需要数分钟。
- 优化措施
- 创建文本索引,将
title
和content
字段都包含在索引中。 - 代码:
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();
- 创建文本索引,将
- 优化后的性能提升
- 使用文本索引后,查询性能得到极大改善。文本索引支持高效的全文搜索,能够快速定位到包含关键词的新闻文章。同样20万篇文章的集合,查询时间可能缩短到几秒甚至更短。
索引使用注意事项
- 索引维护成本
- 创建索引会占用额外的磁盘空间,并且每次插入、更新或删除文档时,MongoDB都需要更新相关的索引。所以,不要过度创建索引,只创建那些对查询性能有显著提升的索引。例如,在一个很少查询的字段上创建索引,不仅浪费空间,还会降低写操作的性能。
- 索引顺序
- 对于复合索引,字段顺序至关重要。要根据查询条件中字段的选择性和使用频率来确定顺序。一般将选择性高(能过滤出较少文档)的字段放在前面。如在案例二中,
product_id
和customer_id
相对sale_date
选择性更高,所以放在复合索引的前面。
- 对于复合索引,字段顺序至关重要。要根据查询条件中字段的选择性和使用频率来确定顺序。一般将选择性高(能过滤出较少文档)的字段放在前面。如在案例二中,
- 覆盖索引
- 尽量使用覆盖索引,即查询所需的所有字段都包含在索引中。这样MongoDB无需再从文档中读取数据,直接从索引中获取结果,能大幅提升查询性能。例如,查询
employees
集合中employee_id
和name
字段,若创建的索引包含这两个字段{ 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();
- 尽量使用覆盖索引,即查询所需的所有字段都包含在索引中。这样MongoDB无需再从文档中读取数据,直接从索引中获取结果,能大幅提升查询性能。例如,查询
- 索引失效情况
- 某些查询操作可能导致索引失效。例如,使用
$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索引与查询性能调优有了更深入的理解。在实际应用中,需要根据具体的业务场景和数据特点,合理创建和使用索引,以实现最佳的查询性能。