# DIRECTIVA: DISENO DE BASE DE DATOS Y NORMALIZACION

**Ambito:** GLOBAL - Todos los proyectos del workspace
**Version:** 2.0.0
**Fecha:** 2025-12-05
**Tipo:** Directiva Obligatoria
**Stack:** PostgreSQL 15+

---

## PROPOSITO

Establecer criterios claros de diseno de base de datos que garanticen:
- **Normalizacion adecuada** sin sacrificar performance
- **Escalabilidad** para agregar nuevos modulos
- **Integridad** de datos con constraints apropiados
- **Performance optima** con indexacion estrategica
- **Mantenibilidad** a largo plazo

---

## ALCANCE Y PROCESO DE IMPLEMENTACION

### Que cubre esta directiva

**Esta directiva define QUE disenar:**
- Niveles de normalizacion (3NF minimo)
- Cuando desnormalizar (performance critico)
- Diseno de schemas y contextos
- Claves, constraints e indices
- Tipos de datos y estructuras

**Esta directiva NO cubre COMO implementar:**
- Proceso de creacion/modificacion de DDL
- Flujo de trabajo para cambios en BD
- Validacion y deployment

### Como implementar los disenos de esta directiva

**IMPORTANTE:** TODO diseno documentado aqui DEBE implementarse siguiendo:

**DIRECTIVA-POLITICA-CARGA-LIMPIA.md** - Proceso DDL-First
- Crear/actualizar archivo DDL en `{DB_DDL_PATH}/schemas/{schema}/`
- Validar con recreacion completa
- **NUNCA** ejecutar CREATE/ALTER directamente sin archivo DDL
- **NUNCA** crear migrations incrementales

### Regla de oro

```yaml
Esta directiva te dice QUE crear:
  - Tablas normalizadas (3NF)
  - Constraints apropiados
  - Indices estrategicos

DIRECTIVA-POLITICA-CARGA-LIMPIA.md te dice COMO crearlo:
  - DDL primero
  - Validar con recreacion
  - NO migrations
```

---

## NIVELES DE NORMALIZACION

### Nivel Minimo: Tercera Forma Normal (3NF)

**OBLIGATORIO:** Todas las tablas DEBEN cumplir minimo 3NF.

#### Primera Forma Normal (1NF)

```yaml
Requisitos:
  - Valores atomicos (no listas/arrays en columnas)
  - Cada columna contiene un solo tipo de dato
  - Cada fila es unica (tiene PK)
  - No hay grupos repetitivos
```

**Correcto (1NF)**

```sql
-- Tabla cumple 1NF
CREATE TABLE {schema}.projects (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  code VARCHAR(50) UNIQUE NOT NULL,
  name VARCHAR(200) NOT NULL,
  status VARCHAR(20) NOT NULL,
  -- Cada columna tiene valor atomico
  created_by_id UUID NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

**Incorrecto (Viola 1NF)**

```sql
-- Viola 1NF: columna con lista de valores
CREATE TABLE {schema}.projects (
  id UUID PRIMARY KEY,
  code VARCHAR(50),
  name VARCHAR(200),
  -- Multiples valores en una columna
  responsible_users TEXT,  -- "user1,user2,user3"
  -- Grupos repetitivos
  phone1 VARCHAR(20),
  phone2 VARCHAR(20),
  phone3 VARCHAR(20)
);
```

#### Segunda Forma Normal (2NF)

```yaml
Requisitos:
  - Cumple 1NF
  - No hay dependencias parciales (todos los atributos no-key dependen de TODA la PK)
```

**Correcto (2NF)**

```sql
-- Tabla cumple 2NF
CREATE TABLE {schema}.project_budgets (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  project_id UUID NOT NULL,
  version INTEGER NOT NULL,
  total_amount DECIMAL(15,2) NOT NULL,
  -- Todos los atributos dependen de id (PK completa)
  approved_at TIMESTAMPTZ,
  approved_by_id UUID,

  CONSTRAINT fk_project_budgets_to_projects
    FOREIGN KEY (project_id) REFERENCES {schema}.projects(id)
);

-- Indice para queries frecuentes
CREATE INDEX idx_project_budgets_project_id
  ON {schema}.project_budgets(project_id);
