MongoDB 中的分页 – 如何真正避免性能不佳?






4.75/5 (6投票s)
一些关于如何在 MongoDB 中更好地分页结果并实现出色性能的见解
从性能角度来看,分页 MongoDB 结果的最佳方法是什么?尤其当您还想获取结果总数时?
*您可以在 https://github.com/fpetru/WebApiQueryMongoDb 找到解决方案以及数据文件。
更新 2017 年 9 月 23 日
解决方案已使用 Visual Studio 2017 转换为 .NET Core 2.0。
从哪里开始?
为了回答这些问题,让我们从我之前那篇文章 如何搜索适合旅行的好地方(MongoDB、LINQ 和 .NET Core) 中定义的 数据集
开始。那篇文章快速介绍了如何加载大量数据,然后使用 WebApi 和 LINQ 检索值。在这里,我将从那个项目开始,并添加更多与分页查询结果相关的详细信息。
涵盖的主题
- 使用 skip 和 limit 分页查询结果
- 使用最后一个位置分页查询结果
- MongoDb BSonId
- 使用 MongoDb .NET 驱动程序进行分页
安装说明
以下是所有需要安装的东西
- Visual Studio Community 2017,包含 .NET Core 选项
- MongoDB 和 Robomongo
查看结果
以下是一些让解决方案准备就绪并立即查看结果的步骤
- 下载项目.
- 从Data 文件夹(可在 GitHub 中找到)运行 import.bat 文件。这将创建数据库 (
TravelDb
) 并填充两个集合。 - 使用 Visual Studio 2017 打开解决方案,并检查 appsettings.json 中的连接设置。
- 运行解决方案。
如果您在安装 MongoDb、设置数据库或项目结构方面遇到任何问题,请回顾我之前的 文章。
使用 cursor.skip() 和 cursor.limit() 分页结果
如果您在 Google 上搜索,这通常是 MongoDB 中分页查询结果的首选方法。这是一种直接的方法,但性能成本也很高。它要求服务器每次都从集合或索引的开头开始遍历,以获取偏移量或跳过的位置,然后再开始返回您需要的结果。
例如
db.Cities.find().skip(5200).limit(10);
服务器将需要解析 WikiVoyage 集合中的前 5200 个项目,然后返回接下来的 10 个。由于 skip()
命令,这扩展性不佳。
使用最后一个位置进行分页
为了更快,我们应该从最后一个检索到的项目开始搜索和检索详细信息。例如,假设我们需要查找所有人口超过 15,000 人的法国城市。
遵循此方法,检索前 200 条记录的初始请求将是
LINQ 格式
我们首先检索 AsQueryable 接口
var _client = new MongoClient(settings.Value.ConnectionString);
var _database = _client.GetDatabase(settings.Value.Database);
var _context = _database.GetCollection<City>("Cities").AsQueryable<City>();
然后我们运行实际的查询
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 之前的最新记录。
query = _context.CitiesLinq
.Where(x => x.CountryCode == "FR"
&& x.Population >= 15000
&& x.Id < ObjectId.Parse("58fc8ae631a8a6f8d000f9c3"))
.OrderByDescending(x => x.Id)
.Take(200);
List<City> cityList = await query.ToListAsync();
Mongo 的 ID
在 MongoDB 中,集合中存储的每个文档都需要一个 唯一的 _id
字段,该字段充当主键。它不可变,并且可以是除数组以外的任何类型(默认情况下,MongoDB ObjectId
,一个自然的唯一标识符,如果可用;或者只是一个自动递增的数字)。
使用默认的 ObjectId
类型,
[BsonId]
public ObjectId Id { get; set; }
它带来了更多优势,例如在记录添加到数据库时可以获取 日期
和 时间戳
。此外,按 ObjectId
排序
将返回添加到 MongoDb 集合中的最后实体。
cityList.Select(x => new
{
BSonId = x.Id.ToString(), // unique hexadecimal number
Timestamp = x.Id.Timestamp,
ServerUpdatedOn = x.Id.CreationTime
/* include other members */
});
返回更少的元素
虽然 City
类有 20 个成员,但只返回我们实际需要的属性会更相关。这将减少从服务器传输的数据量。
cityList.Select(x => new
{
BSonId = x.Id.ToString(), // unique hexadecimal number
Name,
AlternateNames,
Latitude,
Longitude,
Timezone,
ServerUpdatedOn = x.Id.CreationTime
});
MongoDB 中的索引 – 一些细节
我们很少需要按 MongoDB 内部 ID (_id)I
的确切顺序获取数据,而没有任何过滤器(仅使用 find()
)。在大多数情况下,我们将使用过滤器检索数据,然后对结果进行排序。对于包含排序操作但没有索引的查询,服务器必须将所有文档加载到内存中才能执行排序,然后才能返回任何结果。
我们如何添加索引?
使用 RoboMongo,我们直接在服务器上创建索引
db.Cities.createIndex( { CountryCode: 1, Population: 1 } );
我们如何检查我们的查询是否实际使用了索引?
使用 explain
命令运行查询将返回有关索引使用情况的详细信息
db.Cities.find({ CountryCode: "FR", Population : { $gt: 15000 }}).explain();
是否有办法查看 MongoDB LINQ 语句背后的实际查询?
我能找到的唯一方法是通过 GetExecutionModel()
方法。这提供了详细信息,但其中的元素不容易访问。
query.GetExecutionModel();
使用调试器,我们可以查看元素以及发送到 MongoDb 的完整实际查询。
然后,我们可以使用 RoboMongo 工具获取查询并在 MongoDB 上执行它,并查看执行计划的详细信息。
非 LINQ 方法 – 使用 MongoDb .NET 驱动程序
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();
其中 _context
定义为
var _context = _database.GetCollection<City>("Cities");
实现
总而言之,这是我对 paginate
函数的建议。OR
谓词受 MongoDb 支持,但查询优化器通常很难预测 OR
两侧的不相交集。尽可能避免它们是查询优化中一个众所周知的技巧。
// building where clause
//
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();
// select just few elements
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)
{
// log or manage the exception
throw ex;
}
}
以及在控制器中
[NoCache]
[HttpGet]
public async Task<object> Get(string countryCode, int? population, string lastId)
{
return await _travelItemRepository
.GetCitiesLinq(countryCode, lastId, population ?? 0);
}
初始请求(示例)
https://:61612/api/city?countryCode=FR&population=10000
然后是其他请求,我们在其中指定最后检索到的 Id
https://:61612/api/city?countryCode=FR&population=10000&lastId=58fc8ae631a8a6f8d00101f9
这里只是一个示例
结尾
希望这有帮助,如果您需要扩展它或有任何疑问,请告诉我。