【问题标题】:Mongoengine is very slow on large documents compared to native pymongo usage与原生 pymongo 使用相比,Mongoengine 在大型文档上非常慢
【发布时间】:2016-05-17 09:17:43
【问题描述】:

我有以下 mongoengine 模型:

class MyModel(Document):
    date        = DateTimeField(required = True)
    data_dict_1 = DictField(required = False)
    data_dict_2 = DictField(required = True)

在某些情况下,数据库中的文档可能非常大(大约 5-10MB),并且 data_dict 字段包含复杂的嵌套文档(字典列表的字典等...)。

我遇到了两个(可能相关的)问题:

  1. 当我运行本机 pymongo find_one() 查询时,它会在一秒钟内返回。当我运行 MyModel.objects.first() 时,需要 5-10 秒。
  2. 当我从数据库中查询单个大文档,然后访问其字段时,只需 10-20 秒即可完成以下操作:

    m = MyModel.objects.first()
    val = m.data_dict_1.get(some_key)
    

对象中的数据不包含对任何其他对象的任何引用,因此这不是对象取消引用的问题。
我怀疑这与 mongoengine 的内部数据表示的一些低效率有关,这会影响文档对象的构造以及字段访问。有什么办法可以改善吗?

【问题讨论】:

  • 似乎有两种不同的东西。本机方法加载 1 个文档并停止,而 mongoengine 加载所有文档并返回第一个。尝试从find_one() 更改为list(db.collection.find())[0] 到equal 方法。
  • 我还尝试在 mongoengine 查询中添加 limit(1),但没有帮助。似乎大部分时间都花在构建 mongoengine Document 对象上,以及所有嵌套对象..
  • skiplimit 在加载文档后工作:( 尝试按特定查询过滤...http://docs.mongoengine.org/guide/querying.html#query-operators
  • 我认为这是不正确的 - 从我在使用 limit(1) 时在 mongoengine 查询的“解释”中看到的内容来看,它只查询来自 mongo 的单个文档(“解释”结果直接来自mongo)。因此,在同样的意义上,与 list(db.collection.find())[0] 进行比较是不正确的 - 因为这会将整个集合的内容加载到内存中 - 而不是 mongoengine 正在做的事情。
  • skiplimit 应用于加载的文档,它们作为查询参数发送。

标签: python mongodb pymongo mongoengine


【解决方案1】:

TL;DR:mongoengine 花费很长时间将所有返回的数组转换为字典

为了测试这一点,我构建了一个包含 DictField 和大型嵌套 dict 的文档的集合。该文档大致在您的 5-10MB 范围内。

然后我们可以使用timeit.timeit 来确认使用 pymongo 和 mongoengine 读取的差异。

然后我们可以使用pycallgraphGraphViz 来看看是什么让mongoengine 这么长时间。

这是完整的代码:

import datetime
import itertools
import random
import sys
import timeit
from collections import defaultdict

import mongoengine as db
from pycallgraph.output.graphviz import GraphvizOutput
from pycallgraph.pycallgraph import PyCallGraph

db.connect("test-dicts")


class MyModel(db.Document):
    date = db.DateTimeField(required=True, default=datetime.date.today)
    data_dict_1 = db.DictField(required=False)


MyModel.drop_collection()

data_1 = ['foo', 'bar']
data_2 = ['spam', 'eggs', 'ham']
data_3 = ["subf{}".format(f) for f in range(5)]

m = MyModel()
tree = lambda: defaultdict(tree)  # http://stackoverflow.com/a/19189366/3271558
data = tree()
for _d1, _d2, _d3 in itertools.product(data_1, data_2, data_3):
    data[_d1][_d2][_d3] = list(random.sample(range(50000), 20000))
m.data_dict_1 = data
m.save()


def pymongo_doc():
    return db.connection.get_connection()["test-dicts"]['my_model'].find_one()


def mongoengine_doc():
    return MyModel.objects.first()


if __name__ == '__main__':
    print("pymongo took {:2.2f}s".format(timeit.timeit(pymongo_doc, number=10)))
    print("mongoengine took", timeit.timeit(mongoengine_doc, number=10))
    with PyCallGraph(output=GraphvizOutput()):
        mongoengine_doc()

并且输出证明 mongoengine 与 pymongo 相比非常慢:

pymongo took 0.87s
mongoengine took 25.81118331072267

生成的调用图非常清楚地说明了瓶颈所在:

本质上,mongoengine 将在它从数据库返回的每个DictField 上调用 to_python 方法。 to_python 非常慢,在我们的示例中,它被称为疯狂的次数。

Mongoengine 用于优雅地将您的文档结构映射到 python 对象。如果您有非常大的非结构化文档(mongodb 非常适合),那么 mongoengine 并不是真正合适的工具,您应该只使用 pymongo。

但是,如果您知道结构,则可以使用 EmbeddedDocument 字段从 mongoengine 获得稍好的性能。我运行了类似但不等效的测试code in this gist,输出为:

pymongo with dict took 0.12s
pymongo with embed took 0.12s
mongoengine with dict took 4.3059175412661075
mongoengine with embed took 1.1639373211854682

所以你可以让 mongoengine 更快,但 pymongo 仍然更快。

更新

这里 pymongo 界面的一个很好的捷径是使用聚合框架:

def mongoengine_agg_doc():
    return list(MyModel.objects.aggregate({"$limit":1}))[0]

【讨论】:

  • 很好的分析,非常感谢! (我应该自己做的!)。我不确定 mongoengine 以 DictField 格式保存所有嵌入数据是否明智,或者至少有办法避免它。我在mongoengine的github上开了一个issue,希望对你有帮助:github.com/MongoEngine/mongoengine/issues/1230
  • 谢谢,是的,您似乎应该能够将一个字段指定为通用 pymongo 引用。
  • 这是无价之宝。感谢分享。
猜你喜欢
  • 2021-10-28
  • 2021-02-13
  • 2017-05-07
  • 1970-01-01
  • 2017-06-05
  • 2015-01-27
  • 2014-09-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多