```

**Incorrecto (Viola 2NF)**

```sql
-- Viola 2NF con PK compuesta
CREATE TABLE {schema}.project_budgets (
  project_id UUID,
  version INTEGER,
  total_amount DECIMAL(15,2),
  -- project_name depende solo de project_id, no de (project_id, version)
  project_name VARCHAR(200),
  -- project_status depende solo de project_id
  project_status VARCHAR(20),

  PRIMARY KEY (project_id, version)
);

-- Solucion: Mover project_name y project_status a tabla projects
-- y usar FK desde project_budgets
```

#### 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 projects cumple 3NF
CREATE TABLE {schema}.projects (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  code VARCHAR(50) UNIQUE NOT NULL,
  name VARCHAR(200) NOT NULL,
  -- Referencia a otra tabla (no duplica datos)
  created_by_id UUID NOT NULL,

  CONSTRAINT fk_projects_to_users
    FOREIGN KEY (created_by_id) REFERENCES {auth_schema}.users(id)
);

-- Tabla users separada (no transitiva)
CREATE TABLE {auth_schema}.users (
  id UUID PRIMARY KEY,
  email VARCHAR(100) UNIQUE NOT NULL,
  full_name VARCHAR(200) NOT NULL,
  department VARCHAR(100)
);
```

**Incorrecto (Viola 3NF)**

```sql
-- Viola 3NF: dependencia transitiva
CREATE TABLE {schema}.projects (
  id UUID PRIMARY KEY,
  code VARCHAR(50),
  name VARCHAR(200),
  created_by_id UUID,
  -- created_by_email depende de created_by_id (transitiva)
  created_by_email VARCHAR(100),
  -- created_by_name depende de created_by_id (transitiva)
  created_by_name VARCHAR(200),
  -- created_by_department depende de created_by_id (transitiva)
  created_by_department VARCHAR(100)
);

-- Solucion: Solo guardar created_by_id y hacer JOIN con users
```

---

## CUANDO DESNORMALIZAR

La desnormalizacion es **PERMITIDA** solo en estos casos:

### 1. Performance Critico con Queries Frecuentes

```sql
-- Permitido: Columna desnormalizada para evitar JOIN costoso
CREATE TABLE {schema}.project_budgets (
  id UUID PRIMARY KEY,
  project_id UUID NOT NULL,
  total_amount DECIMAL(15,2) NOT NULL,

  -- Desnormalizado para performance (query usado 10,000 veces/dia)
  project_code VARCHAR(50) NOT NULL,  -- Duplica projects.code

  CONSTRAINT fk_project_budgets_to_projects
    FOREIGN KEY (project_id) REFERENCES {schema}.projects(id)
);

-- IMPORTANTE: Mantener sincronizado con trigger
CREATE OR REPLACE FUNCTION sync_project_code()
RETURNS TRIGGER AS $$
BEGIN
  IF (TG_OP = 'UPDATE' AND OLD.code <> NEW.code) THEN
    UPDATE {schema}.project_budgets
    SET project_code = NEW.code
    WHERE project_id = NEW.id;
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_sync_project_code
AFTER UPDATE ON {schema}.projects
FOR EACH ROW
EXECUTE FUNCTION sync_project_code();

-- Documentar en comentario
COMMENT ON COLUMN {schema}.project_budgets.project_code IS
'Desnormalizado de projects.code para performance. Sincronizado con trigger trg_sync_project_code.';
```

**Requisitos para desnormalizar:**
```yaml
Obligatorio documentar:
  - Razon de desnormalizacion (performance, query frecuente)
  - Tabla/columna origen
  - Mecanismo de sincronizacion (trigger, app logic)
  - Frecuencia de query que justifica desnormalizacion

Obligatorio implementar:
  - Trigger o logica de sincronizacion
  - Test de sincronizacion
  - Comentario SQL explicando desnormalizacion
```

### 2. Agregaciones Precalculadas

