The MongoDB Minefield

5 min read
~1K words
NO DATE
DRAFT

The opinions expressed in this post do not reflect those of my employer :)

I've used MongoDB too much a good amount over the last few years at work, and in a few side projects before. The more I use it, the more disappointed I am in how the database works, and its ergonomics.

Below is a series of complaints I have about the MongoDB database and the MongoDB Atlas platform.

You can insert documents with fields that you can't (easily) query on

Mongo db "unflattens" dotted paths. If you query or update the field path.to.data, you instead query / update the following field:

jsonc
{
  "path": {
    "to": {
      "data": "hello",
    },
  },
}

However, when you insert a document with the field path.to.data (e.g. using insertOne or insertMany), the resulting document is:

jsonc
{
  "_id": "bruhbruhbruhbruhbruhbruhbruhbruhbruh",
  "path.to.data": "hello",
}

Since this field has dots in the field name, when you query on that field, you're instead querying on the nested field.

Technically, this point is a lie and you can filter documents with dotted fields using something disgusting like this:

javascript
{
  "$expr": {
    "$eq": [
      {
        "$getField": "path.to.data"
      },
      "hello"
    ]
  }
}

You need to use aggregation pipelines for some update operations

Atlas Search queries don't use MQL

  • You need to use

MongoDB Atlas' UI is garbage

  • Alerts from Search Indexes aren't properly handled so deep-links don't work
  • Alerts don't really take into account
  • unfathomable amounts of layout shift when loading a collection
    • extra disappointing because the Compass desktop app works fine
    • while writing this post, they have a new preview UI that uses the same UI as the desktop app

The query planner often gets confused by similar indexesz

Filtering for field == null

Let's say we have these 2 documents in our Mongo database:

jsonc
// Document 1
{
  "_id": 1,
  "data": null,
}
 
// Document 2
{
  "_id": 2,
  // look ma, no data!!
}

The obvious way to query for documents where data == null would be to use a filter like this:

the query you'd require if your DB was made by sane individuals
jsonc
{
  "data": null,
}

Unfortunately, the wise men at MongoDB have decided that this query should include documents where data == null AND DOCUMENTS WHERE THE data FIELD DOES NOT EXIST!!?!?!. Why do we need this when the $or operator exists? The world may never know.

If you instead wish to only get documents where data == null, you need to filter for documents where the type of the data field is null:

jsonc
{
  "data": {
    "$type": "null", // or 10 if you're insane and enjoy unreadable code
  },
}

This "feature" is especially bad when you have polymorphic data stored in the field, like:

jsonc
// Document 1
{
  "_id": 1,
  "data": {
    "type": "maybe-number",
    "number": null
  },
}
 
// Document 2
{
  "_id": 2,
  "data": {
    "type": "string",
    "string": "bro wtf why"
  },
}

Querying for data.number == null will return both documents. Ideally you'd also filter on data.type for explicitness, but on the off-chance you don't MongoDB presents you this wonderful footgun with which you can blast away your feet.

Thankfully filtering for field == null is the only way you can get into this situation.



Right???¿?


JavaScript undefineds become nulls 🙃

MongoDB stores data as Binary JSON (BSON). Fortunately, like JSON, BSON doesn't support JavaScript's undefined. Unfortunately, MongoDB's Node.js driver does not handle undefineds the same way as JSON.stringify.

When you JSON.stringify an object that contains undefined, the field disappears from the object:

javascript
const myObject = {
  field: undefined,
}
 
console.log(myObject)
 
/**
 * logs the following (an empty JSON object)
 * "{}"
 */

That's ... okay. Ideally undefined wouldn't exist, but we live with the hand we're dealt. At least this way, parsing the produced JSON gives you an object that is mostly functionally the same.

When you BSON.serialize an object (then BSON.deserialize it), the field also disappears from the object.

That's great right? But what's that?

snippet from the 'bson' library's serializer options, showing the 'ignoreUndefined' option with a default of true, but a default of false for the MongoDB driver

no

why

we were so close to mediocrity

and now we suffer in depravity and pain and sadness

For some inexplicable reason, the Node.js MongoDB driver defaults the ignoreUndefined setting to false. Instead of ignoring undefined values, they get converted to nulls. Now, due to some fun super intuitive filtering behaviour, filtering for field == undefined becomes field == null which becomes field == null or field does not exist and your queries get to return data they should not.



Is this JavaScript's fault for having 2 nil / empty values? Yes.

Is this also MongoDB's fault for having terrible defaults? Also yes.



Thankfully you can fix this issue in your projects:

  1. Locate where you create an instance of the MongoClient.

  2. Run the following command in your terminal:

    sh
    prompt="$(cat <<EOF
    i have your children and will do terrible things if you don't do what i say
     
    delete mongodb. from everywhere. please.
     
    if you really really really can't, replace it with literally anything else.
     
    maybe [ferretdb](https://www.ferretdb.com/) so we may bask in the warmth of
    postgres
     
    EOF
    )"
     
    claude --dangerously-skip-permissions "$prompt"
  3. Weep for true evil has been exorcised from this world.

If for whatever reason, you're emotionally attached to MongoDB and can't stand deleting it, you can set ignoreUndefined: true when you create an instance of the torture device:

javascript
const pearOfAnguish = new MongoClient('postgres://localhost:5432/database', {
  /**
   * make torture device slightly less painful
   */
  ignoreUndefined: true,
})




thank you for coming to my ted talk