Skip to content

Understanding Cloud Firestore Indexing

For the caveman who have yet to know what an index is, a database index is a value stored that maps items in the database to their locations. In other words, this lets queries happen faster as without an index, a query would have to crawl through items in the database one by one and this could be a slow and jarring process for larger databases.

How does Firestore handle indexing?

Fortunately for us, Firestore automatically indexes all queries… but this only applies for the single-field indexing, only one of the index types. So if we want to achieve the most optimal performance with our queries, we have to look into indexing ourselves.

When you define an index, you select a mode and type for the indexed field.

Index Modes

Ascending – Supports <, <=, ==, >=, and > query clauses on the field and supports sorting results in ascending order based on this field value

Descending – Supports <, <=, ==, >=, and > query clauses on the field and supports sorting results in descending order based on this field value.

Array-contains – Supports array_contains query clauses on the field.

Index Types

Single Field

Each entry in a single-field index records a document’s value for a specific field and the location of the document in the database. Firestore automatically handles this type of indexing.

You can exempt a field from this automatic indexing by creating a single-field index exemption in the Firebase Console. It’s considered best practice to do this for large string fields, large array or map fields, and documents with a high write rate.

Take for example. we create a few city documents. (Courtesy of Firebase docs).

var citiesRef = db.collection("cities");

citiesRef.doc("SF").set({
    name: "San Francisco", state: "CA", country: "USA",
    capital: false, population: 860000,
    regions: ["west_coast", "norcal"] });
citiesRef.doc("LA").set({
    name: "Los Angeles", state: "CA", country: "USA",
    capital: false, population: 3900000,
    regions: ["west_coast", "socal"] });
citiesRef.doc("DC").set({
    name: "Washington, D.C.", state: null, country: "USA",
    capital: true, population: 680000,
    regions: ["east_coast"] });
citiesRef.doc("TOK").set({
    name: "Tokyo", state: null, country: "Japan",
    capital: true, population: 9000000,
    regions: ["kanto", "honshu"] });
citiesRef.doc("BJ").set({
    name: "Beijing", state: null, country: "China",
    capital: true, population: 21500000,
    regions: ["jingjinji", "hebei"] });

For each non-array field in each document, Firestore automatic indexing creates an ascending and descending index, then for each array field, it creates an array-contains index.

With these indexes, you can run simple queries like this.

citiesRef.where("state", "==", "CA")
citiesRef.where("population", "<", 100000)
citiesRef.where("regions", "array-contains", "west_coast")

Or even compound queries based on equalities (==).

citiesRef.where("state", "==", "CO").where("name", "==", "Denver")
citiesRef.where("country", "==", "USA").where("capital", "==", false)

However, you can’t run compound queries that use other boolean operators (>, <=, etc.) or sort queries by a different field. That’s where you need to make a composite index.

Composite Index

You can use these to allow you to make more complex queries such as compound queries that use boolean operators (>, <=, etc.) and queries sorted by different fields.

As such, you can perform queries like these:

citiesRef.where("country", "==", "USA").orderBy("population", "desc")

citiesRef.where("country", "==", "USA")
         .where("population", "<", 3800000)
         .orderBy("population", "desc")

citiesRef.where("regions", "array_contains", "east_coast")
         .where("capital", "==", true)

Do note you can only include one array per composite index.

So why not just Index everything?

Indexes use up storage space. Firebase explains how this storage space is calculated, but as a general idea, most indexes (both types) average to about 80 bytes. It may not seem like much, but if your database is anything sizeable, this could build up to some huge chunks.

Indexing Limits

Let me just completely rip off this table from the docs.

Limit Details
Maximum number of composite indexes for a database 200
Maximum number of single-field index exemptions for a database 200
Maximum number of index entries for each document 40,000

The number of index entries is the sum of the following for a document:

  • The number of single-field index entries
  • The number of composite index entries
Maximum size of an index entry 7.5 KiB

To see how Cloud Firestore calculates index entry size, see index entry size.

Maximum sum of the sizes of a document’s index entries 8 MiB

The total size is the sum of the following for a document:

  • The sum of the size of a document’s single-field index entries
  • The sum of the size of a document’s composite index entries
Maximum size of an indexed field value 1500 bytes

Field values over 1500 bytes are truncated. Queries involving truncated field values may return inconsistent results.

Index Merging

For queries with multiple equality clauses (==) and optionally an orderBy clause, Firestore can re-use existing indexes in place of a larger one.

Take for example, this set of queries.

db.collection("restaurants").where("category", "==", "burgers")
                            .orderBy("star_rating")

db.collection("restaurants").where("city", "==", "San Francisco")
                            .orderBy("star_rating")

db.collection("restaurants").where("category", "==", "burgers")
                            .where("city", "==", "San Francisco")
                            .orderBy("star_rating")

db.collection("restaurants").where("category", "==", "burgers")
                            .where("city", "==" "San Francisco")
                            .where("editors_pick", "==", true )
                            .orderBy("star_rating")

You could create an index for each query.

Collection Fields indexed
restaurants ASC category, ASC star_rating
restaurants ASC city, ASC  star_rating
restaurants ASC category, ASC city, ASC star_rating
restaurants ASC  category, ASC city, ASC editors_pick, ASC star_rating

Or you could use a smaller set of indexes where you’ve identified a possible merge.

Collection Fields indexed
restaurants ASC category, ASC star_rating
restaurants ASC city, ASC star_rating
restaurants ASC editors_pick, ASC star_rating

Disclaimer

Most of the content in this article is ripped off of the Firebase official docs and reworded for viewer ease and convenience. If you want to see a full comprehensive tutorial on the topic, I recommend you go check it out.

Tags: