Anatomía de un CREATE TABLE
Un buen CREATE TABLE define columnas con tipos precisos, una primary key clara, foreign keys con sus acciones de cascada, índices sobre columnas que se filtran o se ordenan, y constraints que protegen la integridad. Lo que parece overhead al inicio paga en confiabilidad después.
Primary key: BIGSERIAL vs UUID
Para tablas internas que crecen rápido, BIGSERIAL (Postgres) o BIGINT IDENTITY (SQL Server) siguen siendo lo más eficiente: 8 bytes, ordenable. Para IDs visibles al usuario o compartidos entre servicios, UUID (v7 si querés ordenamiento temporal) evita revelar cardinalidad y permite generar IDs sin coordinar.
Tipos de timestamp
Usá TIMESTAMPTZ en Postgres (timestamp with time zone): guarda en UTC y
convierte al timezone del cliente. TIMESTAMP sin TZ es una trampa: parece
funcionar hasta que cruzás husos horarios. En MySQL, usá DATETIME con la
regla "guardá UTC en la app, formateá en el cliente".
NOT NULL y defaults
Toda columna que pueda ser NULL debería ser una decisión consciente. Si un campo siempre
tiene valor, marcalo NOT NULL desde el principio. created_at TIMESTAMPTZ NOT NULL
DEFAULT NOW() es la configuración por defecto de cualquier tabla nueva.
CHECK constraints
CHECK (price >= 0), CHECK (status IN ('pending', 'paid')),
CHECK (start_date < end_date). Lo que tu app debería validar, validalo
también en la base. Es la última línea de defensa contra datos corruptos.
Foreign keys: CASCADE, RESTRICT, SET NULL
ON DELETE CASCADE borra los hijos cuando se borra el padre; útil para
relaciones de composición (orden → items). RESTRICT prohíbe el borrado
si hay hijos; lo más conservador. SET NULL deja el hijo huérfano; útil para
autores y posts si querés mantener el post.
Índices: cuándo y cuáles
Indexá columnas usadas en WHERE, JOIN, ORDER BY. Para igualdad y rangos, B-tree (default).
Para búsquedas full-text, GIN en Postgres. Para arrays y JSON, también GIN. Demasiados
índices ralentizan los INSERT y ocupan espacio: monitoreá con pg_stat_user_indexes
cuáles no se usan y borralos.
Soft delete
Si vas a "borrar" registros pero querés conservarlos, agregá deleted_at TIMESTAMPTZ
nullable. Tu app filtra WHERE deleted_at IS NULL en lecturas. El trade-off:
todas las queries necesitan ese filtro y los índices únicos requieren parciales.