Data modelling in MongoDB: no joins, arrays, embedded documents and denormalization

Lecture



Let's switch to a different topic and discuss more abstract ideas related to MongoDB. Explaining new terms and syntax is fairly straightforward. However, it's more challenging to talk about building data models within this new paradigm. The core issue is that most of us are used to trying out new technologies by applying them to real-world tasks. We'll discuss this aspect, but ultimately, full understanding will require practice and studying real code.

When we talk about data modeling, document-oriented databases like MongoDB aren't as different from relational databases as other types of NoSQL solutions are. The differences that exist aren't huge, but that doesn't make them any less important.

No JOINs in MongoDB

The first and most fundamental difference you need to get used to is that MongoDB has no equivalent of the JOIN construct.

MongoDB differs from relational databases in that it doesn't use a JOIN operation to combine data from different collections. In relational databases, JOIN lets you link data from different tables based on shared keys, whereas in MongoDB data is stored in documents, and relationships between them are usually not established through JOIN.

Instead, MongoDB supports a more flexible model called embedded documents or references. You can store related data inside a document, creating nested structures, or use references to point to documents in other collections.

So, the absence of JOINs in MongoDB means you need to think through your data structure and how to organize information in advance, in order to minimize the need for multiple JOINs. This may require some reorganization of data and a change in approach to database design, but it also gives you more flexibility and more opportunities to optimize queries.

The reason MongoDB doesn't include JOIN syntax remains unclear, but we can say with confidence that JOIN operations don't scale well. This means that when you distribute your data horizontally, you still have to perform the JOIN on the client side (which, in this case, is the application server). Whatever the reasons, one fact can't be changed: the data is relational in nature, yet MongoDB doesn't support direct JOINs.

