MongoDB 通配符索引 (wildcard index) 的利与弊
2023-12-27 20:16 abce 阅读(113) 评论(0) 编辑 收藏 举报MongoDB 支持在单个字段或多个字段上创建索引,以提高查询性能。MongoDB 支持灵活的模式,这意味着文档字段名在集合中可能会有所不同。使用通配符索引可支持针对任意或未知字段的查询。
·一个集合中可以创建多个通配符索引
·通配符索引可以覆盖与集合中其他索引相同的字段
·通配符索引默认省略 _id 字段。要在通配符索引中包含 _id 字段,必须明确指定 { "_id" : 1 }
·通配符索引是稀疏索引,只包含具有索引字段的文档条目,即使索引字段包含空值也是如此
·通配符索引 (wildcard index) 与通配符文本索引 (wildcard text index) 不同,也不兼容。通配符索引不支持使用 $text 操作符的查询。
要创建通配符索引,使用通配符指定符 ($**) 作为索引键:
db.collection.createIndex( { "$**": <sortOrder> } )
通配符索引的简单理念是,在不预先知道文档中的字段的情况下,提供创建索引的可能性。你可以输入任何你需要的内容,MongoDB 会索引所有内容,无论字段名称如何,无论数据类型如何。这项功能看起来很神奇,但也付出了一些代价。
为了测试通配符索引,让我们创建一个用于存储用户详细信息的小型集合。集合有一些固定字段,如姓名、出生日期和性别,但也有一个子文档 userMetadata,用于存储我们事先不知道的任何其他属性。这样,我们就可以存储所需的一切。
插入数据
db.user.insert( { name: "John", date_of_birth: new ISODate("2001-02-05"), gender: 'M', userMetadata: { "likes" : [ "dogs", "cats" ] } } ) db.user.insert( { name: "Marie", date_of_birth: new ISODate("2008-03-12"), gender: 'F', userMetadata: { "dislikes" : "hamsters" } } ) db.user.insert( { name: "Tom", date_of_birth: new ISODate("1998-12-23"), gender: 'M', userMetadata: { "age" : 25 } } ) db.user.insert( { name: "Adrian", date_of_birth: new ISODate("1991-06-22"), gender: 'M', userMetadata: "inactive" } ) db.user.insert( { name: "Janice", date_of_birth: new ISODate("1995-09-04"), gender: 'F', userMetadata: { "shoeSize": 8, "likes": [ "horses", "dogs" ] } } ) db.user.insert( { name: "Peter", date_of_birth: new ISODate("2004-01-25"), gender: 'M', userMetadata: { "drivingLicense": { class: "A", "expirationDate": new ISODate("2030-05-05") } } } )
查看数据
> db.user.find() [ { _id: ObjectId('658927f4bad8d080878a999e'), name: 'John', date_of_birth: ISODate('2001-02-05T00:00:00.000Z'), gender: 'M', userMetadata: { likes: [ 'dogs', 'cats' ] } }, { _id: ObjectId('658927f4bad8d080878a999f'), name: 'Marie', date_of_birth: ISODate('2008-03-12T00:00:00.000Z'), gender: 'F', userMetadata: { dislikes: 'hamsters' } }, { _id: ObjectId('658927f4bad8d080878a99a0'), name: 'Tom', date_of_birth: ISODate('1998-12-23T00:00:00.000Z'), gender: 'M', userMetadata: { age: 25 } }, { _id: ObjectId('658927f5bad8d080878a99a1'), name: 'Adrian', date_of_birth: ISODate('1991-06-22T00:00:00.000Z'), gender: 'M', userMetadata: 'inactive' }, { _id: ObjectId('658927f5bad8d080878a99a2'), name: 'Janice', date_of_birth: ISODate('1995-09-04T00:00:00.000Z'), gender: 'F', userMetadata: { shoeSize: 8, likes: [ 'horses', 'dogs' ] } }, { _id: ObjectId('658927f6bad8d080878a99a3'), name: 'Peter', date_of_birth: ISODate('2004-01-25T00:00:00.000Z'), gender: 'M', userMetadata: { drivingLicense: { class: 'A', expirationDate: ISODate('2030-05-05T00:00:00.000Z') } } } ]
metaData 子文档包含不同的字段。但所有这些字段都没有索引。
假设数据集包含几百万个文档,如何才能在不触发全数据集扫描的情况下,检索出具有特定驾驶执照类别或特定鞋码的所有用户呢?我们可以使用特殊语法 $** 在 userMetadata 字段上创建通配符索引。
abce> db.user.createIndex({ "userMetadata.$**" : 1 }) userMetadata.$**_1 abce> db.user.getIndexes() [ { v: 2, key: { _id: 1 }, name: '_id_' }, { v: 2, key: { 'userMetadata.$**': 1 }, name: 'userMetadata.$**_1' } ] abce> ]
这样,MongoDB 就会为 userMetadata 中的每个字段和任何数成员在索引中创建一个条目。
现在,我们可以利用该索引执行任何类型的查询。
abce> db.user.find({ "userMetadata.likes": "dogs" }) [ { _id: ObjectId('658927f4bad8d080878a999e'), name: 'John', date_of_birth: ISODate('2001-02-05T00:00:00.000Z'), gender: 'M', userMetadata: { likes: [ 'dogs', 'cats' ] } }, { _id: ObjectId('658927f5bad8d080878a99a2'), name: 'Janice', date_of_birth: ISODate('1995-09-04T00:00:00.000Z'), gender: 'F', userMetadata: { shoeSize: 8, likes: [ 'horses', 'dogs' ] } } ] abce> db.user.find({ "userMetadata.likes": "dogs" }).explain() { explainVersion: '1', queryPlanner: { namespace: 'abce.user', indexFilterSet: false, parsedQuery: { 'userMetadata.likes': { '$eq': 'dogs' } }, queryHash: 'E2BC0D70', planCacheKey: '7C6EEF39', maxIndexedOrSolutionsReached: false, maxIndexedAndSolutionsReached: false, maxScansToExplodeReached: false, winningPlan: { stage: 'FETCH', inputStage: { stage: 'IXSCAN', keyPattern: { '$_path': 1, 'userMetadata.likes': 1 }, indexName: 'userMetadata.$**_1', isMultiKey: true, multiKeyPaths: { '$_path': [], 'userMetadata.likes': [ 'userMetadata.likes' ] }, isUnique: false, isSparse: false, isPartial: false, indexVersion: 2, direction: 'forward', indexBounds: { '$_path': [ '["userMetadata.likes", "userMetadata.likes"]' ], 'userMetadata.likes': [ '["dogs", "dogs"]' ] } } }, rejectedPlans: [] }, command: { find: 'user', filter: { 'userMetadata.likes': 'dogs' }, '$db': 'abce' }, serverInfo: { host: 'test', port: 27017, version: '6.0.12', gitVersion: '21e6e8e11a45dfbdb7ca6cf95fa8c5f859e2b118' }, serverParameters: { internalQueryFacetBufferSizeBytes: 104857600, internalQueryFacetMaxOutputDocSizeBytes: 104857600, internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600, internalDocumentSourceGroupMaxMemoryBytes: 104857600, internalQueryMaxBlockingSortMemoryUsageBytes: 104857600, internalQueryProhibitBlockingMergeOnMongoS: 0, internalQueryMaxAddToSetBytes: 104857600, internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600 }, ok: 1 } abce>
通过关键字 IXSCAN 可以检查一下查询是否用到了索引。
同理,下面的查询也可以从刚才创建的索引受益:
db.user.find( { "userMetadata.age" : { $gt: 20 } } ) db.user.find( { "userMetadata": "inactive" } ) db.user.find( { "userMetadata.drivingLicense.class": "A", "userMetadata.drivingLicense.expirationDate": { $lt: ISODate("2032-01-01") } } ) db.user.find( { "userMetadata.shoeSize": 8})
在整个文档上创建通配符索引
在整个文档上创建通配符索引如何?这可行吗?
是的,可以。如果我们事先对将在集合中获得的文档一无所知,我们就可以这样做。
有一种特殊的语法可以做到这一点。在不指定字段名的情况下,再次使用 $**。
abce> db.user.createIndex( { "$**" : 1 } ) $**_1 abce>
同样可以执行一下刚才执行过的查询。可以看到所有列都被索引了:
abce> db.user.find( { name: "Marie" } )
[
{
_id: ObjectId('658927f4bad8d080878a999f'),
name: 'Marie',
date_of_birth: ISODate('2008-03-12T00:00:00.000Z'),
gender: 'F',
userMetadata: { dislikes: 'hamsters' }
}
]
abce> db.user.find( { name: "Marie" } ).explain()
{
explainVersion: '1',
queryPlanner: {
namespace: 'abce.user',
indexFilterSet: false,
parsedQuery: { name: { '$eq': 'Marie' } },
queryHash: '64908032',
planCacheKey: 'A6C0273F',
maxIndexedOrSolutionsReached: false,
maxIndexedAndSolutionsReached: false,
maxScansToExplodeReached: false,
winningPlan: {
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
keyPattern: { '$_path': 1, name: 1 },
indexName: '$**_1',
isMultiKey: false,
multiKeyPaths: { '$_path': [], name: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
'$_path': [ '["name", "name"]' ],
name: [ '["Marie", "Marie"]' ]
}
}
},
rejectedPlans: []
},
command: { find: 'user', filter: { name: 'Marie' }, '$db': 'abce' },
serverInfo: {
host: 'test',
port: 27017,
version: '6.0.12',
gitVersion: '21e6e8e11a45dfbdb7ca6cf95fa8c5f859e2b118'
},
serverParameters: {
internalQueryFacetBufferSizeBytes: 104857600,
internalQueryFacetMaxOutputDocSizeBytes: 104857600,
internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
internalDocumentSourceGroupMaxMemoryBytes: 104857600,
internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
internalQueryProhibitBlockingMergeOnMongoS: 0,
internalQueryMaxAddToSetBytes: 104857600,
internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600
},
ok: 1
}
abce>
优点和缺点
通配符索引的优点不言而喻,那就是它具有极大的灵活性。只需索引所有内容,甚至是你意想不到的内容。
缺点在于索引的大小。索引如果能被内存缓存,就能发挥最大功效。如果我们无法控制(或无法预见)我们创建的数据量,通配符索引的大小就会爆炸。
测试的数据集非常小,所以不用担心这些数字。但想想如果是非常大的集合,会发生什么情况。索引的大小可能会失控。
可以使用一个简单的技巧来增加集合的大小。运行以下语句,随时将文档数量翻倍。根据你想要的文档数量,执行八次或十次,或者更多。
db.user.find( {}, {_id:0}).forEach(function (doc) { db.user.insertOne(doc); } )
通配符索引一开始的好处是让事情变得更灵活,但最后却成为性能的严重瓶颈,导致更多的内存使用和交换。此外,大多数情况下,最频繁的查询只使用几个字段。使用通配符索引并不总是有意义的。