MongoDB系列之一文总结索引
概述
分类
索引的分类:
- 按照索引包含的字段数量,可分为单键索引(单字段索引)和组合索引(联合索引、复合索引)
- 按照索引字段的类型,可以分为主键索引和非主键索引
- 按照索引节点与物理记录的对应方式来分,可以分为聚簇索引和非聚簇索引,其中聚簇索引是指索引节点上直接包含了数据记录,而后者则仅仅包含一个指向数据记录的指针
- 按照索引的特性不同,可分为唯一索引、稀疏索引、文本索引、地理空间索引等
索引介绍
单键索引和复合索引
创建单键索引:db.user.ensureIndex({ income: 1});
。
单字段索引对内嵌字段创建索引:db.user.ensureIndex({ health.height: 1}); // 健康指标信息
。
创建复合索引:db.user.ensureIndex({ income: 1, health.height: 1});
。
数组索引
数组索引,也被称为多值索引(multikey index),当对数组型的字段创建索引时,这个索引就是多值的。多值索引在使用上与普通索引并没有什么不同,只是在索引键上会同时产生多个值。数组索引必然会使索引的条目和体积发生膨胀。
多值索引和复合索引可以一起使用,即复合索引前面的字段是非数组类型,后面的字段是数组类型(这个先后顺序,和关系型数据库组合索引最左匹配原则是一个意思)。MongoDB不支持一个复合索引中同时出现多个多值索引,即不允许出现多个数组类型字段。
db.user.ensureIndex({ age: 1, hobbies: 1}); // OK, 可以有多个爱好
db.user.ensureIndex({ hobbies: 1, careers: 1}); // wrong, 职业经历(生涯)
地理空间索引
LBS,Location Based Service,基于地理位置的检索。
2dsphere
MongoDB有两种类型的地理空间索引:2dsphere和2d。2dsphere索引可以与基于WGS84基准的地球球面几何模型一起使用。这个基准将地球表面模拟成一个扁圆球体,这意味着在两极会比较扁。使用 2dsphere 索引的距离计算考虑到地球的形状,提供比2d索引更准确的距离处理,如计算两个城市之间的距离。在存储二维平面上的点时使用2d索引。
2dsphere允许以GeoJSON格式指定点、线和多边形。点由一个二元数组给出,[经度,纬度]
,即[longitude,latitude]
。GeoJSON格式是固定的:
"location" : {
"type" : "Point",
"coordinates" : [50, 2]
}
即type和coordinates两个字段名不能更改,type枚举值有:Point、LineString、Polygon。location可以使用其他名称,如loc。
创建一个地理空间索引:db.shop.createIndex({ loc: "2dsphere"})
。可使用三种类型的地理空间查询:交集(intersection)、包含(within)和接近(nearness)。
查询:
db.shop.find({
loc: {
$near: {$geometry: {type: Point, coordinates: [121.615, 31.190] } },
$maxDistance: 1000,
}
})
$near
操作符,用于实现附近店铺的检索,返回数据结果会按距离排序。$geometry
操作符用于指定一个GeoJSON格式的地理空间对象,type=Point
表示地理坐标点,coordinates则是用户当前所在的经纬度位置;$maxDistance
限定最大距离,单位是米。
注意点:
- MongoDB的地理空间检索基于WGS84坐标系,在与一些地图平台集成时需要注意转换,如GCJ-02(火星坐标系)、BD-09(百度中国坐标系)
- MongoDB 4.0版本之后,near可以用于分片集合(sharded collection),而在此版本之前可以使用geoNear聚合操作来代替
2d
对于非球面地图(电子游戏地图、时间序列数据等),可使用2d索引代替2dsphere索引:db.game.createIndex({ 'tile': '2d' }, );
默认情况下,2d 索引会假设取值范围为-180到180。如果希望对边界大小进行调整,则可以指定最小值和最大值作为createIndex的选项:db.game.ensureIndex({ 'tile': '2d'}, {min: -1000, max: 1000});
2d索引支持$geoWithin
、$nearSphere
和$near
查询选择器。
应该使用 $geoWithin
查询在平面上定义的形状内的点。$geoWithin
可以查询矩形、多边形、圆形或球体内的所有点,它使用$geometry
运算符来指定 GeoJSON 对象。
db.game.find({tile: {$geoWithin: {$box: [[0, 0], [10, 10]]}}}); // 查询左下角为[0, 0]、右上角为[10, 10]的矩形内的文档,即坐标
db.game.find({tile: {$geoWithin: {$center: [[0, 0], 5]}}}); // 查询圆心为[0, 0]、半径为5的圆形内的坐标
db.game.find({tile: {$geoWithin: {$polygon: [[0, 0], [3, 6], [3, 0]]}}}); // 查询三个点指定的三角形(多边形)内的所有文档
由于历史遗留原因,MongoDB支持在平面2d索引上执行球面查询,结合$geoWithin
和$centerSphere
运算符。指定一个数组,其中包括圆心坐标和以弧度为单位的圆半径:db.game.find({tile: {$geoWithin: {$centerSphere: [[0, 0], 0.01]}}});
。
临近查询会返回距离给定点最近的坐标对的文档,并按照距离对结果进行排序:db.game.find({tile: {$near: [0, 0]}});
全文搜索索引
MongoDB Atlas全文搜索索引(full-text search index)基于Apache Lucene。
MongoDB text索引支持全文搜索,不同于精确匹配搜索、模糊搜索、正则表达式搜索。text索引需要一定数量的与被索引字段中单词成比例的键。创建text索引可能会消耗大量的系统资源。有分片时,则还会减慢数据移动的速度:当迁移到一个新分片时,所有文本都必须重新进行索引。
创建全文索引:
db.articles.createIndex(
{"title": "text", "body": "text"},
{"weights" : {"title" : 3, "body" : 2}}
)
全文索引中的字段顺序并不重要,等同对待。如果要区别对待,可通过weights
对每个字段指定权重来控制不同字段的相对重要性。索引一旦创建,就不能改变字段的权重,除非删除索引再重建。
对于某些集合,如果不知道文档包含哪个字段。可以使用$**
在文档的所有字符串字段上创建全文本索引。这样做不仅会对顶层的字符串字段建立索引,也会搜索内嵌文档和数组中的字符串字段。
文本索引存在诸多限制,如并未提供中文分词功能,应用场景有限。
TTL索引
并非所有的数据都需要持久化存储,即过了一定时间段后,可以执行硬删除,如监控业务日志。TTL索引对于此场景提供支持。
TTL索引需要声明在一个日期类型的字段上:db.sysLog.ensureIndex({ 'createDate': 1}, { expireAfterSeconds: 3600 });
。为systemlog集合声明一个TTL索引,指向createdDate字段,expireAfterSeconds=3600
表示数据将在createdDate之后3600秒(1小时)后过期。
MongoDB会在周期性运行的后台线程中对该集合进行检查及数据清理工作。TTL索引具有普通索引的功能,同样可以用于加速数据的查询。
修改TTL索引过期时间:db.runCommand({collMod: 'sysLog', index: {keyPattern: {createDate: 1}, expireAfterSeconds: 7200 }});
需要注意以下限制:
- 只能支持单个字段,且必须是非
_id
字段 - TTL索引不能用于固定集合
- TTL索引无法保证及时的数据老化,MongoDB会通过后台的TTL Monitor定时器来清理老化数据,典型的间隔时间是1分钟。当然如果在数据库负载过高的情况下,TTL的行为则会进一步受到影响
- TTL索引对于数据的清理仅仅使用remove命令,并不是很高效。TTL Monitor在运行期间对系统CPU、磁盘都会造成一定的压力。相比之下,按日期分表的方式操作会更加高效
条件索引
partial index,条件索引允许只对部分文档建立索引。
db.book.createIndex({ 'name': 1}, { partialFilterExpression: {rateing: {$gt: 8 } } });
上面的SQL对书籍评分超过8分的文档才创建索引。
稀疏索引
模糊索引
索引特性
唯一性索引
通过unique=true
选项可将索引定义为唯一性索引:db.user.ensureIndex({ name: 1 }, { unique: true });
。
也可用于复合索引:db.book.ensureIndex({ type: 1, title: 1}, { unique: true }); // 分类下的书籍标题保持唯一性
。
也可用于嵌套文档::db.user.ensureIndex({ 'health.height': 1}, { unique: true });
。
嵌套文档的唯一性约束根据不同的MongoDB版本,其行为不太一致。以6.0.5版本来说,字段的位置无所谓,MongoDB会识别出来:
对数组索引使用唯一性约束,可以保证所有的文档之间不会存在重叠的数组元素:db.user.ensureIndex({ 'careers': 1}, { unique: true });
。数组索引上的唯一性约束并无法保证同一个文档中包含重复的元素。需要从应用层进行distinct去重处理,如使用Set集合。
db.user.insertOne({careers: ['DevOps', 'IT manager']});
db.user.insertOne({careers: ['doctor', 'nurse', 'doctor']});
注意事项:
- 唯一性索引对于文档中缺失的字段,会使用null值代替,因此不允许存在多个文档缺失索引字段的情况。
集合现在有2条数据:
对一个新增字段创建索引:db.user.ensureIndex({ 'health.height': 1}, { unique: true });
,报错:Write failed with error code 11000 and error message 'Index build failed: caused by :: E11000 duplicate key error collection: test.user index: health.height_1 dup key: { health.height: null }'
- 对于分片的集合,唯一性约束必须匹配分片规则。换句话说,为了保证全局的唯一性,分片键必须作为唯一性索引的前缀字段。
ensureIndex和createIndex
进阶
explain
和关系型数据库一样,MongoDB也提供explain命令帮助评估指定查询模型(query model)的计划。
命令db.getSiblingDB("corpus").getCollection('mds_factors').find().explain('executionStats');
执行输出:
[
{
"$clusterTime": {
"clusterTime": {"$timestamp": {"t": 1706014465, "i": 1}},
"signature": {
"hash": {"$binary": {"base64": "ZFjBv3to5hMaqrdVckd9c0qZh7M=", "subType": "00"}},
"keyId": 7281537397286764545
}
},
"executionStats": {
"executionSuccess": true,
"nReturned": 1,
"executionTimeMillis": 5,
"totalKeysExamined": 1,
"totalDocsExamined": 1,
"executionStages": {
"stage": "FETCH",
"nReturned": 1,
"executionTimeMillisEstimate": 0,
"works": 2,
"advanced": 1,
"needTime": 0,
"needYield": 0,
"saveState": 0,
"restoreState": 0,
"isEOF": 1,
"invalidates": 0,
"docsExamined": 1,
"alreadyHasObj": 0,
"inputStage": {
"stage": "IXSCAN",
"nReturned": 1,
"executionTimeMillisEstimate": 0,
"works": 2,
"advanced": 1,
"needTime": 0,
"needYield": 0,
"saveState": 0,
"restoreState": 0,
"isEOF": 1,
"invalidates": 0,
"keyPattern": {
"key": 1
},
"indexName": "key",
"isMultiKey": false,
"multiKeyPaths": {
"key": []
},
"isUnique": false,
"isSparse": false,
"isPartial": false,
"indexVersion": 2,
"direction": "forward",
"indexBounds": {
"key": ["[\"factor:Age\", \"factor:Age\"]"]
},
"keysExamined": 1,
"seeks": 1,
"dupsTested": 0,
"dupsDropped": 0,
"seenInvalidated": 0
}
}
},
"ok": 1,
"operationTime": {"$timestamp": {"t": 1706014465, "i": 1}},
"queryPlanner": {
"plannerVersion": 1,
"namespace": "corpus.mds_factors",
"indexFilterSet": false,
"parsedQuery": {
"key": {
"$eq": "factor:Age"
}
},
"winningPlan": {
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"keyPattern": {
"key": 1
},
"indexName": "key",
"isMultiKey": false,
"multiKeyPaths": {
"key": []
},
"isUnique": false,
"isSparse": false,
"isPartial": false,
"indexVersion": 2,
"direction": "forward",
"indexBounds": {
"key": ["[\"factor:Age\", \"factor:Age\"]"]
}
}
},
"rejectedPlans": []
},
"serverInfo": {
"host": "mongodb-replicaset-2",
"port": 27017,
"version": "3.6.20",
"gitVersion": "39c200878284912f19553901a6fea4b31531a899"
}
}
]
解读:
- winningPlan:表示获胜的计划,即数据库经过一系列评估后选择的最优计划,
stage=COLLSCAN
表示全表扫描,IXSCAN表示索引扫描 - executionStats:描述执行的过程信息。nReturned指返回结果条数,而totalDocsExamined表明整个过程扫描多少条记录
参考
- MongoDB进阶与实战:微服务整合、性能优化、架构管理
- MongoDB权威指南
- difference-between-createindex-and-ensureindex-in-java-using-mongodb