GCP Databases
Managed relational, NoSQL, globally distributed, and caching databases.
Database Service Map
| Use case | AWS | GCP |
|---|---|---|
| Managed Postgres | RDS Postgres / Aurora | Cloud SQL / AlloyDB |
| Managed MySQL | RDS MySQL / Aurora MySQL | Cloud SQL MySQL |
| Global ACID SQL | Aurora Global (lag) | Cloud Spanner (TrueTime, no lag) |
| NoSQL document | DynamoDB / DocumentDB | Firestore |
| Wide-column / time-series | DynamoDB single-table | Bigtable |
| In-memory cache | ElastiCache (Redis/Memcached) | Memorystore |
| Analytical / data warehouse | Redshift | BigQuery (see bigquery.md) |
Cloud SQL — Managed MySQL, PostgreSQL, SQL Server
Cloud SQL is the closest thing to RDS. Supports Postgres, MySQL, and SQL Server with automated backups, HA, and read replicas.
Create an Instance
# PostgreSQL instance
gcloud sql instances create my-postgres \
--database-version=POSTGRES_15 \
--region=us-central1 \
--tier=db-n1-standard-4 \ # 4 vCPU, 15 GB RAM
--storage-size=100GB \
--storage-type=SSD \
--storage-auto-increase \ # like Aurora auto-grow
--backup-start-time=04:00 \
--availability-type=REGIONAL # HA (standby in another zone)
# Create a database and user
gcloud sql databases create myapp --instance=my-postgres
gcloud sql users create myuser --instance=my-postgres --password=secret
Connection Methods
localhost and the proxy does the secure hop to Cloud SQL. This is the AWS analog of RDS Proxy, and the recommended default for anything that isn't a quick local test.
# Cloud SQL Auth Proxy (local dev)
./cloud-sql-proxy my-project:us-central1:my-postgres &
# connects on localhost:5432
# Private IP (best for production)
gcloud sql instances patch my-postgres \
--network=my-vpc \
--no-assign-ip # disable public IP
HA and Read Replicas
# HA is set with --availability-type=REGIONAL at creation
# This creates a standby in a different zone (like RDS Multi-AZ)
# Add a read replica
gcloud sql instances create my-postgres-replica \
--master-instance-name=my-postgres \
--region=us-east1 # cross-region read replica
# Promote replica to primary (for migration/disaster recovery)
gcloud sql instances promote-replica my-postgres-replica
Cloud SQL vs RDS Comparison
| Cloud SQL | AWS RDS | |
|---|---|---|
| Postgres max version | 15 | 16 |
| Storage auto-grow | Yes | Yes |
| Multi-AZ HA | Regional (different zone standby) | Multi-AZ (synchronous standby) |
| Read replicas | Yes, cross-region | Yes, cross-region |
| Managed proxy | Cloud SQL Auth Proxy | RDS Proxy |
| IAM auth | Yes (passwordless) | Yes (IAM DB auth) |
| Point-in-time recovery | Yes (7-day window) | Yes (35-day max) |
| Max storage | 64 TB | 64 TB |
| Maintenance window | Configurable | Configurable |
You create a Cloud SQL instance with --availability-type=REGIONAL, then separately add a cross-region read replica. Are these two the same failover mechanism?
promote-replica action, not an automatic failover.AlloyDB — PostgreSQL on Steroids
AlloyDB is GCP's proprietary high-performance Postgres. It's Google's answer to Amazon Aurora — built on Postgres protocol but with a custom storage layer.
graph LR
classDef baseline fill:#7f8c8d,stroke:#616a6b,color:#fff
classDef mid fill:#3498db,stroke:#2471a3,color:#fff
classDef fast fill:#27ae60,stroke:#1e8449,color:#fff
subgraph LINEAGE["Managed Postgres storage-engine lineage"]
RDS["RDS Postgres<br/>Standard community Postgres engine<br/>~3x slower than Aurora<br/>Community Postgres storage limits<br/>No columnar engine"]:::baseline
AURORA["Aurora Postgres<br/>Custom distributed storage engine<br/>4x faster than RDS Postgres<br/>Up to 128 TB<br/>No columnar engine"]:::mid
ALLOY["AlloyDB<br/>Custom distributed storage engine<br/>2x faster than Aurora<br/>Up to 64 TB (growing)<br/>Built-in columnar engine for analytics"]:::fast
end
RDS -->|"Amazon rewrites<br/>the storage layer"| AURORA
AURORA -.->|"Google's answer,<br/>adds a columnar engine"| ALLOY
When to use AlloyDB over Cloud SQL:
- OLTP workloads needing 4-5× more throughput than Cloud SQL
- Hybrid OLTP + analytics (AlloyDB has a columnar engine for fast analytics on live data)
- Require sub-second failover (AlloyDB uses distributed storage, no failover replica sync)
gcloud alloydb clusters create my-cluster \
--region=us-central1 \
--password=admin-pass
gcloud alloydb instances create my-primary \
--instance-type=PRIMARY \
--cluster=my-cluster \
--region=us-central1 \
--cpu-count=4
AlloyDB claims sub-second failover where Cloud SQL's regional HA takes longer. What architectural difference makes that possible?
Cloud Spanner — Globally Distributed ACID SQL
Spanner is the only database in the world that provides both horizontal scaling AND global strong consistency. Aurora Global has seconds of replication lag. Spanner has ~10ms at global scale.
How TrueTime Works
sequenceDiagram
participant APP as Application (us-central1)
participant SP1 as Spanner replica group (us-central1)
participant TS as TrueTime API (atomic clock + GPS per datacenter)
participant SP2 as Spanner replica group (europe-west1)
participant RD as Reader (europe-west1)
APP->>SP1: Commit transaction (write orders row)
SP1->>TS: TT.now()
TS-->>SP1: interval [earliest, latest], width = clock uncertainty epsilon
rect rgb(60, 45, 20)
Note over SP1: Commit-wait — stall until real time passes "latest"
SP1->>SP1: sleep until wall clock later than latest, typically 4-7ms
end
SP1->>SP1: assign commit timestamp = latest, make write visible
SP1-->>APP: commit acknowledged
RD->>SP2: Read at current time
SP2->>TS: TT.now()
TS-->>SP2: current interval, guaranteed later than the commit's latest
SP2-->>RD: return committed row, never a stale or earlier version
us-central1), which calls the local datacenter's TrueTime API — TT.now() — before assigning a timestamp.
[earliest, latest], a bound guaranteed to contain the true current time, typically only a few milliseconds wide.
us-central1. There's no window where the write could be "not-yet-happened" relative to a clock anywhere else.
Spanner's global strong consistency comes from waiting on synchronous network round-trips to remote regions before every commit — true or false?
Google uses GPS receivers and atomic clocks in every datacenter. Every commit waits out the clock uncertainty (typically 4-7ms). This guarantees linearizability globally — reads always see the latest committed state, across continents.
When to Use Spanner
- Financial transactions across regions (banking, payments)
- Inventory systems that must be globally consistent
- Gaming (leaderboards, inventory) at global scale
- Cannot tolerate any read staleness, even milliseconds
- Your workload is single-region (Cloud SQL is cheaper)
- You need complex joins over large datasets (BigQuery is better)
- Budget-constrained ($0.65/node-hour minimum)
# Create a Spanner instance
gcloud spanner instances create my-spanner \
--config=regional-us-central1 \ # or nam4, eur3, global1
--description="Production" \
--nodes=3 # 2,000 QPS per node
# Multi-region config (global consistency)
gcloud spanner instances create global-spanner \
--config=nam-eur-asia1 \ # 3 continents
--nodes=3
# Create database and schema
gcloud spanner databases create mydb --instance=my-spanner
gcloud spanner databases ddl update mydb --instance=my-spanner \
--ddl='CREATE TABLE orders (
order_id STRING(36) NOT NULL,
user_id STRING(36) NOT NULL,
amount NUMERIC NOT NULL,
created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (order_id)'
Spanner vs Aurora Global
| Cloud Spanner | Aurora Global | |
|---|---|---|
| Replication lag | ~0ms (TrueTime, strong consistency) | 1-2 seconds (async replication) |
| Global write | Any region (multi-master) | One primary region only |
| SQL compatibility | GoogleSQL dialect (ANSI SQL + extensions) | Standard MySQL / Postgres |
| Schema changes | Online, no downtime | Downtime for some ALTER TABLE |
| Cost | $0.65/node-hour | ~$0.29/hour for r5.large |
| Auto-scaling | Yes (serverless Spanner) | No (fixed instance sizes) |
Firestore — Serverless Document Database
Firestore = GCP's MongoDB / DynamoDB hybrid. Serverless (scales to zero), document model, real-time listeners.
| DynamoDB concept | Firestore equivalent |
|---|---|
| Tables | Collections |
| Items | Documents |
| Partition + sort key | Document ID (path-based) |
| GSI | Composite indexes |
| Streams | Real-time listeners |
| $0.25/GB + $0.25/RCU | $0.06/GB + $0.06/100K reads |
Firestore Data Model
graph TD
classDef collection fill:#3498db,stroke:#2471a3,color:#fff
classDef document fill:#27ae60,stroke:#1e8449,color:#fff
classDef subcollection fill:#e67e22,stroke:#ba6018,color:#fff
ROOT["/users/<br/>Collection"]:::collection
ROOT --> DOC["user-123/<br/>Document<br/>name: Alice<br/>email: alice@example.com"]:::document
DOC --> SUB["orders/<br/>Sub-collection<br/>nested under this document"]:::subcollection
SUB --> ORDER["order-abc/<br/>Document<br/>amount: 99.99<br/>status: shipped"]:::document
A document's path always alternates collection/document/collection — a sub-collection lives under a specific document, not under the parent collection, which is why orders here only contains user-123's orders, not every order in the system.
from google.cloud import firestore
db = firestore.Client()
# Write
db.collection("users").document("user-123").set({
"name": "Alice",
"email": "alice@example.com",
"created_at": firestore.SERVER_TIMESTAMP
})
# Read
doc = db.collection("users").document("user-123").get()
print(doc.to_dict())
# Query (must create composite index for multi-field queries)
users = db.collection("users")\
.where("status", "==", "active")\
.where("plan", "==", "premium")\
.order_by("created_at", direction=firestore.Query.DESCENDING)\
.limit(10)\
.stream()
# Real-time listener (no equivalent in DynamoDB)
def on_snapshot(docs, changes, read_time):
for change in changes:
print(f"Change: {change.type.name} {change.document.id}")
db.collection("orders").on_snapshot(on_snapshot)
A query filters on status == "active" AND plan == "premium", then orders by created_at. Will Firestore just run it, the way a SQL database would scan and sort on the fly?
Firestore Modes
| Mode | Best for |
|---|---|
| Native | Mobile/web apps, real-time, flexible schema |
| Datastore | Legacy mode, no real-time, lower cost for batch |
Use Native mode for all new projects.
Memorystore — Managed Redis and Memcached
Memorystore = GCP's ElastiCache.
# Create Redis instance
gcloud redis instances create my-redis \
--size=5 \ # 5 GB
--region=us-central1 \
--redis-version=redis_7_0 \
--tier=STANDARD # STANDARD = HA with replica; BASIC = no HA
# Get connection info
gcloud redis instances describe my-redis --region=us-central1
# → host: 10.0.0.50, port: 6379
# Connect from GKE pod (same VPC)
redis-cli -h 10.0.0.50 -p 6379
Memorystore vs ElastiCache
| Memorystore | ElastiCache | |
|---|---|---|
| Redis versions | 6.x, 7.x | 5.x, 6.x, 7.x |
| Cluster mode | Memorystore for Redis Cluster | Cluster Mode Enabled |
| HA | Standard tier (primary + replica) | Multi-AZ with auto-failover |
| Encryption | In-transit + at-rest | In-transit + at-rest |
| Auth | AUTH string | AUTH token |
| Persistence | RDB snapshots | RDB + AOF |
| Cost (5GB) | ~$0.049/hr | ~$0.068/hr (cache.r6g.large) |
You create a Memorystore Redis instance with --tier=BASIC to save cost, and the underlying VM has a hardware failure. What happens to the data?
Redis Cluster (for large workloads)
# Memorystore for Redis Cluster (sharded, scales to TBs)
gcloud redis clusters create my-redis-cluster \
--region=us-central1 \
--shard-count=3 \ # 3 shards × 2 nodes = 6 total
--replica-count=1 \ # 1 replica per shard
--node-type=REDIS_STANDARD_SMALL
Database Selection Guide
graph TD
classDef sql fill:#3498db,stroke:#2471a3,color:#fff
classDef alloy fill:#9b59b6,stroke:#76448a,color:#fff
classDef spanner fill:#27ae60,stroke:#1e8449,color:#fff
classDef firestore fill:#e67e22,stroke:#ba6018,color:#fff
classDef bigtable fill:#f1c40f,stroke:#b7950b,color:#000
classDef bigquery fill:#e74c3c,stroke:#c0392b,color:#fff
classDef memorystore fill:#1abc9c,stroke:#148f77,color:#fff
START{"What shape is<br/>your data and workload?"}
START -->|"Relational, fits one region,<br/>standard Postgres/MySQL"| CSQL["Cloud SQL<br/>cheapest managed option"]:::sql
START -->|"Relational, needs higher throughput<br/>or hybrid OLTP + analytics"| ALLOY["AlloyDB"]:::alloy
START -->|"Relational, multi-region,<br/>globally consistent, financial/inventory"| SPAN["Cloud Spanner"]:::spanner
START -->|"Document store, mobile/web app,<br/>real-time sync, serverless"| FS["Firestore"]:::firestore
START -->|"Wide-column, time-series,<br/>millions of writes/sec"| BT["Bigtable (see bigtable.md)"]:::bigtable
START -->|"Analytics, SQL over petabytes,<br/>cost-per-query model"| BQ["BigQuery (see bigquery.md)"]:::bigquery
START -->|"Caching, session store,<br/>pub/sub, rate limiting"| MS["Memorystore (Redis)"]:::memorystore
Skip it when: you need throughput closer to Aurora, hybrid OLTP+analytics, or any cross-region write consistency — that's AlloyDB or Spanner territory.
Skip it when: Cloud SQL's throughput is already enough — AlloyDB is solving a scaling problem you may not have yet.
Skip it when: the workload is single-region (Cloud SQL is cheaper), needs complex joins over huge datasets (BigQuery is better), or the $0.65/node-hour minimum doesn't fit the budget.
Skip it when: the workload needs SQL joins, ACID transactions across arbitrary rows, or wide-column time-series ingestion at extreme write rates — that's Bigtable's job instead.
Skip it when: the data needs to be durable in its own right — Memorystore is a cache, not a database of record.
| Cloud SQL | AlloyDB | Spanner | Firestore | Bigtable | Memorystore | |
|---|---|---|---|---|---|---|
| SQL | Yes | Yes | Yes (GSql) | No | No | No |
| Scale | Vertical | Vertical | Horizontal | Auto | Horizontal | Vertical |
| Global | No (replicas) | No | Yes | Multi-region | Multi-region | No |
| Strong consistency | Yes | Yes | Yes (global) | Yes | Row-level | N/A |
| Serverless | No | No | Yes (Autoscaler) | Yes | No | No |
| Cost model | Per instance | Per instance | Per node/RU | Per read/write/GB | Per node | Per instance |
A team already runs Cloud SQL for their relational app and now needs globally consistent multi-region writes for a new inventory feature. Does adding cross-region read replicas to Cloud SQL solve that?