```sql
-- Permitido: Columnas de resumen para dashboards
CREATE TABLE {schema}.projects (
  id UUID PRIMARY KEY,
  code VARCHAR(50) NOT NULL,
  name VARCHAR(200) NOT NULL,

  -- Agregaciones precalculadas (actualizadas con triggers)
  total_items INTEGER DEFAULT 0,
  total_amount DECIMAL(15,2) DEFAULT 0,

  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Trigger para mantener contadores actualizados
CREATE OR REPLACE FUNCTION update_project_count()
RETURNS TRIGGER AS $$
BEGIN
  IF (TG_OP = 'INSERT') THEN
    UPDATE {schema}.projects
    SET total_items = total_items + 1
    WHERE id = NEW.project_id;
  ELSIF (TG_OP = 'DELETE') THEN
    UPDATE {schema}.projects
    SET total_items = total_items - 1
    WHERE id = OLD.project_id;
  END IF;
  RETURN NULL;
END;
$$ LANGUAGE plpgsql;
```

### 3. Auditoria/Historicos

```sql
-- Permitido: Snapshot de datos para auditoria
CREATE TABLE {schema}.status_history (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  entity_id UUID NOT NULL,

  -- Snapshot del entity en ese momento (desnormalizado)
  entity_code VARCHAR(50) NOT NULL,
  entity_name VARCHAR(200) NOT NULL,

  old_status VARCHAR(20),
  new_status VARCHAR(20) NOT NULL,
  changed_by_id UUID NOT NULL,
  changed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

COMMENT ON TABLE {schema}.status_history IS
'Historico de cambios de status. Incluye snapshot desnormalizado de code y name para preservar valores al momento del cambio.';
```

---

## DISENO DE SCHEMAS

### Organizacion de Schemas

```yaml
Principio: Un schema por contexto de negocio (Bounded Context de DDD)

Ejemplo de schemas por contexto:
  auth_management: Usuarios, roles, permisos, tenants
  core_business: Entidades principales del dominio
  financial: Presupuestos, pagos, facturacion
  notification: Notificaciones, alertas, mensajeria
  audit: Logs, historicos, trazabilidad
```

**Correcto**

```sql
-- Schema bien definido por contexto
CREATE SCHEMA IF NOT EXISTS {domain_schema};
COMMENT ON SCHEMA {domain_schema} IS
'Descripcion clara del contexto de negocio que maneja este schema';

CREATE TABLE {domain_schema}.main_entity (...);
CREATE TABLE {domain_schema}.related_entity (...);
```

**Incorrecto**

```sql
-- Mezclar contextos en un schema
CREATE SCHEMA general_data;

CREATE TABLE general_data.projects (...);  -- Contexto proyecto
CREATE TABLE general_data.budgets (...);   -- Contexto financiero
CREATE TABLE general_data.contracts (...); -- Contexto compras
CREATE TABLE general_data.users (...);     -- Contexto auth
```

---

## CLAVES Y CONSTRAINTS

### Primary Keys

```yaml
Estandar obligatorio:
  - Tipo: UUID v4
  - Columna: id
  - Default: gen_random_uuid()
  - Nunca usar SERIAL/INTEGER (problemas al escalar)
```

**Correcto**

```sql
CREATE TABLE {schema}.entity (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  -- resto de columnas
);
```

**Incorrecto**

```sql
-- SERIAL no escalable
CREATE TABLE {schema}.entity (
  id SERIAL PRIMARY KEY,
  -- ...
);

-- PK compuesta sin UUID
CREATE TABLE {schema}.entity_relations (
  entity_a_id INTEGER,
  entity_b_id INTEGER,
  PRIMARY KEY (entity_a_id, entity_b_id)  -- Sin UUID
);
```

### Foreign Keys

```yaml
Nomenclatura obligatoria:
  fk_{tabla_origen}_to_{tabla_destino}

Reglas:
  - Siempre definir ON DELETE y ON UPDATE
  - Preferir CASCADE o SET NULL segun logica de negocio
  - Documentar razon de CASCADE vs SET NULL
```

**Correcto**

