Caso 10 · Difícil pero defendible

Manejador de base de datos (DBMS)

Recibir queries SQL, parsear, planear, ejecutar, manejar transacciones y persistir páginas con durabilidad ACID.

Recursos

Query, Transaction, Page, Index, Lock, WAL.

Operaciones

Parse, plan, execute, commit, rollback, recover.

Escala

Miles de queries/seg, terabytes en disco, varias replicas.

Riesgo

Pérdida de datos, deadlocks, lecturas sucias, replication lag.

Requisitos

Un DBMS firma garantías ACID; ahí se gana o se pierde la entrevista.

Funcionales

  • SELECT con índices, joins y agregaciones.
  • INSERT/UPDATE/DELETE dentro de transacciones.
  • Begin / Commit / Rollback con isolation niveles.
  • EXPLAIN y plan estimado.
  • Replicación sync o async, point-in-time recovery.

No funcionales

  • ACID con isolation snapshot / serializable.
  • Durabilidad: WAL fsync antes de ack commit.
  • p99 query indexado < 50 ms.
  • Disponibilidad 99.99% con failover < 30 s.
  • Recovery ARIES tras crash sin pérdida.

Cómo aclarar requisitos en la entrevista

Casos de uso típicos. OLTP banking (transferencias con strong consistency), OLAP analytics (lecturas masivas, agregaciones), time-series (logs, métricas, IoT), multi-tenant SaaS (aislamiento + escala), embedded en app (SQLite-style), distributed (Spanner / CockroachDB para multi-region), graph analytics (Neo4j-like), document store (Mongo-like).

Preguntas que debes hacer:

• ¿OLTP, OLAP o ambos? Cambia storage engine (B+Tree vs columnar).
• ¿Single-node o distributed? Distributed implica consensus (Raft/Paxos).
• ¿SQL completo o subset / NoSQL?
• ¿Strongest isolation requerido (serializable, snapshot, RR, RC)?
• ¿Replicación sync vs async? Trade durabilidad/latencia.
• ¿On-prem o cloud-managed? Cambia tooling de ops.
• ¿Multi-region con consistencia o eventually consistent OK?
• ¿Schemaless o schema enforced (DDL)?
• ¿Workload read-heavy, write-heavy, balanced?

Frase clave: "Voy a diseñar OLTP single-node con SQL, isolation snapshot, replicación async para read replicas. Si me pides multi-region, agrego Raft con sharding y la conversación cambia bastante."

Estimaciones de capacidad

Resoluciones año/día/seg. Lo que separa un B-Tree de un LSM y define el tamaño del buffer pool.

10K QPS
Por instancia
~860M queries/día · ~315B/año.
Mix read/write típico 80/20.
~1–10 TB
Storage por nodo
Antes de sharding.
~3× con réplicas + ~50% para WAL/temp.
~25–50%
RAM como buffer pool
Ej: 64 GB en nodo de 128 GB.
Hit ratio target > 99% en hot working set.
~30 ms
fsync típico SSD
~33 commits/s sin group commit.
~3K commits/s con group commit (batch 100).
~1 KB
Tamaño de WAL record
~10 MB/s a ~10 K writes/s.
~860 GB WAL/día (rotated + archived).
3
Réplicas mínimas
Quorum mayoritario para failover.
Tolera 1 falla con consenso.
< 1 s
Replication lag
Async típico ~100 ms.
Sync penaliza p99 commit con +RTT × n.
99.99%
SLO disponibilidad
~52 min/año · ~4 min/mes.
Failover automático < 30 s.

Endpoints

Aunque la mayoría hablan SQL, exponer un API REST es una buena vista para entrevistas.

MétodoPathDescripciónNotas
POST/api/queryEjecuta SELECT con isolation level.Read-only.
POST/api/transactionsInicia transacción.Devuelve txId.
POST/api/transactions/{txId}/executeEjecuta DML dentro de tx.
POST/api/transactions/{txId}/commitCommit y fsync WAL.Durabilidad.
POST/api/transactions/{txId}/rollbackDeshacer cambios.Atomicidad.
GET/api/explain?sql=...Plan estimado del query.Debugging.

