Modelar datos consiste en decidir qué hechos conserva un sistema, cómo se relacionan y qué reglas deben protegerse. Las tablas o documentos concretan esas decisiones. El modelo también determina cuánto trabajo requiere responder una consulta y qué cambios deben coordinarse.
Una base relacional como Postgres suele ser un punto de partida práctico cuando hay relaciones, restricciones y transacciones locales claras. La elección debe justificarse con los requisitos; otras estructuras pueden encajar mejor con determinados accesos y unidades de actualización.
Empezar por accesos e invariantes
Antes de elegir almacenamiento, conviene describir las operaciones: obtener un usuario, listar sus posts, leer comentarios de un post y crear o borrar esas entidades. Cada acceso tiene filtros, orden, tamaño de resultado y frecuencia. También importa si necesita datos actuales o puede aceptar una proyección con retraso.
La estimación incluye registros, crecimiento, tamaño de campos, lectura y escritura, concurrencia y distribución de carga. Un usuario muy activo puede costar más que muchos usuarios ocasionales. Un conteo de entidades no describe por sí solo el trabajo de las consultas.
Las invariantes completan el contrato: cada post tiene un autor existente, cada comentario pertenece a un post y el identificador de correo sigue una política de unicidad. Después se define quién puede modificar cada dato. Integridad referencial y autorización resuelven preguntas diferentes.
Esos requisitos permiten comparar modelos y diseñar índices antes de introducir duplicación o distribución. El objetivo es conservar hechos coherentes y servir accesos concretos dentro de una capacidad medida.
Tipos de almacenamiento y contratos
| Modelo | Acceso que puede favorecer | Decisiones necesarias |
|---|---|---|
| Relacional | Relaciones, filtros, joins y transacciones sobre varias filas. | Restricciones, aislamiento, índices y coste de las consultas. |
| Documento | Lectura y actualización de un agregado con estructura variable. | Límites del agregado, validación, referencias y evolución del esquema. |
| Key-value | Acceso por claves y estructuras asociadas. | Diseño de claves, operaciones disponibles, durabilidad y accesos secundarios. |
| Wide-column | Consultas organizadas por claves de partición y agrupación. | Localidad, distribución de carga y forma de recuperar los datos. |
Los documentos también tienen contratos de esquema. La validación y las migraciones deben contemplar versiones anteriores y cambios de campos. Postgres puede combinar columnas estructuradas con JSONB cuando parte del contenido necesita flexibilidad; esa combinación conserva decisiones sobre validación, índices y actualización.
Key-value tampoco significa únicamente caché o búsqueda exacta. Redis ofrece varios tipos de datos. DynamoDB puede funcionar como base primaria durable y ofrecer claves de partición y ordenación e índices secundarios. El contrato de cada implementación determina las consultas y garantías disponibles.
Wide-column no equivale al almacenamiento columnar orientado a analítica. Su capacidad depende del diseño de claves y la carga; el nombre del modelo no establece un throughput universal.
Una tabla relacional de aristas puede representar ciertas relaciones entre usuarios. Si predominan recorridos de profundidad variable o consultas específicas sobre caminos, un modelo de grafo merece evaluación. La decisión depende de esos recorridos y sus costes, no de clasificar toda red social de la misma manera.
Entidades, claves y restricciones
El ejemplo usa tres entidades: Users, Posts y Comments. Un usuario puede publicar muchos posts y escribir muchos comentarios. Cada post y comentario tiene un autor; cada comentario pertenece a un post.
--- config: look: handDrawn --- erDiagram USERS ||..o{ POSTS : publica USERS ||..o{ COMMENTS : escribe POSTS ||..o{ COMMENTS : recibe USERS { bigint id PK text email UK } POSTS { bigint id PK bigint user_id FK timestamptz created_at text body } COMMENTS { bigint id PK bigint post_id FK bigint user_id FK timestamptz created_at text body }
Un esquema local ilustrativo puede expresarse así:
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE
);
CREATE TABLE posts (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id bigint NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at timestamptz NOT NULL DEFAULT now(),
body text NOT NULL
);
CREATE TABLE comments (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
post_id bigint NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
user_id bigint NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at timestamptz NOT NULL DEFAULT now(),
body text NOT NULL
);Las identidades bigint sirven para este ejemplo en una sola base. La PK identifica una fila y crea un índice único. Las FK protegen referencias porque están declaradas y se hacen cumplir; un campo llamado user_id sin esa restricción no ofrece la misma garantía.
NOT NULL hace obligatorias las referencias del diagrama. Permitir un autor nulo cambiaría el contrato. Aquí, borrar un post elimina sus comentarios mediante CASCADE, mientras borrar un usuario con referencias queda restringido. Otro producto puede necesitar anonimización o conservación distinta, y debe elegir esa política explícitamente. Una FK no autoriza a un cliente a publicar o borrar contenido ajeno.
Para el correo, el ejemplo exige una forma canónica: recortar espacios externos y tratar el identificador sin distinción de mayúsculas, almacenándolo en minúsculas. Esa política debe aplicarse a todos los caminos de escritura. El DDL mostrado no realiza esas transformaciones: UNIQUE sobre text compara valores según su configuración y no implica que ignore mayúsculas. Si la base debe imponer la equivalencia, necesita una clave normalizada o un índice por expresión acorde con la política.
Índices que siguen a las APIs
Para listar posts de un usuario por created_at DESC, id DESC, un índice B-tree compuesto puede alinear el filtro de igualdad y el orden. Para comentarios de un post se aplica un acceso análogo:
CREATE INDEX posts_user_created_id_idx
ON posts (user_id, created_at DESC, id DESC);
CREATE INDEX comments_post_created_id_idx
ON comments (post_id, created_at DESC, id DESC);
CREATE INDEX comments_user_id_idx
ON comments (user_id);Postgres no crea automáticamente un índice en la columna referenciante de una FK. Los dos primeros índices ya incluyen como prefijo las FK usadas en esos accesos; añadir un índice independiente equivalente puede ser redundante. El tercero ofrece un acceso por autor de comentario, también pertinente al localizar sus referencias durante operaciones sobre usuarios. Su utilidad se evalúa con la carga real.
Una página posterior al cursor del listado de posts puede consultar:
SELECT id, body, created_at
FROM posts
WHERE user_id = $1
AND (created_at, id) < ($2::timestamptz, $3::bigint)
ORDER BY created_at DESC, id DESC
LIMIT $4;El cursor contiene la fecha y el identificador del último resultado. id resuelve empates entre fechas iguales; la primera página omite la condición del cursor. Ese orden determinista no garantiza un snapshot entre peticiones: inserciones, borrados o cambios de los campos de orden pueden alterar lo observado.
Cada índice añade espacio y trabajo de escritura. El plan se valida con EXPLAIN, distribución de datos y selectividad; para medir ejecución real se utiliza un entorno adecuado, considerando que EXPLAIN ANALYZE ejecuta la consulta. Un índice disponible no obliga al optimizador a elegirlo ni garantiza una latencia determinada.
Normalización y proyecciones
Normalizar permite conservar cada hecho fuente en un lugar definido. En este ejemplo, el correo pertenece al usuario; copiarlo en cada post obligaría a decidir qué actualizar cuando cambia. Las referencias evitan esa duplicación para operaciones que necesitan el dato actual.
Otros requisitos pueden pedir una copia histórica, como el nombre mostrado al publicar. Esa copia debe declararse como un hecho histórico con su propio significado, no confundirse con la identidad vigente del autor.
La denormalización puede acelerar un feed, un contador o una vista agregada cuando aporta un beneficio medido. No exige agotar todos los índices posibles, pero sí conocer su coste y definir actualización, frescura y reconciliación. Una proyección retrasada o un contador duplicado necesita una política de recuperación.
SD: Caching distingue una copia reutilizable de la autoridad del dato. Si publicar cambios requiere coordinar escritura y eventos, SD: 2PC vs Saga explica la outbox y sus límites de entrega. Ninguno de esos mecanismos vuelve innecesario definir el hecho fuente.
Localidad, particiones y sharding
Después de medir capacidad, la distribución se evalúa junto a los accesos. Particionar tablas dentro de una sola instalación de Postgres no implica automáticamente repartirlas entre servidores independientes.
Una asignación por post_id puede colocar un post y sus comentarios juntos si ambos usan esa unidad de enrutamiento. El feed por user_id podría entonces requerir fanout o una proyección adicional. Una asignación por usuario o tenant favorece otros accesos, a cambio de concentrar los datos y el tráfico de una entidad muy activa.
Las FK del ejemplo local no se trasladan automáticamente entre shards. Unicidad global, referencias y transacciones necesitan garantías del motor o una arquitectura que las mantenga. Las identidades generadas también requieren un contrato de unicidad en el alcance distribuido; no basta iniciar generadores locales independientes y asumir que nunca coinciden.
SD: Sharding desarrolla esa elección entre localidad, hotspots y operaciones distribuidas. El modelo de datos debe hacer visible qué reglas quedan dentro de una autoridad y cuáles necesitan coordinación.
Qué verificar
El modelo se revisa recorriendo las APIs y sus escrituras: filas tocadas, referencias verificadas y restricciones que impiden estados inválidos. También deben contemplarse borrados, duplicados, variaciones de mayúsculas en el correo y solicitudes sin permisos.
Los planes y la capacidad se comprueban con datos representativos, incluidos usuarios muy activos, fechas empatadas, grandes historiales y escrituras concurrentes. La paginación debe declarar cómo se comporta cuando el conjunto cambia. Las proyecciones se revisan con eventos repetidos, retrasados y con recuperación pendiente.
La elección queda justificada cuando puede explicarse dónde vive cada hecho, qué protege su integridad, qué camino responde cada consulta y qué ocurre al cambiar el esquema o distribuir sus datos.