```sql
CREATE TABLE {schema}.child_entity (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  parent_id UUID NOT NULL,

  CONSTRAINT fk_child_to_parent
    FOREIGN KEY (parent_id)
    REFERENCES {schema}.parent_entity(id)
    ON DELETE CASCADE  -- Si se elimina parent, eliminar children
    ON UPDATE CASCADE
);

COMMENT ON CONSTRAINT fk_child_to_parent ON {schema}.child_entity IS
'CASCADE porque child es dependiente de parent (sin parent no tiene sentido).';

CREATE TABLE {schema}.entity (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  created_by_id UUID NOT NULL,

  CONSTRAINT fk_entity_to_users
    FOREIGN KEY (created_by_id)
    REFERENCES {auth_schema}.users(id)
    ON DELETE SET NULL  -- Si se elimina usuario, entity permanece
    ON UPDATE CASCADE
);

COMMENT ON CONSTRAINT fk_entity_to_users ON {schema}.entity IS
'SET NULL porque queremos preservar entity aunque usuario se elimine.';
```

### Unique Constraints

```yaml
Nomenclatura:
  uq_{tabla}_{columna(s)}

Usar para:
  - Codigos de negocio (code, slug)
  - Emails, usernames
  - Combinaciones unicas de negocio
```

**Correcto**

```sql
CREATE TABLE {schema}.entity (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  code VARCHAR(50) NOT NULL,
  name VARCHAR(200) NOT NULL,

  CONSTRAINT uq_entity_code UNIQUE (code)
);

-- Unique compuesto
CREATE TABLE {schema}.entity_versions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  entity_id UUID NOT NULL,
  version INTEGER NOT NULL,

  CONSTRAINT uq_entity_versions_entity_version
    UNIQUE (entity_id, version)
);
```

### Check Constraints

```yaml
Nomenclatura:
  chk_{tabla}_{columna}_{descripcion}

Usar para:
  - Validar enums/estados
  - Rangos validos
  - Reglas de negocio simples
```

**Correcto**

```sql
CREATE TABLE {schema}.entity (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  status VARCHAR(20) NOT NULL,
  progress_percentage DECIMAL(5,2) NOT NULL DEFAULT 0,

  CONSTRAINT chk_entity_status_valid
    CHECK (status IN ('draft', 'active', 'paused', 'completed', 'archived')),

  CONSTRAINT chk_entity_progress_range
    CHECK (progress_percentage >= 0 AND progress_percentage <= 100)
);
```

---

## INDEXACION ESTRATEGICA

### Indices Obligatorios

```yaml
Siempre crear indice para:
  1. Foreign Keys (para JOINs)
  2. Columnas en WHERE frecuentes
  3. Columnas en ORDER BY frecuentes
  4. Columnas UNIQUE (automatico)
  5. Columnas de busqueda (name, code, email)
```

### Nomenclatura de Indices

```yaml
Formato:
  idx_{tabla}_{columna(s)}_{tipo}

Tipos:
  (sin sufijo): BTREE (default)
  _gin: GIN (full-text search, JSONB)
  _gist: GIST (PostGIS, rangos)
  _hash: HASH (igualdad exacta, raro)
```

**Correcto**

```sql
CREATE TABLE {schema}.entity (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  code VARCHAR(50) UNIQUE NOT NULL,
  name VARCHAR(200) NOT NULL,
  status VARCHAR(20) NOT NULL,
  created_by_id UUID NOT NULL,
  coordinates GEOGRAPHY(POINT, 4326),  -- Si usa PostGIS
  metadata JSONB,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- FK index (para JOINs)
CREATE INDEX idx_entity_created_by_id
  ON {schema}.entity(created_by_id);

-- Status (WHERE frecuente)
CREATE INDEX idx_entity_status
  ON {schema}.entity(status);

-- Busqueda por nombre
CREATE INDEX idx_entity_name
  ON {schema}.entity(name);

-- PostGIS (GIST) - si aplica
CREATE INDEX idx_entity_coordinates_gist
  ON {schema}.entity USING GIST(coordinates);

-- JSONB (GIN) - si aplica
CREATE INDEX idx_entity_metadata_gin
  ON {schema}.entity USING GIN(metadata);

-- Compuesto (queries con multiples filtros)
CREATE INDEX idx_entity_status_created_at
  ON {schema}.entity(status, created_at DESC);
```

### Indices Parciales

```yaml
Usar cuando:
  - Queries filtran por valor especifico frecuentemente
  - Reduce tamano del indice
  - Mejora performance de queries especificos
```

**Correcto**

