> ## Documentation Index
> Fetch the complete documentation index at: https://help.treble.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Optimización de Consultas

> Cómo escribir consultas eficientes contra el Data Warehouse.

# Optimización de Consultas

El Data Warehouse está construido sobre ClickHouse, una base de datos columnar. Esto significa que las consultas se comportan diferente a una base de datos relacional tradicional. Aquí te explicamos cómo aprovechar la estructura para obtener resultados rápidos.

## Principios Clave

### 1. Siempre filtra por fecha

Todas las tablas de hechos están **particionadas por mes** (`toYYYYMM(created_at)`). Filtrar por fecha permite a ClickHouse saltar particiones enteras sin leerlas.

```sql theme={null}
-- Bueno: ClickHouse lee solo 1 partición
SELECT * FROM fact_conversations
WHERE created_at >= '2026-04-01' AND created_at < '2026-05-01'

-- Malo: ClickHouse lee todas las particiones
SELECT * FROM fact_conversations
WHERE agent_name = 'Juan'
```

### 2. No necesitas filtrar por company\_id

Tu usuario tiene una **política de fila** que filtra automáticamente por tu empresa. No necesitas agregar `WHERE company_id = ...` — se aplica de forma transparente.

### 3. Selecciona solo las columnas que necesitas

ClickHouse es columnar: solo lee las columnas que mencionas en tu `SELECT`. Menos columnas = menos datos leídos = más rápido.

```sql theme={null}
-- Bueno: lee solo 3 columnas
SELECT conversation_id, created_at, status
FROM fact_conversations
WHERE created_at >= now() - INTERVAL 7 DAY

-- Evita: lee todas las columnas
SELECT *
FROM fact_conversations
WHERE created_at >= now() - INTERVAL 7 DAY
```

### 4. Usa LIMIT para explorar

Cuando estés explorando datos, usa `LIMIT` para evitar traer millones de filas:

```sql theme={null}
SELECT * FROM fact_agent_messages
WHERE created_at >= now() - INTERVAL 1 DAY
ORDER BY created_at DESC
LIMIT 100
```

## Estructura Interna de las Tablas

Cada tabla tiene un **orden de clasificación** (sort key) que determina cómo se organizan los datos en disco. Las consultas que filtran por las primeras columnas del sort key son mucho más eficientes.

### Sort Keys por Tabla

| Tabla                       | Sort Key                                             | Filtra eficientemente por |
| --------------------------- | ---------------------------------------------------- | ------------------------- |
| `fact_conversations`        | `(company_id, created_at, conversation_id)`          | fecha, conversation\_id   |
| `fact_agent_messages`       | `(company_id, created_at, message_id)`               | fecha, message\_id        |
| `fact_redirections`         | `(company_id, created_at, redirection_id)`           | fecha                     |
| `fact_agent_status_changes` | `(company_id, agent_id, created_at)`                 | agent\_id + fecha         |
| `fact_agent_daily`          | `(company_id, day, agent_id)`                        | día, agent\_id            |
| `fact_sessions`             | `(company_id, created_at, session_id)`               | fecha, session\_id        |
| `fact_deployment_status`    | `(company_id, timestamps_eta, deployment_id)`        | fecha                     |
| `fact_deployment_daily`     | `(company_id, day, poll_id)`                         | día, poll\_id             |
| `fact_inbound_messages`     | `(company_id, created_at, session_id)`               | fecha                     |
| `fact_whatsapp_links`       | `(company_id, created_at, event_id)`                 | fecha                     |
| `fact_hsm_responses`        | `(company_id, response_date, interaction_answer_id)` | fecha                     |

<Note>
  `company_id` es siempre la primera columna del sort key. Como tu usuario tiene un filtro automático por empresa, todas tus consultas aprovechan esta optimización sin que tengas que hacer nada.
</Note>

### Índices Secundarios

Algunas tablas tienen índices adicionales que ayudan a filtrar por columnas que no están en el sort key:

