65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (6投票s)

2017年8月16日

CPOL

5分钟阅读

viewsIcon

18188

downloadIcon

103

一些关于如何在 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 驱动程序进行分页

安装说明

以下是所有需要安装的东西

查看结果

以下是一些让解决方案准备就绪并立即查看结果的步骤

  1. 下载项目.
  2. Data 文件夹(可在 GitHub 中找到)运行 import.bat 文件。这将创建数据库 (TravelDb) 并填充两个集合。
  3. 使用 Visual Studio 2017 打开解决方案,并检查 appsettings.json 中的连接设置。
  4. 运行解决方案。

如果您在安装 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

这里只是一个示例

结尾

希望这有帮助,如果您需要扩展它或有任何疑问,请告诉我。

© . All rights reserved.