```sql
-- Indice parcial para entidades activas (query mas frecuente)
CREATE INDEX idx_entity_active_created_at
  ON {schema}.entity(created_at DESC)
  WHERE status = 'active';

-- Query optimizado
SELECT * FROM {schema}.entity
WHERE status = 'active'  -- Usa indice parcial
ORDER BY created_at DESC
LIMIT 20;
```

### Indices a Evitar

```yaml
NO crear indice para:
  - Columnas con muy baja cardinalidad (ej: boolean)
  - Columnas nunca usadas en WHERE/ORDER BY
  - Tablas muy pequenas (<1000 rows)
  - Todas las columnas (over-indexing)
```

---

## POSTGIS PARA GEOLOCALIZACION

### Tipo de Dato: GEOGRAPHY vs GEOMETRY

```yaml
Usar GEOGRAPHY:
  - Para coordenadas lat/lng
  - Calculos en metros (distancias reales)
  - SRID 4326 (WGS 84)

Usar GEOMETRY:
  - Para mapas planos
  - Calculos en unidades del mapa
  - Performance critico (mas rapido que GEOGRAPHY)
```

**Correcto**

```sql
-- GEOGRAPHY para coordenadas reales
CREATE TABLE {schema}.locations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  code VARCHAR(50) UNIQUE NOT NULL,
  name VARCHAR(200) NOT NULL,

  -- Coordenadas en lat/lng
  coordinates GEOGRAPHY(POINT, 4326),

  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Indice GIST para queries espaciales
CREATE INDEX idx_locations_coordinates_gist
  ON {schema}.locations USING GIST(coordinates);

-- Query: Ubicaciones a menos de 5km
SELECT id, name, code
FROM {schema}.locations
WHERE ST_DWithin(
  coordinates,
  ST_MakePoint(-99.1332, 19.4326)::GEOGRAPHY,
  5000  -- 5000 metros
);
```

### Funciones PostGIS Comunes

```sql
-- Distancia entre dos puntos (en metros)
SELECT ST_Distance(
  ST_MakePoint(-99.1, 19.4)::GEOGRAPHY,
  l.coordinates
) AS distance_meters
FROM {schema}.locations l;

-- Entidades dentro de poligono
SELECT *
FROM {schema}.locations
WHERE ST_Within(
  coordinates::GEOMETRY,
  ST_GeomFromGeoJSON('{"type":"Polygon","coordinates":[...]}')
);

-- Area de poligono (en m2)
SELECT ST_Area(polygon_column::GEOGRAPHY) AS area_sqm
FROM {schema}.areas;
```

---

## TIMESTAMPS Y AUDITORIA

### Columnas Estandar de Auditoria

```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)
  - updated_by_id UUID (FK a users, nullable)

Opcional segun necesidad:
  - deleted_at TIMESTAMPTZ (soft delete)
  - deleted_by_id UUID (soft delete)
```

**Correcto**

```sql
CREATE TABLE {schema}.entity (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  code VARCHAR(50) UNIQUE NOT NULL,
  name VARCHAR(200) NOT NULL,

  -- Auditoria obligatoria
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  created_by_id UUID NOT NULL,
  updated_by_id UUID,

  CONSTRAINT fk_entity_created_by
    FOREIGN KEY (created_by_id) REFERENCES {auth_schema}.users(id)
    ON DELETE SET NULL,

  CONSTRAINT fk_entity_updated_by
    FOREIGN KEY (updated_by_id) REFERENCES {auth_schema}.users(id)
    ON DELETE SET NULL
);

-- Trigger para updated_at automatico
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = now();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_entity_updated_at
BEFORE UPDATE ON {schema}.entity
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
```

### Soft Delete

```sql
-- Soft delete (recomendado para datos importantes)
CREATE TABLE {schema}.entity (
  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(),

  CONSTRAINT fk_entity_deleted_by
    FOREIGN KEY (deleted_by_id) REFERENCES {auth_schema}.users(id)
    ON DELETE SET NULL
);

-- Indice parcial para queries de activos
CREATE INDEX idx_entity_active
  ON {schema}.entity(created_at DESC)
  WHERE deleted_at IS NULL;

-- View para acceso facil a entidades activas
CREATE VIEW {schema}.entity_active AS
SELECT *
FROM {schema}.entity
WHERE deleted_at IS NULL;
```

