Introduction: Working with Your Data

In the previous lesson, you learned how to create Firestore collections and add documents to them using the Python SDK. You now understand the fundamental structure of Firestore: data is organized into collections, which contain documents identified by unique document IDs. Each document can store various fields and data types, and you’ve seen how to organize related data using subcollections or references.

However, creating collections and adding documents is just the beginning. In real-world applications, you need to read individual documents, update them as your data changes, and sometimes remove documents that are no longer needed. You also need to retrieve multiple documents efficiently, whether you’re looking for all orders from a specific customer or searching for documents that meet certain criteria.

This lesson focuses on the complete lifecycle of working with data in Firestore. You’ll learn the four fundamental operations that every database supports: Create, Read, Update, and Delete — commonly known as CRUD operations. While you’ve already seen how to create documents, we’ll explore the other three operations in detail. More importantly, you’ll learn how to retrieve multiple documents using Firestore’s powerful querying capabilities. Understanding how to structure and execute queries is crucial for building efficient and scalable applications with Firestore.

By the end of this lesson, you’ll know how to implement all the essential data operations your applications will need and how to retrieve documents efficiently using Firestore’s query system.

Reading Data: Retrieving a Document by ID

After adding documents to your Firestore collection, the most common operation you’ll perform is reading individual documents back. In Firestore, each document is uniquely identified by its document ID within a collection. The fastest way to access a document is by referencing its ID directly.

Let’s start by creating a products collection, adding a product document, and then retrieving it. Here’s how you can read a single product using its document ID:

from google.cloud import firestore
from datetime import datetime

def setup_firestore():
    """Initialize Firestore client"""
    return firestore.Client()

def create_product(db, product_id, name, price, stock):
    """Add a new product document"""
    doc_ref = db.collection('products').document(product_id)
    doc_ref.set({
        'name': name,
        'price': float(price),
        'stock': stock,
        'created_at': firestore.SERVER_TIMESTAMP
    })
    return product_id

def read_product(db, product_id):
    """Get a product document by ID"""
    doc_ref = db.collection('products').document(product_id)
    doc = doc_ref.get()
    if doc.exists:
        return doc.to_dict()
    else:
        return None

The read_product function demonstrates the essential pattern for retrieving documents. You obtain a reference to the document using its ID, then call .get() to fetch its data. If the document exists, you can access its fields using .to_dict(). If the document does not exist, .exists will be False, and you can return None to indicate that the document was not found. This approach prevents your code from crashing if the document is missing.

When you retrieve a document, Firestore returns all of its fields. In our product example, you’ll get back the name, price, stock, and created_at fields. This operation is very fast because Firestore uses the document ID to locate the document directly.

Updating Data: Modifying Document Fields

As your application runs, you'll frequently need to modify existing documents. While you could use .set() to replace an entire document, this approach overwrites all fields. If you only want to change one or more fields, Firestore's .update() method allows you to modify specific fields without affecting the rest of the document.

Here's a function that updates a product's stock level:

def update_stock(db, product_id, quantity_change):
    """Update product stock by adding or removing quantity"""
    doc_ref = db.collection('products').document(product_id)
    # Use Firestore's atomic increment for safe concurrent updates
    doc_ref.update({
        'stock': firestore.Increment(quantity_change),
        'last_updated': firestore.SERVER_TIMESTAMP
    })
    # Fetch and return the updated document
    updated_doc = doc_ref.get()
    return updated_doc.to_dict() if updated_doc.exists else None

The .update() method takes a dictionary of fields to update. In this example, we use firestore.Increment(quantity_change) to modify the stock field. This operation is atomic, meaning Firestore performs the entire update as a single, indivisible operation on the server. This is crucial when multiple users or processes might update the same document simultaneously.

We also update the last_updated field with the current timestamp. After updating, we fetch the document again to confirm the new values. Firestore's atomic operations, like Increment, make it easy to safely update numeric fields without needing to manually handle concurrent access.

