Google Cloud managed database services including Cloud SQL, Firestore, Bigtable, and Spanner
Scope: Cloud SQL, Firestore, Bigtable, Spanner, and Memorystore configuration and best practices Lines: ~340 Last Updated: 2025-10-25 Format Version: 1.0 (Atomic)
Activate this skill when:
Key features:
# Create PostgreSQL instance with high availability
gcloud sql instances create production-db \
--database-version=POSTGRES_15 \
--tier=db-n1-standard-4 \
--region=us-central1 \
--availability-type=REGIONAL \
--backup-start-time=03:00 \
--enable-bin-log \
--maintenance-window-day=SUN \
--maintenance-window-hour=4
# Create database and user
gcloud sql databases create appdb --instance=production-db
gcloud sql users create appuser \
--instance=production-db \
--password=SECURE_PASSWORD
# Create read replica for scaling reads
gcloud sql instances create production-db-replica \
--master-instance-name=production-db \
--tier=db-n1-standard-2 \
--region=us-central1
Data model:
from google.cloud import firestore
# Initialize Firestore client
db = firestore.Client()
# Create document with auto-generated ID
users_ref = db.collection('users')
new_user = users_ref.add({
'name': 'Alice Johnson',
'email': 'alice@example.com',
'created_at': firestore.SERVER_TIMESTAMP,
'roles': ['admin', 'editor']
})
# Query documents
active_users = users_ref.where('status', '==', 'active').where('roles', 'array_contains', 'admin').stream()
for user in active_users:
print(f'{user.id} => {user.to_dict()}')
# Real-time listener for changes
def on_snapshot(collection_snapshot, changes, read_time):
for change in changes:
if change.type.name == 'ADDED':
print(f'New user: {change.document.id}')
users_ref.on_snapshot(on_snapshot)
Key principles:
from google.cloud import bigtable
from google.cloud.bigtable import column_family, row_filters
# Create Bigtable instance and table
client = bigtable.Client(project='my-project', admin=True)
instance = client.instance('analytics-instance')
# Create table with column family
table = instance.table('events')
cf = table.column_family('metrics', max_age=timedelta(days=30))
cf.create()
# Row key design for time-series: reverse timestamp + user_id (avoid hotspots)
import time
reverse_timestamp = str(2**63 - int(time.time() * 1000))
row_key = f"{reverse_timestamp}#{user_id}".encode()
# Write data
row = table.direct_row(row_key)
row.set_cell('metrics', 'page_views', str(125), timestamp=datetime.utcnow())
row.set_cell('metrics', 'session_duration', str(450), timestamp=datetime.utcnow())
row.commit()
# Read data with prefix scan
rows = table.read_rows(
start_key=f"{reverse_timestamp}#".encode(),
limit=100
)
Features:
from google.cloud import spanner
# Create Spanner instance with multi-region configuration
spanner_client = spanner.Client()
instance = spanner_client.instance(
'global-instance',
configuration_name='nam-eur-asia1', # Multi-region
node_count=3
)
database = instance.database('orders')
# Insert with transaction
def insert_order(transaction):
transaction.execute_update(
"""INSERT INTO Orders (OrderId, CustomerId, Amount, CreatedAt)
VALUES (@order_id, @customer_id, @amount, CURRENT_TIMESTAMP())""",
params={'order_id': 'ORD-123', 'customer_id': 'CUST-456', 'amount': 99.99},
param_types={'order_id': spanner.param_types.STRING,
'customer_id': spanner.param_types.STRING,
'amount': spanner.param_types.FLOAT64}
)
database.run_in_transaction(insert_order)
# Query with strong consistency
with database.snapshot() as snapshot:
results = snapshot.execute_sql(
"""SELECT o.OrderId, o.Amount, c.Name
FROM Orders o JOIN Customers c ON o.CustomerId = c.CustomerId
WHERE o.CreatedAt > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)"""
)
for row in results:
print(f"Order {row[0]}: ${row[1]} - {row[2]}")
When to use:
# ❌ Bad: Single-zone instance (no automatic failover)
gcloud sql instances create db \
--tier=db-n1-standard-2 \
--region=us-central1
# Zone failure causes downtime
# ✅ Good: Regional HA instance with automatic failover
gcloud sql instances create db \
--tier=db-n1-standard-2 \
--region=us-central1 \
--availability-type=REGIONAL \
--enable-bin-log
# Standby instance automatically promoted on primary failure
# Typically <60 seconds of downtime
Benefits:
Use case: Secure document access based on authentication and data content
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Users can only read/write their own documents
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Public read, authenticated write
match /posts/{postId} {
allow read: if true;
allow create: if request.auth != null;
allow update, delete: if request.auth != null &&
request.auth.uid == resource.data.author_id;
}
// Admin-only collection
match /admin/{document=**} {
allow read, write: if request.auth.token.admin == true;
}
}
}
Use case: Store sensor data with efficient time-range queries
# ❌ Bad: Sequential timestamps create hotspot on single tablet
row_key = f"{sensor_id}#{timestamp}".encode()
# All recent writes go to same tablet server
# ✅ Good: Hash prefix distributes writes, reverse timestamp for recent scans
import hashlib
def create_row_key(sensor_id, timestamp):
# Hash prefix (first 2 hex chars) distributes writes across tablets
prefix = hashlib.md5(sensor_id.encode()).hexdigest()[:2]
# Reverse timestamp for efficient recent-data scans
reverse_ts = str(2**63 - int(timestamp * 1000))
return f"{prefix}#{sensor_id}#{reverse_ts}".encode()
# Write to Bigtable
row = table.direct_row(create_row_key('sensor-42', time.time()))
row.set_cell('data', 'temperature', '72.5')
row.commit()
# Read recent data for sensor with prefix scan
prefix = hashlib.md5('sensor-42'.encode()).hexdigest()[:2]
rows = table.read_rows(
start_key=f"{prefix}#sensor-42#".encode(),
end_key=f"{prefix}#sensor-42#~".encode(),
limit=100
)
Use case: Physically co-locate related rows for faster joins
-- ❌ Bad: Separate tables (data distributed, slow joins)
CREATE TABLE Customers (
CustomerId STRING(36),
Name STRING(100)
) PRIMARY KEY (CustomerId);
CREATE TABLE Orders (
OrderId STRING(36),
CustomerId STRING(36),
Amount FLOAT64
) PRIMARY KEY (OrderId);
-- Orders scattered across nodes, expensive joins
-- ✅ Good: Interleave Orders within Customers
CREATE TABLE Customers (
CustomerId STRING(36),
Name STRING(100)
) PRIMARY KEY (CustomerId);
CREATE TABLE Orders (
CustomerId STRING(36),
OrderId STRING(36),
Amount FLOAT64
) PRIMARY KEY (CustomerId, OrderId),
INTERLEAVE IN PARENT Customers ON DELETE CASCADE;
-- Orders stored physically with parent Customer row
-- Fast queries for customer's orders
SELECT * FROM Customers c JOIN Orders o USING (CustomerId)
WHERE c.CustomerId = 'CUST-123'; -- Efficient, single node
Use case: Avoid connection exhaustion in serverless environments
import sqlalchemy
from sqlalchemy import create_engine
# ❌ Bad: Create new connection per request (connection exhaustion)
def query_database():
engine = create_engine('postgresql://user:pass@host/db')
with engine.connect() as conn:
result = conn.execute("SELECT * FROM users")
return result
# ✅ Good: Reuse connection pool across requests
# Cloud SQL Proxy handles connection pooling
connection_pool = create_engine(
'postgresql+pg8000://',
creator=lambda: connector.connect(
'project:region:instance',
'pg8000',
user='appuser',
password='SECRET',
db='appdb'
),
pool_size=5, # Max 5 concurrent connections
max_overflow=2,
pool_timeout=30,
pool_recycle=1800 # Recycle connections every 30 min
)
def query_database():
with connection_pool.connect() as conn:
result = conn.execute(sqlalchemy.text("SELECT * FROM users"))
return result
Use case: Enable complex queries on multiple fields
# Query requiring composite index
users_ref = db.collection('users')
query = users_ref.where('status', '==', 'active') \
.where('country', '==', 'US') \
.order_by('created_at', direction=firestore.Query.DESCENDING)
# Firestore automatically prompts to create index via error message
# Or create via index configuration:
{
"indexes": [
{
"collectionGroup": "users",
"queryScope": "COLLECTION",
"fields": [
{"fieldPath": "status", "order": "ASCENDING"},
{"fieldPath": "country", "order": "ASCENDING"},
{"fieldPath": "created_at", "order": "DESCENDING"}
]
}
]
}
Use case: Cache frequently accessed database queries in Redis
import redis
from google.cloud import secretmanager
# Connect to Memorystore Redis
redis_client = redis.Redis(
host='10.0.0.3', # Memorystore instance IP
port=6379,
decode_responses=True
)
def get_user(user_id):
# Try cache first
cache_key = f"user:{user_id}"
cached_user = redis_client.get(cache_key)
if cached_user:
return json.loads(cached_user)
# Cache miss - query database
with connection_pool.connect() as conn:
result = conn.execute(
sqlalchemy.text("SELECT * FROM users WHERE id = :id"),
{"id": user_id}
)
user = dict(result.fetchone())
# Store in cache (expire after 1 hour)
redis_client.setex(cache_key, 3600, json.dumps(user))
return user
Use case: Migrate from on-premises or other clouds to Cloud SQL
# Create migration job from MySQL source to Cloud SQL
gcloud database-migration migration-jobs create migrate-prod-db \
--region=us-central1 \
--type=CONTINUOUS \
--source=on-prem-mysql \
--destination=projects/my-project/instances/production-db \
--dump-path=gs://migration-bucket/dumps
# Monitor migration progress
gcloud database-migration migration-jobs describe migrate-prod-db \
--region=us-central1
# Promote Cloud SQL instance (cutover)
gcloud database-migration migration-jobs promote migrate-prod-db \
--region=us-central1
Service | Type | Scale | Use Case
-------------|----------------|---------------|---------------------------
Cloud SQL | SQL (managed) | Up to 64 TB | Traditional SQL apps
Firestore | Document NoSQL | Unlimited | Mobile/web apps, real-time
Bigtable | Wide-column | Petabyte+ | Time-series, analytics
Spanner | Global SQL | Petabyte+ | Global apps, strong consistency
Memorystore | In-memory | Up to 300 GB | Caching, session storage
Tier | vCPUs | Memory | Use Case
------------------|-------|---------|------------------
db-f1-micro | 1 | 0.6 GB | Dev/test
db-g1-small | 1 | 1.7 GB | Small apps
db-n1-standard-1 | 1 | 3.75 GB | Production start
db-n1-standard-4 | 4 | 15 GB | Medium workloads
db-n1-standard-16 | 16 | 60 GB | Large workloads
# Cloud SQL
gcloud sql instances create NAME --database-version=POSTGRES_15 --tier=TIER
gcloud sql instances list
gcloud sql databases create DB --instance=INSTANCE
gcloud sql backups create --instance=INSTANCE
gcloud sql instances failover INSTANCE # Manual failover test
# Firestore (via Firebase CLI)
firebase deploy --only firestore:rules
firebase deploy --only firestore:indexes
# Spanner
gcloud spanner instances create INSTANCE --config=CONFIG --nodes=3
gcloud spanner databases create DB --instance=INSTANCE
✅ DO: Enable automated backups for all production databases
✅ DO: Use read replicas to scale read traffic
✅ DO: Implement connection pooling for Cloud SQL
✅ DO: Design Bigtable row keys to avoid hotspots
✅ DO: Use Firestore security rules for access control
✅ DO: Test failover procedures regularly
❌ DON'T: Use Spanner for small databases (expensive, use Cloud SQL)
❌ DON'T: Create Bigtable row keys with sequential timestamps
❌ DON'T: Store large blobs in Firestore documents (max 1 MB)
❌ DON'T: Disable binary logging on Cloud SQL HA instances
❌ DON'T: Use Firestore for analytics queries (use BigQuery)
# ❌ NEVER: Store large files in Firestore documents
db.collection('users').document('user-123').set({
'name': 'Alice',
'profile_image': base64_encode(image_data) # 10MB image - FAILS!
})
# Firestore has 1MB document size limit
# ✅ CORRECT: Store files in Cloud Storage, reference in Firestore
from google.cloud import storage
storage_client = storage.Client()
bucket = storage_client.bucket('user-uploads')
blob = bucket.blob(f'profiles/{user_id}.jpg')
blob.upload_from_filename(image_path)
db.collection('users').document('user-123').set({
'name': 'Alice',
'profile_image_url': f'gs://user-uploads/profiles/{user_id}.jpg'
})
❌ Large documents in Firestore: Documents are limited to 1MB. Storing large files causes write failures. ✅ Correct approach: Store files in Cloud Storage, save references in Firestore documents.
# ❌ Don't: Use sequential row keys in Bigtable
row_key = f"{timestamp}#{sensor_id}".encode()
# Creates hotspot - all writes go to single tablet server
# ✅ Correct: Distribute writes with hash prefix or reverse timestamp
import hashlib
prefix = hashlib.md5(sensor_id.encode()).hexdigest()[:2]
reverse_ts = str(2**63 - int(timestamp * 1000))
row_key = f"{prefix}#{sensor_id}#{reverse_ts}".encode()
❌ Sequential Bigtable keys: Sequential row keys create hotspots where all writes go to a single tablet server. ✅ Better: Use hash prefixes or reverse timestamps to distribute writes across tablet servers.
# ❌ Don't: Use Spanner for small databases
gcloud spanner instances create small-app-db --config=regional-us-central1 --nodes=1
# Minimum cost: ~$650/month for single node!
# ✅ Correct: Use Cloud SQL for small/medium databases
gcloud sql instances create small-app-db \
--database-version=POSTGRES_15 \
--tier=db-n1-standard-1
# Cost: ~$50/month
❌ Spanner for small databases: Spanner has high minimum cost. Using it for small databases wastes money. ✅ Better: Use Cloud SQL for databases <1TB. Only use Spanner when you need global distribution or horizontal scaling.
# ❌ Don't: Perform analytics queries on Firestore
# Query all orders from last year (millions of documents)
orders = db.collection('orders').where('created_at', '>', last_year).stream()
total_revenue = sum(order.to_dict()['amount'] for order in orders)
# Slow, expensive, and hits quota limits
# ✅ Correct: Export to BigQuery for analytics
# Use Firestore-BigQuery extension or scheduled export
# Then query in BigQuery:
"""
SELECT SUM(amount) as total_revenue
FROM `project.dataset.orders`
WHERE created_at > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 YEAR)
"""
❌ Firestore for analytics: Firestore is optimized for transactional queries, not large-scale analytics. ✅ Better: Export Firestore data to BigQuery for analytics and reporting queries.
gcp-compute.md - Connecting Compute Engine instances to databasesgcp-storage.md - Database backups to Cloud Storagegcp-iam-security.md - Database user management and service account accessgcp-networking.md - Private IP configuration for database securityLast Updated: 2025-10-25 Format Version: 1.0 (Atomic)