Cómo defiendo estas APIs en la entrevista

El protocolo real es binario, REST es solo una vista. En producción los DBs usan protocolos custom optimizados (PostgreSQL wire protocol, MySQL protocol). Pero exponer REST en una entrevista hace los conceptos visibles: transactions como recursos con state, queries como POST con body SQL.

Transacciones como recursos con state. POST /transactions crea una tx, devuelve txId. POST /transactions/{id}/execute con body SQL ejecuta dentro de esa tx. commit y rollback son endpoints separados — cada uno con state machine claro.

EXPLAIN como herramienta de debug. GET /explain?sql=... devuelve el plan estimado por el optimizer. Crítico para entender por qué un query es lento. En producción esto es EXPLAIN ANALYZE en SQL.

Frase clave: "El DBMS expone APIs simples (query, tx, commit) pero atrás hay parser, planner, executor, buffer pool, WAL, lock manager. La API es la fachada; la complejidad está en cómo se ejecuta."

Ejemplo

POST /api/transactions
→ { "txId": "tx_91", "isolation": "REPEATABLE_READ" }

POST /api/transactions/tx_91/execute
{ "sql": "UPDATE accounts SET balance = balance - 100 WHERE id = 1" }
→ { "rowsAffected": 1 }

POST /api/transactions/tx_91/execute
{ "sql": "UPDATE accounts SET balance = balance + 100 WHERE id = 2" }
→ { "rowsAffected": 1 }

POST /api/transactions/tx_91/commit
→ { "committed": true, "lsn": 8421192 }

Arquitectura

