持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情
mongodb中我们可以对任何查询进行排序。现在有两种方式可以排序。内存中排序或索引排序。
内存中排序
集合中的文档无序存储在磁盘磁盘上,因此,当我们查询时,我们的文档将按照服务器找到他们的顺序返回。我们需要的排序一般情况下和服务器存储的顺序是不一致的。
这意味着,我们想要排序时,服务器必须先将文档从磁盘读取到内存中,然后在内存中,会按照某种排序算法排序。排序的时间和文档数量有关。
此外,由于在内存中对大量文档进行排序可能是一项耗费较大的操作,因此排序使用了32MB内存时,服务器将中止在内存中排序。所以我们在排序的过程中需要引入索引排序。
内存排序
在创建索引的时候,我们会对指定的字段进行排序。服务器可以通过利用索引的排序,如果使用索引扫描,则返回的文档的顺序是按照索引键排序的。这意味着不需要重新排序。需要注意的是:这些文档只会根据索引的字段进行排序。
如果我们对last_name升序索引,则文档将根据last_name排序。查询时会考虑对查询有帮助的索引。
继续使用 person.json的数据。
先查看一下该表有哪些索引
db.people.getIndexes()
[
{ v: 2, key: { _id: 1 }, name: '_id_' },
{ v: 2, key: { ssn: 1 }, name: 'ssn_1' }
]
我们对ssn 字段进行升序排序。
db.people.find({}, { _id : 0, last_name: 1, first_name: 1, ssn: 1 }).sort({ ssn: 1 })
[
{ last_name: 'Moreno', ssn: '001-02-2512', first_name: 'Stephen' },
{ last_name: 'Woods', ssn: '001-04-7274', first_name: 'Brian' },
{ last_name: 'Olson', ssn: '001-11-1920', first_name: 'Jorge' },
{ last_name: 'Sandoval', ssn: '001-11-9854', first_name: 'Jonathan' },
{ last_name: 'Freeman', ssn: '001-12-6534', first_name: 'Russell' },
{ last_name: 'Dillon', ssn: '001-17-9801', first_name: 'Diamond' },
{
last_name: 'Montgomery',
ssn: '001-18-5224',
first_name: 'Abigail'
},
{ last_name: 'Rodriguez', ssn: '001-19-0546', first_name: 'Tara' },
{ last_name: 'Webb', ssn: '001-19-7087', first_name: 'Shawn' },
{ last_name: 'Hendricks', ssn: '001-20-1316', first_name: 'Gabriel' },
{ last_name: 'Rivas', ssn: '001-20-2471', first_name: 'Rebecca' },
{ last_name: 'French', ssn: '001-20-5492', first_name: 'Holly' },
{ last_name: 'Thomas', ssn: '001-21-4337', first_name: 'Erica' },
{ last_name: 'Palmer', ssn: '001-23-1002', first_name: 'Christina' },
{ last_name: 'Perez', ssn: '001-23-9555', first_name: 'Antonio' },
{ last_name: 'Wright', ssn: '001-28-5499', first_name: 'Natasha' },
{ last_name: 'Chambers', ssn: '001-29-9184', first_name: 'Colleen' },
{ last_name: 'Robinson', ssn: '001-30-2794', first_name: 'Amy' },
{ last_name: 'Collins', ssn: '001-30-4018', first_name: 'Maria' },
{ last_name: 'Delgado', ssn: '001-32-4444', first_name: 'Kathleen' }
]
Type "it" for more
可以看到按照ssn正序返回了,返回了前20个文档。现在我们对排序进行分析:
var exp = db.people.explain('executionStats')
exp.find({}, { _id : 0, last_name: 1, first_name: 1, ssn: 1 }).sort({ ssn: 1 })
{
queryPlanner: {
winningPlan: {
stage: 'PROJECTION_SIMPLE',
transformBy: { _id: 0, last_name: 1, first_name: 1, ssn: 1 },
inputStage: {
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
indexName: 'ssn_1',
indexBounds: { ssn: [ '[MinKey, MaxKey]' ] }
}
}
},
},
executionStats: {
executionSuccess: true,
nReturned: 50474,
executionTimeMillis: 256,
totalKeysExamined: 50474,
totalDocsExamined: 50474,
executionStages: {
stage: 'PROJECTION_SIMPLE',
nReturned: 50474,
executionTimeMillisEstimate: 131,
works: 50475,
advanced: 50474,
inputStage: {
stage: 'FETCH',
nReturned: 50474,
executionTimeMillisEstimate: 128,
works: 50475,
advanced: 50474,
docsExamined: 50474,
inputStage: {
stage: 'IXSCAN',
nReturned: 50474,
executionTimeMillisEstimate: 10,
works: 50475,
advanced: 50474,
indexName: 'ssn_1',
//索引方向,向前
direction: 'forward',
keysExamined: 50474,
}
}
}
}
}
可以看到必须查看5万条文档,返回了5万条文档,同时页扫描了5万个索引键,我们依然进行了索引扫描。
如果改为first_name 排序,不使用索引呢。
{
queryPlanner: {
winningPlan: {
stage: 'SORT',
sortPattern: { first_name: 1 },
memLimit: 104857600,
type: 'simple',
inputStage: {
stage: 'PROJECTION_SIMPLE',
transformBy: { _id: 0, last_name: 1, first_name: 1, ssn: 1 },
inputStage: { stage: 'COLLSCAN', direction: 'forward' }
}
}
},
executionStats: {
executionSuccess: true,
nReturned: 50474,
executionTimeMillis: 178,
totalKeysExamined: 0,
totalDocsExamined: 50474,
executionStages: {
stage: 'SORT',
nReturned: 50474,
executionTimeMillisEstimate: 58,
works: 100951,
advanced: 50474,
inputStage: {
stage: 'PROJECTION_SIMPLE',
nReturned: 50474,
executionTimeMillisEstimate: 14,
works: 50476,
advanced: 50474,
inputStage: {
stage: 'COLLSCAN',
nReturned: 50474,
executionTimeMillisEstimate: 2,
works: 50476,
advanced: 50474
}
}
}
}
}
这次依然检查了5万条文档,返回了5万条。到那时没有查看索引键,那是因为我们在内存中做了排序,stage=SORT,进行了集合扫描,将所有文档读入内存,然后在内存中进行的排序。
接下来ssn按照降序排序。
exp.find({}, { _id : 0, last_name: 1, first_name: 1, ssn: 1 }).sort({ ssn: -1 })
{
queryPlanner: {
winningPlan: {
stage: 'PROJECTION_SIMPLE',
transformBy: { _id: 0, last_name: 1, first_name: 1, ssn: 1 },
inputStage: {
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
//索引顺序,向后
direction: 'backward',
indexBounds: { ssn: [ '[MaxKey, MinKey]' ] }
}
}
}
},
executionStats: {
executionSuccess: true,
nReturned: 50474,
executionTimeMillis: 98,
totalKeysExamined: 50474,
totalDocsExamined: 50474,
executionStages: {
stage: 'PROJECTION_SIMPLE',
nReturned: 50474,
executionTimeMillisEstimate: 10,
works: 50475,
advanced: 50474,
inputStage: {
stage: 'FETCH',
nReturned: 50474,
executionTimeMillisEstimate: 7,
works: 50475,
advanced: 50474,
inputStage: {
stage: 'IXSCAN',
nReturned: 50474,
executionTimeMillisEstimate: 3,
works: 50475,
advanced: 50474,
direction: 'backward',
}
}
}
}
}
结果几乎和正序一样,只不过索引的顺序不一致。所以当我们使用单个字段索引排序时。无论索引键的顺序如何,都可以对文档升序或降序,这块看起来可能不是很重要。当我们使用复合索引的时候,升序和降序会很重要。
对比
通过上述解释我们可以对比,发现索引排序和内存排序结果相差并不是很大。更有甚者内存可能比索引还要快,扫描的文档数和扫描键数也都一致。
但是我们往往在使用的过程中,不会单单只做排序并且拿到所有数据。往往是结合skip和limit 来分页查询。
接下来我们对比一下只查询前10条记录:
内存排序
exp.find({}, { _id : 0, last_name: 1, first_name: 1, ssn: 1 }).sort({ first_name: 1 }).limit(10)
{
executionStats: {
executionSuccess: true,
nReturned: 10,
executionTimeMillis: 30,
totalKeysExamined: 0,
totalDocsExamined: 50474,
}
}
索引排序
exp.find({}, { _id : 0, last_name: 1, first_name: 1, ssn: 1 }).sort({ ssn: 1 }).limit(10)
{
executionStats: {
executionSuccess: true,
nReturned: 10,
executionTimeMillis: 0,
totalKeysExamined: 10,
totalDocsExamined: 10,
}
}
| 索引键 | 内存 | |
|---|---|---|
| 耗时 | <1ms | 30ms |
| 返回数 | 10 | 10 |
| 索引键 | 10 | 0 |
| 文档 | 10 | 5万条 |