MongoDB——如何利用索引进行排序?

337 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情

mongodb中我们可以对任何查询进行排序。现在有两种方式可以排序。内存中排序或索引排序

内存中排序

集合中的文档无序存储在磁盘磁盘上,因此,当我们查询时,我们的文档将按照服务器找到他们的顺序返回。我们需要的排序一般情况下和服务器存储的顺序是不一致的。

这意味着,我们想要排序时,服务器必须先将文档从磁盘读取到内存中,然后在内存中,会按照某种排序算法排序。排序的时间和文档数量有关。

此外,由于在内存中对大量文档进行排序可能是一项耗费较大的操作,因此排序使用了32MB内存时,服务器将中止在内存中排序。所以我们在排序的过程中需要引入索引排序。

内存排序

在创建索引的时候,我们会对指定的字段进行排序。服务器可以通过利用索引的排序,如果使用索引扫描,则返回的文档的顺序是按照索引键排序的。这意味着不需要重新排序。需要注意的是:这些文档只会根据索引的字段进行排序

image.png

如果我们对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',
        }
      }
    }
  }
}

结果几乎和正序一样,只不过索引的顺序不一致。所以当我们使用单个字段索引排序时。无论索引键的顺序如何,都可以对文档升序或降序,这块看起来可能不是很重要。当我们使用复合索引的时候,升序和降序会很重要

对比

通过上述解释我们可以对比,发现索引排序和内存排序结果相差并不是很大。更有甚者内存可能比索引还要快,扫描的文档数和扫描键数也都一致。

但是我们往往在使用的过程中,不会单单只做排序并且拿到所有数据。往往是结合skiplimit 来分页查询。

接下来我们对比一下只查询前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,
  }
}
索引键内存
耗时<1ms30ms
返回数1010
索引键100
文档105万条