mongodb学习系列3:查询和索引

查询和索引

Posted by Chris on November 2, 2019

在互联网公司的应用中,一般都是读多写少,因此,对于查询的方式和怎么加快查询,有很多内容。

前面讲过查询的命令是使用db.{COLLECTION_NAME}.find(query, projection)。
query:使用查询操作符组成的查询条件,可选。若省略该参数,则返回集合中的所有文档。
projection:使用投影操作符指定返回的键,可选。若省略该参数,则查询时返回文档中所有键值。

下面先讲query和projection具体可以有哪些使用方式,然后再讲索引和explain。

1 查询条件

查询条件类似于mysql里面where后面的部分,用于根据条件筛选出结果。有各种各样的方式,可以用来灵活地指定查询条件。

键值匹配

可以通过直接指定键值的方式来匹配所需要的文档。如下面的示例所示,直接指定目标文档的title为“php”。

> db.col_book.find({
    "title": "php"
})
返回结果:
{
	"_id" : ObjectId("5daef9d7aaa734d6955486aa"),
	"title" : "php",
	"price" : 10
}

范围

查询值在一定的跨度范围内的文档。类似于mysql中的”<”和”>”比较运算符。 下面的例子中,查找价格介于10到300的书籍。

> db.col_book.find({
    "price": {
        "$gt": 10,
        "$lt": 300
    }
})

常见的范围查询运算符如下表所示。

集合运算符

将一个集合作为查询的条件,类似于mysql中的”in”。

例如,下面的例子,查询category为”computer”或者”communication”的文档。

> db.col_book.find({
    "category": {
        "$in": [
            "computer", 
            "communication"
        ]
    }
})

集合操作符有如下这些

注意:表中所说的属性值,可以是一个列表,比如[1, 2, 3]

比如某文档的内容如下:

{
	"title" : "Head First MongoDB",
	"price" : 10,
	"category" : ["computer", "database"]
},
{
	"title" : "Head First Java",
	"price" : 10,
	"category" : "computer"
}

那么,如下语句只会返回”Head First MongoDB”这条记录。

db.col_book.find({
    "category": {
        "$all": [
            "computer", 
            "database"
        ]
    }
})

下面的这条语句,则会返回两条记录。

db.col_book.find({
    "category": {
        "$in": [
            "computer", 
            "database"
        ]
    }
})

注意$in$all可以利用索引,而$nin不能利用索引。

布尔运算符

布尔运算符如下表所示。

下面的命令,使用$and运算符,来查询类目包含computer并且价格小于200的书籍。

> db.col_book.find({
    "$and": [
        {
          "category": {
               "$in": ["computer"]
          }
        },
        {
            "price": {
                "$lt": 200
            }
        }
    ]
})

下面的命令,使用$not来查找价格不小于100的书籍。

> db.col_book.find({
    "price": {
        "$not": {
            "$lt": 100
        }
    }
})

$exists运算符

$exists运算符作为查询条件,可以用来判断文档中是否存在某个关键字。

下面的命令,查询不存在category键的书籍。

> db.col_book.find({
    "category": {
        "$exists": false
    }
})

匹配子文档

当某个文档中有嵌套子文档时,如果查询条件中有子文档的信息,此时,就需要匹配子文档。匹配子文档,可以使用一个点(.)来指定子文档的键。

{
	"_id" : ObjectId("5db1018e2f681505d89ea375"),
	"title" : "Head First Java",
	"price" : 10,
	"category" : "computer",
	"author" : {
		"name" : "Chris",
		"interest" : [
			"database",
			"java"
		]
	}
}

假设数据库中有如上数据,那么,怎么找到作者的insterest包含database的书籍呢?使用如下的方式,就可以做到。

> db.col_book.find({
    "author.interest": {
        "$in": ["database"]
    }
})

数组

当查询条件中需要匹配数组时,可以使用$sizeelemMatch数组操作符。

$size的使用如下。查找有两个标签的书籍。

> db.col_book.find({
    "category": {
        "$size": 2
    }
})

注意$size不能利用索引。

$elemMatch的使用如下。查找作者的家的地址在上海的书籍信息。 假设数据如下。

{
	"_id" : ObjectId("5db130f62f681505d89ea377"),
	"title" : "Head First Mysql",
	"price" : 2000,
	"category" : "computer",
	"author" : {
		"name" : "Cook",
		"address" : [
			{
				"name" : "home",
				"city" : "Shanghai"
			},
			{
				"name" : "work",
				"city" : "Beijing"
			}
		]
	}
}

则命令如下

> db.col_book.find({
    "author.address": {
        "$elemMatch": {
            "name": "work"
        }
    }
})

JavaScript运算符

$where运算符可以传递一个javascript表达式。

下面的例子查询价格大于1000的书籍。下面只是一个例子,一般我们不会这么使用javascript表达式。

> db.col_book.find({
    "$where": "function() {return this.price > 1000}"
})

注意:javascript无法利用索引,也有注入攻击的风险,因此,最好作为最后兜底的选择。

正则表达式

> db.col_book.find({
  "category": /Database/i
})

