Top 10 Errores en Diseño de Base de Datos que Cometen Desarrolladores (y Cómo Evitarlos)
Un mal diseño de base de datos no se anuncia a sí mismo el primer día. Aparece seis meses después cuando las queries tardan 30 segundos o una característica "simple" requiere refactorizar la mitad del schema. Estos son los diez errores más comunes y cómo evitarlos.
Un mal diseño de base de datos no se anuncia a sí mismo en el primer día. Aparece seis meses después cuando las queries tardan 30 segundos, cuando una característica "simple" requiere refactorizar la mitad del schema, o cuando te das cuenta de que has estado almacenando precios como floats y tus reportes financieros no suman.
Estos son los diez errores más comunes que vemos cometer a los desarrolladores cuando diseñan bases de datos — y cómo evitar cada uno.
1. No diseñar antes de codificar
El error más caro sucede antes de que se escriba cualquier SQL: saltar directamente al código sin pensar en el modelo de datos. Los desarrolladores crean tablas conforme las necesitan, añaden columnas reactivamente, y terminan con un schema que refleja el orden en que construyeron características en lugar de la estructura lógica del dominio.
La solución es directa: pasa 30 minutos con un diagrama ER antes de escribir cualquier código. Mapea tus entidades, relaciones, y atributos core. No necesitas perfección — necesitas un punto de partida que capture las decisiones estructurales mayores. Una herramienta visual hace esto más rápido de lo que piensas. Puedes diseñar un schema de 20 tablas en ER Flow en menos de 30 minutos, y el tiempo ahorrado por no tener que refactorizar después se mide en días.
2. Usar el tipo de dato incorrecto para dinero
Almacenar precios, totales, o cualquier valor financiero como FLOAT o DOUBLE es un error clásico. La aritmética de punto flotante es inherentemente imprecisa — 0.1 + 0.2 es igual a 0.30000000000000004 en la mayoría de lenguajes. Durante millones de transacciones, los errores de redondeo se acumulan en discrepancias financieras reales.
Usa DECIMAL(10,2) para valores de moneda, o almacena cantidades como enteros en la unidad de moneda más pequeña (centavos). Un producto con precio de $29.99 se almacena como 2999. La aritmética de enteros es exacta, y conviertes a formato de visualización solo en la capa de presentación.
3. No indexar foreign keys
Cada columna de foreign key debería tener un índice. Cuando consultabas todas las órdenes de un usuario (WHERE user_id = 123), la base de datos escanea la tabla completa de órdenes si user_id no está indexado. Algunas bases de datos (como MySQL con InnoDB) crean índices en foreign keys automáticamente. PostgreSQL no lo hace.
Más allá de foreign keys, añade índices en cualquier columna que frecuentemente filtres, ordenes, o en la que hagas join: status, created_at, email, slug. Pero no sobre-indexes — cada índice ralentiza las escrituras y consume almacenamiento. Indexa las columnas que tus queries realmente usan, no cada columna que teóricamente podría ser consultada.
4. Almacenar datos repetidos en lugar de usar relaciones
Cuando un desarrollador almacena customer_name, customer_email, y customer_phone directamente en cada orden en lugar de referenciar una tabla de customers, han creado un problema de denormalización. Cuando el customer actualiza su email, necesitas actualizarlo en cada orden. Si te pierdes uno, tus datos son inconsistentes.
La solución es normalización: almacena cada hecho en un lugar y usa foreign keys para referenciarlo. Las órdenes tienen un customer_id que apunta a la tabla de customers. El email del customer existe en exactamente una fila.
Hay razones legítimas para denormalizar — almacenar el nombre del producto y el precio en los items de la orden (porque el producto podría cambiar después de la orden), o cachear valores computados por performance. Pero denormaliza intencionalmente, no accidentalmente. Sabe cuándo estás haciendo un trade-off versus cuándo estás creando un desastre.
5. Usar una única tabla "Dios"
Todos lo hemos visto: una tabla data o una tabla records con 50+ columnas, donde diferentes tipos de registros usan diferentes subconjuntos de columnas. Una fila podría ser un usuario, un producto, u una orden, distinguida por una columna type. La mayoría de columnas son NULL para cualquier fila dada.
Esto sucede cuando los desarrolladores intentan evitar crear nuevas tablas. El resultado es un schema que es imposible de entender, imposible de indexar eficientemente, y una pesadilla de mantener.
Cada entidad distinta merece su propia tabla. Si usuarios, productos, y órdenes tienen diferentes atributos y diferentes ciclos de vida, deberían ser tablas separadas. El ligero overhead de crear una nueva tabla y migration es trivial comparado con el costo a largo plazo de una tabla Dios.
6. Ignorar relaciones muchos-a-muchos
Cuando dos entidades tienen una relación muchos-a-muchos (estudiantes inscritos en cursos, productos en categorías, usuarios con roles), algunos desarrolladores intentan almacenarla como una lista separada por comas: categories = "1,2,5,12". Esto hace que consultar sea casi imposible — ¿cómo encuentras todos los productos en la categoría 5? — y viola la primera forma normal.
El enfoque correcto es una junction table: product_categories con columnas product_id y category_id, cada una con una foreign key. Esto es consultable, indexable, y permite atributos adicionales en la relación (como sort_order o added_at).
7. No planificar para soft deletes
Eliminar un usuario de la base de datos parece simple hasta que te das cuenta de que sus órdenes, comentarios, reviews, y otros datos asociados o se eliminan mediante cascade (perdiendo registros críticos del negocio) o dejan foreign keys huérfanos.
Los soft deletes — añadiendo una columna de timestamp deleted_at que marca registros como eliminados sin realmente removerlos — resuelven esto limpiamente. Un deleted_at no-null significa que el registro está "eliminado." Tu aplicación filtra estos por defecto, pero los datos permanecen intactos para registros históricos, cumplimiento, y registros de auditoría.
Planifica esto desde el inicio. Añadir soft deletes a un schema existente con millones de filas y docenas de queries es significativamente más difícil que incluir deleted_at desde el día uno.
8. Almacenar JSON para todo
El tipo de columna jsonb de PostgreSQL y JSON de MySQL son poderosos — pero no son un reemplazo para un diseño de schema apropiado. Almacenar datos estructurados como JSON significa que pierdes seguridad de tipos (un "price" dentro de JSON podría ser un string, número, o null), no puedes aplicar restricciones (sin foreign keys, sin NOT NULL, sin unique constraints dentro de JSON), el soporte de índices es limitado (los índices GIN ayudan, pero no son tan eficientes como los índices B-tree en columnas tipadas), y los cambios de schema se vuelven invisibles (cualquiera puede añadir o remover una clave en el JSON, sin migration o documentación).
Las columnas JSON son excelentes para datos verdaderamente no estructurados: preferencias de usuario, cachés de respuesta de API, metadata que varía por registro. Son una mala opción para datos que tienen estructura consistente y relaciones — esos datos pertenecen en columnas apropiadamente tipadas y tablas relacionadas.
9. No considerar patrones de query durante el diseño
Un schema normalizado es teóricamente correcto, pero si tu query más común requiere unir 8 tablas, tienes un problema de performance. El diseño de base de datos debería balancear normalización con las queries que tu aplicación realmente ejecuta.
Antes de finalizar tu schema, lista tus diez queries más comunes. Si una query crítica requiere joins costosos a través de muchas tablas, considera denormalización estratégica: añade una columna computada, crea una materialized view, o reestructura las tablas para reducir la profundidad de joins.
Esta es una decisión de tiempo de diseño, no una tarea de "optimizaremos después". Añadir una columna total_orders_count a la tabla de users durante el diseño inicial es fácil. Añadirla después de un año de datos de producción requiere una migration que actualiza millones de filas.
10. Sin convenciones de nombre
El nombrado inconsistente hace schemas confusos: algunas tablas son singulares (user), otras plurales (orders). Algunas columnas usan camelCase (firstName), otras usan snake_case (first_name). Los foreign keys a veces son userId, a veces user_id, a veces fk_user.
Elige una convención y apégate a ella. La más común en la industria es nombres de tabla plurales, snake_case (users, order_items), nombres de columna singulares, snake_case (first_name, created_at), y foreign keys nombrados como singular_table_id (user_id, order_id). Las columnas booleanas prefijadas con is_ o has_ (is_active, has_verified).
Documenta tu convención y aplicala en code reviews. Un schema consistente es dramáticamente más fácil de trabajar que uno donde cada tabla sigue reglas diferentes.
La Prevención es Más Barata que la Cura
Todos estos errores comparten un rasgo común: son fáciles de evitar en tiempo de diseño y costosos de arreglar después. Pasar una hora diseñando tu schema visualmente — viendo todas las tablas, relaciones, y tipos de datos en una vista — captura la mayoría de estos problemas antes de que se incrusten en código de producción y datos.
Herramientas como ER Flow hacen este paso de diseño rápido y colaborativo. El diseño visual superficies problemas estructurales (relaciones faltantes, problemas de denormalización, índices faltantes) que son invisibles en una lista lineal de archivos de migration. Y con características como diseño asistido por IA y generación de migrations, el paso de diseño no ralentiza tu desarrollo — lo acelera.