Indexes
With twelve orders, every query is instant. With twelve million, a query for one customer's orders would make MongoDB read every single document, which can take seconds and slow the whole server down. An index fixes this. Like the index at the back of a textbook, it lets MongoDB jump straight to the right documents instead of reading every page.
In this lesson you will create the main kinds of index on the shop data: single-field, compound, multikey, unique, partial, TTL and text. You will also learn which fields to index and what indexes cost. Start with freshly loaded data and use shop. The next lesson, Query Plans with explain(), shows how to prove an index is being used.
What an index is
An index is a separate, sorted structure (a B-tree) that holds the values of one or more fields together with a pointer to each document. Because the values are sorted, MongoDB can find a value, or a range of values, by walking a few levels of the tree instead of scanning the collection. A query that reads every document is called a collection scan (COLLSCAN); a query that uses an index is an index scan (IXSCAN).
Every collection already has one index, on _id, which you cannot drop:
db.orders.getIndexes()[ { v: 2, key: { _id: 1 }, name: '_id_' } ]key shows the indexed field and its direction, name is generated from the key, and v: 2 is the index format version.
Single-field indexes
The orders page of a customer's account runs find({ customerId: ... }) constantly, so index that field. createIndex() takes the field with 1 (ascending) or -1 (descending) and returns the new index's name:
db.orders.createIndex({ customerId: 1 })customerId_1
For a single field the direction does not matter, because MongoDB can walk the index both ways. Creating the same index again does nothing and simply returns the name.
Compound indexes and the ESR rule
A compound index covers several fields, in a fixed order. The support team often asks for "pending orders, newest first", which filters on status and sorts by date:
db.orders.createIndex({ status: 1, date: -1 })
db.orders.find({ status: 'pending' }, { date: 1, total: 1 }).sort({ date: -1 })status_1_date_-1
[
{ _id: 1011, date: ISODate('2026-10-19T20:25:00.000Z'), total: 3499 },
{ _id: 1010, date: ISODate('2026-10-11T13:00:00.000Z'), total: 2098 }
]Inside the index the entries are grouped by status, and within each status they are already sorted by date, newest first. MongoDB reads the "pending" part of the index in order and never has to sort in memory.
Field order matters. This index can also serve queries on status alone, because status is its prefix (its leading field), but it cannot efficiently serve a query on date alone. A good guide for ordering fields is the ESR rule: put fields tested for Equality first, then fields you Sort on, then fields used in Ranges ($gt, $lt). For "UPI orders over ₹2,000, newest first", that means { payment: 1, date: -1, total: 1 }.
Multikey indexes on arrays
Index a field that holds an array and MongoDB creates one index entry for each element. This is called a multikey index, and you do not need any special option:
db.products.createIndex({ tags: 1 })
db.orders.createIndex({ 'items.productId': 1 })tags_1 items.productId_1
Now { tags: 'office' } and "which orders contain product 101?" ({ 'items.productId': 101 }) are both index lookups. A compound index may include at most one array field, so { tags: 1, 'items.productId': 1 } style combinations of two arrays in one document are not allowed.
Unique and partial indexes
A unique index refuses a second document with the same value. Two accounts must never share an email:
db.customers.createIndex({ email: 1 }, { unique: true })
db.customers.insertOne({ _id: 9, name: 'Aarav S.', email: 'aarav@example.com', city: 'Jaipur' })email_1
MongoServerError: E11000 duplicate key error collection: shop.customers index: email_1 dup key: { email: "aarav@example.com" }This is the same E11000 error you met for _id, now protecting email. Uniqueness is enforced by the server, so it holds even when two sign-ups arrive at the same moment, which an "if not exists, insert" check in your code cannot promise. Values are compared exactly, so store emails in lowercase to stop Aarav@Example.com slipping through.
A partial index includes only documents that match a filter. That makes it smaller, and for a unique index it means "unique among the documents that have the field". Customers do not all have a phone number yet, but those who do must have different ones:
db.customers.createIndex(
{ phone: 1 },
{ unique: true, partialFilterExpression: { phone: { $exists: true } } }
)phone_1
Without the partial filter, every customer without a phone would count as having the value null, and the second one would fail with a duplicate key error.
TTL indexes: automatic expiry
A TTL index (time to live) deletes documents automatically a set number of seconds after the date stored in a field. One-time passwords sent by SMS should vanish after ten minutes:
db.otps.createIndex({ createdAt: 1 }, { expireAfterSeconds: 600 })
db.otps.insertOne({ phone: '+91 90000 00001', code: '482913', createdAt: new Date() })createdAt_1
{
acknowledged: true,
insertedId: ObjectId('6ae4452ac3f5e8a91d2b7a0c')
}A background task on the server checks for expired documents about once a minute, so a document may live slightly longer than 600 seconds. The indexed field must hold a date (or an array of dates); documents without it never expire. TTL indexes must be on a single field.
Text indexes
A text index splits strings into words, so you can search for words anywhere in a field. A collection can have only one text index, but it can cover several fields:
db.products.createIndex({ name: 'text', tags: 'text' })
db.products.find({ $text: { $search: 'wireless mouse' } }, { name: 1 }).sort({ score: { $meta: 'textScore' } })name_text_tags_text
[
{ _id: 101, name: 'Wireless Mouse' },
{ _id: 105, name: 'Bluetooth Earbuds' }
]$search finds documents containing any of the words, and sorting by textScore puts the best match first: the mouse matches both words, the earbuds only the tag wireless. Text indexes ignore case and common English words. For typo tolerance, autocomplete and relevance tuning, use MongoDB's full search engine instead, described in Atlas Search & Vector Search.
Managing indexes and their cost
List indexes with getIndexes() and remove one by name with dropIndex(), for example db.orders.dropIndex('customerId_1'). Before dropping an index you are unsure about, you can hide it with db.orders.hideIndex('customerId_1'): the query planner stops using it but MongoDB keeps it up to date, so unhideIndex() brings it back instantly if queries slow down.
Indexes are not free:
- Every insert, update and delete must also update every index on the collection, so writes get slower as you add indexes.
- Indexes use disk space and, for good speed, memory.
- An index that no query uses is pure cost.
Index the fields your frequent queries filter and sort on, and nothing else. Compass shows each index's size and usage, and Atlas has a Performance Advisor that suggests indexes from your slow queries.
When you have finished, reload the shop data with load('shop-data.js'), which also removes these indexes.
Common mistakes
- Indexing every field. Writes slow down and memory fills up. Index for real queries only.
- Wrong field order in compound indexes.
{ date: -1, status: 1 }serves "pending, newest first" poorly. Follow ESR: equality, sort, range. - Creating a unique index on data that already has duplicates. The build fails with E11000. Find and fix duplicates first.
- Expecting TTL deletes to be exact. Expiry runs about once a minute, and only on date values.
Key points
- An index is a sorted structure that lets MongoDB find documents without a collection scan;
_idis always indexed. createIndex({ field: 1 })creates a single-field index and returns its name.- Compound index field order matters: queries can use the index prefix, and the ESR rule (equality, sort, range) guides the order.
- Indexes on arrays are multikey automatically, with one entry per element.
unique: trueenforces uniqueness on the server;partialFilterExpressionindexes only some documents.- TTL indexes expire documents by a date field, and a collection can have one text index for word search.
Exercise
The shop's most common queries are: (1) products in a category sorted by price, cheapest first; (2) customers by city, and (3) delivered orders with a total above ₹2,000, newest first. Create the best index for each, and make the customer email unique.
Show solution
db.products.createIndex({ category: 1, price: 1 })
db.customers.createIndex({ city: 1 })
db.orders.createIndex({ status: 1, date: -1, total: 1 })
db.customers.createIndex({ email: 1 }, { unique: true })The products index puts the equality field first and the sort field second. The orders index follows ESR exactly: status is tested for equality, date is the sort and total is the range, so MongoDB can read matching entries already in date order and filter the range inside the index.
Quick Quiz
1. Which index serves find({ status: 'pending' }).sort({ date: -1 }) best?
Equality on status first, then the sort field date, lets MongoDB read matching entries already in order.
2. What does a multikey index store for a document whose tags array has three values?
MongoDB indexes each array element separately, so queries on any tag can use the index.
3. Why use a partial filter with a unique index on phone?
Missing fields count as null in a normal index; the partial filter leaves those documents out.
4. What is the main cost of adding many indexes?
Indexes speed up reads but must be maintained on every insert, update and delete.
Learn MongoDB with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
