使用索引对查询结果进行排序
由于索引包含有序记录,MongoDB 可以从包含排序字段的索引中获取排序结果。如果此排序使用与查询谓词相同的索引,MongoDB 则可能会使用多个索引来支持排序操作。
如果 MongoDB 无法使用一个或多个索引来获取排序顺序,则 MongoDB 必须对数据执行阻塞排序操作。阻塞排序表示 MongoDB 必须在返回结果之前消耗并处理排序的所有输入文档。阻塞排序不会阻塞对集合或数据库的并发操作。
从 MongoDB 6.0 开始,如果服务器在管道执行阶段需要超过 100 MB 的内存,MongoDB 会自动将临时文件写入磁盘,除非该查询指定了 { allowDiskUse: false }
。如果服务器需要超过 100 MB 的系统内存来执行阻塞排序操作,则 MongoDB 将返回错误,除非该查询指定 cursor.allowDiskUse()
。有关详细信息,请参阅 allowDiskUseByDefault
。
使用索引的排序通常比阻塞排序的性能更好。
注意
使用单字段索引排序
如果升序或降序索引位于单个字段上,则该字段上的排序操作可以是任一方向。
例如,在集合 records
的字段 a
上创建升序索引:
db.records.createIndex( { a: 1 } )
该索引可以支持对 a
升序排序:
db.records.find().sort( { a: 1 } )
该索引还可以以相反顺序遍历索引,从而支持 a
上的以下降序排序:
db.records.find().sort( { a: -1 } )
对多个字段进行排序
创建复合索引以支持对多个字段进行排序。
您可以指定对索引的所有键排序,也可以指定对子集排序;但是排序键的排列顺序必须与其在索引中出现的顺序相同。例如,索引键模式 { a: 1, b: 1
}
可以支持对 { a: 1, b: 1 }
排序,但不支持对 { b: 1, a:
1 }
排序。
要使查询使用复合索引排序,cursor.sort()
文档中所有键的指定排序方向必须与索引键模式相匹配,或与索引键的反向模式相匹配。例如,索引键模式 { a: 1, b: -1 }
可以支持对 { a: 1, b: -1 }
和 { a: -1, b: 1 }
排序,但不支持对 { a: -1, b: -1 }
或 {a: 1, b: 1}
排序。
排序和索引前缀
如果排序键对应于索引键或索引前缀,则 MongoDB 可以用索引对查询结果排序。复合索引的前缀是由索引键模式开头的一个或多个键组成的子集。
例如,在 data
集合上创建复合索引:
db.data.createIndex( { a:1, b: 1, c: 1, d: 1 } )
然后,以下是该索引的前缀:
{ a: 1 } { a: 1, b: 1 } { a: 1, b: 1, c: 1 }
以下查询和排序操作使用索引前缀对结果排序。这些操作不需要对内存中的结果集排序。
例子 | Index Prefix |
---|---|
db.data.find().sort( { a: 1 } ) | { a: 1 } |
db.data.find().sort( { a: -1 } ) | { a: 1 } |
db.data.find().sort( { a: 1, b: 1 } ) | { a: 1, b: 1 } |
db.data.find().sort( { a: -1, b: -1 } ) | { a: 1, b: 1 } |
db.data.find().sort( { a: 1, b: 1, c: 1 } ) | { a: 1, b: 1, c: 1 } |
db.data.find( { a: { $gt: 4 } } ).sort( { a: 1, b: 1 } ) | { a: 1, b: 1 } |
以下示例中,索引的前缀键同时出现在查询谓词和排序操作内:
db.data.find( { a: { $gt: 4 } } ).sort( { a: 1, b: 1 } )
在这种情况下,MongoDB 可以使用索引按照排序指定的顺序获取文档。如示例所示,查询谓词中的索引前缀可以与排序中的前缀不同。
索引的排序和非前缀子集
索引可以支持对索引键模式的非前缀子集执行排序操作。为此,查询必须在排序键之前的所有前缀键上包含相等条件。
例如,集合 data
具有以下索引:
{ a: 1, b: 1, c: 1, d: 1 }
以下操作可以使用索引来获取排序顺序:
例子 | Index Prefix |
---|---|
db.data.find( { a: 5 } ).sort( { b: 1, c: 1 } ) | { a: 1 , b: 1, c: 1 } |
db.data.find( { b: 3, a: 4 } ).sort( { c: 1 } ) | { a: 1, b: 1, c: 1 } |
db.data.find( { a: 5, b: { $lt: 3} } ).sort( { b: 1 } ) | { a: 1, b: 1 } |
如上一操作所示,只有排序子集前面的索引字段必须具备查询文档中的相等条件;其他索引字段可以指定其他条件。
如果查询未在排序规范之前或与排序规范重叠的索引前缀上指定相等条件,操作将无法有效使用索引。例如,以下操作指定的排序文档为 { c: 1 }
,但查询文档不包含前面索引字段 a
和 b
的相等匹配项:
db.data.find( { a: { $gt: 2 } } ).sort( { c: 1 } ) db.data.find( { c: 5 } ).sort( { c: 1 } )
这些操作不会高效地使用索引 { a: 1, b: 1,
c: 1, d: 1 }
,甚至可能不会使用索引来检索文档。
索引排序顺序
对于一个已建立索引的文档集合,它的关键字段可能有多种数据类型。
当索引的键具有多种数据类型时,索引将根据 BSON 类型排序顺序进行排序。
在数组比较时:
小于比较,或是升序排序,根据 BSON 类型排序顺序来比较数组的最小元素。
大于比较,或是降序排序,根据反向 BSON 类型排序顺序来比较数组的最大元素。
比较值为单元素数组的字段(如
[ 1 ]
)与非数组字段(如2
)时,比较对象为1
和2
。比较空数组(如
[ ]
)时将空数组视为小于null
值或缺少字段值。
请参阅 索引排序示例。
索引使用和排序规则
要使用索引进行字符串比较,操作还必须指定相同的排序规则。换言之,如果一个操作对索引字段进行字符串比较,但又设定了与索引字段不同的排序规则,那么这个设有排序规则的索引将无法支持该操作。
警告
由于配置了排序规则的索引是通过 ICU 排序规则键来实现排序,因此,相比未配置排序规则的索引的索引键,有排序规则感知的索引键可能会更大。
例如,集合 myColl
在字符串字段 category
上具有一个索引,排序规则语言环境为 "fr"
。
db.myColl.createIndex( { category: 1 }, { collation: { locale: "fr" } } )
以下查询操作指定了与索引相同的排序规则,因此可以使用索引:
db.myColl.find( { category: "cafe" } ).collation( { locale: "fr" } )
而以下查询操作默认使用“简易的”二进制排序器,因此无法使用索引:
db.myColl.find( { category: "cafe" } )
如果一个复合索引的前缀键不是字符串、数组和嵌入式文档,在这种情况下,即使查询操作指定了一个与索引不同的排序规则,它仍然可以利用该复合索引来支持对其前缀健的比较。
例如,集合 myColl
在数值字段 score
和 price
以及字符串字段 category
上有一个复合索引;该索引使用排序规则语言环境 "fr"
创建,用于进行字符串比较:
db.myColl.createIndex( { score: 1, price: 1, category: 1 }, { collation: { locale: "fr" } } )
以下操作使用 "simple"
二进制排序规则进行字符串比较,它们可以使用索引:
db.myColl.find( { score: 5 } ).sort( { price: 1 } ) db.myColl.find( { score: 5, price: { $gt: NumberDecimal( "10" ) } } ).sort( { price: 1 } )
以下操作使用 "simple"
二进制排序规则对索引的 category
字段进行字符串比较,它们可以使用索引仅完成查询的 score: 5
部分:
db.myColl.find( { score: 5, category: "cafe" } )
重要
与文档键(包括嵌入式文档键)的匹配使用简单的二进制比较。这意味着类似“foo.bár”的键的查询不会匹配“foo.bar”键,无论您为 strength 参数设置了什么值。
示例
以下示例展示了当索引键具有相同或不同类型时的排序情况。
创建 keyTypes
集合:
db.keyTypes.insertMany( [ { seqNum: 1, seqType: null, type: "null" }, { seqNum: 29, seqType: null, type: "null" }, { seqNum: 2, seqType: Int32("10"), type: "Int32" }, { seqNum: 28, seqType: Int32("10"), type: "Int32" }, { seqNum: 3, seqType: Long("10"), type: "Long" }, { seqNum: 27, seqType: Long("10"), type: "Long" }, { seqNum: 4, seqType: Decimal128("10"), type: "Decimal128" }, { seqNum: 26, seqType: Decimal128("10"), type: "Decimal128" }, { seqNum: 5, seqType: Double("10"), type: "Double" }, { seqNum: 25, seqType: Double("10"), type: "Double" }, { seqNum: 6, seqType: String("10"), type: "String" }, { seqNum: 24, seqType: String("10"), type: "String" }, { seqNum: 7, seqType: [ "1", "2", "3" ], type: "Array" }, { seqNum: 23, seqType: [ "1", "2", "3" ], type: "Array" }, { seqNum: 8, seqType: [ [1], [2], [3] ], type: "Array" }, { seqNum: 22, seqType: [ [1], [2], [3] ], type: "Array " }, { seqNum: 9, seqType: [ 1, 2, 3 ], type: "Array" }, { seqNum: 21, seqType: [ 1, 2, 3 ], type: "Array" }, { seqNum: 10, seqType: true, type: "Boolean" }, { seqNum: 11, seqType: new Timestamp(), type: "Timestamp" }, { seqNum: 12, seqType: new Date(), type: "Date" }, { seqNum: 13, seqType: new ObjectId(), type: "ObjectId" }, ] )
在序列号 (seqNum
) 和序列类型 (seqType
) 字段上创建索引:
db.keyTypes.createIndex( { seqNum: 1 } ) db.keyTypes.createIndex( { seqType: 1 } )
使用 find()
查询集合。投影文档 { _id: 0 }
隐藏输出显示中的 _id
字段。
db.keyTypes.find( {}, { _id: 0 } )
文档会按插入顺序返回:
{ seqNum: 1, seqType: null, type: 'null' }, { seqNum: 29, seqType: null, type: 'null' }, { seqNum: 2, seqType: 10, type: 'Int32' }, { seqNum: 28, seqType: 10, type: 'Int32' }, { seqNum: 3, seqType: Long("10"), type: 'Long' }, { seqNum: 27, seqType: Long("10"), type: 'Long' }, { seqNum: 4, seqType: Decimal128("10"), type: 'Decimal128' }, // Output truncated
序列号 (seqNum
) 索引具有相同类型的值。使用 seqNum
索引查询 keyTypes
集合:
db.keyTypes.find( {}, { _id: 0 } ).sort( { seqNum: 1} )
seqNum
键是整数。文档将按数值顺序返回:
{ seqNum: 1, seqType: null, type: 'null' }, { seqNum: 2, seqType: 10, type: 'Int32' }, { seqNum: 3, seqType: Long("10"), type: 'Long' }, { seqNum: 4, seqType: Decimal128("10"), type: 'Decimal128' }, { seqNum: 5, seqType: 10, type: 'Double' }, { seqNum: 6, seqType: '10', type: 'String' }, { seqNum: 7, seqType: [ '1', '2', '3' ], type: 'Array' }, // Output truncated
序列类型 (seqType
) 索引具有不同类型的值。使用 seqType
索引查询 keyTypes
集合:
db.keyTypes.find( {}, { _id: 0 } ).sort( { seqType: 1} )
文档以 BSON 类型排序顺序返回:
{ seqNum: 1, seqType: null, type: 'null' }, { seqNum: 29, seqType: null, type: 'null' }, { seqNum: 9, seqType: [ 1, 2, 3 ], type: 'Array' }, { seqNum: 21, seqType: [ 1, 2, 3 ], type: 'Array' }, { seqNum: 2, seqType: 10, type: 'Int32' }, { seqNum: 28, seqType: 10, type: 'Int32' }, { seqNum: 3, seqType: Long("10"), type: 'Long' }, { seqNum: 27, seqType: Long("10"), type: 'Long' }, { seqNum: 4, seqType: Decimal128("10"), type: 'Decimal128' }, { seqNum: 26, seqType: Decimal128("10"), type: 'Decimal128' }, { seqNum: 5, seqType: 10, type: 'Double' }, { seqNum: 25, seqType: 10, type: 'Double' }, { seqNum: 7, seqType: [ '1', '2', '3' ], type: 'Array' }, { seqNum: 23, seqType: [ '1', '2', '3' ], type: 'Array' }, { seqNum: 6, seqType: '10', type: 'String' }, { seqNum: 24, seqType: '10', type: 'String' }, { seqNum: 8, seqType: [ [ 1 ], [ 2 ], [ 3 ] ], type: 'Array' }, { seqNum: 22, seqType: [ [ 1 ], [ 2 ], [ 3 ] ], type: 'Array ' }, { seqNum: 13, seqType: ObjectId("6239e3922604d5a7478df071"), type: 'ObjectId' }, { seqNum: 10, seqType: true, type: 'Boolean' }, { seqNum: 12, seqType: ISODate("2022-03-22T14:56:18.100Z"), type: 'Date' }, { seqNum: 11, seqType: Timestamp({ t: 1647960978, i: 1 }), type: 'Timestamp' }
在数组比较时:
小于比较,或是升序排序,根据 BSON 类型排序顺序来比较数组的最小元素。
大于比较,或是降序排序,根据反向 BSON 类型排序顺序来比较数组的最大元素。
比较值为单元素数组的字段(如
[ 1 ]
)与非数组字段(如2
)时,比较对象为1
和2
。比较空数组(如
[ ]
)时将空数组视为小于null
值或缺少字段值。
与其他类型相比时,数值类型(Int32、Long、Decimal128、Double)是等价的。
在 Numbers BSON 类型中,数字类型排序如下:
Int32
Long
Decimal128
double