MongoDB中的分页–如何真正避免性能下降?

目录

从哪儿开始 ?

涵盖的主题

安装

查看结果

使用cursor.skip()和cursor.limit()分页结果

使用最后一个位置分页

LINQ格式

Mongo的ID

返回较少的元素

MongoDB中的索引–少量细节

我们如何添加索引?

我们如何使用索引检查查询是否真实?

有没有办法查看MongoDB LINQ语句后面的实际查询?

非LINQ方式–使用MongoDb .NET驱动程序

实现


MongoDB中分页结果的最佳方法是什么?特别是当您还想获得结果总数时?

从哪儿开始

为了回答这些问题,让我们从我之前的文章第1部分:如何搜索旅行的好地方(MongoDb LINQ和.NET Core)定义的数据集开始。那篇文章是快速入门,介绍如何加载大块数据,然后使用WebApiLINQ检索值。在这里,我将从该项目开始,用与分页查询结果有关的更多详细信息扩展它。您还可以检查第3部分– MongoDb和LINQ:如何汇总和加入集合

您可以在此处找到完整的解决方案以及数据:https : //github.com/fpetru/WebApiQueryMongoDb

涵盖的主题

  • 使用 skip和 limit来分页查询结果
  • 使用最后位置分页查询结果
  • MongoDb BSonId
  • 使用MongoDb .NET驱动程序分页

安装

这是所有需要安装的东西:

查看结果

以下是准备解决方案的几个步骤,并立即查看结果:

  1. 克隆或下载项目
  2. 从Data文件夹运行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();

MongoID

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_idI的确切顺序的数据,而无需任何过滤器(仅使用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");

实现

总结一下,这是我对分页功能的建议。MongoDb支持OR谓词,但是查询优化器通常很难从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);
}

初始请求(样本):

http://localhost:61612/api/city?countryCode=FR&population=10000

随后是其他请求,其中我们指定了最后一个检索到的ID

http://localhost:61612/api/city?countryCode=FR&population=10000&lastId=58fc8ae631a8a6f8d00101f9

这只是一个示例:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值