<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Renga&#x27;s Learning - databases</title>
    <subtitle>Blog to add my leaning on tech contents</subtitle>
    <link rel="self" type="application/atom+xml" href="https://blog.rengaonline.in/tags/databases/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://blog.rengaonline.in"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-06-23T00:00:00+00:00</updated>
    <id>https://blog.rengaonline.in/tags/databases/atom.xml</id>
    <entry xml:lang="en">
        <title>Database Mind Map</title>
        <published>2026-06-23T00:00:00+00:00</published>
        <updated>2026-06-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Renganatha
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://blog.rengaonline.in/blog/database-mindmap/"/>
        <id>https://blog.rengaonline.in/blog/database-mindmap/</id>
        
        <content type="html" xml:base="https://blog.rengaonline.in/blog/database-mindmap/">&lt;h2 id=&quot;the-starting-point-sql-databases&quot;&gt;The Starting Point — SQL Databases&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;how-data-is-stored-on-disk&quot;&gt;How Data is Stored on Disk&lt;&#x2F;h3&gt;
&lt;p&gt;A relational database stores rows in &lt;strong&gt;pages&lt;&#x2F;strong&gt; (typically 8 KB each). Pages are grouped into files on disk — one per table. When you insert a row it lands on whatever page has free space. There is no inherent sort order; rows for the same user might be scattered across dozens of pages.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Disk (heap file)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┌──────────┬──────────┬──────────┐&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│  Page 1  │  Page 2  │  Page 3  │  ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ row 1    │ row 4    │ row 7    │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ row 2    │ row 5    │ row 8    │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ row 3    │ row 6    │ row 9    │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└──────────┴──────────┴──────────┘&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Without an index, every query reads every page — a full table scan. Fine for small tables, slow for millions of rows.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;indexes-fast-paths-to-your-data&quot;&gt;Indexes — Fast Paths to Your Data&lt;&#x2F;h3&gt;
&lt;p&gt;An index is a separate data structure that maps column values to the page(s) where matching rows live. The database reads the index first, then jumps directly to only the relevant pages.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;B-Tree Index&lt;&#x2F;strong&gt; — the default. Stores column values in sorted order in a tree. Good for equality (&lt;code&gt;=&lt;&#x2F;code&gt;) and range queries (&lt;code&gt;&amp;gt;&lt;&#x2F;code&gt;, &lt;code&gt;&amp;lt;&lt;&#x2F;code&gt;, &lt;code&gt;BETWEEN&lt;&#x2F;code&gt;). Most columns you index get a B-tree.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;CREATE INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; orders (user_id);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;-- lookup user_id = 42: walk the tree → find page pointers → fetch only those pages&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Hash Index&lt;&#x2F;strong&gt; — stores a hash of the column value. Faster than B-tree for exact equality lookups, but useless for ranges. Rarely used because B-tree is fast enough and more versatile.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Composite Index&lt;&#x2F;strong&gt; — a B-tree built on multiple columns together. Useful when queries filter on more than one column. Order matters: an index on &lt;code&gt;(user_id, created_at)&lt;&#x2F;code&gt; helps queries that filter by &lt;code&gt;user_id&lt;&#x2F;code&gt; or by &lt;code&gt;user_id + created_at&lt;&#x2F;code&gt;, but not queries that only filter by &lt;code&gt;created_at&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;CREATE INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; orders (user_id, created_at);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Clustered vs Non-Clustered&lt;&#x2F;strong&gt; — in MySQL (InnoDB) the primary key index physically orders the rows on disk (clustered). All other indexes are non-clustered — they point back to the primary key which points to the row. PostgreSQL has no clustered index by default; all indexes are pointers to the heap.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-trade-off&quot;&gt;The Trade-off&lt;&#x2F;h3&gt;
&lt;p&gt;Every index speeds up reads but slows down writes — on every insert or update the database must update both the table and all its indexes. Add indexes where query speed matters; avoid indexing columns that are rarely queried.&lt;&#x2F;p&gt;
&lt;p&gt;This setup works well on a single machine. Transactions are ACID. Joins span any table. The problem arrives when data grows beyond what one machine can hold.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;storage-engine-internals-b-tree-vs-lsm-tree&quot;&gt;Storage Engine Internals — B-Tree vs LSM Tree&lt;&#x2F;h2&gt;
&lt;p&gt;There are two dominant storage engine architectures in the database world. Everything else is a variation or hybrid.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;b-tree-engines&quot;&gt;B-Tree Engines&lt;&#x2F;h3&gt;
&lt;p&gt;A B-tree engine updates data &lt;strong&gt;in place&lt;&#x2F;strong&gt;. When you update a row, it finds the page on disk that holds it and writes the new value there. This is efficient for reads — the data is always at a known location — but writes are random I&#x2F;O. Every update must seek to the right page before writing.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Where you find B-tree engines:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Database&lt;&#x2F;th&gt;&lt;th&gt;Engine&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;PostgreSQL&lt;&#x2F;td&gt;&lt;td&gt;Heap access method + B-tree indexes&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;MySQL &#x2F; MariaDB&lt;&#x2F;td&gt;&lt;td&gt;InnoDB (B-tree, MVCC via undo logs)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;MongoDB&lt;&#x2F;td&gt;&lt;td&gt;WiredTiger (defaults to B-tree row format; also supports LSM)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SQLite&lt;&#x2F;td&gt;&lt;td&gt;Custom B-tree page format&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;PostgreSQL and InnoDB both implement &lt;strong&gt;MVCC (Multi-Version Concurrency Control)&lt;&#x2F;strong&gt; on top of B-tree storage. Instead of locking a row during an update, they write a new version and let concurrent readers see the old version until their transaction is done. PostgreSQL keeps old row versions in the heap itself (called &quot;dead tuples&quot;), while InnoDB keeps them in a separate undo log.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;lsm-tree-engines&quot;&gt;LSM-Tree Engines&lt;&#x2F;h3&gt;
&lt;p&gt;LSM stands for &lt;strong&gt;Log-Structured Merge-Tree&lt;&#x2F;strong&gt;. Instead of finding the right spot on disk and updating in place, an LSM engine always &lt;strong&gt;appends&lt;&#x2F;strong&gt;. Every write goes into an in-memory buffer first, then gets flushed to disk as an immutable file. This turns random I&#x2F;O into sequential I&#x2F;O, which is dramatically faster — especially on spinning disks, but also beneficial on SSDs because it reduces write amplification.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;The LSM write path:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Write ──► Commit Log (WAL on disk, for durability)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       ──► Memtable (in-memory sorted buffer)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              ▼ (when Memtable fills up, ~64–256 MB)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          SSTable (Sorted String Table, immutable file on disk)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              ▼ (background process)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          Compaction (merge multiple SSTables, remove deleted&#x2F;overwritten keys)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;The trade-off:&lt;&#x2F;strong&gt; Writes are extremely fast because they are sequential and in-memory first. Reads are slower because the same key might exist across multiple SSTables at different levels, and the engine must check them all (or use bloom filters to skip most of them).&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;mongodb-document-store-on-wiredtiger&quot;&gt;MongoDB — Document Store on WiredTiger&lt;&#x2F;h2&gt;
&lt;p&gt;MongoDB stores data as &lt;strong&gt;BSON documents&lt;&#x2F;strong&gt; (binary JSON) grouped into collections. There is no fixed schema — each document in a collection can have different fields.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;how-data-is-stored&quot;&gt;How Data is Stored&lt;&#x2F;h3&gt;
&lt;p&gt;Under the hood MongoDB uses &lt;strong&gt;WiredTiger&lt;&#x2F;strong&gt; as its storage engine. WiredTiger stores documents in a B-tree by default, with each document identified by &lt;code&gt;_id&lt;&#x2F;code&gt;. Documents are stored as-is — nested objects, arrays, and all — in a single record on disk.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Collection: orders&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;──────────────────────────────────────────&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{ _id: 1, user: &amp;quot;Alice&amp;quot;, items: [...], address: { city: &amp;quot;NY&amp;quot; } }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{ _id: 2, user: &amp;quot;Bob&amp;quot;,   items: [...] }                          ← no address field, that&amp;#39;s fine&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{ _id: 3, user: &amp;quot;Alice&amp;quot;, items: [...], tags: [&amp;quot;urgent&amp;quot;] }        ← extra field, also fine&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;No joins needed to load a full order — everything is in one document. When you do need to query across collections, MongoDB supports &lt;code&gt;$lookup&lt;&#x2F;code&gt; in the aggregation pipeline, though it is heavier than a SQL join and best avoided on hot paths.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;indexes&quot;&gt;Indexes&lt;&#x2F;h3&gt;
&lt;p&gt;MongoDB supports the same index types as SQL databases, all built as B-trees on top of WiredTiger:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single field&lt;&#x2F;strong&gt; — &lt;code&gt;{ user_id: 1 }&lt;&#x2F;code&gt;, works for equality and range.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Compound&lt;&#x2F;strong&gt; — &lt;code&gt;{ user_id: 1, created_at: -1 }&lt;&#x2F;code&gt;, same left-prefix rules as SQL composite indexes.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Multikey&lt;&#x2F;strong&gt; — automatically created when indexing an array field; one index entry per array element.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Text&lt;&#x2F;strong&gt; — inverted index for full-text search on string fields.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;TTL&lt;&#x2F;strong&gt; — special single-field index that auto-expires documents after a time threshold; used for session data, logs.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;transactions&quot;&gt;Transactions&lt;&#x2F;h3&gt;
&lt;p&gt;Single-document operations have always been atomic in MongoDB. Multi-document ACID transactions were added in MongoDB 4.0 — they work but carry a performance cost, so the recommended pattern is still to model data so most operations touch one document.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;clickhouse-columnar-storage-and-the-mergetree&quot;&gt;ClickHouse — Columnar Storage and the MergeTree&lt;&#x2F;h2&gt;
&lt;p&gt;ClickHouse is an OLAP (Online Analytical Processing) database built for analytical queries over billions of rows. It combines two key ideas: &lt;strong&gt;columnar storage&lt;&#x2F;strong&gt; and a &lt;strong&gt;MergeTree&lt;&#x2F;strong&gt; engine family that resembles LSM in structure.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;columnar-storage&quot;&gt;Columnar Storage&lt;&#x2F;h3&gt;
&lt;p&gt;Traditional (row-based) storage writes each row contiguously on disk. Columnar storage writes each column contiguously instead.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Row store:     [id=1, name=&amp;quot;Alice&amp;quot;, age=30] [id=2, name=&amp;quot;Bob&amp;quot;, age=25] ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Column store:  [id: 1, 2, 3, ...]  [name: &amp;quot;Alice&amp;quot;, &amp;quot;Bob&amp;quot;, ...]  [age: 30, 25, ...]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;When you run &lt;code&gt;SELECT avg(age) FROM users&lt;&#x2F;code&gt;, a column store reads only the &lt;code&gt;age&lt;&#x2F;code&gt; column — a tiny fraction of the total data. A row store reads every row, including &lt;code&gt;id&lt;&#x2F;code&gt; and &lt;code&gt;name&lt;&#x2F;code&gt; that are not needed.&lt;&#x2F;p&gt;
&lt;p&gt;Columnar layout also compresses far better because values in the same column share a data type and often share value ranges. ClickHouse applies &lt;strong&gt;Run-Length Encoding (RLE)&lt;&#x2F;strong&gt; and other codecs per column — a column that repeats &quot;US&quot; for 10 million rows compresses to almost nothing.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;mergetree&quot;&gt;MergeTree&lt;&#x2F;h3&gt;
&lt;p&gt;ClickHouse&#x27;s &lt;strong&gt;MergeTree&lt;&#x2F;strong&gt; engine is the write path that makes high-throughput ingestion practical:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Inserts land as data parts&lt;&#x2F;strong&gt; — each INSERT batch creates a new immutable directory of column files on disk.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Background merges&lt;&#x2F;strong&gt; — ClickHouse continuously merges small parts into larger parts in the background (analogous to SSTable compaction in Cassandra).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Sorted by primary key&lt;&#x2F;strong&gt; — each part is sorted by the table&#x27;s &lt;code&gt;ORDER BY&lt;&#x2F;code&gt; key, enabling fast primary-key range scans.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;INSERT batch 1 ──► part_1 (sorted column files)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;INSERT batch 2 ──► part_2 (sorted column files)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;INSERT batch 3 ──► part_3 (sorted column files)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                              │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                   Background merge&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                              │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                              ▼&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                   merged_part (larger sorted column files)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is why ClickHouse requires &lt;strong&gt;bulk inserts&lt;&#x2F;strong&gt; (thousands of rows at a time) rather than single-row inserts — each insert creates a part, and too many tiny parts slow down merges and queries until background merges catch up. The recommended pattern is to buffer inserts in the application and flush in batches.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;ReplicatedMergeTree&lt;&#x2F;strong&gt; adds multi-node replication using ZooKeeper (or ClickHouse Keeper) to coordinate which merges are happening and replicate parts across nodes.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;traditional-sql-at-scale-the-limits&quot;&gt;Traditional SQL at Scale — The Limits&lt;&#x2F;h2&gt;
&lt;p&gt;The typical growth path for a SQL application looks like this:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Single PostgreSQL node&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ▼ (read traffic grows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Primary + Read Replicas (streaming replication)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ▼ (write traffic grows, single primary bottleneck)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Table Partitioning&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ▼ (outgrows single machine)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Sharding&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;read-replicas&quot;&gt;Read Replicas&lt;&#x2F;h3&gt;
&lt;p&gt;PostgreSQL replication streams WAL records from the primary to standbys. Reads can be directed to standbys, but &lt;strong&gt;all writes must go to the primary&lt;&#x2F;strong&gt;. This solves read scaling but does nothing for write scaling — the primary is still a single bottleneck.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;table-partitioning&quot;&gt;Table Partitioning&lt;&#x2F;h3&gt;
&lt;p&gt;PostgreSQL supports declarative partitioning — a logical table split across multiple physical child tables, by range, list, or hash. Queries that filter on the partition key only scan relevant partitions (&quot;partition pruning&quot;). Partitioning lives within a single database instance; it does not distribute load across machines.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sharding&quot;&gt;Sharding&lt;&#x2F;h3&gt;
&lt;p&gt;Sharding splits the data across completely separate database instances — each &lt;strong&gt;shard&lt;&#x2F;strong&gt; holds a subset of rows. A routing layer directs queries to the correct shard based on a shard key (typically hashed user ID, tenant ID, etc.).&lt;&#x2F;p&gt;
&lt;p&gt;The benefits are real: write throughput scales linearly with the number of shards, and each shard can live on its own machine in any data center. The costs are severe:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No cross-shard joins&lt;&#x2F;strong&gt; — queries that touch rows on multiple shards require application-level data assembly.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;No cross-shard transactions&lt;&#x2F;strong&gt; — distributed transactions (2PC) are possible but expensive and rarely implemented well.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Re-sharding is painful&lt;&#x2F;strong&gt; — moving data when you need to add shards requires careful migration and downtime planning.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Schema changes are multiplied&lt;&#x2F;strong&gt; — you apply migrations to every shard.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Tools like Citus (PostgreSQL extension), Vitess (MySQL), and PlanetScale (MySQL) exist precisely to make sharding less painful, but the fundamental constraints remain.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;nosql-scaling-as-a-first-principle&quot;&gt;NoSQL — Scaling as a First Principle&lt;&#x2F;h2&gt;
&lt;p&gt;NoSQL databases were built with horizontal scaling as a design constraint, not an afterthought. The trade-offs they accepted to achieve this:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No joins&lt;&#x2F;strong&gt; — data must be modeled for the queries you will run, not normalized into tables.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Flexible schema (document stores)&lt;&#x2F;strong&gt; — each document can have different fields; schema enforcement is the application&#x27;s responsibility.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Eventual consistency as the default&lt;&#x2F;strong&gt; — most NoSQL databases favor availability over strong consistency during a partition.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;document-stores-mongodb&quot;&gt;Document Stores (MongoDB)&lt;&#x2F;h3&gt;
&lt;p&gt;Documents (JSON-like objects) are stored together, so a read that previously required joining &lt;code&gt;users&lt;&#x2F;code&gt;, &lt;code&gt;orders&lt;&#x2F;code&gt;, and &lt;code&gt;addresses&lt;&#x2F;code&gt; now reads a single document. No join needed — but you can no longer query across embedded data the way SQL can.&lt;&#x2F;p&gt;
&lt;p&gt;MongoDB uses WiredTiger under the hood and supports single-document ACID transactions. Multi-document transactions exist since MongoDB 4.0 but carry a performance cost.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;key-value-stores-redis-dynamodb&quot;&gt;Key-Value Stores (Redis, DynamoDB)&lt;&#x2F;h3&gt;
&lt;p&gt;The simplest model — a distributed hash map. Every access is by a primary key. Redis keeps all data in memory with optional persistence (RDB snapshots and AOF log). Sub-millisecond latency is the selling point.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;DynamoDB&lt;&#x2F;strong&gt; is AWS&#x27;s managed key-value &#x2F; document store. It partitions data automatically using consistent hashing, and each partition holds up to 10 GB and handles up to 3,000 read capacity units or 1,000 write capacity units. When a partition exceeds these limits, DynamoDB splits it automatically — the operational burden of repartitioning falls on AWS.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wide-column-stores-cassandra-hbase&quot;&gt;Wide-Column Stores (Cassandra, HBase)&lt;&#x2F;h3&gt;
&lt;p&gt;Wide-column databases store data as rows keyed by a partition key, with each row holding a variable number of columns. They have a schema (unlike document stores) but no foreign keys and no joins. Cassandra distributes rows across nodes using consistent hashing on the partition key — given a partition key, Cassandra knows which node(s) own it with no routing table lookup.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;achieving-high-availability-replication-in-nosql&quot;&gt;Achieving High Availability — Replication in NoSQL&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;cassandra-s-replication-model&quot;&gt;Cassandra&#x27;s Replication Model&lt;&#x2F;h3&gt;
&lt;p&gt;Every row in Cassandra is replicated across &lt;strong&gt;N&lt;&#x2F;strong&gt; nodes (configured per keyspace as the &lt;strong&gt;replication factor&lt;&#x2F;strong&gt;). With &lt;code&gt;replication_factor=3&lt;&#x2F;code&gt;, three nodes hold a copy of each row. Which nodes? The token ring assigns each node a range of the hash space; a row&#x27;s partition key is hashed and placed at the appropriate position on the ring, then copied to the next N-1 nodes clockwise.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Consistency is tunable per query:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Consistency Level&lt;&#x2F;th&gt;&lt;th&gt;Writes&lt;&#x2F;th&gt;&lt;th&gt;Reads&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ONE&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;1 replica acknowledgement&lt;&#x2F;td&gt;&lt;td&gt;Read from 1 replica&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;QUORUM&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Majority (N&#x2F;2 + 1) must acknowledge&lt;&#x2F;td&gt;&lt;td&gt;Read from majority&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ALL&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;All replicas must acknowledge&lt;&#x2F;td&gt;&lt;td&gt;Read from all replicas&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;LOCAL_QUORUM&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Quorum within local data center only&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;QUORUM&lt;&#x2F;code&gt; reads + &lt;code&gt;QUORUM&lt;&#x2F;code&gt; writes gives you &lt;strong&gt;strong consistency&lt;&#x2F;strong&gt; even with eventual replication, because the quorum sets overlap and always include at least one node with the latest data. &lt;code&gt;ONE&lt;&#x2F;code&gt; on both sides gives maximum availability but may return stale reads.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dynamodb-s-high-availability&quot;&gt;DynamoDB&#x27;s High Availability&lt;&#x2F;h3&gt;
&lt;p&gt;DynamoDB replicates each partition across three Availability Zones within a region. Writes are committed synchronously to a majority (2 of 3) before acknowledging. Global Tables extend this across multiple AWS regions, with multi-active writes — every region accepts writes and replicates asynchronously to others (last-writer-wins conflict resolution).&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;transactions-and-isolation-acid-vs-base&quot;&gt;Transactions and Isolation — ACID vs BASE&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;acid-relational-databases&quot;&gt;ACID (Relational Databases)&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;Atomicity&lt;&#x2F;strong&gt; — a transaction either commits all its changes or rolls all of them back. There is no partial success.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Consistency&lt;&#x2F;strong&gt; — the database transitions from one valid state to another. Constraints (foreign keys, unique indexes, CHECK constraints) are enforced.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Isolation&lt;&#x2F;strong&gt; — concurrent transactions appear to execute serially. The degree of isolation is configurable:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Isolation Level&lt;&#x2F;th&gt;&lt;th&gt;Dirty Reads&lt;&#x2F;th&gt;&lt;th&gt;Non-Repeatable Reads&lt;&#x2F;th&gt;&lt;th&gt;Phantom Reads&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Read Uncommitted&lt;&#x2F;td&gt;&lt;td&gt;Possible&lt;&#x2F;td&gt;&lt;td&gt;Possible&lt;&#x2F;td&gt;&lt;td&gt;Possible&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Read Committed&lt;&#x2F;td&gt;&lt;td&gt;Prevented&lt;&#x2F;td&gt;&lt;td&gt;Possible&lt;&#x2F;td&gt;&lt;td&gt;Possible&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Repeatable Read&lt;&#x2F;td&gt;&lt;td&gt;Prevented&lt;&#x2F;td&gt;&lt;td&gt;Prevented&lt;&#x2F;td&gt;&lt;td&gt;Possible&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Serializable&lt;&#x2F;td&gt;&lt;td&gt;Prevented&lt;&#x2F;td&gt;&lt;td&gt;Prevented&lt;&#x2F;td&gt;&lt;td&gt;Prevented&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;PostgreSQL&#x27;s default is &lt;strong&gt;Read Committed&lt;&#x2F;strong&gt; — you never see uncommitted data from other transactions, but a second read within your transaction may see rows that another transaction committed between your two reads. PostgreSQL&#x27;s &lt;strong&gt;Serializable&lt;&#x2F;strong&gt; level uses Serializable Snapshot Isolation (SSI), which detects conflicting transaction patterns and aborts one of them rather than using table-level locks — making it practical for production use.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Durability&lt;&#x2F;strong&gt; — a committed transaction survives crashes. Both PostgreSQL and InnoDB achieve this via &lt;strong&gt;Write-Ahead Logging (WAL)&lt;&#x2F;strong&gt;: changes are written to the WAL on disk before they are applied to data pages. On crash recovery, the WAL is replayed.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;base-nosql-databases&quot;&gt;BASE (NoSQL Databases)&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;Basically Available&lt;&#x2F;strong&gt; — the system always returns a response, even if it is stale or an error during a partition, rather than waiting until the network heals.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Soft State&lt;&#x2F;strong&gt; — the system&#x27;s state may change over time even without new input, as replication catches up.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Eventually Consistent&lt;&#x2F;strong&gt; — given no new updates, all replicas will converge to the same value &lt;em&gt;eventually&lt;&#x2F;em&gt;. The window can be milliseconds to seconds under normal conditions.&lt;&#x2F;p&gt;
&lt;p&gt;Cassandra is the canonical BASE database. By default, a write is acknowledged after one replica receives it. Other replicas catch up asynchronously. A subsequent read from a different replica may return the old value until replication completes. &lt;strong&gt;Hinted handoff&lt;&#x2F;strong&gt; and &lt;strong&gt;read repair&lt;&#x2F;strong&gt; are the two mechanisms Cassandra uses to drive convergence:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hinted handoff&lt;&#x2F;strong&gt; — if a replica is temporarily down when a write occurs, the coordinator stores a &quot;hint&quot; and replays the write when the replica comes back.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Read repair&lt;&#x2F;strong&gt; — when a read detects divergence across replicas (by comparing digests), it writes the latest value back to stale replicas in the background.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;cap-theorem&quot;&gt;CAP Theorem&lt;&#x2F;h3&gt;
&lt;p&gt;A distributed system can only guarantee two of three properties simultaneously:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Consistency&lt;&#x2F;strong&gt; — every read returns the most recent write.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Availability&lt;&#x2F;strong&gt; — every request receives a response (not an error).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Partition Tolerance&lt;&#x2F;strong&gt; — the system continues operating when network partitions occur.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Since network partitions are a physical reality that cannot be engineered away, the real choice is between &lt;strong&gt;CP&lt;&#x2F;strong&gt; and &lt;strong&gt;AP&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;System&lt;&#x2F;th&gt;&lt;th&gt;Choice&lt;&#x2F;th&gt;&lt;th&gt;Behavior During Partition&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;PostgreSQL (single node)&lt;&#x2F;td&gt;&lt;td&gt;CA&lt;&#x2F;td&gt;&lt;td&gt;Partition impossible by definition&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CockroachDB &#x2F; Spanner&lt;&#x2F;td&gt;&lt;td&gt;CP&lt;&#x2F;td&gt;&lt;td&gt;Some requests may block or fail to preserve consistency&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Cassandra (ONE consistency)&lt;&#x2F;td&gt;&lt;td&gt;AP&lt;&#x2F;td&gt;&lt;td&gt;Always responds, may return stale data&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Cassandra (QUORUM)&lt;&#x2F;td&gt;&lt;td&gt;CP&lt;&#x2F;td&gt;&lt;td&gt;May reject writes if quorum unavailable&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DynamoDB (eventual)&lt;&#x2F;td&gt;&lt;td&gt;AP&lt;&#x2F;td&gt;&lt;td&gt;Always responds&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DynamoDB (strong)&lt;&#x2F;td&gt;&lt;td&gt;CP&lt;&#x2F;td&gt;&lt;td&gt;May fail reads if majority unavailable&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;The key insight is that most NoSQL databases let you &lt;strong&gt;dial the CAP trade-off per query&lt;&#x2F;strong&gt; via consistency levels, rather than making it a fixed architectural decision.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;summary-choosing-a-database&quot;&gt;Summary — Choosing a Database&lt;&#x2F;h2&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;What is your primary concern?&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Structured data + complex queries + ACID?&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    └─► PostgreSQL (single node or read replicas)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         └─► Citus &#x2F; Vitess &#x2F; PlanetScale if you need sharding&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;High write throughput + time-series &#x2F; wide rows + geographic distribution?&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    └─► Cassandra&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Analytical queries over billions of rows?&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    └─► ClickHouse (OLAP) or BigQuery &#x2F; Redshift (managed)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Sub-millisecond latency + caching?&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    └─► Redis&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Managed, serverless, infinite scale on AWS?&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    └─► DynamoDB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Document model + flexible schema?&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    └─► MongoDB (with the trade-off that joins become application logic)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;No database is universally best. The decision is always about which trade-offs — write amplification vs read latency, strong consistency vs availability, schema rigidity vs flexibility — align with your workload&#x27;s actual access patterns.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;sql-can-scale-too-so-why-does-nosql-exist&quot;&gt;SQL Can Scale Too — So Why Does NoSQL Exist?&lt;&#x2F;h2&gt;
&lt;p&gt;A common misconception is that NoSQL exists purely because SQL cannot scale. That was the rallying cry in 2009, but it was never the full story — and today it is largely outdated.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;SQL scales just fine&lt;&#x2F;strong&gt; with the right tooling:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Read replicas&lt;&#x2F;strong&gt; — distribute read traffic across standbys.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Table partitioning&lt;&#x2F;strong&gt; — split large tables across physical partitions, pruned automatically at query time.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Sharding&lt;&#x2F;strong&gt; — Citus (PostgreSQL), Vitess (MySQL), PlanetScale distribute writes across machines.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;NewSQL&lt;&#x2F;strong&gt; — CockroachDB and Google Spanner give you full SQL with global horizontal scaling built in.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So if SQL can scale, why choose MongoDB or any other NoSQL database? The real reasons have always been about &lt;strong&gt;developer experience and data model&lt;&#x2F;strong&gt;, not scaling.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-nosql-actually-wins-on&quot;&gt;What NoSQL Actually Wins On&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;No schema&lt;&#x2F;strong&gt; — you can store a document without defining its shape upfront. Add a new field to one document and nothing breaks. No &lt;code&gt;ALTER TABLE&lt;&#x2F;code&gt;, no migration script, no downtime.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;JSON-like documents&lt;&#x2F;strong&gt; — a user with nested addresses, preferences, and order history maps directly to one document. No joins needed to reassemble it. What you store is what you get back.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Developer flexibility&lt;&#x2F;strong&gt; — spin up a collection and start inserting. No schema file, no ORM, no migration workflow. For fast-moving teams or evolving data shapes this removes real friction.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-honest-trade-off&quot;&gt;The Honest Trade-off&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;SQL&lt;&#x2F;th&gt;&lt;th&gt;NoSQL (Document)&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Schema&lt;&#x2F;td&gt;&lt;td&gt;Strict, enforced&lt;&#x2F;td&gt;&lt;td&gt;Flexible, per document&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Joins&lt;&#x2F;td&gt;&lt;td&gt;Native, expressive&lt;&#x2F;td&gt;&lt;td&gt;Application-side&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Scaling&lt;&#x2F;td&gt;&lt;td&gt;Possible, requires tooling&lt;&#x2F;td&gt;&lt;td&gt;Built in from day one&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Transactions&lt;&#x2F;td&gt;&lt;td&gt;Full ACID&lt;&#x2F;td&gt;&lt;td&gt;Improving (MongoDB 4.0+)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Data model&lt;&#x2F;td&gt;&lt;td&gt;Normalized tables&lt;&#x2F;td&gt;&lt;td&gt;Denormalized documents&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;The lines have also blurred: PostgreSQL supports &lt;code&gt;jsonb&lt;&#x2F;code&gt; for flexible document storage with full SQL on top. MongoDB added ACID transactions and schema validation. Neither camp is a pure archetype anymore.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Choose SQL&lt;&#x2F;strong&gt; when your data is relational, your queries are complex, and you need strong consistency. &lt;strong&gt;Choose a document store&lt;&#x2F;strong&gt; when your data is naturally document-shaped, your schema evolves rapidly, and developer speed matters more than join flexibility.&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