注意:javascript无法利用索引,因此,最好作为最后兜底的选择。

2 查询结果选择

当使用查询条件查询到文档集合之后,下一步就是对结果集进行一些处理,包括选择要返回的键、排序、限制结果集大小。

映射

选择要返回的键,叫做映射。选择要返回的键,可以最小化网络延迟和反序列化。

下面的语句,查询价格小于200的书籍,然后选择只返回_id键(默认返回)、title键和category键。

> db.col_book.find({
    "price": {
        "$lt": 200
    }
}, {
    "title": 1,
    "category": 1
})

注意:在查询结果选择中,当键设置为1时,表明返回该键;当设置为0时,表明不返回该键。

如果不需要返回_id字段,则命令如下。

> db.col_book.find({
    "price": {
        "$lt": 200
    }
}, {
    "_id": 0,
    "title": 1,
    "category": 1
})

排序

对将要返回的文档,按照某个键进行升序或者降序排列。使用sort方法做到。

比如,下面的命令,对将要返回的文档,第一层先按照价格升序排列;若价格相同,则按照title从小到大排列(ASCII大小)。

> db.col_book.find({})
        .sort({ 
            "price": 1,
            "title": 1
        })

注意:sort可以利用索引,与mysql是一样的。

跳过和限制

跳过使用skip命令,限制将要返回的结果集的个数使用limit命令,与mysql是一样的。

当skip的值很大时,比如,超过10000,则可能会出现查询效率下降的问题。比如下面的查询语句效率就比较低。

> db.col_book.find({}).skip(10000).limit(10)

一种改进的做法就是在查询条件中限制将要返回的结果集的个数,然后再使用limit。下面的命令使用日期来限制查询出来的结果集大小。

> b.col_book.find({
    "date": {
        "$gt": new Date(2019, 10, 1)
    }
}).limit(10)

3 索引

索引用来对查询进行加速。mongodb的索引(WiredTiger存储引擎)与mysql(innodb和myisam)的索引原理很类似。mongodb的索引使用B-树,mysql使用B+树。

3.1 索引的CRUD操作

  • 创建索引

语法

db.{COLLECTION_NAME}.createIndex(index_field, params)

COLLECTION_NAME:集合(表)的名字
index_field:需要创建索引的字段,可以为多个。
params:索引的属性,比如,是否为唯一索引,是否为稀疏索引。

示例
假设数据如下,

{
	"_id" : ObjectId("53c2ae8528d75d572c06adba"),
	"title" : "Jakarta Commons Online Bookshelf",
	"isbn" : "1932394524",
	"pageCount" : NumberInt(402),
	"publishedDate" : {
		"date" : "2005-03-01T00:00:00.000-0800"
	},
	"thumbnailUrl" : "https://s3.amazonaws.com/AKIAJC5RLADLUMVRPFDQ.book-thumb-images/goyal.jpg",
	"longDescription" : "Written for developers and architects with real work to do, the Jakarta Commons Online Bookshelf is a collection of 14 PDF modules, each focused on one of the main Commons components. Commons is a collection of over twenty open-source Java tools broadly ranging from logging, validation, bean utilities and XML parsing. The Jakarta Commons Online Bookshelf summarizes the rationale behind each component and then provides expert explanations and hands-on examples of their use. You will learn to easily incorporate the Jakarta Commons components into your existing Java applications.    Why spend countless hours writing thousands of lines of code, when you can use the Jakarta Commons re-usable components instead  Each of the packages is independent of the others, and Manning lets you pick which of the Commons components you want to learn about. Each Module can be purchased separately or purchased together in the entire Jakarta Commons Online Bookshelf.    Why is Jakarta Commons so popular  Because it provides re-usable solutions to your everyday development tasks. Make your work life better starting today. Purchase one of the modules or the entire Bookshelf and get the guidance of an experienced Jakarta Commons pro.",
	"status" : "PUBLISH",
	"authors" : [ ],
	"categories" : ["computer", "fiction"]
}

对title创建唯一索引的示例如下。在键title上创建索引,不是唯一索引。

> db.books.createIndex(
    {
        "title": 1
    },
    {
        "unique": 0
    }
)
  • 查询存在哪些索引

语法

> db.{COLLECTION_NAME}.getIndexes()

示例

> db.books.getIndexes()
  • 删除索引

语法

db.{COLLECTION_NAME}.dropIndex(INDEX_NAME)

COLLECTION_NAME:集合名字
INDEX_NAME:索引名字,并不是索引的键名。可以先查询集合上有哪些索引来找到对应的名字。

示例

> db.books.dropIndex("title_1")

3.2 索引的分类

唯一索引

唯一索引可以确保集合中文档的键值是唯一的。创建唯一索引指定unique参数为1即可。下面的示例在键title上创建唯一索引。

> db.books.createIndex(
    {
        "title": 1
    },
    {
        "unique": 1
    }
)

组合索引

一个组合索引可以对多个键来创建索引结构,与mysql中的组合索引是一样的。用法如下

db.{COLLECTION_NAME}.createIndex( { <field1>: <type>, <field2>: <type2>, ... } )

