필사 모드: Database Complete Guide — Internals, Indexes, Query Planner, Partitioning, Vector DB (Season 2 Ep 13, 2025)
EnglishIntro — Why You Have to Know Databases Deeply
Counterexamples to "surely knowing SQL is enough?":
- P99 latency spikes → undebuggable without understanding indexes and the planner
- Transaction isolation issues → unreproducible and unfixable without knowing isolation levels
- Exploding storage costs → partitioning and compression strategy required
- Choosing a distributed DB → not knowing the internals leads to the wrong choice
- Vector search → embedding store design required
For a senior engineer in 2025, deep database knowledge is required literacy.
Part 1 — Two Philosophies of Storage Engines
1.1 B-Tree (Balanced Tree)
Read-optimized. The default in traditional RDBMSs (PostgreSQL, MySQL InnoDB).
[50]
/ \
[20] [80]
/ \ / \
[10][30][70][90]
Characteristics:
- Balanced binary tree (in practice the fanout is in the hundreds)
- O(log N) lookup
- In-place update → low write amplification
- Widely used as an index structure
Downside: many random writes cause splits plus I/O.
1.2 LSM-Tree (Log-Structured Merge)
Write-optimized. RocksDB, Cassandra, ScyllaDB, LevelDB, ClickHouse.
Memtable (RAM)
↓ flush
L0 SSTables (disk)
↓ compact
L1 SSTables
↓ compact
L2 SSTables ...
Characteristics:
- Every write is append-only → sequential I/O
- Compaction tidies things up afterwards
- Extremely fast writes
- Spends space and CPU on compaction
Downside: reads have to search several layers → mitigated with Bloom filters and friends.
1.3 How to Choose
| Workload | Preference |
|---|---|
| OLTP (reads ~ writes) | B-Tree (Postgres, MySQL) |
| Write-heavy (logs, messages) | LSM (Cassandra, RocksDB) |
| Time-series | LSM or columnar (InfluxDB, TimescaleDB) |
| Analytics | Columnar (ClickHouse, DuckDB) |
1.4 2024-2025 Trend: B-Tree and LSM Converge
- Aurora, Neon: storage layer split out (object storage such as S3 plus a local cache)
- TigerBeetle: finance-specific, LSM plus determinism
- FoundationDB: layered, ACID plus key-value
- Postgres Incremental Materialized View: read-optimal plus real-time
Part 2 — Indexes in Depth
2.1 Eight Kinds of Index
| Kind | Purpose |
|---|---|
| B-Tree | Range queries (most common) |
| Hash | Equality only |
| GIN | Full-text search, JSONB, Array |
| GiST | Geo and geometry, range types |
| SP-GiST | Spatial data |
| BRIN | Approximate index for huge tables |
| Bloom | Multi-column OR search |
| Covering (INCLUDE) | Extra columns in the index (index-only scan) |
2.2 Five Principles of Index Design
- Queries come first: design from the queries you actually run
- Selectivity: highly selective columns first
- Composite index order: start with the filter you use most often
- Cover with INCLUDE: avoid touching the table
- Drop indexes you do not use: they degrade write performance
2.3 Top 10 Reasons an Index Is Not Used
- A function is applied:
WHERE LOWER(email) = ...→ needs an expression index - Implicit type conversion:
WHERE id = '123'(id is an int) - Leading wildcard:
LIKE '%foo'→ full scan - OR conditions: the optimizer may give up → use UNION
- Not equals (
!=): mostly does not use an index - NULL comparison: Postgres can index
IS NULL, but design with care - The data is small: the planner prefers a sequential scan
- Wrong composite index order:
WHERE b = ?against an(a, b)index - Stale statistics: ANALYZE needed
- Index bloat: REINDEX needed
Part 3 — The Query Planner and EXPLAIN
3.1 What the Planner Does
It converts SQL (declarative) into an execution plan (procedural). Cost-based optimization (CBO).
3.2 Reading PostgreSQL EXPLAIN
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT u.name, COUNT(o.id)
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.country = 'KR'
GROUP BY u.name;
Example output:
HashAggregate (cost=1234..5678 rows=100)
Group Key: u.name
-> Hash Join (cost=500..1200 rows=10000)
Hash Cond: o.user_id = u.id
-> Seq Scan on orders o (cost=0..800 rows=100000)
-> Hash (cost=300..300 rows=500)
-> Index Scan on users_country_idx (cost=0..300 rows=500)
How to read it:
- From the inside outward
cost=startup..totalrows: estimated row count (compare against the actual figure from ANALYZE)actual time: real elapsed timeBuffers: memory and disk access
3.3 Important Node Types
| Node | Meaning |
|---|---|
| Seq Scan | Full scan (fine for small tables or low selectivity) |
| Index Scan | Uses an index |
| Index Only Scan | Answered from the index alone (fast) |
| Bitmap Heap Scan | Find rows via the index, then hit the table in bulk |
| Nested Loop Join | Small outer plus an indexed inner |
| Hash Join | Builds a hash table and matches against it |
| Merge Join | When both sides are already sorted |
| Hash Aggregate | GROUP BY |
| Sort | ORDER BY |
3.4 Postgres Has No Plan Hints
MySQL and Oracle have hints. In Postgres you steer the planner by tuning ANALYZE, statistics targets, and random_page_cost.
Part 4 — Transaction Isolation
4.1 ACID Restated
- Atomicity: all or nothing
- Consistency: no constraint violations
- Isolation: concurrent execution behaves like serial execution
- Durability: once committed, it stays
4.2 Four Isolation Levels (SQL-92)
| Level | Prevents |
|---|---|
| Read Uncommitted | (prevents almost nothing) |
| Read Committed | Dirty Read |
| Repeatable Read | + Non-repeatable Read |
| Serializable | + Phantom |
4.3 Default Isolation Level by DB
| DB | Default |
|---|---|
| PostgreSQL | Read Committed |
| MySQL InnoDB | Repeatable Read |
| Oracle | Read Committed (Serializable available) |
| SQL Server | Read Committed |
4.4 Snapshot Isolation vs Serializable
- Snapshot Isolation (SI): each transaction sees a snapshot taken when it started
- Downside: write skew is possible
- Serializable (MVCC SSI): SI plus conflict detection → abort
- Postgres offers SSI through
SERIALIZABLE
- Postgres offers SSI through
4.5 A Write Skew Example
At least one of two doctors must be on call:
-- T1: reads on_call, sees 2 → OK
-- T2: concurrently reads on_call, sees 2 → OK
-- Both flip their own status to off-call
-- Commit → 0 doctors on call. Constraint violated
Only Serializable detects this.
4.6 Deadlock
Two or more transactions wait on each other's locks. The DB detects it → sacrifices one (abort).
Prevention:
- Consistent lock ordering (ascending ID)
SELECT FOR UPDATE NOWAIT- Retry logic (idempotency plus backoff)
Part 5 — MVCC (Multi-Version Concurrency Control)
5.1 MVCC Basics
"Writers do not block readers, and readers do not block writers."
Multiple versions of each row are kept, and each transaction reads the version belonging to its own snapshot.
5.2 MVCC in Postgres
- Tuple header: xmin (creating transaction), xmax (deleting transaction)
- Vacuum: cleans up dead tuples. Mandatory.
- autovacuum: automatic, but it still needs tuning
5.3 The Vacuum Bloat Problem
- A long-running transaction blocks autovacuum
- Table and index bloat → degraded performance
- Fix: pg_repack, VACUUM FULL, regular monitoring
5.4 MySQL InnoDB MVCC
- Previous versions live in the undo log
- The purge thread cleans them up
- Clustered index (physically ordered by primary key)
Part 6 — Partitioning and Sharding
6.1 Vertical vs Horizontal
- Vertical: split by column (almost never used)
- Horizontal: split by row. Partitioning and sharding.
6.2 Partitioning (Inside a Single DB)
Native in Postgres (10+):
CREATE TABLE events (
id bigserial,
event_time timestamptz,
data jsonb
) PARTITION BY RANGE (event_time);
CREATE TABLE events_2025_01 PARTITION OF events
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
Upside: partition pruning, easy maintenance (DROP old partition). Limit: still one DB. Bounded by a single server's capacity.
6.3 Sharding (Multiple DBs)
Spread the data across several servers.
Choosing a shard key:
- High cardinality
- Account for the access pattern
- Easy to rebalance
- Avoid hot shards
Sharding methods:
- Hash: even spread, range queries ❌
- Range: range queries OK, hot shard risk
- Directory-based: flexible, metadata to maintain
6.4 Cross-Shard Queries
Expensive by default. When a JOIN or ORDER BY spans several shards, the merge happens at the application level.
Fixes:
- Keep related data on the same shard (by tenant_id)
- Read replica plus materialized view
- Keep OLAP separate (data warehouse)
6.5 Distributed SQL DBs (Sharding Handled for You)
- CockroachDB
- YugabyteDB
- Spanner
- TiDB
- Vitess (on top of MySQL)
- Citus (a Postgres extension)
Part 7 — Replication and HA
7.1 Replication Styles
- Physical (WAL/binlog): byte level, exact
- Logical: SQL or row level, flexible (heterogeneous, partial)
7.2 Postgres Replication in 2025
- Streaming Replication: ships WAL
- Logical Replication (10+): per table
- Synchronous option: certain but slow
- Patroni: HA management (the standard)
7.3 The Failover Pattern
Primary failure detected
↓ (consensus: etcd/Patroni)
Pick the most up-to-date standby
↓
Promote the new primary + the application switches DSN
↓
Rebuild the former primary
Key point: prevent split-brain (fencing).
7.4 Putting Read Replicas to Work
- Spread out read-heavy workloads
- Accept the replication lag
- "Read-your-writes" goes to the primary
Part 8 — PostgreSQL Runs Away With 2025
8.1 Why Postgres Won
Why Postgres became the de facto default when picking a DB in 2024-2025:
- Extensibility: a huge number of extensions
- JSONB: NoSQL features built in
- pgvector: vector DB capability
- PostGIS: unmatched GIS
- Timescale: time-series
- Citus: distribution
- Logical Replication: CDC and migrations
- License: the PostgreSQL License (permissive)
- Community: stable, not captive to one big vendor
- Managed in the cloud: Aurora, RDS, Cloud SQL, Neon, Supabase
8.2 Top 15 Postgres Extensions (2025)
| Extension | Purpose |
|---|---|
| pgvector | Vector Search |
| PostGIS | Geospatial |
| TimescaleDB | Time-series |
| Citus | Distribution |
| pg_partman | Partition management |
| pg_cron | Scheduling |
| pg_stat_statements | Query performance |
| hypopg | Hypothetical indexes |
| pg_repack | Online reorganization |
| pg_hint_plan | Plan hints |
| pg_trgm | Similarity search |
| unaccent | Accent removal |
| uuid-ossp | UUID generation |
| pgcrypto | Cryptography |
| hstore | Key-value |
8.3 The 2024-2025 Postgres Ecosystem
- Neon: Serverless Postgres, branching
- Supabase: Postgres plus Auth plus Realtime
- Xata: DevEx first
- Aurora Postgres: managed by AWS
- ParadeDB: search extensions
Part 9 — NoSQL: When to Reach for It
9.1 Five NoSQL Categories
| Category | Examples | Purpose |
|---|---|---|
| Document | MongoDB, Firestore | Flexible schema |
| Key-Value | Redis, DynamoDB | Fast lookups |
| Wide-column | Cassandra, ScyllaDB, HBase | Large-scale writes |
| Graph | Neo4j, Neptune | Relationship-centric |
| Time-Series | InfluxDB, Timescale | Time-series |
9.2 The 2025 Reality: "Postgres Is Enough"
Postgres covers most NoSQL use cases:
- Document → JSONB
- Key-Value → an UNLOGGED table, or Redis alongside it
- Time-Series → TimescaleDB
- Graph → the Apache AGE extension
Still pick NoSQL when you need:
- Global low latency (DynamoDB Global Table)
- Extreme write volume (Cassandra)
- Specialized graph algorithms (Neo4j)
9.3 Where Redis Sits
In-memory cache, queue, ranking, rate limiting. The tool "everybody has at least one of."
The 2024 license change → the Valkey fork (driven by AWS and Google). More options to choose from now.
Part 10 — Vector DB
10.1 Why a Vector DB Is Needed
Approximate nearest neighbor (ANN) search, for things like LLM embedding search and recommendations.
10.2 ANN Algorithms
- HNSW (Hierarchical Navigable Small World): fast and accurate, memory ↑
- IVF (Inverted File): clustering based
- PQ (Product Quantization): saves memory
- HNSW + PQ: hybrid
10.3 2025 Vector DB Comparison
| DB | Traits |
|---|---|
| pgvector | A Postgres extension, best integration |
| Qdrant | Rust, fast, strong filtering |
| Weaviate | Hybrid search, GraphQL API |
| Milvus | Large scale, Zilliz |
| Pinecone | SaaS, easy to operate |
| LanceDB | File based, embedded |
| Turbopuffer | Serverless |
10.4 2025 Recommendations
- Simple RAG: pgvector (bolt it onto the Postgres you already have)
- Large scale with advanced filters: Qdrant or Weaviate
- Fully managed: Pinecone
- Local or edge: LanceDB
10.5 A pgvector Example
CREATE EXTENSION vector;
CREATE TABLE documents (
id bigserial PRIMARY KEY,
content text,
embedding vector(1536)
);
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);
-- Search
SELECT content
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;
Part 11 — A Six-Month DB Roadmap
Month 1: SQL in Depth
- Window functions, CTEs, recursion
- Hands-on transaction isolation and deadlocks
- Index design
Month 2: Postgres Internals
- Fully understand EXPLAIN
- MVCC and Vacuum
- WAL and Replication
Month 3: Storage Engines
- DDIA chapter 3 (Storage)
- Try implementing a B-Tree and an LSM
- Study RocksDB and Cassandra
Month 4: Distributed DBs
- The architecture of CockroachDB and Spanner
- Sharding design
- Operating replication
Month 5: Vector DB
- pgvector in practice
- Understand HNSW
- Build a RAG data layer
Month 6: Optimization and Operations
- Slow query analysis
- Index tuning
- Connection Pooling (PgBouncer)
- Zero-downtime schema migration (Postgres in depth)
Part 12 — A 12-Point DB Checklist
- You know the criteria for choosing B-Tree vs LSM-Tree
- You know five Postgres index types and what each is for
- You can name five reasons an index is not used
- You can read EXPLAIN ANALYZE output
- You know the four transaction isolation levels along with the anomalies they prevent
- You can explain write skew
- You know the relationship between MVCC and Vacuum
- You know the difference between partitioning and sharding
- You know the trade-offs of hash vs range sharding
- You know the top 5 Postgres extensions
- You know how to choose between pgvector and Qdrant
- You know why a connection pool (PgBouncer) is necessary
Part 13 — Ten DB Antipatterns
- Trusting the ORM and never inspecting the queries: N+1 and loading far too many columns are common
- Adding indexes one by one without limit: writes slow down. Drop the ones that go unused
- Long transactions: MVCC bloat. Keep transactions short
- Blind faith in the default isolation level: not knowing the limits of Read Committed leads to write skew bugs
- Managing connections one at a time: thousands of connections with no pool → Postgres falls over
- Everything into JSONB: you give up the strengths of a schema. Structured data belongs in columns
- The wrong shard key: hot shard → resharding hell
- Unplanned migration downtime: a zero-downtime migration strategy is mandatory
- Ignoring vacuum tuning: your table bloats 10x without you noticing
- Using the DB as a message queue: possible, but not recommended. Use Redis or Kafka
Closing — The DB Is "the Gravity of a Production System"
You can replace the application, but you cannot easily change the DB schema. The DB is the part of a production system with the most inertia.
Which is why you have to design it well the first time:
- An index design that holds up for five years
- Partitioning and sharding ready for 10x growth
- Isolation levels that guarantee data integrity
- Observability and backups that prepare you for disaster
DB engineering in 2025 looks like this:
- Postgres First: most of it is solved here
- Vector DB: mandatory in the LLM era
- Distributed DB: only for the very largest services
- NoSQL: only when you have a clear reason
Next Post — "Networking Complete Guide: HTTP/3, QUIC, TLS 1.3, gRPC, WebSocket, CDN"
Season 2 Ep 14 is the plumbing of the internet: networking. The next post covers:
- TCP vs UDP vs QUIC
- The HTTP/1.1 → 2 → 3 evolution
- The TLS 1.3 handshake
- gRPC vs REST vs GraphQL vs tRPC
- WebSocket vs SSE vs Long Polling
- CDN architecture (Cloudflare, Fastly)
The wires you cannot see, in the next post.
현재 단락 (1/334)
Counterexamples to "surely knowing SQL is enough?":