Instead, we need to perform the JOIN manually in our application code. Essentially, we have to run a second query to find the related data. This is similar to creating foreign keys in relational databases. Now let's move from the unicorn example to an employee example. First, we'll create an employee record (I'm explicitly specifying _id here so our examples stay consistent).

db.employees.insert({_id: ObjectId("wefewfwefewf"), name: 'Intellect'})

Now let's add a couple of employees and make Intellect their manager:

db.employees.insert({_id: ObjectId("4d85c7039awefwef117d731"), name: 'Duncan', manager: ObjectId("4d85c70wefwef70a117d730")});
db.employees.insert({_id: ObjectId("4d85c7039abwefewf17d732"), name: 'Moneo', manager: ObjectId("4d85c7wefwefew70a117d730")});

(it's worth repeating that _id can be any unique value. Since in practice you'll most likely use ObjectId, we use it here too.)

To find all employees belonging to Intellect, we simply run:

db.employees.find({manager: ObjectId("4d85c7039aewfewfewfd730")})

No magic here. In the worst case, the lack of JOINs usually just requires one extra (typically indexed) query.

Arrays and embedded documents in MongoDB

However, the fact that MongoDB has no JOINs is made up for by other perks.

In MongoDB you can use arrays and embedded documents to organize data. This lets you store structured information inside a single document, including arrays of values or even embedded documents.

Arrays let you store a list of values in a single document field. For example, you might have a document representing an employee, and store an array holding the history of projects that employee worked on inside that document. This is handy for storing repeating or related data within a single document.

Embedded documents let you build a hierarchy of data, where one document contains another as one of its fields. For example, you might have a document representing an order, and inside it another document holding information about the items in that order. This kind of structure helps keep related data organized into logically connected blocks.

Using arrays and embedded documents in MongoDB gives you a lot more flexibility in organizing data, lets you avoid unnecessary JOIN operations, and improves query performance, since related data is stored inside a single document.

Remember we briefly mentioned earlier that MongoDB supports arrays as first-class objects? That turns out to be very handy, especially when you need to model "one-to-many" or "many-to-many" relationships. For example, say an employee can have several managers - in that case we can simply store them as an array:

db.employees.insert({_id: ObjectId("4d85c7039wefwef17d733"), name: 'Siona', manager: [ObjectId("4d85c7wefwefa117d730"), ObjectId("4d85c7039ab0fdwefewf732")] })

And here's the interesting part: in some documents manager can be a scalar value, and in others - an array. And our earlier find query works in both cases:

db.employees.find({manager: ObjectId("4d85edewdewd70a117d730")})

You'll soon see that arrays of values are much more convenient to work with than "many-to-many" join tables. Besides arrays, MongoDB also supports embedded documents. Try inserting a document with an embedded document, for example:

db.employees.insert({_id: ObjectId("4dHULEIHWEpeowkf734"), name: 'Ghanima', family: {mother: 'Chani', father: 'Paul', brother: ObjectId("dsfSDvSSverervrev")}})

Embedded documents can be queried using dot notation:

db.employees.find({'family.mother': 'Chani'})

We'll briefly discuss where embedded documents can be used, and how you should apply them.

DBRef in MongoDB - a mechanism for working with references between documents

MongoDB supports a concept called DBRef, which is a convention adopted by many drivers.

MongoDB has a special mechanism for working with references between documents, called DBRef (Database Reference). A DBRef is a way of creating references to documents in other collections of the database.

With DBRef you can create references to other documents by specifying their identifiers (_id) and the name of the corresponding collection. This lets you establish relationships between documents even if they're located in different collections.

However, it's worth noting that DBRef isn't a built-in MongoDB feature, but rather a standard way of representing references recommended in the official documentation. Using DBRef gives you flexibility in managing relationships between documents, but it also requires some extra work handling references in your application code.

When using DBRef, keep in mind that these references aren't resolved automatically by MongoDB itself, and your application has to run the queries to fetch the related data on its own.

When a driver sees a DBRef, it can automatically fetch the referenced document. A DBRef includes the collection and the _id of the document it points to. This means the following - documents from the same collection can reference documents in different collections. That is, document 1 might reference a document from the managers collection, while document 2 might reference a document from the employees collection.

Denormalization

Another option besides using JOIN operations is denormalizing your data. Historically, denormalization was used to improve performance or in cases where you needed to keep a snapshot of the data (as, for example, in an audit log). However, with the rise of many NoSQL solutions that don't support JOINs, denormalization has become common practice. That doesn't mean you should just duplicate all the data in every document. Instead, you can avoid data redundancy by thinking carefully about your database structure.

Let's imagine we're building a forum. The traditional approach to linking a user to their post involves a userid column in the "posts" table. However, with that model you can't easily get a list of posts without an extra operation (JOIN) against the users table. One possible solution is to store the username (name) alongside the userid for each post. You could also include a small embedded document containing user information, to avoid extra JOINs on queries.

user: {id: ObjectId('Something'), name: 'Intellect'}.

Yes, if you let users change their name, you'll need to update every document (post) - that's one extra query.

Not everyone finds it easy to adapt to this approach. In many cases it doesn't even make sense to. Still, don't be afraid to experiment with it. Sometimes it turns out to be useful - practically the only right solution.

What's better to use?

Another useful approach in situations involving a "one-to-many" or "many-to-many" relationship is using an array of identifiers. There's a view that DBRef isn't used all that often, but of course you're welcome to try it out yourself. Beginning developers often feel unsure about choosing between embedded documents and DBRef, trying to work out which fits their situation better.

First, remember that a single document is limited to 4 megabytes in size. This size limit (generous as it is) gives you a sense of how documents should be used. It's now clear that most developers lean toward using manually assigned references. Embedded documents are used often, but for small amounts of data that you'd always want to retrieve together with the parent document. A real-world example could be an accounts document stored with each user, for example:

db.users.insert({name: 'Intellect', email: 'intellect@intellect.icu', account: {allowed_gholas: 5, spice_ration: 10}})

This doesn't mean you can underestimate the power of embedded documents, or dismiss them as a minor, secondary utility. Life is much easier when the structure of your data directly mirrors the structure of your objects. What's especially valuable is that MongoDB lets you query and index fields inside embedded documents.

Few or many MongoDB collections

Given that MongoDB has no rigid schemas for collections, you have the option of using a single collection holding documents with different structures. Examples of MongoDB-based systems I've worked with often end up resembling relational databases. In other words, what would be a table in a relational database is usually implemented as a collection in MongoDB (though tables for "many-to-many" relationships are the exception).

However, things change once you use embedded documents. A good example is a blog. Suppose we have a "posts" collection and a "comments" collection, and each post needs to contain an array of embedded comments. Aside from the 4 MB size limit (and even "Hamlet" in English isn't that big), many developers prefer to keep entities separate. That makes the structure clearer and easier to follow.

There are no hard-and-fast rules for data modeling in MongoDB (aside from the 4 MB limit). You should experiment with different approaches to figure out what works best.

The goal of this part was to give you some useful guidelines for designing data in MongoDB. You can treat them as a starting point. Modeling in document-oriented systems is different from relational modeling, but not by that much. There's more flexibility here, though there is a 4 MB size limit, which usually works fine for most new systems. What matters most is simply trying things out and experimenting.

See also

  • [[b8218]]
  • [[b9900]]
  • [[b9901]]
  • [[b9902]]
  • [[b9903]]
  • [[b9904]]
  • [[b9905]]
  • [[b9906]]
  • [[b9907]]

See also

Comments

To leave a comment

If you have any suggestion, idea, thanks or comment, feel free to write. We really value feedback and are glad to hear your opinion.
To reply

Lectures and tutorial on "Databases -MongoDB"

Terms: Databases -MongoDB