Skip to main content

Databases

One-line summary: Choosing the right database — and modeling data correctly — is the single biggest lever on a system's scalability, consistency, and cost.


🧩 Core Concepts​

A database stores, organizes, and retrieves data reliably. The two broad families are relational (SQL) and non-relational (NoSQL). Picking between them (and the right sub-type) is a foundational system-design decision that drives your consistency model, query flexibility, and how you shard and replicate later.

flowchart TD
DB[Databases] --> SQL[Relational / SQL]
DB --> NoSQL[Non-Relational / NoSQL]
SQL --> S1[PostgreSQL, MySQL,<br/>SQL Server, Oracle]
NoSQL --> N1[Document<br/>MongoDB, Couchbase]
NoSQL --> N2[Key-Value<br/>Redis, DynamoDB]
NoSQL --> N3[Column-Family<br/>Cassandra, HBase]
NoSQL --> N4[Graph<br/>Neo4j, Neptune]

🗃️ SQL vs NoSQL​

Relational (SQL)​

Data lives in tables (rows + columns) with a fixed schema and enforced relationships (foreign keys). Queried with SQL, and typically ACID-compliant.

Strengths: structured data, complex joins, strong consistency, mature tooling. Weaknesses: rigid schema, harder to scale writes horizontally.

Non-Relational (NoSQL)​

A family of stores optimized for flexible schemas, horizontal scale, and specific access patterns. Usually favor availability + partition tolerance (see CAP Theorem) with eventual consistency (see Consistency Models).

TypeData ModelBest ForExamples
DocumentJSON/BSON documentsSemi-structured data, evolving schemas, catalogsMongoDB, Couchbase
Key-Valuekey → value mapCaching, sessions, high-throughput lookupsRedis, DynamoDB
Column-FamilySparse wide rows by columnTime-series, write-heavy, huge datasetsCassandra, HBase
GraphNodes + edgesHighly connected data, relationshipsNeo4j, Neptune

Head-to-Head​

DimensionSQLNoSQL
SchemaFixed, enforcedFlexible / schema-less
ScalingPrimarily vertical (harder horizontal)Built for horizontal (sharding)
ConsistencyStrong (ACID)Tunable, often eventual (BASE)
JoinsFirst-class, powerfulLimited / denormalized
Query languageStandard SQLVaries per engine
Best fitTransactions, reporting, relationsScale, flexible data, specific access patterns

🎯 When to Use Each​

Choose SQL when:

  • You need transactions and strong consistency (payments, inventory, banking).
  • Data is highly relational with complex queries and joins.
  • The schema is well-understood and stable.

Choose NoSQL when:

  • You need massive horizontal scale and high write throughput.
  • The schema is flexible or rapidly evolving.
  • Access patterns are known and simple (key lookups, document fetches).
  • Eventual consistency is acceptable.

💡 Polyglot persistence: real systems often mix both — e.g., PostgreSQL for orders, Redis for sessions, Cassandra for event logs.


🔍 Indexing​

An index is an auxiliary data structure that speeds up reads at the cost of extra storage and slower writes (the index must be updated on every write).

flowchart LR
Q[Query: WHERE email = ?] --> I{Index on email?}
I -- Yes --> F[O log n lookup]
I -- No --> S[O n full table scan]
Index TypeStructureGreat ForWeak For
B-TreeBalanced sorted treeRange queries, sorting, >, <, BETWEEN, prefix LIKENothing major — the default
HashHash tableExact-match equality (=)Range queries (not supported)
  • B-Tree is the default in most engines: O(log n) lookups and supports ordered scans.
  • Hash indexes give O(1) average equality lookups but cannot serve ranges.
  • Composite indexes cover multi-column queries (order matters: leftmost-prefix rule).
  • Over-indexing hurts write performance and wastes storage — index deliberately.

🧱 Normalization vs Denormalization​

ApproachIdeaProsCons
NormalizationSplit data into related tables, no redundancy (3NF)No duplication, consistent updates, less storageMore joins, slower reads
DenormalizationDuplicate data to avoid joinsFast reads, fewer joinsRedundancy, harder/costly writes, risk of drift
  • Normalize for write-heavy, consistency-critical OLTP systems.
  • Denormalize for read-heavy systems and NoSQL, trading storage & write cost for read speed. Pairs well with caching.

⚖️ ACID vs BASE​

flowchart LR
subgraph ACID [ACID - SQL]
A1[Atomicity]
A2[Consistency]
A3[Isolation]
A4[Durability]
end
subgraph BASE [BASE - NoSQL]
B1[Basically Available]
B2[Soft state]
B3[Eventually consistent]
end

ACID — guarantees for reliable transactions:

  • Atomicity — all-or-nothing; a transaction fully completes or fully rolls back.
  • Consistency — moves the DB from one valid state to another (constraints hold).
  • Isolation — concurrent transactions don't interfere (via isolation levels).
  • Durability — committed data survives crashes.

BASE — the relaxed philosophy for scale (see CAP Theorem):

  • Basically Available — the system always responds, even if degraded.
  • Soft state — state may change over time without input.
  • Eventually consistent — replicas converge given enough time.

🔄 Transactions & Isolation Levels​

A transaction groups multiple operations into one atomic unit. Isolation levels trade consistency for concurrency:

Isolation LevelDirty ReadNon-Repeatable ReadPhantom Read
Read Uncommitted✅ possible✅ possible✅ possible
Read Committed❌✅ possible✅ possible
Repeatable Read❌❌✅ possible
Serializable❌❌❌

Higher isolation = more correctness but more locking/contention and lower throughput.


🔌 Connection Pooling​

Opening a DB connection is expensive (TCP + auth handshake). A connection pool keeps a set of reusable open connections that application threads borrow and return.

flowchart LR
App1[App Thread 1] --> Pool[(Connection Pool)]
App2[App Thread 2] --> Pool
App3[App Thread 3] --> Pool
Pool --> DB[(Database)]
  • Reduces latency — no per-request handshake.
  • Caps concurrency — protects the DB from connection exhaustion.
  • Tune pool size to match DB limits and workload (e.g., HikariCP, PgBouncer).

🧠 Trade-offs / When to Use​

  • SQL → correctness, relations, transactions; scale vertically or via read replicas.
  • NoSQL → scale and flexibility; accept eventual consistency and denormalization.
  • Indexing → speeds reads, slows writes — index for your real query patterns.
  • Normalization → write integrity; denormalization → read speed.
  • When one DB isn't enough, combine sharding (scale writes) + replication (scale reads/availability) + caching (reduce load).

Interview Questions​

  • Given a mixed workload (writes for orders, reads for analytics), how would you design the datastore layer?
  • When would you denormalize data and what trade-offs would you accept?
  • How do you pick indexing strategies for a new table with evolving query patterns?

Production Checklist​

  • Baseline query performance and index usage; remove unused indexes to speed writes
  • Monitor slow queries, lock contention, and connection pool saturation
  • Establish backup/restore and run regular recovery drills
  • Enforce schema migrations with versioning and rollout strategies (blue/green migrations)

Testing & Monitoring​

  • Run realistic load tests that include OLTP and analytical workloads where applicable
  • Test failover of primary instances and validate replica promotions
  • Validate connection pool tuning under concurrency and chaos tests that drop DB nodes
  • Sharding — partition data across nodes to scale writes
  • Replication — copy data for availability and read scaling
  • Caching — reduce database load with fast in-memory reads
  • CAP Theorem — consistency vs availability under partitions
  • Consistency Models — strong vs eventual consistency
  • Scalability — vertical vs horizontal scaling

← Back to System Design · © sparshjaswal