To understand why atomic operations matter, consider what would happen without them. If you read a document's current stock (say, 50 units), subtract 5, and then write back 45, another process might do the same thing at the same time. Both processes would read 50, both would calculate 45, and you'd end up with 45 instead of 40 — losing one of the updates. With firestore.Increment(-5), Firestore handles the read-modify-write cycle on the server in a single atomic step, guaranteeing that both decrements are applied correctly.

Deleting Data: Removing a Document

Sometimes you need to remove documents from your collection entirely. Firestore’s .delete() method on a document reference removes the document and all its fields. This operation is straightforward and idempotent — deleting a document that does not exist has no effect and does not raise an error.

Here’s how you delete a product document:

def delete_product(db, product_id):
    """Delete a product document"""
    doc_ref = db.collection('products').document(product_id)
    doc_ref.delete()
    return product_id

The .delete() method requires only the document reference. After deleting a document, you can verify the deletion by trying to read it again — if the document no longer exists, .get().exists will be False.

Here’s an example of the complete lifecycle:

if __name__ == "__main__":
    db = setup_firestore()
    product_id = create_product(db, 'LAPTOP-001', 'Gaming Laptop', 1299.99, 50)
    item = read_product(db, product_id)
    print(f"Created: {item['name']} - ${item['price']}")

    updated = update_stock(db, product_id, -5)
    print(f"Updated stock: {updated['stock']}")

    item = read_product(db, product_id)
    print(f"Current stock: {item['stock']}")

    delete_product(db, product_id)
    deleted_item = read_product(db, product_id)
    print(f"Deleted: {deleted_item is None}")

When you run this code, you’ll see output like:

Created: Gaming Laptop - $1299.99
Updated stock: 45
Current stock: 45
Deleted: True
Query: Efficient Data Retrieval

While reading a document by its ID is fast and direct, many applications need to retrieve multiple related documents. For example, you might want to get all orders placed by a specific customer or all products with low stock. Firestore supports powerful queries that allow you to retrieve documents matching specific field values.

Let's create an orders collection and demonstrate how to query for all orders from a specific customer:

def setup_orders(db):
    """Create sample orders for query demo"""
    orders = [
        {'customer_id': 'CUST-001', 'order_date': firestore.SERVER_TIMESTAMP, 'total': 99.99, 'status': 'delivered'},
        {'customer_id': 'CUST-001', 'order_date': firestore.SERVER_TIMESTAMP, 'total': 149.99, 'status': 'shipped'},
        {'customer_id': 'CUST-002', 'order_date': firestore.SERVER_TIMESTAMP, 'total': 79.99, 'status': 'delivered'},
        {'customer_id': 'CUST-003', 'order_date': firestore.SERVER_TIMESTAMP, 'total': 199.99, 'status': 'pending'}
    ]
    for order in orders:
        # Let Firestore auto-generate document IDs
        db.collection('orders').add(order)

def query_customer_orders(db, customer_id):
    """Query all orders for a specific customer"""
    orders_ref = db.collection('orders')
    query = orders_ref.where(filter=firestore.FieldFilter('customer_id', '==', customer_id))
    results = [doc.to_dict() for doc in query.stream()]
    return results

The where method allows you to filter documents by field values. In this example, we retrieve all orders where the customer_id field matches the value we provide. Firestore queries are efficient because they use indexes to quickly find matching documents, even in large collections. When using the modern SDK, prefer where(filter=firestore.FieldFilter(...)) to avoid deprecation warnings.

The result of a query is a list of document snapshots, which you can convert to dictionaries using .to_dict().

Collection-wide Queries: Retrieving All Documents or Unfiltered Queries