示例如下,对于title键和isbn键创建组合索引。

> db.books.createIndex({
    "title": 1,
    "isbn": 1
})

多键索引

当键的值是一个数组时,对该键创建索引,就会自动使用多键索引。创建多键索引,mongodb会将数组中的每个值都加入索引结构中。例如,categories键对应的值是一个数组,那么,使用如下方式,就可以创建多键索引了。

> db.books.createIndex({
    "categories": 1
})

稀疏索引

例如某个允许匿名用户评论的电商网站,可能大多数都是匿名评论,那么,很多评论的user_id键就是null。如果在user_id字段上建立索引,会有两个问题:1)增加索引的大小;2)当添加或者删除user_id为null的评论时,需要更新索引。这个时候,可以考虑使用稀疏索引。
稀疏索引是指,当键存在对应的值时,为该文档建立索引;当键不存在对应的值时,则不建立索引。 当我们不需要或者很少根据user_id=null条件来查询评论时,则可以为user_id建立稀疏索引。示例如下。

> db.books.createIndex(
    {
        "title": 1
    },
    {
        "unique": 0,
        "sparse": 1
    }
)

哈希索引

对某个键创建哈希索引,那么,在创建索引结构时,会根据键值的哈希值来进行排序。即查找过程变为:

索引结构中查找:键值 -> 哈希值 -> 主键ID
数据存储中查找:主键ID -> 文档

创建哈希索引的示例为:

> db.books.createIndex({
    "title": "hashed"
})

地理空间索引

现实情况下,经常有需要查找某个位置A附近的商户这样的需要。一般的思路就是通过经纬度计算位置A和其他商户之间的距离,然后进行排序。可见,这样一件事情是很麻烦的。
mongodb提供了地理空间索引,可以解决地理位置上的搜索的问题。 例如,有如下数据shop集合,loc键代表位置,第一个元素为精度,第二个元素为纬度。

{
	"_id" : ObjectId("5dbd2963f87ca60a68bb4203"),
	"category" : "food",
	"loc" : [
		120,
		30
	]
}
{
	"_id" : ObjectId("5dbd2963f87ca60a68bb4205"),
	"category" : "beauty",
	"loc" : [
		120,
		31
	]
}

我们想要查找位置[120, 31]附近的商户,就得先创建索引。

> db.shop.createIndex(
    {
        "loc": "2d"
    }
)

接下来,下面的语句就可以找到位置[120, 31]附近的商户,结果按照从近到远排序。

> db.shop.find({
    "loc": {
        "$near": [120, 31]
    }
})

3.3 索引的原理

上面介绍了各种索引,那么,什么情况下使用对应的索引呢?想要弄懂这个问题,就得知道索引的实现原理。

mongodb3.0以上版本使用WiredTiger存储引擎作为默认的存储引擎,WiredTiger存储引擎在索引实现上采用了B-树。mysql innodb存储引擎和myisam存储引擎采用的是B+树,因此,mysql基于索引的调优的经验是适用于mongodb的。
对于想要了解B+树索引的读者,可以参考我的另一篇关于mysql索引原理的文章

注意:mysql innodb使用B+树(读作“B加树”)作为存储引擎,而WiredTiger使用了B-树(读作“B树”)作为存储引擎。他们的一个主要区别是,B-树的每个节点都可以存放数据块,B+树只有叶子节点存放数据块。

4 explain

explain用来对查询语句进行“解释”,看看查询语句使用了怎样的查询计划,比如,是否使用了索引、预计扫描多少个文档、预计返回多少个文档。
使用示例如下:

> db.books.find({
    "title": "Specification by Example"
}).explain("executionStats")

返回内容如下

{
	"queryPlanner" : {
		"plannerVersion" : 1,
		"namespace" : "sample_library.books",  // 库名和表名
		"indexFilterSet" : false,
		"parsedQuery" : {
			"title" : {
				"$eq" : "Specification by Example"
			}
		},
		"queryHash" : "6E0D6672",
		"planCacheKey" : "B1CDA929",
		"winningPlan" : {
			"stage" : "FETCH",  // 2)再执行FETCH的操作,读取文档
			"inputStage" : {
				"stage" : "IXSCAN",   // 1)先执行索引扫描(index scan)
				"keyPattern" : {
					"title" : 1
				},
				"indexName" : "title_1",  // 使用的索引名
				"isMultiKey" : false,
				"multiKeyPaths" : {
					"title" : [ ]
				},
				"isUnique" : false,
				"isSparse" : false,
				"isPartial" : false,
				"indexVersion" : 2,
				"direction" : "forward",
				"indexBounds" : {
					"title" : [
						"[\"Specification by Example\", \"Specification by Example\"]"
					]
				}
			}
		},
		"rejectedPlans" : [ ]
	},
	"serverInfo" : {
		"host" : "Xxx.local",
		"port" : 27017,
		"version" : "4.2.1",
		"gitVersion" : "edf6d45851c129ee15548f0f847df141764a317e"
	},
	"ok" : 1
}

更多关于查询计划的内容,可以参考Explain Results

参考

Introduction to MongoDB Indexes