---

## PERFORMANCE Y OPTIMIZACION

### 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
CREATE TABLE {schema}.event_log (
  id UUID DEFAULT gen_random_uuid(),
  entity_id UUID NOT NULL,
  event_type VARCHAR(50) NOT NULL,
  event_data JSONB,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
) PARTITION BY RANGE (created_at);

-- Particiones por mes
CREATE TABLE {schema}.event_log_2025_01
  PARTITION OF {schema}.event_log
  FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');

CREATE TABLE {schema}.event_log_2025_02
  PARTITION OF {schema}.event_log
  FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');

-- Indice en cada particion
CREATE INDEX idx_event_log_2025_01_entity_id
  ON {schema}.event_log_2025_01(entity_id);
```

### Materializacion de Vistas

```yaml
Usar MATERIALIZED VIEW cuando:
  - Query complejo ejecutado frecuentemente
  - Datos cambian poco (actualizar periodicamente)
  - Performance critico (dashboards)
```

**Ejemplo: Vista materializada para dashboard**

```sql
-- Vista materializada con agregaciones para dashboard
CREATE MATERIALIZED VIEW {schema}.dashboard_summary AS
SELECT
  e.id,
  e.code,
  e.name,
  COUNT(DISTINCT r.id) AS total_related,
  COALESCE(SUM(r.amount), 0) AS total_amount,
  COALESCE(AVG(r.score), 0) AS average_score
FROM {schema}.entity e
LEFT JOIN {schema}.related r ON r.entity_id = e.id
WHERE e.deleted_at IS NULL
GROUP BY e.id, e.code, e.name;

-- Indice en vista materializada
CREATE INDEX idx_dashboard_summary_code
  ON {schema}.dashboard_summary(code);

-- Refrescar periodicamente (ej: cada hora via cron)
REFRESH MATERIALIZED VIEW CONCURRENTLY {schema}.dashboard_summary;
```

---

## CHECKLIST DE DISENO DE TABLA

```markdown
Antes de crear tabla, verificar:

**Normalizacion:**
- [ ] Cumple 3NF?
- [ ] Hay dependencias transitivas?
- [ ] Si desnormaliza, esta 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?
- [ ] Esta documentada la razon de CASCADE vs SET NULL?

**Constraints:**
- [ ] UNIQUE en codigos de negocio?
- [ ] CHECK para enums/rangos?
- [ ] NOT NULL en columnas obligatorias?

**Indices:**
- [ ] Indice en cada FK?
- [ ] Indice en columnas de busqueda (WHERE)?
- [ ] Indice en columnas de ordenamiento (ORDER BY)?
- [ ] Indice GIST para PostGIS?
- [ ] Indice GIN para JSONB?
- [ ] Sin over-indexing?

**Auditoria:**
- [ ] Tiene created_at, updated_at?
- [ ] Tiene created_by_id?
- [ ] Trigger para updated_at automatico?

**Documentacion:**
- [ ] Comentario en tabla (COMMENT ON TABLE)?
- [ ] Comentario en columnas importantes?
- [ ] Comentario en constraints especiales?

**PostGIS (si aplica):**
- [ ] Usa GEOGRAPHY para lat/lng?
- [ ] SRID 4326?
- [ ] Indice GIST creado?

**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)
- [PostgreSQL Performance Optimization](https://www.postgresql.org/docs/15/performance-tips.html)

### Documentos Relacionados
- **DIRECTIVA-POLITICA-CARGA-LIMPIA.md** - Proceso DDL-First
- **ESTANDARES-NOMENCLATURA-BASE.md** - Nomenclatura general

### Ver implementacion especifica por proyecto
- Cada proyecto define sus schemas en `orchestration/directivas/DIRECTIVA-DISENO-BASE-DATOS.md`
- Contexto del proyecto en `orchestration/00-guidelines/CONTEXTO-PROYECTO.md`

---

**Version:** 2.0.0
**Fecha:** 2025-12-05
**Tipo:** Directiva Global (aplica a todos los proyectos)
**Proxima revision:** Al identificar necesidad de mejoras
