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.
Mix read/write típico 80/20.
~3× con réplicas + ~50% para WAL/temp.
Hit ratio target > 99% en hot working set.
~3K commits/s con group commit (batch 100).
~860 GB WAL/día (rotated + archived).
Tolera 1 falla con consenso.
Sync penaliza p99 commit con +RTT × n.
Failover automático < 30 s.
Endpoints
Aunque la mayoría hablan SQL, exponer un API REST es una buena vista para entrevistas.
| Método | Path | Descripción | Notas |
|---|---|---|---|
| POST | /api/query | Ejecuta SELECT con isolation level. | Read-only. |
| POST | /api/transactions | Inicia transacción. | Devuelve txId. |
| POST | /api/transactions/{txId}/execute | Ejecuta DML dentro de tx. | |
| POST | /api/transactions/{txId}/commit | Commit y fsync WAL. | Durabilidad. |
| POST | /api/transactions/{txId}/rollback | Deshacer 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
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
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.
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.
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.
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.
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.
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.
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.
LSN para recovery (ARIES)
Range scan recorre next_leaf
Replicado a réplicas y archivado
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
| Tema | Decisión | Por qué |
|---|---|---|
| Storage engine | B+Tree para reads-balanced, LSM para write-heavy. | Cada uno optimiza un patrón distinto. |
| Buffer pool | Cache de páginas con LRU/Clock. | Disco es lento; RAM acelera reads y writes. |
| WAL | Append-only log antes de aplicar a páginas. | Durabilidad y recuperación tras crash. |
| Concurrencia | 2PL o MVCC. | 2PL es estricto, MVCC da reads sin bloqueo. |
| Replicación | WAL shipping síncrono o asíncrono. | Trade durabilidad vs latencia. |
| Recovery | ARIES (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
El commit no es durable hasta que el WAL toca disco. Cuidado con fsync deshabilitado.
Con 2PL, dos tx pueden esperarse infinito. Detecta ciclos y aborta una.
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.
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.
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.