flowchart LR Client(["Cliente / driver"]) -->|SQL| Parser Parser{{"🌐 PARSER + Wire Protocol
SQL → AST"}} --> Planner["Planner / Optimizer
AST → plan + costos"] Planner --> Executor["Executor
itera op-tree"] Executor --> TxMgr Executor --> Buffer Buffer <--> Storage TxMgr --> WAL Storage --> Disk WAL --> Replica subgraph TxMgr["🔒 Tx Manager · locks / MVCC"] direction TB t1["tx_table"] t2["lock_table"] t3["wait_for_graph"] end subgraph Buffer["⚡ Buffer Pool · RAM"] direction TB b1["heap_pages"] b2["index_pages"] b3["dirty_list (LRU/Clock)"] end subgraph Storage["💾 Storage Engine · B+Tree / LSM"] direction TB s1["heap"] s2["btree_indexes"] s3["catalog (system tables)"] end subgraph WAL["📝 WAL · append-only · fsync en commit"] direction TB w1["wal_records (LSN, txid, op)"] w2["checkpoints"] end subgraph Disk["🗄️ Disk pages"] direction TB d1["data files"] d2["index files"] d3["temp files"] end subgraph Replica["💾 Read Replicas · WAL shipping"] direction TB r1["replica_1"] r2["replica_2"] end classDef gateway fill:#fbbf24,stroke:#f59e0b,stroke-width:3px,color:#0f172a,font-weight:bold classDef service fill:#4c1d95,stroke:#a78bfa,stroke-width:2px,color:#ede9fe classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class Parser gateway class Planner,Executor service class Client client

Cómo lo explico en la entrevista

Query entra. Cliente envía SQL. El Parser tokeniza y construye un AST. Si hay error de sintaxis, devuelve error 4xx — no malgastes recursos en queries malformadas.

Plan + cost. El Planner / Optimizer toma el AST. Consulta el catalog (estadísticas: cardinality, selectivity, índices disponibles, distribución) y enumera planes posibles: nested loop join vs hash join vs merge join, qué índice usar, qué orden de joins. Calcula costo estimado de cada plan y elige el mínimo. Output: árbol de operadores físicos.

Execute con buffer pool. El Executor itera el plan. Cada operador (scan, filter, hash) pide tuples al hijo. Cuando necesita una página, pregunta al Buffer Pool. Hit en RAM → bien. Miss → lee del disco y la mete al pool, evictando con LRU/Clock. El buffer pool típicamente es 25–50% de la RAM.

Write path con WAL. Para INSERT/UPDATE/DELETE: el Executor coordina con el Tx Manager. Antes de modificar la página en RAM, escribe un WAL record con before/after image. El WAL es append-only: secuencial es ~100× más rápido que random. La página queda dirty y se flushea al disco eventualmente.

Commit = durabilidad. En commit, fsync del WAL hasta esa LSN. Antes del fsync, el commit no es ack al cliente. Esto es lo que da la D de ACID. Sin fsync, un crash perdería commits ya prometidos.

Recovery (ARIES). Tras crash: (1) analysis lee WAL desde último checkpoint para reconstruir tx_table y dirty_page_table; (2) redo reaplica todo lo committed; (3) undo deshace lo no committed. Estado consistente, garantizado matemáticamente.

Replicación. El primary stream del WAL a réplicas. Sync espera ack de quorum (penaliza p99 pero garantiza durabilidad cross-node), async no espera (rápido pero puede perder en failover).

Frase clave: el truco está en separar memoria (rápida pero volátil) del WAL (lento pero durable). Las páginas viven en buffer pool; el WAL es la fuente de verdad para recovery.

Por qué cada componente

Parser → ASTGarantiza syntax válida antes de cómputo costoso. Errores rápidos.
Planner con statsUn buen plan vs uno malo es 100× en latencia. Estadísticas frescas son críticas.
Buffer PoolDisco es 100K× más lento que RAM. Sin buffer pool, cada query es lenta.
B+Tree vs LSMB+Tree para read-balanced (Postgres, MySQL). LSM para write-heavy (Cassandra, RocksDB).
WAL append-onlySecuencial es ~100× más rápido que random. Single point para durabilidad y recovery.
fsync at commitDa la D de ACID. Sin esto, "commit" es solo un log entry en RAM.
MVCCReads no bloquean writes. Cada tx ve un snapshot consistente del momento que empezó.
2PL strictGarantiza serializable. Se usa en escrituras críticas combinado con MVCC para reads.
ARIES recoveryAlgoritmo probado. Analysis-Redo-Undo garantiza consistency tras crash.
WAL shippingRéplicas leen el mismo WAL del primary. Trade sync vs async según necesidad de durabilidad.

Cuellos de botella

  • Lock contention en hot rows con muchas escrituras concurrentes.
  • Buffer pool too small → thrashing constante, queries 100× más lentas.
  • WAL fsync latency (~10 ms en SSD) define el commit throughput máximo.
  • Long-running tx con undo log enorme — bloquea VACUUM y vaciado.
  • Bad plan choice por estadísticas viejas o sesgadas.
  • Replication lag en async cuando primary está bajo carga.

Mejoras / cómo escala

  • MVCC en lugar de 2PL puro para que reads largos no bloqueen.
  • Group commit (fsync N tx juntas) — amortiza el costo de fsync.
  • ANALYZE periódico y auto-vacuum agresivo para stats frescas.
  • Adaptive buffer pool con feedback (warm-up tras restart).
  • Read replicas para escalar reads (eventualmente consistente).
  • Sharding por clave natural (user_id, tenant_id) cuando un nodo no alcanza.
  • Connection pooling (PgBouncer) — abrir conexiones es caro.

Por qué estas decisiones internas (vs alternativas)

En este caso no usamos servicios cloud — estamos diseñando el motor de DB. La pregunta es: por qué cada estructura interna y no las otras.

B+Treestorage engine para read-balanced

Por qué: reads y range scans en O(log N), updates in-place, escala bien con índices. Es el default de Postgres, MySQL/InnoDB, Oracle.

vs LSM-Tree: mejor para write-heavy (Cassandra, RocksDB) pero compaction periódica genera write amplification y read amplification por niveles. vs Hash Index: O(1) en point lookup pero no soporta range scans. vs Skip List: simpler pero peor disk locality.

WAL append-only + fsync at commitdurabilidad

Por qué: escritura secuencial en disco es ~100× más rápida que random. Single source of truth para recovery. Replicable a réplicas streaming bytes del WAL.

vs no-WAL (in-place writes): un crash con escritura parcial corrompe páginas. vs shadow paging: simpler conceptualmente pero overhead de copy. vs synchronous replication sin WAL: requiere consenso costoso por escritura.

MVCC + 2PL strictconcurrencia

Por qué: MVCC permite que reads no bloqueen writes (cada tx ve un snapshot del momento que empezó); 2PL strict en escrituras críticas garantiza serializable. Combinación estándar.

vs solo 2PL: reads largos bloquean writes — inaceptable para OLTP moderno. vs solo MVCC sin lock en writes: permite write skew y otras anomalías. vs OCC (Optimistic Concurrency Control): aceptable para conflict-rate bajo, malo si hay contención alta.

Buffer Pool con LRU/Clockcaché de páginas

Por qué: RAM es ~100K× más rápida que SSD. LRU evict las páginas frías; Clock es aproximación con menos overhead que LRU puro. Hit ratio > 99% en hot working set.

vs ARC (Adaptive Replacement Cache): mejor en algunos workloads pero más complejo. vs FIFO simple: mucho peor hit ratio. vs OS page cache solo: el DB sabe mejor qué páginas son hot.

ARIES Recoverycrash recovery

Por qué: algoritmo probado matemáticamente: analysis (reconstruir tx_table desde último checkpoint), redo (reaplicar todo en WAL), undo (deshacer no-committed). Garantiza estado consistente tras cualquier crash.

vs shadow paging recovery: simpler pero menos eficiente para writes. vs no-undo / no-redo: requiere force-write, lento. vs custom ad-hoc: garantías débiles, bug factory.

WAL Shipping (replicación)read replicas + HA

Por qué: stream el WAL del primary a réplicas; éstas re-aplican para mantener consistencia. Sync para durabilidad cross-node, async para latencia.

vs Logical Replication: más flexible (filtros, esquemas distintos) pero overhead mayor. vs Multi-master con consenso (Raft/Paxos): serializability garantizada pero +RTT por escritura, complejidad alta. vs Snapshot replication: no realtime.

Diseño de estructuras internas

No hay "tablas de usuario" aquí — son las estructuras que hacen funcionar el motor.

page (heap / index)
page_idUINT32
lsnUINT64
checksumCRC32
tuples / nodesVAR
free_space_mapBITMAP
Tamaño 4–16 KB típico
LSN para recovery (ARIES)
b+tree index node
keys[ ]sorted
children[ ]page_id refs
leaf?BOOLEAN
next_leafpage_id
Lookup O(logk N)
Range scan recorre next_leaf
WAL record
lsnUINT64
txidUINT64
opENUM update/begin/commit
page_idUINT32
before / after imageBYTES
Append-only, fsync antes de ack commit
Replicado a réplicas y archivado
tx table + lock table
tx:{id}state, snapshot xmin/xmax
lock:{key}mode S/X, holders
wait_for_graphdeadlock detection
MVCC: cada tx ve un snapshot
Wait-for graph para abortar deadlocks

Cómo defiendo este diseño de estructuras internas

Page como unidad atómica. Todo se organiza en páginas de 4–16 KB porque el OS lee del disco en bloques. Una página tiene LSN (último log que la modificó), checksum (detectar corrupción), tuples organizados con free space map. El buffer pool maneja páginas, no rows.

B+Tree índice por su disk locality. Las hojas están encadenadas (next_leaf pointer), lo que hace range scans muy rápidos. La altura logarítmica garantiza I/Os predictibles. Para inserts, splits balancean el árbol.

WAL records = source of truth. Cada modificación a una página primero genera un WAL record con before/after image. La page modificada en RAM queda "dirty" y eventualmente se flushea. Si crashea antes del flush, el WAL replay reconstruye.

MVCC con xmin/xmax. Cada row tiene xmin (tx que la creó) y xmax (tx que la borró). Una tx con snapshot ID ve solo rows donde xmin <= snapshot < xmax. Reads no bloquean writes; writes nuevas crean nuevas versiones.

Frase clave: "Todo está organizado en páginas porque el disco lee en bloques. El WAL es secuencial porque random writes son lentos. MVCC mantiene snapshot consistency sin bloquear. Cada estructura existe por una razón física del hardware."

Decisiones técnicas

TemaDecisiónPor qué
Storage engineB+Tree para reads-balanced, LSM para write-heavy.Cada uno optimiza un patrón distinto.
Buffer poolCache de páginas con LRU/Clock.Disco es lento; RAM acelera reads y writes.
WALAppend-only log antes de aplicar a páginas.Durabilidad y recuperación tras crash.
Concurrencia2PL o MVCC.2PL es estricto, MVCC da reads sin bloqueo.
ReplicaciónWAL shipping síncrono o asíncrono.Trade durabilidad vs latencia.
RecoveryARIES (analysis, redo, undo).Patrón clásico para volver a un estado consistente.

Cómo defiendo estas decisiones técnicas

Constraints físicos definen las decisiones. Disco es lento (random ~10 ms, secuencial ~50 MB/s); RAM es rápida pero finita; CPU es muy rápida. Cada decisión maximiza uso de RAM y secuencial, minimiza random reads.

Métricas que validan. Buffer hit ratio (target > 99% en hot working set; baja → thrashing). Commit throughput (limitado por fsync; group commit lo amortiza). Lock wait time (alta contención → revisar isolation level). Replication lag (sync vs async tradeoff). Recovery time post-crash (target < 30 s).

Cuándo cambio de opinión. Workload write-heavy → LSM-tree en lugar de B+Tree (Cassandra-style). Multi-region strong consistency → consensus protocol (Raft/Paxos) reemplaza WAL shipping single-master. Time-series con high write throughput → Druid/InfluxDB con compression columnar.

Frase clave: "Un DBMS es un montón de tradeoffs físicos: WAL es secuencial porque disco; buffer pool es LRU porque RAM finita; B+Tree porque range scans; MVCC porque snapshot isolation. Cada decisión sigue de un constraint físico."

Pitfalls

Commit sin fsync

El commit no es durable hasta que el WAL toca disco. Cuidado con fsync deshabilitado.

Deadlocks

Con 2PL, dos tx pueden esperarse infinito. Detecta ciclos y aborta una.

Replication lag

Async ofrece latencia baja pero pierdes datos en failover. Síncrono cuesta latencia.

Script de respuesta: “El query entra, se parsea, el planner elige índices y join order, el executor itera el plan. Las páginas viven en buffer pool; toda escritura va primero al WAL antes de modificar la página, así garantizo durabilidad. Para concurrencia uso MVCC para leer sin bloquear y 2PL en escrituras críticas. La replicación va por WAL shipping; recovery sigue ARIES. Mido p99, lock wait, replication lag y cache hit ratio.”

Tiers de almacenamiento

Un DBMS tiene su propia jerarquía: RAM → SSD local → SSD remoto → archive. Cada tier define latencia y costo.

Hot · RAM (buffer pool)
Páginas activas en buffer pool, lock table, transaction table, undo log activo.
Process memory · target hit ratio > 99%
~$5–10/GB compute · sub-µs latency
Warm · SSD local (data + WAL)
Páginas de tablas e índices, WAL activo, temp space para sorting/hashing grandes.
Local NVMe SSD / EBS gp3
~$0.10–0.20/GB/mes · ~100 µs latency
Cold · object storage WAL archive
WAL rotated > 7 días, backups incrementales, tablespace archivado de tablas históricas.
S3 Standard / GCS Standard
~$0.023/GB/mes
Frozen · backup archive
Snapshots completos > 30 días, point-in-time recovery archives, compliance retention.
S3 Glacier / GCS Coldline
~$0.004/GB/mes · retrieval para PITR ~h

Cómo defenderlo en la entrevista

Buffer pool es el tier más caliente y más caro. 64 GB de buffer pool en un nodo es ~$5K/mes en cloud (~25–50% del nodo). Ese tier ahorra millones en latencia: con hit ratio <99%, el sistema thrasha y queries que deberían ser ms se vuelven s.

WAL en SSD local, archive en S3. WAL activo necesita fsync <10 ms (SSD local). WAL rotado (ya aplicado a páginas y a réplicas) puede ir a S3 — sirve para PITR y replication catch-up de réplicas que estuvieron desconectadas.

Backups con retention escalonado. Daily backups en Standard 7 días → IA 30 días → Glacier 1 año. Esto da Recovery Point Objective (RPO) granular en hot tier y compliance retention en cold a costo mínimo.

Frase clave: "Cada tier del DBMS está justificado por el patrón físico: RAM porque cada query toca páginas, SSD porque WAL fsync, S3 porque WAL archive y backups, Glacier porque PITR mensual o anual. Tier wrong = costo o performance perdidos."

Versión alternativa: con Load Balancer (no API Gateway)

Para un DBMS, lo típico es exponer puerto TCP directo al driver. LB L4 es lo natural.

flowchart LR Client(["Cliente / driver SQL"]) --> LB LB{{"⚖️ L4 NLB
TCP 5432
health check + failover"}} --> Primary["Primary Node
+ pgbouncer connection pool"] LB -.read replicas.-> Replica1["Read Replica 1"] LB -.read replicas.-> Replica2["Read Replica 2"] Primary --> Parser["Parser → Planner → Executor"] Parser --> Buffer Buffer <--> Storage Primary --> WAL WAL --> Replica1 WAL --> Replica2 subgraph Buffer["⚡ Buffer Pool · RAM"] direction TB b1["heap_pages"] b2["index_pages"] end subgraph Storage["💾 Storage Engine · B+Tree / LSM"] direction TB s1["heap"] s2["btree_indexes"] s3["catalog"] end subgraph WAL["📝 WAL · append-only"] direction TB w1["wal_records"] w2["checkpoints"] end classDef lb fill:#34d399,stroke:#10b981,stroke-width:3px,color:#0f172a,font-weight:bold classDef service fill:#4c1d95,stroke:#a78bfa,stroke-width:2px,color:#ede9fe classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class LB lb class Primary,Parser,Replica1,Replica2 service class Client client

API Gateway vs Load Balancer: cómo defender cada uno

El DBMS habla protocolo binario nativo (PostgreSQL wire protocol, MySQL protocol). API Gateway es L7 HTTP-aware — no entiende el protocolo. Por eso lo natural es L4 NLB que solo enruta TCP. Para read/write split (writes al primary, reads a réplicas) puedes usar pgbouncer/HAProxy intermedio, no Gateway.

Si expusieras una "REST API DB" (PostgREST, Hasura) que traduce HTTP a SQL, ahí sí Gateway hace sentido por delante: auth, rate limit, transformations. Pero la DB nativa siempre va por LB.

Frase clave: "Para SQL nativo uso L4 NLB; el protocolo binario no es ruteable por Gateway. Si pones una capa REST encima (PostgREST, Hasura, Supabase), entonces Gateway sí encaja para esa capa, pero el DB sigue detrás de LB."

Cuándo elegir API Gateway

  • REST API encima del DB (PostgREST, Hasura, Supabase).
  • DBaaS managed con auth y quotas por tenant.
  • GraphQL frontend con schemas y rate limits por field.
  • Multi-tenant SaaS con isolation por organización.

Cuándo elegir Load Balancer

  • SQL nativo (puerto 5432 / 3306): protocolo binario, gateway no entiende.
  • Connection pooling con PgBouncer / ProxySQL detrás del LB.
  • Read/write split con LB-aware (HAProxy con backend selection).
  • Latencia mínima en queries OLTP (sub-50 ms p99).
  • Failover automático del primary con health checks del LB.
  • Throughput masivo (10K+ QPS por instancia): NLB L4 es lo más rápido.