05 · Firestore & Bigtable Deep Dive¶
Level 1's Cloud SQL module covered relational storage. Not everything fits a table with a fixed schema, and not everything needs SQL joins — this module covers two of GCP's NoSQL databases, built for very different shapes of problem: Firestore (serverless documents, built for app backends) and Bigtable (wide-column, built for massive, low-latency throughput).
Choosing between them¶
| Firestore | Bigtable | |
|---|---|---|
| Data model | Documents (JSON-like) in collections | Wide-column, rows keyed by a single row key |
| Scale target | App-sized: thousands to millions of docs | Petabyte-scale, millions of ops/sec |
| Query model | Rich queries, indexes, real-time listeners | Row-key range scans only — no secondary-index queries |
| Ops model | Fully serverless, zero capacity planning | You choose node count/type; scales but isn't free-tier friendly |
| Typical use | Mobile/web app backend, user profiles, carts | Time-series/IoT telemetry, ad-tech, analytics ingestion |
| Pricing | Per read/write/delete + storage | Per node-hour + storage, regardless of traffic |
The short version: if you're building an app and aren't sure, start with Firestore. Reach for Bigtable only once you have a specific, very-high-volume workload (millions of writes/sec, single-row-key access patterns) that Firestore's per-operation pricing and query model don't fit.
Firestore: enable and create data¶
Firestore has two modes chosen once, at project creation, and not changeable afterward: Native mode (the modern document/real-time model) and Datastore mode (legacy compatibility). New projects should always use Native mode.
gcloud services enable firestore.googleapis.com
gcloud firestore databases create \
--location=nam5 \
--type=firestore-native
gcloud firestore databases list
# NAME LOCATION TYPE
# projects/gcp-mastery-path-123/databases/(default) nam5 FIRESTORE_NATIVE
Firestore: documents and collections¶
Firestore has no gcloud command for writing individual documents — reads
and writes go through a client library (or the console/emulator) since
documents are arbitrary JSON-like structures, not something a flag-based CLI
models well. The structure looks like:
users (collection)
└── uid_abc123 (document)
├── name: "Priya"
├── plan: "pro"
└── orders (subcollection)
└── order_001 (document)
├── total: 42.50
└── items: [...]
# Export existing data for backup/inspection (works via gcloud)
gcloud firestore export gs://gcp-mastery-path-123-firestore-backups/2026-08-03 \
--collection-ids=users
Waiting for [projects/gcp-mastery-path-123/databases/(default)/operations/...] to finish...done.
metadata:
'@type': type.googleapis.com/google.firestore.admin.v1.ExportDocumentsMetadata
operationState: SUCCESSFUL
Firestore: indexes and security rules¶
Every query that filters/sorts on more than one field needs a composite index, defined declaratively and deployed:
# firestore.indexes.json
{
"indexes": [
{
"collectionGroup": "orders",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "status", "order": "ASCENDING" },
{ "fieldPath": "createdAt", "order": "DESCENDING" }
]
}
]
}
gcloud firestore indexes composite create \
--collection-group=orders \
--field-config=field-path=status,order=ascending \
--field-config=field-path=createdAt,order=descending
Security rules — not IAM — are how Firestore controls access from client apps (mobile/web SDKs bypass IAM entirely and talk to Firestore directly):
// firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}
Bigtable: instance and table¶
Bigtable is provisioned, not serverless — you pick node count and cluster placement up front:
gcloud services enable bigtable.googleapis.com bigtableadmin.googleapis.com
gcloud bigtable instances create telemetry-instance \
--display-name="Telemetry Instance" \
--cluster-config=id=telemetry-cluster,zone=us-central1-b,nodes=1
gcloud bigtable instances list
# INSTANCE_NAME DISPLAY_NAME STATE
# telemetry-instance Telemetry Instance READY
# cbt is the Bigtable-specific CLI (installed via gcloud components)
gcloud components install cbt
cbt -instance=telemetry-instance createtable sensor-readings
cbt -instance=telemetry-instance createfamily sensor-readings metrics
Bigtable: row keys are the whole design¶
Bigtable has exactly one index: the row key, sorted lexicographically. There
are no secondary indexes and no ad-hoc WHERE clauses — query performance is
entirely a function of row-key design.
# Row key: reverse-timestamp prefix keeps recent data together for range scans
cbt -instance=telemetry-instance set sensor-readings \
sensor42#2026-08-03T10:00:00Z metrics:temp=21.4
cbt -instance=telemetry-instance read sensor-readings \
prefix=sensor42#
----------------------------------------
sensor42#2026-08-03T10:00:00Z
metrics:temp @ 2026/08/03-10:00:00.000000
"21.4"
A row key like a raw incrementing integer or a plain timestamp creates hotspotting: all new writes land on the same node/tablet, capping your real throughput far below what more nodes should provide. Prefixing with a well-distributed field (device ID, hash) before the timestamp is the standard fix.
Gotchas¶
- Firestore mode is permanent. Native vs. Datastore mode is chosen once when the database is created and cannot be switched later — get it right the first time (Native, for anything new).
- Firestore composite indexes must be pre-declared, not created
automatically at query time — a query that needs one you haven't deployed
fails with an error that includes a direct console link to auto-create it,
which is worth clicking during development but should be checked into
firestore.indexes.jsonfor real deployments. - Firestore security rules ≠ IAM. IAM controls access from your backend (using service-account credentials); security rules control access from client SDKs that connect directly with a user's Firebase Auth token — both layers exist independently and neither substitutes for the other.
- Bigtable bills per node-hour, not per request. An idle 3-node cluster costs the same as a fully loaded one — this is the single biggest reason Bigtable is the wrong default for anything but sustained high-throughput workloads.
- Bigtable row-key design is the entire performance model. There's no query planner to bail you out of a bad key — hotspotting from sequential keys is the most common real-world Bigtable production incident.
Cleanup¶
gcloud firestore indexes composite list --format="value(name)" | \
xargs -I{} gcloud firestore indexes composite delete {} --quiet
cbt -instance=telemetry-instance deletetable sensor-readings
gcloud bigtable instances delete telemetry-instance --quiet
Firestore's default database itself cannot be deleted through most projects' lifecycle — for a learning project, deleting the individual collections/ documents (or the whole GCP project) is the practical cleanup path.
Cheat sheet¶
| Command | Purpose |
|---|---|
gcloud firestore databases create --type=firestore-native |
Provision the (one-time, permanent) Firestore mode. |
gcloud firestore export gs://<bucket>/<path> |
Back up Firestore data to Cloud Storage. |
gcloud firestore indexes composite create |
Declare a multi-field composite index. |
gcloud bigtable instances create --cluster-config= |
Provision a Bigtable instance and its first cluster. |
cbt createtable / createfamily |
Create a table and column family. |
cbt set / read |
Write/read rows via the Bigtable-specific CLI. |
How It Actually Works¶
Firestore and Bigtable sit at opposite ends of the same underlying idea — distributing rows across many servers by key range — but solve very different consistency problems. Firestore layers a document/collection model with real-time listeners on top of a globally replicated, strongly consistent backend: when a client subscribes to a query, the server keeps a live watch and pushes only the diff (added/modified/ removed documents) whenever a write changes the result set, which is why Firestore listeners feel instantaneous but scale poorly past a few thousand concurrent listeners on the same query — each write potentially fans out to every listener watching a matching query. Bigtable instead optimizes for raw throughput over a single flat, sorted key space: rows are physically stored in row-key order and split into contiguous key-range shards (tablets) as data grows, so query performance is entirely a function of row-key design — sequential keys (like a timestamp prefix) create write hotspots because all new writes land on the same tablet, while a well-distributed key (like a hashed prefix) spreads writes evenly across tablets and nodes.
Exercise¶
Create a Firestore Native database, then (using the Firestore emulator or
console, since gcloud doesn't write individual documents) add a handful of
documents to a users collection with an orders subcollection. Separately,
create a single-node Bigtable instance, create a table with one column
family, and write 3-4 rows using row keys with a device-ID prefix before a
timestamp. Use cbt read ... prefix= to confirm the range scan returns only
that device's rows in time order.