In some cases, you may need to retrieve documents based on fields that are not indexed, or you may want to fetch all documents in a collection. In Firestore, all simple queries are indexed by default, and you can retrieve all documents in a collection by simply iterating over it. However, if you perform queries that require composite indexes and those indexes are not yet created, Firestore will prompt you to add them (you can create indexes from the Firebase Console or the Google Cloud Console under Firestore → Indexes).

Here’s how you can retrieve all orders above a certain total amount (an amount-only, collection-wide query):

def scan_high_value_orders(db, min_amount):
    """Retrieve orders above a threshold (collection-wide amount-only query)"""
    orders_ref = db.collection('orders')
    query = orders_ref.where(filter=firestore.FieldFilter('total', '>', min_amount))
    results = [doc.to_dict() for doc in query.stream()]
    return results

Firestore queries like this are efficient because they use indexes. However, if you need to retrieve all documents in a collection without any filters, you can do so:

def get_all_orders(db):
    """Retrieve all orders in the collection"""
    orders_ref = db.collection('orders')
    results = [doc.to_dict() for doc in orders_ref.stream()]
    return results

Keep in mind that reading large result sets can be slower and more expensive, as you pay for each document read. For very large collections, consider paginating your results.

Combining Query with Filters

Firestore queries become even more powerful when you combine multiple filters. You can chain multiple where clauses to narrow down your results based on several fields. For example, you might want to find all shipped orders for a specific customer:

def get_orders_by_status(db, customer_id, status):
    """Query orders for a customer with a specific status"""
    orders_ref = db.collection('orders')
    query = orders_ref.where(filter=firestore.FieldFilter('customer_id', '==', customer_id)).where(filter=firestore.FieldFilter('status', '==', status))
    results = [doc.to_dict() for doc in query.stream()]
    return results

Firestore supports compound queries, but there are some limitations. For example, you can only perform range comparisons (<, >, etc.) on a single field in a compound query, and you may need to create composite indexes for certain combinations of filters. If your query requires an index that does not exist, Firestore will provide a link to create it in the Firebase Console or in the Google Cloud Console (Firestore → Indexes).

Unlike some databases, Firestore does not require you to handle reserved words or use attribute name placeholders in queries.

Here’s an example of using these operations together:

if __name__ == "__main__":
    db = setup_firestore()
    setup_orders(db)

    customer_orders = query_customer_orders(db, 'CUST-001')
    print(f"\nQuery result: {len(customer_orders)} orders")

    high_value = scan_high_value_orders(db, 100)
    print(f"High value orders: {len(high_value)} orders")

    shipped = get_orders_by_status(db, 'CUST-001', 'shipped')
    print(f"Filtered query: {len(shipped)} shipped orders")

Sample output:

Query result: 2 orders
High value orders: 2 orders
Filtered query: 1 shipped orders
Summary: Mastering Firestore Operations

You’ve now learned all the essential operations for working with data in Firestore. The CRUD operations — create, read, update, and delete — form the foundation of any database application. Use .set() to create new documents or replace existing ones. Use .get() to retrieve a document by its ID, which is the fastest way to access data. Use .update() to modify specific fields without affecting the rest of the document, and take advantage of atomic operations like Increment for safe concurrent updates. Use .delete() to remove documents, knowing that the operation is idempotent.

Firestore’s query system allows you to efficiently retrieve documents that match specific field values. Use where filters to find documents by field, and chain multiple filters for more complex queries. Firestore automatically uses indexes to make queries fast and scalable, but be aware of index requirements for compound queries. When you need to retrieve all documents in a collection, you can iterate over the collection, but be mindful of performance and cost for large datasets.

In the upcoming practice exercises, you’ll implement these operations yourself. You’ll create collections, perform CRUD operations, and experiment with Firestore’s query capabilities to see how they work in practice. Pay attention to how queries remain fast even as your collections grow, and try combining multiple filters to build flexible data retrieval patterns. These exercises will help you develop the intuition for choosing the right operation for each situation, a skill that’s essential for building production-ready applications with Firestore.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal