MongoDB 通配符索引 (wildcard index) 的利与弊
2023-12-27 20:16 abce 阅读(130) 评论(0) 编辑 收藏 举报MongoDB 支持在单个字段或多个字段上创建索引,以提高查询性能。MongoDB 支持灵活的模式,这意味着文档字段名在集合中可能会有所不同。使用通配符索引可支持针对任意或未知字段的查询。
·一个集合中可以创建多个通配符索引
·通配符索引可以覆盖与集合中其他索引相同的字段
·通配符索引默认省略 _id 字段。要在通配符索引中包含 _id 字段,必须明确指定 { "_id" : 1 }
·通配符索引是稀疏索引,只包含具有索引字段的文档条目,即使索引字段包含空值也是如此
·通配符索引 (wildcard index) 与通配符文本索引 (wildcard text index) 不同,也不兼容。通配符索引不支持使用 $text 操作符的查询。
要创建通配符索引,使用通配符指定符 ($**) 作为索引键:
1 | db.collection.createIndex( { "$**" : <sortOrder> } ) |
通配符索引的简单理念是,在不预先知道文档中的字段的情况下,提供创建索引的可能性。你可以输入任何你需要的内容,MongoDB 会索引所有内容,无论字段名称如何,无论数据类型如何。这项功能看起来很神奇,但也付出了一些代价。
为了测试通配符索引,让我们创建一个用于存储用户详细信息的小型集合。集合有一些固定字段,如姓名、出生日期和性别,但也有一个子文档 userMetadata,用于存储我们事先不知道的任何其他属性。这样,我们就可以存储所需的一切。
插入数据
1 2 3 4 5 6 | 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" ) } } } ) |
查看数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | > 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 字段上创建通配符索引。
1 2 3 4 5 6 7 8 9 | 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 中的每个字段和任何数成员在索引中创建一个条目。
现在,我们可以利用该索引执行任何类型的查询。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | 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 可以检查一下查询是否用到了索引。
同理,下面的查询也可以从刚才创建的索引受益:
1 2 3 4 | 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}) |
在整个文档上创建通配符索引
在整个文档上创建通配符索引如何?这可行吗?
是的,可以。如果我们事先对将在集合中获得的文档一无所知,我们就可以这样做。
有一种特殊的语法可以做到这一点。在不指定字段名的情况下,再次使用 $**。
1 2 3 | abce> db. user .createIndex( { "$**" : 1 } ) $**_1 abce> |
同样可以执行一下刚才执行过的查询。可以看到所有列都被索引了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | 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> |
优点和缺点
通配符索引的优点不言而喻,那就是它具有极大的灵活性。只需索引所有内容,甚至是你意想不到的内容。
缺点在于索引的大小。索引如果能被内存缓存,就能发挥最大功效。如果我们无法控制(或无法预见)我们创建的数据量,通配符索引的大小就会爆炸。
测试的数据集非常小,所以不用担心这些数字。但想想如果是非常大的集合,会发生什么情况。索引的大小可能会失控。
可以使用一个简单的技巧来增加集合的大小。运行以下语句,随时将文档数量翻倍。根据你想要的文档数量,执行八次或十次,或者更多。
1 | db. user .find( {}, {_id:0}).forEach( function (doc) { db. user .insertOne(doc); } ) |
通配符索引一开始的好处是让事情变得更灵活,但最后却成为性能的严重瓶颈,导致更多的内存使用和交换。此外,大多数情况下,最频繁的查询只使用几个字段。使用通配符索引并不总是有意义的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
2022-12-27 找出PostgreSQL schema变更差异