在处理大量数据时,分页查询是数据库操作中的一项基本需求。本文将介绍如何在MongoDB中实现分页查询,并探讨如何通过优化来提高查询性能。
在开始之前,需要搭建好开发环境。以下是所需安装的软件和工具:
安装完成后,可以按照以下步骤快速查看结果:
如果在安装MongoDB、设置数据库或项目结构时遇到问题,请参考之前的文章。
MongoDB提供了多种分页查询的方法,其中最常用的是使用skip和limit方法。但是,这种方法在性能上存在一定的问题,因为它需要服务器从集合或索引的开始处遍历到指定的跳过位置,然后才开始返回结果。
db.Cities.find().skip(5200).limit(10);
为了提高性能,可以使用基于最后位置的分页方法。假设需要找到所有人口超过15000的法国城市,初始请求可以这样实现:
var _client = new MongoClient(settings.Value.ConnectionString);
var _database = _client.GetDatabase(settings.Value.Database);
var _context = _database.GetCollection<City>("Cities").AsQueryable<City>();
var query = _context.CitiesLinq
.Where(x => x.CountryCode == "FR" && x.Population >= 15000)
.OrderByDescending(x => x.Id)
.Take(200);
List<City> cityList = await query.ToListAsync();
后续的查询将从最后检索到的Id开始。通过按BSonId排序,可以获取在最后Id之前服务器上最新创建的记录。
在MongoDB中,每个文档都需要一个唯一的_id字段作为主键。这个字段是不可变的,可以是任何类型(默认情况下,如果没有可用的MongoDB ObjectId,它是一个自然的唯一标识符;或者只是一个自增数字)。
[BsonId]
public ObjectId Id { get; set; }
使用默认的ObjectId类型可以带来一些优势,例如可以获取记录添加到数据库时的日期和时间戳。此外,按ObjectId排序将返回MongoDB集合中最后添加的实体。
虽然City类有20个成员,但通常只需要返回实际需要的属性,这将减少从服务器传输的数据量。
cityList.Select(x => new {
BSonId = x.Id.ToString(),
Name,
AlternateNames,
Latitude,
Longitude,
Timezone,
ServerUpdatedOn = x.Id.CreationTime
});
在大多数情况下,很少需要按MongoDB内部ID(_id)的确切顺序获取数据,而不使用任何过滤器(仅使用find())。在大多数情况下,会使用过滤器检索数据,然后对结果进行排序。对于包含排序操作但没有索引的查询,服务器必须在返回任何结果之前将所有文档加载到内存中以执行排序。
可以使用RoboMongo直接在服务器上创建索引:
db.Cities.createIndex({ CountryCode: 1, Population: 1 });
要检查查询是否实际使用了索引,可以使用explain命令:
db.Cities.find({ CountryCode: "FR", Population: { $gt: 15000 } }).explain();
唯一找到实际查询的方法是通过GetExecutionModel()方法。这提供了详细信息,但内部元素不容易访问。
query.GetExecutionModel();
使用调试器,也可以看到元素以及发送到MongoDB的完整实际查询。然后,可以使用RoboMongo工具执行查询,并查看执行计划的详细信息。
LINQ比直接使用API慢一些,因为它为查询添加了抽象。这种抽象允许轻松地将MongoDB更改为另一个数据源(MS SQL Server / Oracle / MySQL等),而无需进行太多代码更改,这种抽象会带来轻微的性能损失。
尽管如此,MongoDB .NET驱动程序的较新版本已经简化了过滤和运行查询的方式。流畅的接口(IFindFluent)带来了很多与LINQ编写代码的方式。
var filterBuilder = Builders<City>.Filter;
var filter = filterBuilder.Eq(x => x.CountryCode, "FR")
& filterBuilder.Gte(x => x.Population, 10000)
& filterBuilder.Lte(x => x.Id, ObjectId.Parse("58fc8ae631a8a6f8d000f9c3"));
return await _context.Cities.Find(filter)
.SortByDescending(p => p.Id)
.Limit(200)
.ToListAsync();
private Expression<Func<City, bool>> GetConditions(string countryCode, string lastBsonId, int minPopulation = 0)
{
Expression<Func<City, bool>> conditions = (x => x.CountryCode == countryCode
&& x.Population >= minPopulation);
ObjectId id;
if (!string.IsNullOrEmpty(lastBsonId) && ObjectId.TryParse(lastBsonId, out id))
{
conditions = (x => x.CountryCode == countryCode
&& x.Population >= minPopulation
&& x.Id < id);
}
return conditions;
}
public async Task<object> GetCitiesLinq(string countryCode, string lastBsonId, int minPopulation = 0)
{
try
{
var items = await _context.CitiesLinq
.Where(GetConditions(countryCode, lastBsonId, minPopulation))
.OrderByDescending(x => x.Id)
.Take(200)
.ToListAsync();
var returnItems = items.Select(x => new {
BsonId = x.Id.ToString(),
Timestamp = x.Id.Timestamp,
ServerUpdatedOn = x.Id.CreationTime,
x.Name,
x.CountryCode,
x.Population
});
int countItems = await _context.CitiesLinq
.Where(GetConditions(countryCode, "", minPopulation))
.CountAsync();
return new {
count = countItems,
items = returnItems
};
}
catch (Exception ex)
{
throw ex;
}
}