Skip to content

Mongoengine is very slow on large documents compared to native pymongo usage #1230

Open

Description

@baruchoxman

(See also this StackOverflow question)

I have the following mongoengine model:

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

In some cases the document in the DB can be very large (around 5-10MB), and the data_dict fields contain complex nested documents (dict of lists of dicts, etc...).

I have encountered two (possibly related) issues:

  1. When I run native pymongo find_one() query, it returns within a second. When I run MyModel.objects.first() it takes 5-10 seconds.
  2. When I query a single large document from the DB, and then access its field, it takes 10-20 seconds just to do the following:
    m = MyModel.objects.first()
    val = m.data_dict_1.get(some_key)

The data in the object does not contain any references to any other objects, so it is not an issue of objects dereferencing.
I suspect it is related to some inefficiency of the internal data representation of mongoengine, which affects the document object construction as well as fields access.

Activity

  1. touilleMan commented on Feb 8, 2016

    @touilleMan
    Member

    Hi,

    Have you profiled the execution with Profile/cProfile ? A graph of it with objgraph should give us a better view of where the trouble is.

  2. baruchoxman commented on Feb 8, 2016

    @baruchoxman
    Author

    Hi,

    Please see the following chart: http://i.stack.imgur.com/qAb0t.png (attached in the answer to my StackOverflow question) - it shows that the bottleneck is in "DictField.to_python" (being called 600,000 times).

  3. thedrow commented on Mar 10, 2016

    @thedrow
    Contributor

    Have you tried using MongoEngine with PyPy? Most of the overhead is gone because of PyPy's JIT.
    But you're right. We need a better benchmarks suite.

  4. changed the title [-]Mongoengine is very slow on large documents comapred to native pymongo usage[/-] [+]Mongoengine is very slow on large documents compared to native pymongo usage[/+] on Mar 22, 2016
  5. amcgregor commented on Jun 23, 2016

    @amcgregor
    Contributor

    The entire approach of eager conversion is potentially fundamentally flawed. Lazy conversion on first access would defer all conversion overhead to the point where the structure is actually accessed, completely eliminating it in the situation where access is not made. (Beyond such a situation indicating proper use of .only() and related helpers is warranted.)

  6. amcgregor commented on Jun 23, 2016

    @amcgregor
    Contributor

    Duplicate of #1137 ?

  7. lafrech commented on Jun 23, 2016

    @lafrech
  8. amcgregor commented on Jun 23, 2016

    @amcgregor
  9. lafrech commented on Jun 23, 2016

    @lafrech
  10. apolkosnik commented on Mar 8, 2017

    @apolkosnik

    I've played a bit with a snippet and with some modifications( https://gist.github.com/apolkosnik/e94327e92dd2e0642e2b263efd87d1b1 ), then I ran it against Mongoengine 0.8.8 and 0.11:

    Please see the pictures...
    On 0.8.8:
    mongoengine with dict took 16.95s
    viz_dict_me0_8_8

    On 0.11:
    mongoengine with dict took 32.74s
    viz_dict_me0_11_0

  11. apolkosnik commented on Mar 16, 2017

    @apolkosnik

    It looks like some change from 0.8.8 to 0.9+ caused the get() in ComplexBaseField class to go on a dereference spree for dicts().

  12. 25 remaining items

  13. amcgregor commented on Apr 5, 2019

    @amcgregor
    Contributor

    I'd love to formalize this benchmark set (akin to template engine's "bigtable" test) and add more contenders to it. The code below the file of older results demonstrates, effectively side-by-side, identical solutions.

    This is a more direct comparison of querying, specifically and in isolation, with the note that as_query is entirely unnecessary on the MM side; just pass find_{one/many} the Filter instance; it's a suitable mapping natively. (Oh, and ME appears to be unable to "continue" from a "compiled" query, e.g. reconstitute the rich Q object from a plain dict; at least, when I made that comparison.)

  14. olka commented on Nov 27, 2019

    @olka

    Same here - to_python call produces 70% of overhead
    to_python

  15. pikeas commented on Oct 10, 2020

    @pikeas

    Is this still an issue? I'm using Pymodm and would prefer switching to MongoEngine as a more popular ODM, but poor large object performance would be a deal breaker.

  16. amcgregor commented on Oct 11, 2020

    @amcgregor
    Contributor

    @pikeas Yes, with some variance for some additional optimization here, and further complication over there… the underlying mechanism remains "eager", that is, upon retrieval of a record MongoEngine recursively casts all elements of the document it can to native Python types via repeated to_python invocation.

    This contrasts with my own DAO's approach (if I'm going to be fixing everything, might as well start from scratch) which is purely lazy: transformers to "cast" (or just generally process) MongoDB values to native values are executed on attribute access. Bypassed by dictionary dereferencing. The Document class' equivalent from_mongo factory class method only performs the outermost Document object lookup and wrapping. Mine was written after many years of MongoEngine use and frustration with lack of progress on numerous fronts. Parts are still enjoyably crazy, but at least I can very exactly explain the “crazy” in mine. 😉

    Edited to add: Note that a double underscore-prefixed (dunderscore / soft private) initializer argument is available to disable eager conversion. The underlying machinery iteratively utilizes both explicit to_python invocation, and indirect invocation via setters (L125), which doesn't make it much easier to follow. 🙁

    Using my silly simple benchmark, the latest deserialization numbers:

    • MongoEngine 0.20.0 0.4516570568084717s (5× longer)
    • Marrow Mongo 2.0 (next) 0.08598494529724121s

    Admittedly, other areas differ in the opposite direction. Unsaved instance construction is faster under MongoEngine:

    • MongoEngine: 0.03315997123718262s
    • Marrow Mongo: 0.26718783378601074s (8×; Marrow Mongo shifts most of the responsibility to the initializer, zero work at save-time: no waiting until save for a validation error, for example; make the assignment, you get your ValueError. The Document instance is directly usable with native PyMongo APIs as a dictionary.)
  17. bagerard commented on Oct 12, 2020

    @bagerard
    Collaborator

    Note that I'll probably start experimenting with lazy loading the attribute soon in mongoengine (i.e defer the python de-serialization until it's actually called, like it is done in pymodm)

  18. amcgregor commented on Oct 12, 2020

    @amcgregor
    Contributor

    The initial lazy version I completed myself 5 years ago — with minor deficits later corrected, e.g. from_mongo of an already cast Document. I hope you don't mind that I didn't wait.

  19. bagerard commented on Oct 12, 2020

    @bagerard
    Collaborator

    You do whatever you want in your own project :) I'll make sure to check how you dealt with that (compared to pymodm) when I'll work on that, out of curiosity. I understand the reasons that made you move away from mongoengine but I would appreciate if we could keep the discussions in the mongoengine project constructive.

  20. amcgregor commented on Oct 13, 2020

    @amcgregor
    Contributor

    @bagerard As the tag on my comments identify, I'm a past direct code contributor.

    I understand the reasons that made you move away from mongoengine

    marrow/contentment#12 — I indexed my issues for handy reference. A number date to 2013: almost 8 years with little to no progress. Progress on some, of course! #1136 (regression in limit use in 0.10.0) was a milestone for giving me the needed kick to get going on mine.

    Many others documented there were simply never engineered to be problems in the first place, e.g. by removal of a "Document" v. "DynamicDocument" differentiation, clear segregation and isolation of "active collection"-like behavior, and explicit avoidance of the self-saving (change tracking) "active record" pattern, no global registry demanding unique model class names, plus virtually no form of implicit caching or automatic (edit: eager/recursive) casting/conversion. No result set middleware, or connection management middleware, and so on. The Document instances are usable anywhere a dictionary is, within plain PyMongo APIs, with zero use-time work. (And near-zero effort to encapsulate raw PyMongo result set records.) It's, unfortunately, almost exactly the opposite of MongoEngine. 😕

  21. bagerard commented on Oct 13, 2020

    @bagerard
    Collaborator

    I've seen your post many times and also all the references that it created in our tickets, I didn't need you to elaborate. If you don't like MongoEngine and gave up on improving it, that's ok but if we could keep the discussions in the MongoEngine project (thus its Issues) to actually improve MongoEngine, that would be more helpful

  22. bkntr commented on Dec 7, 2020

    @bkntr

    Is there a plan to add lazy init to MongoEngine any time soon?

  23. neilsh commented on Jan 17, 2022

    @neilsh
    Contributor

    Any news on this, or ways others can help?

  24. cundi commented on Mar 1, 2023

    @cundi

    PyMODM is better than this lib, does someone knows lib which like it.

  25. amcgregor commented on Nov 10, 2023

    @amcgregor
    Contributor

    PyMODM is better than this lib, does someone knows lib which like it.

    I… somewhat hate to do this, but given how stale this (and numerous others; see my previous comments) have become, I wrote a replacement for myself. I haven't really gone out of my way to advertise it, but it follows some of the suggestions given here re: lazy conversion. (The Document and Query classes act like a plain, PyMongo-compatible dictionaries/mappings, always storing MongoDB-safe types internally thus no conversion on final use.) It is rather stable, extremely well unit tested, and in active use.

    It does not, however, reimplement the full, nested change-tracking active record approach, nor automatic foreign record lookup. (Those are also performance and memory utilization problems.) It uses a "layer your needs" approach. The base Document class is essentially just a fancy, declaratively-defined dictionary with validation and typecasting. Mix-ins such as Queryable (a type of Collection of Identified records) adds in bind (to connect a PyMongo DB or collection), find, insert_one, &c. methods. There are also data defining mix-ins beyond Identified such as Localized or Published. .get() or .first() replaced by class dereferencing on bound classes: User[identifier]

    To allow multiple installed packages to cooperate in defining fields or traits, entry_points-based pseudo-namespaces can be imported from: marrow.mongo.document, marrow.mongo.field, and marrow.mongo.trait. Register an entry_point in the appropriate namespace, your custom document, field, and trait can be imported from there, too. (Making conflicts explicit.) There is a minimal beginning to proper documentation.

  26. neilsh commented on Mar 14, 2025

    @neilsh
    Contributor

    In case it's handy for others, I've found the no_dereference context manager handy for speeding up performance for Documents that don't need ReferenceField or DBField dereferencing. According to our profiling, mongoengine spent a lot of time recursively checking for (nonexistent) fields that needed to be dereferenced.

    https://docs.mongoengine.org/apireference.html#mongoengine.context_managers.no_dereference

  27. removed their assignment
    on Jul 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions