与本机 pymongo 使用相比,Mongoengine 在大型文档上非常慢
Mongoengine is very slow on large documents compared to native pymongo usage
我有以下 mongoengine 模型:
class MyModel(Document):
date = DateTimeField(required = True)
data_dict_1 = DictField(required = False)
data_dict_2 = DictField(required = True)
在某些情况下,数据库中的文档可能非常大(大约 5-10MB),并且 data_dict 字段包含复杂的嵌套文档(字典列表的字典等...)。
我遇到了两个(可能相关的)问题:
- 当我 运行 原生 pymongo find_one() 查询时,它 returns 一秒钟之内。当我 运行 MyModel.objects.first() 需要 5-10 秒。
当我从数据库中查询单个大型文档,然后访问其字段时,仅执行以下操作就需要 10-20 秒:
m = MyModel.objects.first()
val = m.data_dict_1.get(some_key)
对象中的数据不包含对任何其他对象的任何引用,因此这不是对象取消引用的问题。
我怀疑这与 mongoengine 的内部数据表示效率低下有关,这会影响文档对象的构造以及字段访问。我可以做些什么来改善这个吗?
TL;DR: mongoengine 花费了很长时间将所有返回的数组转换为字典
为了测试这一点,我构建了一个集合,其中包含一个 DictField
和一个大嵌套 dict
的文档。该文档大致在您的 5-10MB 范围内。
然后我们可以使用 timeit.timeit
来确认使用 pymongo 和 mongoengine 的读取差异。
然后我们可以使用 pycallgraph and GraphViz 来查看是什么让 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) #
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 模型:
class MyModel(Document):
date = DateTimeField(required = True)
data_dict_1 = DictField(required = False)
data_dict_2 = DictField(required = True)
在某些情况下,数据库中的文档可能非常大(大约 5-10MB),并且 data_dict 字段包含复杂的嵌套文档(字典列表的字典等...)。
我遇到了两个(可能相关的)问题:
- 当我 运行 原生 pymongo find_one() 查询时,它 returns 一秒钟之内。当我 运行 MyModel.objects.first() 需要 5-10 秒。
当我从数据库中查询单个大型文档,然后访问其字段时,仅执行以下操作就需要 10-20 秒:
m = MyModel.objects.first() val = m.data_dict_1.get(some_key)
对象中的数据不包含对任何其他对象的任何引用,因此这不是对象取消引用的问题。
我怀疑这与 mongoengine 的内部数据表示效率低下有关,这会影响文档对象的构造以及字段访问。我可以做些什么来改善这个吗?
TL;DR: mongoengine 花费了很长时间将所有返回的数组转换为字典
为了测试这一点,我构建了一个集合,其中包含一个 DictField
和一个大嵌套 dict
的文档。该文档大致在您的 5-10MB 范围内。
然后我们可以使用 timeit.timeit
来确认使用 pymongo 和 mongoengine 的读取差异。
然后我们可以使用 pycallgraph and GraphViz 来查看是什么让 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) #
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]