Database Performance Tuning and Scaling Questions
System-level performance work beyond a single query: configuration and resource tuning, capacity planning, handling large data volumes, and scaling read and write throughput. Covers identifying bottlenecks, growth management, and the vertical-versus-horizontal scaling decision. Tests whether a candidate can keep a database healthy as load grows.
Frontend reports intermittent 5-10 second request latencies. Traces point to occasional long-running PostgreSQL queries. Describe how you would triage: identify problematic queries, detect lock or contention issues, diagnose IO stalls, and find indexing problems. Include the SQL commands and Postgres views you would use and low-risk mitigation steps you can apply without downtime.
Sample Answer
Start by confirming scope and reproducing: collect timestamps of slow frontend requests, correlate to backend service logs and DB host at that time (metrics: CPU, iowait, connections).
- Identify problematic queries
- Ensure pg_stat_statements is enabled. Query top offenders:
sql
-- top by total_time or avg_time SELECT query, calls, total_time, mean_time, rows FROM pg_stat_statements ORDER BY total_time DESC LIMIT 20; - Live long-running queries:
sql
SELECT pid, now()-query_start AS duration, state, query FROM pg_stat_activity WHERE state <> 'idle' ORDER BY duration DESC LIMIT 20;
Low-risk mitigation: set statement_timeout for the service or session to prevent extreme tails; cancel individual runaway queries (pg_cancel_backend(pid)).
- Detect locks/contention
- Find waiting backends and who holds locks:
sql
SELECT a.pid AS waiting_pid, a.query AS waiting_query, l.locktype, l.mode, l.granted, p.pid AS holder_pid, p.query AS holder_query FROM pg_locks l JOIN pg_stat_activity a ON a.pid = l.pid LEFT JOIN pg_locks hl ON hl.locktype = l.locktype AND hl.locktype IS NOT NULL AND hl.pid <> l.pid AND hl.database IS NOT NULL LEFT JOIN pg_stat_activity p ON p.pid = hl.pid WHERE NOT l.granted OR a.wait_event IS NOT NULL; - Simpler common view:
sql
SELECT waiting.pid AS waiting_pid, waiting.query AS waiting_query, blocking.pid AS blocking_pid, blocking.query AS blocking_query FROM pg_stat_activity waiting JOIN pg_locks wl ON wl.pid = waiting.pid AND NOT wl.granted JOIN pg_locks bl ON bl.locktype = wl.locktype AND bl.database IS NOT NULL AND bl.granted JOIN pg_stat_activity blocking ON blocking.pid = bl.pid;
Mitigation: identify and fix long transactions (VACUUM/updates left open). If safe, gently terminate holder (pg_terminate_backend) during maintenance windows; set lock_timeout to fail fast for future transactions.
- Diagnose IO stalls
- Correlate OS metrics: iostat -x 1, vmstat, dstat during spikes.
- From Postgres:
sql
-- table/index IO stats SELECT relname, heap_blks_read, heap_blks_hit, idx_blks_read, idx_blks_hit FROM pg_statio_user_tables; SELECT relname, indexrelname, idx_scan, idx_tup_read, idx_tup_fetch FROM pg_stat_user_indexes JOIN pg_index ON pg_stat_user_indexes.indexrelid = pg_index.indexrelid; - High heap_blks_read with low hit ratio => IO-bound; compare with bgwriter and checkpoints:
sql
SELECT * FROM pg_stat_bgwriter; SELECT * FROM pg_stat_database;
Mitigation without downtime: increase effective_cache_size, ensure OS page cache available (reduce cache pressure), tune shared_buffers/work_mem, move WAL or data to faster disks, or offload reads to a replica.
- Find indexing problems
- Look for sequential scans on large tables with high cost:
sql
EXPLAIN (ANALYZE, BUFFERS) SELECT ...; -- run for suspect slow query - Check index usage:
sql
SELECT relname, seq_scan, seq_tup_read, idx_scan, idx_tup_fetch FROM pg_stat_user_tables; - Missing or suboptimal index patterns: WHERE on functions, non-SARGable predicates, mismatched types.
Low-risk fixes:
- Create indexes CONCURRENTLY:
sql
CREATE INDEX CONCURRENTLY idx_name ON table(column); - Add supporting (covering) columns or partial indexes to reduce I/O.
- Run ANALYZE (or VACUUM ANALYZE) to refresh planner statistics:
sql
ANALYZE VERBOSE table; VACUUM (VERBOSE, ANALYZE) table; - If a query plan shows inefficient joins, add appropriate composite indexes or rewrite query to be sargable.
Workflow summary / runbook
- During spike: grab pg_stat_activity, pg_stat_statements, pg_locks, pg_statio_user_tables, OS iostat.
- If locks → identify and resolve long transactions; set lock_timeout/statement_timeout.
- If IO-bound → check hit ratios, iostat, consider caching/tuning or faster storage.
- If planner/index issue → EXPLAIN ANALYZE, update stats, create indexes CONCURRENTLY, and deploy query fixes.
- Longer-term: enable slow-query logging (log_min_duration_statement), monitor pg_stat_statements, set alerts for rising mean_time or queueing.
These steps let you triage, apply low-risk mitigations (timeouts, ANALYZE, concurrent indexes, cancel/terminate problematic sessions, offload to replicas) and prepare for safe maintenance changes with minimal downtime.
A compaction job in production corrupted several chunks, causing loss of 72 hours of series for a subset of tenants. Walk through the incident response steps you would take as the SRE lead, including immediate mitigation, communication, root cause analysis, remediation, and long-term changes to prevent recurrence.
Sample Answer
Situation: I'm the SRE lead and a compaction job in production corrupted chunks, causing ~72 hours of series loss for a subset of tenants.
Immediate mitigation (first 0–2 hours)
- Stop the compaction pipeline and job scheduler; block further compactions via feature-flag/kill-switch.
- Quarantine affected storage nodes and mark corrupted chunks read-only to prevent further writes/compaction.
- Take immediate immutable snapshots of current storage and service state (logs, offsets, compaction metadata).
- Assess scope: which tenants, time ranges, and volumes; estimate customer impact and affected SLOs.
Communication (first 30–60 minutes and ongoing)
- Open an incident channel (Slack/Jira) and notify stakeholders: Engineering, Product, Support, Legal if needed.
- Publish an initial customer-facing status with known impact, action being taken, expected next update time (e.g., every 2 hours).
- Provide a private briefing to executives with impact, mitigation, and ETA for root-cause investigation.
Containment and short-term recovery (2–8 hours)
- Attempt safe recovery paths in parallel:
- Restore corrupted chunks from the most recent healthy snapshots/backups for affected tenants/time ranges.
- If backups unavailable, reconstruct from replicas (remote read replicas or replica sets).
- Fallback: serve degraded read-only data and clearly label missing ranges to customers.
- Validate restored data with checksums and query-level sampling; run consistency checks.
Root cause analysis (8–72 hours)
- Preserve all artifacts, enable detailed logging, and reproduce safely in staging with sampled data.
- Follow blameless postmortem process:
- Map exact code/ops change that allowed corrupted chunks (bug in compaction algorithm, incorrect checksum handling, race condition, unsafe compaction on live chunks).
- Identify contributing factors: lack of pre-commit verification, insufficient unit/integration tests, missing canary or dry-run, poor deployment gating.
- Quantify timelines and decision points.
Remediation (72 hours)
- Roll back or patch compaction code to fix corruption vector; add atomic write guarantees and post-compaction integrity verification.
- Re-run compaction in staging with full dataset and automated validation before re-enabling in production.
- Restore lost data where possible; if irreversible, coordinate customer remediation (credits, support playbook).
Long-term prevention
- Implement automated post-compaction checksum verification and tombstone safety checks.
- Enforce canary deployments and dry-run compactions on shadow copies before production rollouts.
- Improve backup cadence and retention to cover worst-case recovery windows; verify backups via automated restores.
- Add SLO-aligned monitoring and alerting: sudden chunk loss, compaction error rates, checksum mismatches, and tenant-level anomaly detection.
- Create playbooks and runbooks for compaction incidents; run regular chaos/failure drills.
- Strengthen code review and testing: fuzzing compaction logic, property tests for invariants, and CI integration tests that simulate merges.
- Update incident postmortem with action items, owners, deadlines, and follow-up verification; share learnings company-wide.
Outcome & metrics
- Aim to restore >90% of affected data within SLA window; reduce recurrence risk by measurable signals: 100% compaction verification, successful canary rate, and recovery time objective improvements.
Explain how WAL (write-ahead log) retention and recovery interact with point-in-time recovery for a TSDB. How would you manage WAL retention for a system with heavy write rates and limited disk, while keeping an acceptable RPO?
Sample Answer
WAL retention and PITR interaction:
WAL (write-ahead log) is the sequential record of every write; PITR (point-in-time recovery) replays WAL from the nearest base snapshot to restore state at a chosen timestamp. Therefore WAL retention window directly limits the maximum PITR horizon: if WAL segments older than T are removed, you cannot restore earlier than snapshot_time + retained_WAL_window.
Managing WAL retention under heavy writes and limited disk while keeping acceptable RPO:
- Define RPO target in time (e.g., 5–15 minutes). That determines minimum WAL retention you need.
- Use tiered storage: stream WAL to remote durable storage (object store like S3) in near-real time and keep only a short local buffer. This offloads disk usage while preserving full PITR capability.
- Configure local retention low (e.g., 30–60 minutes) for fast local recovery and rely on remote WAL for longer restores.
- Compress and batch WAL uploads to reduce network and storage cost; use checksums and manifests to ensure integrity.
- Rotate snapshots frequently (incremental if supported) so recovery needs less WAL replay.
- Implement backpressure and admission control to prevent WAL flood from overwhelming disk: rate-limit writes, shed noncritical write-heavy workloads, or auto-scale ingestion nodes.
- Monitoring and automation: alert on WAL disk utilization, WAL upload lag, and recovery-testing probes. Automate pruning policy and emergency snapshot creation when disk crosses thresholds.
Trade-offs:
- Shorter local retention reduces fast recoverability but is fine if remote WAL ingestion is low-latency. Frequent snapshots increase CPU/I/O.
- Bandwidth/cost vs. local disk usage must be balanced.
Example: set local WAL retention = 1 hour, continuous upload to S3 (retention 30 days), hourly incremental snapshots. RPO for most incidents = 1–5 minutes using local + buffers; full PITR to any time in 30 days using remote WAL + snapshot replay.
You need to implement retention for two data classes: high-resolution metrics (keep 90 days at full resolution) and logs (keep 7 years, lower resolution). Describe how you'd implement retention and TTL policies across a TSDB and object storage tier, including any automation, lifecycle rules, and query implications for an SRE-run platform.
Sample Answer
Approach summary:
- Keep high-res metrics at full resolution for 90 days in the TSDB layer; beyond 90 days keep downsampled metrics for long-term trends (up to 7 years) in object storage.
- Keep logs full-fidelity for a short hot window (e.g., 30–90 days) in the logging cluster; archive compressed, indexed summaries to object storage for 7 years with lifecycle to colder tiers.
Implementation (metrics):
- TSDB: use Prometheus/Cortex/Thanos/Mimir as the ingest and short-term store with local/fast object-backed block storage. Configure retention: 90d WAL/blocks TTL.
- Downsampling: run automated rollup jobs (Thanos Sidecar/Prometheus recording rules or Mimir downsampling) that aggregate to 1m/5m/1h series at target resolutions (e.g., 1m for 1 year, 1h for 7 years).
- Long-term store: store compacted/downsampled blocks to object storage (S3/MinIO). Configure lifecycle: transition to Infrequent Access after 30d, to Glacier/Deep Archive after 365d as needed.
- TTL: TSDB blocks older than 90d are deleted automatically by the retention process; downsampling job writes long-term blocks with metadata TTL=7y in object store lifecycle rules.
Implementation (logs):
- Hot cluster: ingest logs into Loki/Elastic/Fluentd with retention of 30–90 days; store indexes for hot queries.
- Cold archive: periodic compaction jobs convert logs to compressed, columnar format (Parquet/NDJSON compressed), partitioned by time and minimal metadata index. Upload to object storage under a predictable prefix (logs/YYYY/MM/...).
- Lifecycle: S3 lifecycle rules: move to IA -> Glacier -> Deep Archive per policy until 7y then delete. Optionally use S3 Object Lock/Compliance if required.
- TTL: hot cluster index TTL triggers deletion; archive TTL enforced by lifecycle rules.
Automation & reliability:
- Implement these with IaC (Terraform) and pipeline jobs (GitOps + CI) that deploy retention policies and downsampling jobs. Use Kubernetes CronJobs or Airflow for rollups/compactions.
- Integrate with monitoring/alerting: instrument retention jobs, failed uploads, and object lifecycle events; create SLOs for retention job success and restore latency.
- Provide a “restore” automation: on-demand staged restore from cold (initiate S3 restore or rehydrate Glacier) with RBAC and cost-approval workflow.
Query implications & UX:
- Query router (Thanos Querier / custom proxy) should transparently read recent full-res blocks and long-term downsampled blocks. Expose a query parameter to request “raw” vs “downsampled” or time-range-aware resolution. Clearly document expected resolution beyond 90 days.
- For logs, queries within hot window return full detail; older queries hit archive, which may be slower and may return summarized results unless a restore is requested. Provide an async “fetch full logs” path with estimated restore time and cost.
- Add query rate-limits and cost warnings for ad-hoc deep historical queries.
Cost, security, and compliance:
- Use compression, schema (Parquet), partitioning, and selective indexing to reduce storage/cost. Automate bucket lifecycle and monitor egress costs.
- Ensure encryption-at-rest, IAM policies, and audit logging for restores and long-term access. Enforce retention compliance with immutable locks if needed.
Edge cases & testing:
- Test restore workflows and query routing under load. Validate data consistency after downsampling. Simulate failed uploads and ensure retry/alerting.
- Maintain provenance: keep metadata mapping original series/log IDs to archived objects for easier restores.
Outcome:
- Fast, cost-controlled hot-store for operational alerting and SRE troubleshooting (90d metrics, 30–90d logs), reliable long-term retention for compliance and trend analysis (7 years) with transparent query semantics and automated lifecycle management.
Explain how buffer pool sizing in an OLTP database affects read latency and IO amplification when the working set is slightly larger than available RAM. Using cache-miss curves and cost modeling, propose a method to choose buffer size that minimizes total cost (memory cost + IO cost), and describe experiments to measure the 'knee' in the hit-rate curve.
Sample Answer
Situation: You manage an OLTP service whose working set slightly exceeds RAM; you must choose buffer-pool size to minimize combined memory cost and IO cost while keeping read latency SLOs.
Concepts / intuition:
- Hit rate vs buffer size is a concave increasing curve with a "knee" where additional memory yields diminishing returns.
- When working set > RAM, miss rate (and IO) rises non-linearly; IO amplification occurs because misses trigger more reads, prefetches, checkpoint-induced churn, and filesystem cache interactions.
- Read latency = hit_latency * hit_rate + miss_latency * miss_rate; miss_latency includes IO queueing and amplification.
Cost model:
- Let M = buffer size, C_mem per GB (annualized), C_io per IO (or per MB read).
- Measure or estimate hit_rate(M) from cache-miss curve.
- TotalCost(M) = C_mem * M + C_io * IO_rate(M)
- IO_rate(M) = request_rate * (1 - hit_rate(M)) * avg_IOs_per_miss (captures amplification).
Method to choose M:
- Empirically measure hit_rate(M) for a range around expected knee (see experiments).
- Fit a smooth function (e.g., logistic or piecewise power-law) to hit_rate(M).
- Compute TotalCost(M) and find argmin M subject to latency/SLO constraints (e.g., enforce miss_rate ≤ threshold).
- If multiple minima due to discrete pricing, choose minimal M satisfying both cost and SLO.
Experiments to find the knee:
- Controlled workload replay (real traffic or representative trace) at production request rate.
- Sweep M from low to slightly > working set in small steps (e.g., 1–5% increments).
- For each M run long enough for steady-state (several minutes to an hour depending on workload) and measure: hit_rate, miss latency, IO/sec, IO bytes, queue depth, and CPU.
- Plot hit_rate vs M and derivative d(hit_rate)/dM; knee ≈ M where derivative drops below a threshold (e.g., 10% of max slope) or where second derivative changes sign.
- Validate amplification by measuring avg_IOs_per_miss (bytes read per miss) and latency tail percentiles.
- Run sensitivity tests: higher concurrency, background compaction/checkpoint activity, and different access skew to ensure knee stability.
Operationalize:
- Automate periodic sweeps in staging and trigger re-evaluation when workload or cost metrics change.
- Use the model to produce a recommended buffer setting and expected latency/cost trade-offs; route decisions through SLO governance.
Unlock Full Question Bank
Get access to all 47 Database Performance Tuning and Scaling interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.