- Configure workspace Git repository with comprehensive .gitignore - Add Odoo as submodule for ERP reference code - Include documentation: SETUP.md, GIT-STRUCTURE.md - Add gitignore templates for projects (backend, frontend, database) - Structure supports independent repos per project/subproject level Workspace includes: - core/ - Reusable patterns, modules, orchestration system - projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.) - knowledge-base/ - Reference code and patterns (includes Odoo submodule) - devtools/ - Development tools and templates - customers/ - Client implementations template 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
728 lines
18 KiB
Markdown
728 lines
18 KiB
Markdown
# DIRECTIVA: DISEÑO DE BASE DE DATOS Y NORMALIZACIÓN
|
|
|
|
**Versión:** 1.0.0
|
|
**Fecha:** 2025-11-29
|
|
**Fuente:** Consolidado desde orchestration/directivas/DIRECTIVA-DISENO-BASE-DATOS.md
|
|
**Ámbito:** Database-Agent y desarrolladores
|
|
**Stack:** PostgreSQL 15+ con PostGIS
|
|
|
|
---
|
|
|
|
## PROPÓSITO
|
|
|
|
Establecer criterios claros de diseño de base de datos que garanticen:
|
|
- **Normalización adecuada** sin sacrificar performance
|
|
- **Escalabilidad** para agregar nuevos módulos
|
|
- **Integridad** de datos con constraints apropiados
|
|
- **Performance óptima** con indexación estratégica
|
|
- **Mantenibilidad** a largo plazo
|
|
|
|
---
|
|
|
|
## PROCESO DE IMPLEMENTACIÓN
|
|
|
|
### Política de Carga Limpia (DDL-First)
|
|
|
|
**TODO diseño de esta directiva DEBE implementarse siguiendo:**
|
|
|
|
1. **Crear/actualizar archivo DDL** en `apps/database/ddl/schemas/{schema}/`
|
|
2. **Validar con recreación completa:** `./drop-and-recreate-database.sh`
|
|
3. **NUNCA** ejecutar CREATE/ALTER directamente sin archivo DDL
|
|
4. **NUNCA** crear migrations incrementales
|
|
|
|
### Ejemplo de Implementación Correcta
|
|
|
|
```sql
|
|
-- ✅ CORRECTO: Diseño + Proceso DDL-First
|
|
-- File: apps/database/ddl/schemas/gamification_system/tables/05-missions.sql
|
|
|
|
DROP TABLE IF EXISTS gamification_system.missions CASCADE;
|
|
|
|
CREATE TABLE gamification_system.missions (
|
|
-- Primary Key (UUID estándar)
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
-- Normalización 3NF
|
|
code VARCHAR(50) NOT NULL UNIQUE,
|
|
name VARCHAR(200) NOT NULL,
|
|
mission_type VARCHAR(20) NOT NULL,
|
|
|
|
-- Foreign Key con nomenclatura correcta
|
|
category_id UUID,
|
|
CONSTRAINT fk_missions_to_categories
|
|
FOREIGN KEY (category_id)
|
|
REFERENCES gamification_system.mission_categories(id)
|
|
ON DELETE SET NULL,
|
|
|
|
-- Check Constraint para validar valores
|
|
CONSTRAINT chk_missions_type_valid
|
|
CHECK (mission_type IN ('daily', 'weekly', 'special')),
|
|
|
|
-- Auditoría obligatoria
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Índices estratégicos
|
|
CREATE INDEX idx_missions_type ON gamification_system.missions(mission_type);
|
|
CREATE INDEX idx_missions_category_id ON gamification_system.missions(category_id);
|
|
|
|
-- Comentarios
|
|
COMMENT ON TABLE gamification_system.missions IS 'Misiones del sistema de gamificación';
|
|
```
|
|
|
|
```sql
|
|
-- ❌ INCORRECTO: Viola Política de Carga Limpia
|
|
psql -d gamilit_platform -c "CREATE TABLE gamification_system.missions (...);"
|
|
-- Crear tabla directamente sin archivo DDL
|
|
```
|
|
|
|
---
|
|
|
|
## NIVELES DE NORMALIZACIÓN
|
|
|
|
### Nivel Mínimo: Tercera Forma Normal (3NF)
|
|
|
|
**OBLIGATORIO:** Todas las tablas DEBEN cumplir mínimo 3NF.
|
|
|
|
### Primera Forma Normal (1NF)
|
|
|
|
```yaml
|
|
Requisitos:
|
|
- Valores atómicos (no listas/arrays en columnas)
|
|
- Cada columna contiene un solo tipo de dato
|
|
- Cada fila es única (tiene PK)
|
|
- No hay grupos repetitivos
|
|
```
|
|
|
|
**✅ Correcto (1NF)**
|
|
|
|
```sql
|
|
CREATE TABLE exercises (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
code VARCHAR(50) UNIQUE NOT NULL,
|
|
name VARCHAR(200) NOT NULL,
|
|
difficulty VARCHAR(20) NOT NULL,
|
|
-- Cada columna tiene valor atómico
|
|
created_by_id UUID NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
```
|
|
|
|
**❌ Viola 1NF**
|
|
|
|
```sql
|
|
CREATE TABLE exercises (
|
|
id UUID PRIMARY KEY,
|
|
code VARCHAR(50),
|
|
-- ❌ Múltiples valores en una columna
|
|
tags TEXT, -- "tag1,tag2,tag3"
|
|
-- ❌ Grupos repetitivos
|
|
hint1 TEXT,
|
|
hint2 TEXT,
|
|
hint3 TEXT
|
|
);
|
|
```
|
|
|
|
### Segunda Forma Normal (2NF)
|
|
|
|
```yaml
|
|
Requisitos:
|
|
- Cumple 1NF
|
|
- No hay dependencias parciales (todos los atributos no-key dependen de TODA la PK)
|
|
```
|
|
|
|
### Tercera Forma Normal (3NF)
|
|
|
|
```yaml
|
|
Requisitos:
|
|
- Cumple 2NF
|
|
- No hay dependencias transitivas (atributos no-key NO dependen de otros atributos no-key)
|
|
```
|
|
|
|
**✅ Correcto (3NF)**
|
|
|
|
```sql
|
|
-- Tabla exercise_attempts cumple 3NF
|
|
CREATE TABLE exercise_attempts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL,
|
|
exercise_id UUID NOT NULL,
|
|
score INTEGER NOT NULL,
|
|
xp_earned INTEGER NOT NULL,
|
|
-- Referencia a otra tabla (no duplica datos)
|
|
|
|
CONSTRAINT fk_attempts_to_users
|
|
FOREIGN KEY (user_id) REFERENCES auth_management.users(id),
|
|
|
|
CONSTRAINT fk_attempts_to_exercises
|
|
FOREIGN KEY (exercise_id) REFERENCES educational_content.exercises(id)
|
|
);
|
|
```
|
|
|
|
**❌ Viola 3NF**
|
|
|
|
```sql
|
|
CREATE TABLE exercise_attempts (
|
|
id UUID PRIMARY KEY,
|
|
user_id UUID,
|
|
-- ❌ user_email depende de user_id (transitiva)
|
|
user_email VARCHAR(100),
|
|
-- ❌ user_name depende de user_id (transitiva)
|
|
user_name VARCHAR(200),
|
|
exercise_id UUID,
|
|
score INTEGER
|
|
);
|
|
|
|
-- ✅ Solución: Solo guardar user_id y hacer JOIN con users
|
|
```
|
|
|
|
---
|
|
|
|
## CUÁNDO DESNORMALIZAR
|
|
|
|
La desnormalización es **PERMITIDA** solo en estos casos:
|
|
|
|
### 1. Performance Crítico con Queries Frecuentes
|
|
|
|
```sql
|
|
-- ✅ Permitido: Columna desnormalizada para evitar JOIN costoso
|
|
CREATE TABLE exercise_attempts (
|
|
id UUID PRIMARY KEY,
|
|
user_id UUID NOT NULL,
|
|
exercise_id UUID NOT NULL,
|
|
|
|
-- Desnormalizado para performance (dashboard usado 10,000 veces/día)
|
|
exercise_type VARCHAR(50) NOT NULL, -- Duplica exercises.exercise_type
|
|
|
|
CONSTRAINT fk_attempts_to_exercises
|
|
FOREIGN KEY (exercise_id) REFERENCES educational_content.exercises(id)
|
|
);
|
|
|
|
-- IMPORTANTE: Mantener sincronizado con trigger
|
|
CREATE OR REPLACE FUNCTION sync_exercise_type()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF (TG_OP = 'UPDATE' AND OLD.exercise_type <> NEW.exercise_type) THEN
|
|
UPDATE exercise_attempts
|
|
SET exercise_type = NEW.exercise_type
|
|
WHERE exercise_id = NEW.id;
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Documentar en comentario
|
|
COMMENT ON COLUMN exercise_attempts.exercise_type IS
|
|
'Desnormalizado de exercises.exercise_type para performance. Sincronizado con trigger.';
|
|
```
|
|
|
|
**Requisitos para desnormalizar:**
|
|
|
|
```yaml
|
|
Obligatorio documentar:
|
|
- Razón de desnormalización (performance, query frecuente)
|
|
- Tabla/columna origen
|
|
- Mecanismo de sincronización (trigger, app logic)
|
|
|
|
Obligatorio implementar:
|
|
- Trigger o lógica de sincronización
|
|
- Test de sincronización
|
|
- Comentario SQL explicando desnormalización
|
|
```
|
|
|
|
### 2. Agregaciones Precalculadas
|
|
|
|
```sql
|
|
-- ✅ Permitido: Columnas de resumen para dashboards
|
|
CREATE TABLE gamification_system.user_stats (
|
|
id UUID PRIMARY KEY,
|
|
user_id UUID NOT NULL UNIQUE,
|
|
|
|
-- Agregaciones precalculadas (actualizadas con triggers)
|
|
total_xp INTEGER DEFAULT 0,
|
|
exercises_completed INTEGER DEFAULT 0,
|
|
current_streak INTEGER DEFAULT 0,
|
|
ml_coins INTEGER DEFAULT 0,
|
|
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Trigger para mantener total_xp actualizado
|
|
CREATE OR REPLACE FUNCTION update_user_xp_on_attempt()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
UPDATE gamification_system.user_stats
|
|
SET total_xp = total_xp + NEW.xp_earned,
|
|
exercises_completed = exercises_completed + 1
|
|
WHERE user_id = NEW.user_id;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
```
|
|
|
|
### 3. Auditoría/Históricos
|
|
|
|
```sql
|
|
-- ✅ Permitido: Snapshot de datos para auditoría
|
|
CREATE TABLE audit_logging.exercise_submission_history (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
submission_id UUID NOT NULL,
|
|
|
|
-- Snapshot del ejercicio en ese momento (desnormalizado)
|
|
exercise_code VARCHAR(50) NOT NULL,
|
|
exercise_name VARCHAR(200) NOT NULL,
|
|
|
|
old_status VARCHAR(20),
|
|
new_status VARCHAR(20) NOT NULL,
|
|
changed_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
COMMENT ON TABLE audit_logging.exercise_submission_history IS
|
|
'Histórico de cambios de status. Incluye snapshot desnormalizado para preservar valores al momento del cambio.';
|
|
```
|
|
|
|
---
|
|
|
|
## DISEÑO DE SCHEMAS
|
|
|
|
### Organización por Contexto de Negocio
|
|
|
|
**Principio:** Un schema por contexto de negocio (Bounded Context)
|
|
|
|
```yaml
|
|
Schemas de GAMILIT:
|
|
auth_management: Usuarios, roles, permisos, tenants, sesiones
|
|
educational_content: Módulos, ejercicios, rubrics, media
|
|
gamification_system: Achievements, rangos, ML coins, comodines, missions
|
|
progress_tracking: Intentos, submissions, progreso de módulos
|
|
social_features: Escuelas, aulas, equipos, amistades
|
|
notifications: Notificaciones multi-canal
|
|
content_management: Templates, media, contenido
|
|
audit_logging: Logs y auditoría del sistema
|
|
admin_dashboard: Vistas materializadas para admin
|
|
```
|
|
|
|
**✅ Correcto**
|
|
|
|
```sql
|
|
-- Schema bien definido por contexto
|
|
CREATE SCHEMA IF NOT EXISTS gamification_system;
|
|
COMMENT ON SCHEMA gamification_system IS
|
|
'Sistema de gamificación: achievements, rangos, ML coins, comodines, missions.';
|
|
|
|
CREATE TABLE gamification_system.maya_ranks (...);
|
|
CREATE TABLE gamification_system.achievements (...);
|
|
CREATE TABLE gamification_system.user_stats (...);
|
|
CREATE TABLE gamification_system.missions (...);
|
|
```
|
|
|
|
**❌ Incorrecto**
|
|
|
|
```sql
|
|
-- ❌ Mezclar contextos en un schema
|
|
CREATE SCHEMA general_data;
|
|
|
|
CREATE TABLE general_data.exercises (...); -- ❌ Contexto educativo
|
|
CREATE TABLE general_data.users (...); -- ❌ Contexto auth
|
|
CREATE TABLE general_data.missions (...); -- ❌ Contexto gamificación
|
|
```
|
|
|
|
---
|
|
|
|
## CLAVES Y CONSTRAINTS
|
|
|
|
### Primary Keys
|
|
|
|
```yaml
|
|
Estándar obligatorio:
|
|
- Tipo: UUID v4
|
|
- Columna: id
|
|
- Default: gen_random_uuid()
|
|
- Nunca usar SERIAL/INTEGER (problemas al escalar)
|
|
```
|
|
|
|
**✅ Correcto**
|
|
|
|
```sql
|
|
CREATE TABLE exercises (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
-- resto de columnas
|
|
);
|
|
```
|
|
|
|
**❌ Incorrecto**
|
|
|
|
```sql
|
|
-- ❌ SERIAL no escalable
|
|
CREATE TABLE exercises (
|
|
id SERIAL PRIMARY KEY,
|
|
-- ...
|
|
);
|
|
```
|
|
|
|
### Foreign Keys
|
|
|
|
```yaml
|
|
Nomenclatura obligatoria:
|
|
fk_{tabla_origen}_to_{tabla_destino}
|
|
|
|
Reglas:
|
|
- Siempre definir ON DELETE y ON UPDATE
|
|
- Preferir CASCADE o SET NULL según lógica de negocio
|
|
- Documentar razón de CASCADE vs SET NULL
|
|
```
|
|
|
|
**✅ Correcto**
|
|
|
|
```sql
|
|
CREATE TABLE exercise_attempts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
exercise_id UUID NOT NULL,
|
|
|
|
CONSTRAINT fk_attempts_to_exercises
|
|
FOREIGN KEY (exercise_id)
|
|
REFERENCES educational_content.exercises(id)
|
|
ON DELETE CASCADE -- Si se elimina ejercicio, eliminar intentos
|
|
ON UPDATE CASCADE
|
|
);
|
|
|
|
COMMENT ON CONSTRAINT fk_attempts_to_exercises ON exercise_attempts IS
|
|
'CASCADE porque attempts dependen de exercise (sin exercise no tienen sentido).';
|
|
```
|
|
|
|
### Check Constraints
|
|
|
|
```yaml
|
|
Nomenclatura:
|
|
chk_{tabla}_{columna}_{descripción}
|
|
|
|
Usar para:
|
|
- Validar enums/estados
|
|
- Rangos válidos
|
|
- Reglas de negocio simples
|
|
```
|
|
|
|
```sql
|
|
CREATE TABLE gamification_system.maya_ranks (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
rank_level INTEGER NOT NULL,
|
|
min_xp INTEGER NOT NULL,
|
|
|
|
CONSTRAINT chk_maya_ranks_level_range
|
|
CHECK (rank_level >= 1 AND rank_level <= 7),
|
|
|
|
CONSTRAINT chk_maya_ranks_min_xp_positive
|
|
CHECK (min_xp >= 0)
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## INDEXACIÓN ESTRATÉGICA
|
|
|
|
### Índices Obligatorios
|
|
|
|
```yaml
|
|
Siempre crear índice para:
|
|
1. Foreign Keys (para JOINs)
|
|
2. Columnas en WHERE frecuentes
|
|
3. Columnas en ORDER BY frecuentes
|
|
4. Columnas UNIQUE (automático)
|
|
5. Columnas de búsqueda (name, code, email)
|
|
```
|
|
|
|
### Nomenclatura de Índices
|
|
|
|
```yaml
|
|
Formato:
|
|
idx_{tabla}_{columna(s)}_{tipo}
|
|
|
|
Tipos:
|
|
(sin sufijo): BTREE (default)
|
|
_gin: GIN (full-text search, JSONB)
|
|
_gist: GIST (PostGIS, rangos)
|
|
```
|
|
|
|
**✅ Correcto**
|
|
|
|
```sql
|
|
CREATE TABLE educational_content.exercises (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
code VARCHAR(50) UNIQUE NOT NULL,
|
|
name VARCHAR(200) NOT NULL,
|
|
module_id UUID NOT NULL,
|
|
exercise_type VARCHAR(50) NOT NULL,
|
|
difficulty VARCHAR(20) NOT NULL,
|
|
metadata JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- FK index (para JOINs)
|
|
CREATE INDEX idx_exercises_module_id
|
|
ON educational_content.exercises(module_id);
|
|
|
|
-- Type (WHERE frecuente)
|
|
CREATE INDEX idx_exercises_type
|
|
ON educational_content.exercises(exercise_type);
|
|
|
|
-- Difficulty (WHERE frecuente)
|
|
CREATE INDEX idx_exercises_difficulty
|
|
ON educational_content.exercises(difficulty);
|
|
|
|
-- JSONB (GIN)
|
|
CREATE INDEX idx_exercises_metadata_gin
|
|
ON educational_content.exercises USING GIN(metadata);
|
|
|
|
-- Compuesto (queries con múltiples filtros)
|
|
CREATE INDEX idx_exercises_module_type
|
|
ON educational_content.exercises(module_id, exercise_type);
|
|
```
|
|
|
|
### Índices Parciales
|
|
|
|
```yaml
|
|
Usar cuando:
|
|
- Queries filtran por valor específico frecuentemente
|
|
- Reduce tamaño del índice
|
|
- Mejora performance de queries específicos
|
|
```
|
|
|
|
```sql
|
|
-- Índice parcial para ejercicios publicados (query más frecuente)
|
|
CREATE INDEX idx_exercises_published_created
|
|
ON educational_content.exercises(created_at DESC)
|
|
WHERE status = 'published';
|
|
|
|
-- Query optimizado
|
|
SELECT * FROM educational_content.exercises
|
|
WHERE status = 'published' -- Usa índice parcial
|
|
ORDER BY created_at DESC
|
|
LIMIT 20;
|
|
```
|
|
|
|
### Índices a Evitar
|
|
|
|
```yaml
|
|
NO crear índice para:
|
|
- Columnas con muy baja cardinalidad (ej: boolean)
|
|
- Columnas nunca usadas en WHERE/ORDER BY
|
|
- Tablas muy pequeñas (<1000 rows)
|
|
- Todas las columnas (over-indexing)
|
|
```
|
|
|
|
---
|
|
|
|
## TIMESTAMPS Y AUDITORÍA
|
|
|
|
### Columnas Estándar de Auditoría
|
|
|
|
```yaml
|
|
Obligatorio en TODAS las tablas:
|
|
- created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
- updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
- created_by_id UUID (FK a users, si aplica)
|
|
|
|
Opcional según necesidad:
|
|
- deleted_at TIMESTAMPTZ (soft delete)
|
|
- deleted_by_id UUID (soft delete)
|
|
```
|
|
|
|
**✅ Correcto**
|
|
|
|
```sql
|
|
CREATE TABLE educational_content.exercises (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
code VARCHAR(50) UNIQUE NOT NULL,
|
|
name VARCHAR(200) NOT NULL,
|
|
|
|
-- Auditoría obligatoria
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
created_by_id UUID NOT NULL,
|
|
|
|
CONSTRAINT fk_exercises_created_by
|
|
FOREIGN KEY (created_by_id) REFERENCES auth_management.users(id)
|
|
ON DELETE SET NULL
|
|
);
|
|
|
|
-- Trigger para updated_at automático
|
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = now();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_exercises_updated_at
|
|
BEFORE UPDATE ON educational_content.exercises
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_at_column();
|
|
```
|
|
|
|
### Soft Delete
|
|
|
|
```sql
|
|
-- Soft delete (recomendado para datos importantes)
|
|
CREATE TABLE educational_content.exercises (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
code VARCHAR(50) NOT NULL,
|
|
name VARCHAR(200) NOT NULL,
|
|
|
|
-- Soft delete
|
|
deleted_at TIMESTAMPTZ,
|
|
deleted_by_id UUID,
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Índice parcial para queries de activos
|
|
CREATE INDEX idx_exercises_active
|
|
ON educational_content.exercises(created_at DESC)
|
|
WHERE deleted_at IS NULL;
|
|
|
|
-- View para acceso fácil a ejercicios activos
|
|
CREATE VIEW educational_content.exercises_active AS
|
|
SELECT *
|
|
FROM educational_content.exercises
|
|
WHERE deleted_at IS NULL;
|
|
```
|
|
|
|
---
|
|
|
|
## PERFORMANCE Y OPTIMIZACIÓN
|
|
|
|
### Particionamiento
|
|
|
|
```yaml
|
|
Considerar particionamiento cuando:
|
|
- Tabla > 10 millones de registros
|
|
- Queries filtran por rango de fechas frecuentemente
|
|
- Archivado regular de datos antiguos
|
|
```
|
|
|
|
**Ejemplo: Particionamiento por rango de fechas**
|
|
|
|
```sql
|
|
-- Tabla particionada para exercise_attempts
|
|
CREATE TABLE progress_tracking.exercise_attempts_partitioned (
|
|
id UUID DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL,
|
|
exercise_id UUID NOT NULL,
|
|
score INTEGER NOT NULL,
|
|
completed_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
) PARTITION BY RANGE (completed_at);
|
|
|
|
-- Particiones por mes
|
|
CREATE TABLE progress_tracking.exercise_attempts_2025_01
|
|
PARTITION OF progress_tracking.exercise_attempts_partitioned
|
|
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
|
|
|
|
CREATE TABLE progress_tracking.exercise_attempts_2025_02
|
|
PARTITION OF progress_tracking.exercise_attempts_partitioned
|
|
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
|
|
```
|
|
|
|
### Vistas Materializadas
|
|
|
|
```yaml
|
|
Usar MATERIALIZED VIEW cuando:
|
|
- Query complejo ejecutado frecuentemente
|
|
- Datos cambian poco (actualizar periódicamente)
|
|
- Performance crítico (dashboards)
|
|
```
|
|
|
|
**Ejemplo: Vista materializada para leaderboard**
|
|
|
|
```sql
|
|
-- Vista materializada para leaderboard global
|
|
CREATE MATERIALIZED VIEW gamification_system.v_leaderboard_global AS
|
|
SELECT
|
|
u.id AS user_id,
|
|
u.username,
|
|
u.display_name,
|
|
COALESCE(us.total_xp, 0) AS total_xp,
|
|
COALESCE(us.exercises_completed, 0) AS exercises_completed,
|
|
COALESCE(us.current_streak, 0) AS current_streak,
|
|
mr.name AS maya_rank,
|
|
ROW_NUMBER() OVER (ORDER BY COALESCE(us.total_xp, 0) DESC) AS rank_position
|
|
FROM auth_management.users u
|
|
LEFT JOIN gamification_system.user_stats us ON us.user_id = u.id
|
|
LEFT JOIN gamification_system.user_ranks ur ON ur.user_id = u.id
|
|
LEFT JOIN gamification_system.maya_ranks mr ON mr.id = ur.rank_id
|
|
WHERE u.deleted_at IS NULL
|
|
ORDER BY total_xp DESC;
|
|
|
|
-- Índice en vista materializada
|
|
CREATE INDEX idx_v_leaderboard_global_xp
|
|
ON gamification_system.v_leaderboard_global(total_xp DESC);
|
|
|
|
-- Refrescar periódicamente (ej: cada hora via cron)
|
|
REFRESH MATERIALIZED VIEW CONCURRENTLY gamification_system.v_leaderboard_global;
|
|
```
|
|
|
|
---
|
|
|
|
## CHECKLIST DE DISEÑO DE TABLA
|
|
|
|
```markdown
|
|
Antes de crear tabla, verificar:
|
|
|
|
**Normalización:**
|
|
- [ ] ¿Cumple 3NF?
|
|
- [ ] ¿Hay dependencias transitivas?
|
|
- [ ] Si desnormaliza, ¿está justificado y documentado?
|
|
|
|
**Primary Key:**
|
|
- [ ] ¿Usa UUID v4?
|
|
- [ ] ¿Columna se llama "id"?
|
|
- [ ] ¿Tiene DEFAULT gen_random_uuid()?
|
|
|
|
**Foreign Keys:**
|
|
- [ ] ¿Nomenclatura fk_{origen}_to_{destino}?
|
|
- [ ] ¿Tiene ON DELETE y ON UPDATE definidos?
|
|
- [ ] ¿Está documentada la razón de CASCADE vs SET NULL?
|
|
|
|
**Constraints:**
|
|
- [ ] ¿UNIQUE en códigos de negocio?
|
|
- [ ] ¿CHECK para enums/rangos?
|
|
- [ ] ¿NOT NULL en columnas obligatorias?
|
|
|
|
**Índices:**
|
|
- [ ] ¿Índice en cada FK?
|
|
- [ ] ¿Índice en columnas de búsqueda (WHERE)?
|
|
- [ ] ¿Índice en columnas de ordenamiento (ORDER BY)?
|
|
- [ ] ¿Sin over-indexing?
|
|
|
|
**Auditoría:**
|
|
- [ ] ¿Tiene created_at, updated_at?
|
|
- [ ] ¿Tiene created_by_id (si aplica)?
|
|
- [ ] ¿Trigger para updated_at automático?
|
|
|
|
**Documentación:**
|
|
- [ ] ¿Comentario en tabla (COMMENT ON TABLE)?
|
|
- [ ] ¿Comentario en columnas importantes?
|
|
- [ ] ¿Comentario en constraints especiales?
|
|
|
|
**Performance:**
|
|
- [ ] ¿Necesita particionamiento?
|
|
- [ ] ¿Queries principales optimizados?
|
|
```
|
|
|
|
---
|
|
|
|
## REFERENCIAS
|
|
|
|
- [PostgreSQL Documentation](https://www.postgresql.org/docs/15/)
|
|
- [PostGIS Documentation](https://postgis.net/documentation/)
|
|
- [Database Normalization](https://en.wikipedia.org/wiki/Database_normalization)
|
|
- [docs/98-standards/NAMING-CONVENTIONS-COMPLETE.md](../98-standards/NAMING-CONVENTIONS-COMPLETE.md)
|
|
|
|
---
|
|
|
|
**Última actualización:** 2025-11-29
|
|
**Fuente original:** orchestration/directivas/DIRECTIVA-DISENO-BASE-DATOS.md
|
|
**Mantenido por:** Architecture-Analyst
|