| Tabla                    | Índice                 | Columna            | Tipo          | Útil para                                          |
| ------------------------ | ---------------------- | ------------------ | ------------- | -------------------------------------------------- |
| `fact_agent_messages`    | `idx_conversation_id`  | `conversation_id`  | minmax        | Buscar mensajes de una conversación específica     |
| `fact_agent_messages`    | `idx_sender`           | `sender`           | bloom\_filter | Filtrar por `sender = 'AGENT'` o `sender = 'USER'` |
| `fact_sessions`          | `idx_poll_id`          | `poll_id`          | minmax        | Filtrar por campaña                                |
| `fact_sessions`          | `idx_inbound_outbound` | `inbound_outbound` | bloom\_filter | Filtrar por tipo INBOUND/OUTBOUND                  |
| `fact_deployment_status` | `idx_poll_id`          | `poll_id`          | minmax        | Filtrar por campaña                                |
| `fact_deployment_status` | `idx_status`           | `status`           | bloom\_filter | Filtrar por estado de envío                        |
| `fact_inbound_messages`  | `idx_poll_id`          | `poll_id`          | minmax        | Filtrar por campaña                                |
| `fact_hsm_responses`     | `idx_hsm_id`           | `hsm_id`           | minmax        | Filtrar por plantilla HSM                          |
| `fact_hsm_responses`     | `idx_poll_id`          | `poll_id`          | minmax        | Filtrar por campaña                                |

## Límites del Sistema

Tu usuario tiene los siguientes límites para proteger la estabilidad del sistema:

| Límite                       | Valor       |
| ---------------------------- | ----------- |
| Tiempo máximo de ejecución   | 30 segundos |
| Máximo de filas leídas       | 50 millones |
| Máximo de bytes leídos       | 5 GB        |
| Máximo de filas en resultado | 500,000     |
| Máximo de memoria            | 2 GB        |

Si tu consulta excede alguno de estos límites, será cancelada automáticamente. Para evitarlo:

* Agrega filtros de fecha más estrechos
* Selecciona menos columnas
* Usa `LIMIT`
* Pre-agrega con `GROUP BY` en lugar de traer filas individuales

## Patrones Comunes

### JOIN entre tablas de hechos

Puedes cruzar tablas usando `conversation_id` o `survey_user_id`:

```sql theme={null}
-- Mensajes de una conversación con datos de la conversación
SELECT
    fc.conversation_id,
    fc.agent_name,
    fm.created_at AS mensaje_fecha,
    fm.sender,
    fm.content
FROM client_analytics.fact_conversations fc
INNER JOIN client_analytics.fact_agent_messages fm
    ON fm.conversation_id = fc.conversation_id
WHERE fc.created_at >= now() - INTERVAL 7 DAY
ORDER BY fm.created_at
LIMIT 1000
```

### JOIN con dimensiones

```sql theme={null}
-- Productividad por equipo (team_name viene de la dimensión)
SELECT
    at.team_name,
    sum(ad.chats_handled) AS chats,
    round(avg(ad.avg_first_response_sec), 0) AS respuesta_promedio_seg
FROM client_analytics.fact_agent_daily ad
INNER JOIN client_analytics.dim_agent_tags at ON at.agent_id = ad.agent_id
WHERE ad.day >= today() - 30
GROUP BY at.team_name
ORDER BY chats DESC
```

### Nivel de servicio personalizado

```sql theme={null}
-- Define tu propio umbral de SLA
SELECT
    toDate(created_at) AS dia,
    count() AS conversaciones,
    countIf(first_response_sec <= 60) AS dentro_60s,
    countIf(first_response_sec <= 120) AS dentro_120s,
    countIf(first_response_sec <= 300) AS dentro_5min,
    round(countIf(first_response_sec <= 120) * 100.0 / count(), 1) AS sla_pct
FROM client_analytics.fact_conversations
WHERE created_at >= now() - INTERVAL 30 DAY
  AND first_response_sec IS NOT NULL
GROUP BY dia
ORDER BY dia DESC
```
