Both loved and hated in almost equal amounts, MongoDB has become the poster child of the NoSQL movement, boasting a flexible JSON document oriented model, good performance, and easy horizontal scalability. Over it's years of development, MongoDB has added a host of new features, such as aggregation pipelines and ACID transactions, that have further solidified it's position as a top-class database.
Cloud Firestore on the other hand is an up-and-comer in the document store space, offering a unique set of capabilities, performance focused design, and out-of-the-box real-time support. Due to it's age, Firestore does not have the widespread adoption of MongoDB, but it's popularity continues to grow as more people learn and experience its unique advantages.
In this article we are going to compare and contrast these two popular document stores, analyze their similarities and differences, and zero in on the use cases where they excel.
As in the previous article, all Firestore queries included in this article can be run directly on Ezfire by signing up for our app.
But with that out of the way, let's take a look at how data is modelled for both MongoDB and Cloud Firestore.
Data Model
In MongoDB, data is represented as collections of JSON documents. The collections in Mongo are schemalass, so any collection can contain any data. The only constraint on a Mongo document is that the _id
field is required in each document. If _id
is not included when creating the document, it will be randomly generated and added to the document by Mongo.
The maximum size of a document that Mongo can store is 16 megabytes. This is a very large amount of data, especially if you consider that Mongo documents are stored as BSON, a storage-efficient binary representation of JSON.
Cloud Firestore also uses collections of documents as its fundamental building block, but in contrast to Mongo, it does not require any specific fields be present in the document, such as _id
. Instead, documents in Firestore are referred to by a REST-like resource path, such as users/:userId
where userId
is some unique token for the collection users
. One interesting caveat of this model is that documents can be nested as subcollections of other documents, for example users/:userId/addresses/:addressId
. This makes it easy to express the hierarchy of one's data model directly in the references to its documents.
The maximum size of a document that Firestore can store is 1 megabyte, which is actually a much lower limit than what is available to Mongo. You need to be more careful with document size when working with Firestore to avoid situations where your writes fail.
Reading Data
For the following section we are going to use Mongo Shell to demonstrate queries to MongoDB. We assume the reader is familiar with Mongo Shell syntax. If not, please read here.
Now, when using MongoDB, querying data is done by calling the find
method on the target collection with a special JSON object as the first parameter. This JSON object can contain special operators that specify the constraints of the query. For example, a query that looks for all the users in the database 20 years of age and under might looks something like:
db.users.find({
age: { $lte: 20 },
});
The object { $lte: 20}
is the "less than or equal to" operator in this case. A whole list of query operators for Mongo can be found here. Using these operators, any sort of query can be built and executed. This includes constraints such as AND, OR and NOT.
Cloud Firestore in contrast, requires that you talk to it using one of the official SDKs, such as the NodeJS SDK used to run queries on Ezfire. With this SDK, there are two ways to read data. First, if you know the resource path of a specific document (e.g. users/:userId/addresses/:addressId
), you can fetch that document directly using the following query:
db.doc("resource/path").get();
You can also query a collection directly and get back a list of the documents in the collection:
db.collection("foos").get();
You can add a list of constraints to the collection before you fetch to filter out unwanted documents:
db.collection("foos")
.where("bar", "==", true)
.where("baz", "<=" 10)
.get();
There are some limitations to the kinds of constraints you can add to your collections however. Cloud Firestore only supports AND and NOT queries at the database level. Joining results for an OR type query needs to be done manually. Furthermore, a given query can only have either one inequality constraint or one not equal to constraint. This is due to the fact that every query in Cloud Firestore must be backed by an index, so arbitrary or hard to index queries are not legal. The trade off here is that Firestore queries always perform well and scale favorably as the size of the dataset grows. It is worth noting that MongoDB doesn't not have these limitations on the types of queries it can perform.
Something to note about MongoDB is that as of later versions, MongoDB now supports a robust aggregation framework for efficiently joining and aggregating data across multiple collections at the database level. Firestore has no such support for aggregation queries. This makes Firestore a very bad choice for applications that need to efficiently aggregate data across documents and collections.
Writing Data
In Mongo, writes are done using the insertOne
and updateOne
methods on the collection. For example, inserting a new document would look something like:
db.users.insertOne({ name: "New User" });
An update would look something like:
db.users.updateOne({ _id: "abcdefg", name: "Updated Name" });
You can also delete documents from the collection using deleteOne
:
db.users.deleteOne({ _id: "abcdefg" });
Each of these methods also supports a bulk mode accessed as insertMany
, updateMany
and deleteMany
respectively.
Cloud Firestore supports all these methods of writing documents. For example, to insert a new document into a collection, you can use the add
method on the collection:
db.collection("users").add({ name: "New Name" });
You can also use the create
or set
methods on a document reference to choose your own document id when inserting:
db.collection("users").doc("userId").create({ name: "New Name" });
To update a document, you can use update
on the document reference to selectively update fields:
db.collection("users").doc("userId").update({ name: "Updated Name" });
To delete a document, just call delete
on the document reference:
db.collection("users").doc("userId").delete();
Unlike Mongo, there is no way to do bulk operations with Cloud Firestore. The closest thing is a write batch, but write batches are effectively just a bunch of single document operations run all at the same time.
It should also be mentioned that both Mongo and Firestore offer ACID transactions for maintaining consistency when performing multiple writes.
Cost
Cloud Firestore has a single, easy to understand pay-per-use cost structure. The costs are $0.06 for 100000 reads, $0.18 for 100000 writes, and $0.02 dollars for 100000 deletes. This makes for an easy to understand cost for an application but also has the potential to become very expensive if app traffic grows rapidly.
In terms of cost, MongoDB has a lot of flexibility and a lot more options for how you can be charged. The common option is to provision a Mongo cluster on Mongo Atlas to hold your data. The prices here vary with instance size but a basic instance costs around $57 per month at a minimum.
Mongo also recently added a "Serverless" pricing tier that behaves similar to Cloud Firestore. The cost is $0.30 for one million reads and $1.25 for one million writes, so at that price it is cheaper than Cloud Firestore.
All in all, Mongo offers competitive pricing that is cheaper than Cloud Firestore in a lot of cases.
Conclusion
In this article we took a look a MongoDB and Cloud Firestore and compared their features and use cases. We found that there are a lot of similarities between MongoDB and Cloud Firestore in terms of data model and feature set. However, due to its maturity, MongoDB excels when complex queries and aggregations of the application data are required as Firestore does not support aggregating data and its query constraints make some queries impossible. Conversely, Cloud Firestore is useful in situations where query scaling is important and for implementing realtime applications. Thanks for reading, I hope you learned something useful.
If you are interested in getting more experience with Cloud Firestore, sign up for our app and try out some of the queries in this article on our sample database.