Create an app fast with Firebase, even if you’re intimidated by NoSQL databases—the translation of queries to SQL is easier than you think.
SQL databases are quite different than NoSQL databases, so you must think a little differently when modeling your data. However, most translations from SQL code to NoSQL are identical or easier. Here, we use the Firebase Modular API, but the firebase-admin
API will be the same.
Firestore is a document database that doesn’t allow joins. You can simulate them by duplicating your data. Most regular queries, however, work as expected. The lack of a schema allows you to get up and going extremely quickly. You need to learn the data modeling patterns.
Firestore is a NoSQL document database.
You create a table when you create a document. A table does not exist without a document.
There is nothing to alter.
To drop a table, you must delete all documents in a collection. This can also be done in the Firebase Console.
There is no schema, but you can declare constraints using Firebase Rules.
You will usually see camel case field names instead of snake case.
Each document has a unique path. The documentId()
of the document is the primary key. In the collection posts/2xelskel
, the 2xelskel
is the document ID. You can use any alphanumeric string, as well as _
characters. Otherwise, it will be automatically generated.
You can insert data in SQL by declaring each field and value.
📝 Notice the serverTimestamp()
function, which will use the date on the server. Since Firestore can be safely run on the client, this would be secured in Firestore Rules.
Updating is basically the same.
📝 Notice the document ID is 1
in this case. The ID will probably be a uniquely generated string in a real application.
You can upsert a document with setDoc
.
📝 This will vary between SQL languages.
📝 setDoc
behaves like addDoc
without the { merge: true }
parameter.
In Firestore, you can only select all fields from a document unless you use the REST API with your own code.
Select all posts.
In SQL, you SELECT
first.
In Firestore, you must read the document to check its existence.
Of course, we limit our selection to one or more in actual use cases. We don’t want all documents in a collection or all rows in a table.
There is nothing surprising here.
Firestore queries use AND
by default, but you can also add OR
queries.
⚠️ Unlike in SQL, each query has a strict limit of 30 disjunctions.
You can search for IN
as well in Firestore.
Keep in mind this is a simplification of:
The same goes for SQL. IN
is just several OR
clauses on the same field, condensed.
You can order your data similar to SQL.
📝 The default sort order is asc
.
📝 An orderBy
also sorts data, so the field must exist. Otherwise, it is filtered out.
⚠️ Changing orderBy
fields requires a composite index for each query. If you change the field, you need another index.
You can have inequalities on multiple fields.
📝 Order index fields by equalities followed by most selective range or inequality field.
⚠️ Order fields in decreasing order of query constraint selectivity.
⚠️ Queries with range or inequality filters on document fields and only equality constraints on the document key (__name__)
aren’t supported.
⚠️ Cloud Firestore limits the number of range or inequality fields to 10. This prevents queries from becoming too expensive to run.
You can only have a !=
on one field in Firestore per query.
⚠️ A !=
query clause might match many documents in a collection. To control the number of results returned, use a limit clause.
📝 Not-equal queries exclude documents where the given field does not exist.
📝 For multiple not equals on the same field, use not-in
.
You don’t need to increment columns in SQL until your database gets big. Either way, the code is simple.
📝 This is more useful in NoSQL databases. You can also use increment(-1)
for decrementing.
Counting can get complex in Firestore, but generally, you want to count on the server.
📝 You need two separate calls to count a query and return the query results.
You can also do sum
and average
aggregations.
📝 min
and max
are not supported.
See Aggregation Queries.
You can’t sort by a count directly, so you must first count in Firestore.
Let’s say we want to get all users and them by post_count
, or the number of posts they have.
In Firestore, we must create the post_count
with each post created in the user’s document. We can do this in a batch
transaction.
And finally, we can sort the documents later.
You can secure this in Firestore Rules or create a Firebase Trigger Function on onCreate
that automatically increments and decrements the user document.
Counters get extremely complicated in Firestore. See Four Ways to Count in Firestore for niche cases.
The only way to keep something unique is to set it as the document ID in Firestore. If you want a unique field, you must put it in two places in a transaction.
For uniqueness, we can save a username in the users
collection on the user document and in a usernames
collection in a single transaction. We can use Firestore Rules to secure this.
If you update the username, also consider removing the old username in the usernames
collection first.
If you want to search for all countries users belong to, you must also store a unique countries
collection.
You could also create the countries using a Firebase Trigger Function. You should also consider keeping a count of the number of users in each country. This could come in handy if you want to search for the most popular country your users belong to.
🖋️ The common theme you start to see is that Firestore requires you to create your database for the queries instead of vice versa.
There is no LIKE
equivalent in Firestore.
However, there is a startsWith
hack you can use.
This function can be used to find a string that starts with a term. It only works in parent objects and does not work with arrays or objects.
Use an external database or store your search index inside Firestore if you want Fuzzy Searching capabilities. You can do this with Vectors or a custom function like Soundex.
You can easily do an inner join in Firestore by querying two different collections, but this will cost you an extra document read and could potentially slow down your app.
The class example is adding user information to a post.
You could easily do this without JSON, but I wanted an equivalent object.
This could be done in many ways, but the query is easy.
Again, you need to consider when a user updates their username
, displayName
or other profile information. You should create a Firebase Trigger Function to handle it.
Here, we have posts and tags. Each post can have many tags, but you won’t have an infinite number of tags on a post. Firestore allows you to store data as arrays and search based on the array. An array works perfectly in this case.
In SQL, we may have a tags table with the user_id
.
Or, we may have a junction table post_tag
.
In Firestore, we must create the tags in two different places, similar to the unique index.
🖋️ We must also remember to decrement the postCount
on tags when a post is modified or deleted. This should be done with Firebase Functions, as Firestore Rules would get too complex for multiple tags. Firestore Rules also do not have loops.
The tags will already be attached to the post document.
Firestore does not handle complex queries, but there are workarounds for most use cases. This post only scratches the surface.
A NoSQL database is quite different from an SQL database. You must think about your queries before you even create a document. Once you learn the patterns, you can create any structure.
🌐 I am still a firm believer that all data is relational!
But, Firebase allows you to create an app faster than any database platform I have used. Firestore is amazing in this aspect.
Jonathan Gamble has been an avid web programmer for more than 20 years. He has been building web applications as a hobby since he was 16 years old, and he received a post-bachelor’s in Computer Science from Oregon State. His real passions are language learning and playing rock piano, but he never gets away from coding. Read more from him at https://code.build/.