DBA Junior Ciberecus MX
Temas · Capítulo 3 · PF-020

Tema 20: Monitoreo, métricas y diagnóstico — el tlamatini del rendimiento

#PF-020 | XP: 300 | Insignia: Vigía del Tlamatini | Tier: Senior | Tiempo: 4 h

Visión mexica — tlamatini observando las estrellas para anticipar eventos

Objetivo

Configurarás el monitoreo continuo de tu base de datos: métricas de rendimiento (CPU, memoria, I/O, conexiones), logs de queries lentas, diagnóstico de bloqueos, y alertas automáticas. También aprenderás a usar `EXPLAIN` para entender por qué una query es lenta y cómo optimizarla.

Mapa del tema

    Las 4 métricas que todo DBA debe vigilar Slow query log y pg_stat_statements EXPLAIN y EXPLAIN ANALYZE Diagnóstico de bloqueos y deadlocks Alertas y dashboards (Prometheus + Grafana, o lo simple) LAB: detectar y resolver una query lenta

Visión mexica — Por Citlalli

El tlamatini (sabio mexica) era el astrónomo del altépetl. Cada noche observaba las estrellas y los movimientos de Venus para anticipar las lluvias, las cosechas y los eclipses. No actuaba sobre eventos: los preveía. Cuando Venus se ocultaba, sabía que la sequía vendría; cuando reaparecía, era tiempo de sembrar.

El DBA moderno es un tlamatini de su base. Las métricas son sus estrellas: conexiones activas, latencia de queries, uso de disco, cache hit ratio. Cuando una métrica cambia de patrón, sabe que algo viene. Cuando el cache hit ratio baja del 95%, la tormenta se acerca. Cuando las conexiones llegan al 80% del máximo, hay que actuar antes del colapso.


Bloque 1 — Las 4 métricas que todo DBA debe vigilar

Métrica 1: Conexiones activas vs máximas

MySQL:

SHOW STATUS LIKE 'Threads_connected';
-- Actual: 12

SHOW VARIABLES LIKE 'max_connections'; -- Máximo: 151

Ratio: `12/151 = 7.9%`. Verde. Si supera 80% (`120/151`), amarillo. Si llega a 95%, rojo: te vas a quedar sin conexiones.

PostgreSQL:

SELECT count() AS conexiones, (SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS maximo
FROM pg_stat_activity;

Métrica 2: Cache hit ratio (InnoDB Buffer Pool / PostgreSQL shared_buffers)

MySQL (InnoDB):

SHOW STATUS LIKE 'Innodb_buffer_pool_read_requests';
-- Lecturas del buffer (rápido)
SHOW STATUS LIKE 'Innodb_buffer_pool_reads';
-- Lecturas de disco (lento)
-- Cache hit ratio = (read_requests - reads) / read_requests  100

Si está por debajo del 99%, el buffer pool es pequeño. Considera aumentar:

[mysqld]
innodb_buffer_pool_size = 1G  # Para 4 GB RAM, dedicado a MySQL

PostgreSQL:

SELECT
  sum(blks_hit) AS hits,
  sum(blks_read) AS reads,
  round(100.0  sum(blks_hit) / (sum(blks_hit) + sum(blks_read)), 2) AS cache_hit_ratio
FROM pg_stat_database;
-- Esperado: > 99%

Métrica 3: Queries lentas (slow queries)

MySQL: habilita el slow query log y cuenta las queries que tardan más de N segundos.

[mysqld]
slow_query_log = ON
long_query_time = 2
slow_query_log_file = /var/log/mysql/slow.log
log_queries_not_using_indexes = ON
SHOW STATUS LIKE 'Slow_queries';
-- Acumulado desde el último reinicio

PostgreSQL: usa `pg_stat_statements` (extensión estándar).

# postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = top
CREATE EXTENSION pg_stat_statements;

SELECT substring(query, 1, 80) AS query, calls, round(mean_exec_time::numeric, 2) AS mean_ms, round(total_exec_time::numeric / 1000, 2) AS total_sec FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;

Métrica 4: Replicación (si tienes replica)

MySQL:

SHOW SLAVE STATUS\G
-- Seconds_Behind_Master: 0 (sincronizado)
-- Si > 60 segundos, la replica está atrasada

PostgreSQL:

SELECT
  client_addr,
  state,
  sent_lsn,
  replay_lsn,
  (sent_lsn - replay_lsn) AS lag_bytes
FROM pg_stat_replication;

Bloque 2 — Slow query log y pg_stat_statements en acción

Caso: tienda_tlalli con 110 pedidos y 50 clientes

Ejecutas 1000 veces la misma query y notas latencia:

SELECT c.nombre, c.email, COUNT(p.pedido_id) AS total_pedidos
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.cliente_id
GROUP BY c.cliente_id
ORDER BY total_pedidos DESC;

Si tarda 800 ms con 50 clientes pero 30 segundos con 50,000 clientes, el problema no es la query, es la escala. Aquí entra `EXPLAIN`.

Habilitar pg_stat_statements

-- En la base que quieres monitorear
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- Resetear estadísticas SELECT pg_stat_statements_reset();

-- Esperar un rato (1 hora o 1 día según tráfico) -- Ver las queries más lentas SELECT substring(query for 100) AS query_snippet, calls, round(mean_exec_time::numeric, 2) AS mean_ms, round(total_exec_time::numeric / 1000, 2) AS total_sec, rows FROM pg_stat_statements WHERE query NOT LIKE '%pg_stat%' -- Excluir esta misma query ORDER BY mean_exec_time DESC LIMIT 20;

Interpretar el resultado

Una fila típica:

| query_snippet | calls | mean_ms | total_sec | rows | |---|---|---|---|---| | `SELECT FROM pedidos WHERE cliente_id = $1` | 8421 | 12.3 | 103.6 | 4.2 | | `SELECT nombre FROM clientes WHERE email = $1` | 2310 | 0.8 | 1.8 | 1.0 | | `SELECT producto_id FROM productos WHERE categoria_id = $1 ORDER BY precio DESC` | 4521 | 85.4 | 386.2 | 23.1 |

La tercera query es la candidata a optimizar: 85 ms promedio, 386 segundos totales en 4521 llamadas. Si añadimos un índice, podemos reducirla a 5 ms.


Bloque 3 — EXPLAIN y EXPLAIN ANALYZE

MySQL

EXPLAIN SELECT c.nombre, COUNT(p.pedido_id) AS total
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.cliente_id
GROUP BY c.cliente_id;

Salida:

id  select_type  table  type   possible_keys  key       rows  Extra
1   SIMPLE       c      ALL    NULL           NULL      50    Using temporary
1   SIMPLE       p      ref    cliente_id     cliente_id 2    NULL

Interpretación:

  • `c` (clientes): `type=ALL` y `rows=50` → escaneo completo de tabla. 50 clientes no es problema, pero con 50,000 sí.
  • `p` (pedidos): `type=ref` y `key=cliente_id` → usa el índice, eficiente.
  • `Extra: Using temporary` → MySQL está creando una tabla temporal para el GROUP BY. Aceptable para 50 filas.
`EXPLAIN FORMAT=JSON` da más detalle, y `EXPLAIN ANALYZE` (en MySQL 8.0.18+) ejecuta la query y mide tiempos reales.

PostgreSQL

EXPLAIN ANALYZE
SELECT c.nombre, COUNT(p.pedido_id) AS total
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.cliente_id
GROUP BY c.cliente_id;

Salida:

HashAggregate  (cost=12.5..14.0 rows=50 width=40) (actual time=0.5..0.6 rows=50.0 loops=1)
  Group Key: c.cliente_id
  ->  Hash Right Join  (cost=4.0..10.0 rows=110 width=40) (actual time=0.1..0.3 rows=110.0 loops=1)
        Hash Cond: (p.cliente_id = c.cliente_id)
        ->  Seq Scan on pedidos p  (cost=0.0..2.1 rows=110 width=8) (actual time=0.01..0.05 rows=110.0 loops=1)
        ->  Hash  (cost=3.5..3.5 rows=50 width=36) (actual time=0.05..0.05 rows=50.0 loops=1)
              ->  Seq Scan on clientes c  (cost=0.0..3.5 rows=50 width=36) (actual time=0.02..0.04 rows=50.0 loops=1)
Planning Time: 0.2 ms
Execution Time: 0.8 ms

Interpretación:

  • `Seq Scan on clientes` y `Seq Scan on pedidos` → escaneos completos. Esperable para tablas pequeñas.
  • `Execution Time: 0.8 ms` → excelente. Si la tabla creciera, veríamos `Index Scan` recomendado.

Patrones problemáticos en EXPLAIN

| Patrón | Problema | Solución | |---|---|---| | `Seq Scan` en tabla grande | Escaneo completo (lento) | Crear índice en la columna del WHERE | | `Using filesort` (MySQL) | Ordenamiento en disco | Índice en la columna del ORDER BY | | `Using temporary` (MySQL) | Tabla temporal | Índice compuesto que cubra el GROUP BY | | `Nested Loop` con `rows` muy grande | Join ineficiente | Considerar `Hash Join` o reescribir | | `actual time=... rows=... loops=N` con N alto | Subquery ejecutada N veces | Reescribir como JOIN |


Bloque 4 — Diagnóstico de bloqueos y deadlocks

MySQL: detectar bloqueos

-- Ver transacciones activas
SELECT  FROM information_schema.INNODB_TRX;

-- Ver locks SELECT FROM performance_schema.data_locks;

O más visual:

SELECT
  trx_id,
  trx_state,
  trx_started,
  trx_query,
  trx_rows_locked
FROM information_schema.INNODB_TRX
ORDER BY trx_started;

MySQL: deadlock detectado

-- Ver el último deadlock
SHOW ENGINE INNODB STATUS\G

En la sección `LATEST DETECTED DEADLOCK`, MySQL te dice:

  • Qué transacciones estaban involucradas.
  • Qué queries ejecutaban.
  • Qué lock se liberó (la víctima del rollback).

PostgreSQL: bloqueos

-- Ver queries bloqueadas y quién las bloquea
SELECT
  blocked.pid          AS pid_bloqueado,
  blocked.usename      AS usuario_bloqueado,
  blocking.pid         AS pid_bloqueador,
  blocking.usename     AS usuario_bloqueador,
  blocked.query        AS query_bloqueada,
  blocking.query       AS query_bloqueadora
FROM pg_stat_activity blocked
JOIN pg_locks bl ON bl.pid = blocked.pid
JOIN pg_locks kl ON kl.locktype = bl.locktype
  AND kl.database IS NOT DISTINCT FROM bl.database
  AND kl.relation IS NOT DISTINCT FROM bl.relation
  AND kl.page IS NOT DISTINCT FROM bl.page
  AND kl.tuple IS NOT DISTINCT FROM bl.tuple
  AND kl.transactionid IS NOT DISTINCT FROM bl.transactionid
  AND kl.pid != bl.pid
  AND kl.granted = true
JOIN pg_stat_activity blocking ON blocking.pid = kl.pid
WHERE NOT bl.granted;

Solución a bloqueos

    Identificar la transacción bloqueadora (query de arriba). Evaluar si es legítima (un `UPDATE` largo en horas de baja actividad puede ser OK). Si es legítima: esperar a que termine. Si es ilegítima o demasiado larga: terminarla manualmente:
-- PostgreSQL
SELECT pg_cancel_backend(pid_bloqueador);
-- O más agresivo:
SELECT pg_terminate_backend(pid_bloqueador);

-- MySQL
-- Encontrar el thread_id y matarlo
SELECT id, query, time FROM information_schema.processlist WHERE db = 'tienda_tlalli';
KILL 12345;

Bloque 5 — Alertas y dashboards

La pila simple: cron + script + email

Un script bash que verifica métricas y envía alerta:

#!/usr/bin/env bash

/opt/scripts/check_db.sh

THRESHOLD=80 MAX_CONN=$(mysql -u monitor -p"${MONITOR_PWD}" -N -e "SHOW VARIABLES LIKE 'max_connections';" | awk '{print $2}') CURR_CONN=$(mysql -u monitor -p"${MONITOR_PWD}" -N -e "SHOW STATUS LIKE 'Threads_connected';" | awk '{print $2}') RATIO=$((CURR_CONN 100 / MAX_CONN))

if [ "$RATIO" -gt "$THRESHOLD" ]; then echo "ALERTA: Conexiones MySQL al ${RATIO}% (${CURR_CONN}/${MAX_CONN})" | mail -s "[DB ALERTA] Conexiones altas" admin@ciberecus.mx fi

/5     /opt/scripts/check_db.sh

La pila profesional: Prometheus + Grafana

Para bases de producción reales:

    MySQL exporter (`mysqld_exporter` de Prometheus): expone métricas en `:9104/metrics`. PostgreSQL exporter (`postgres_exporter`): expone métricas en `:9187/metrics`. Prometheus: scrapea cada 15s, almacena series temporales. Grafana: visualiza con dashboards pre-hechos (hay dashboards de la comunidad para MySQL y PostgreSQL). Alertmanager: reglas de alerta (ratio de cache < 95%, conexiones > 80%, replicación lag > 60s).
Dashboards recomendados:

AI Mission

Pídele a tu IA que analice un `EXPLAIN ANALYZE`. Prompt sugerido:

Tengo esta query que tarda 8 segundos en MySQL 8 con 1M filas:

SELECT c.nombre, c.email, COUNT(p.pedido_id) AS total FROM clientes c LEFT JOIN pedidos p ON p.cliente_id = c.cliente_id WHERE p.fecha_pedido &gt; &apos;2026-01-01&apos; GROUP BY c.cliente_id ORDER BY total DESC LIMIT 20;

[pega aquí el EXPLAIN ANALYZE]

Dame:

    Cuál es el principal cuello de botella. Qué índice añadirías y por qué. Si reescribirías la query, cómo. Cuánto esperas mejorar (estimación).
La IA puede sugerir índices y reescrituras, pero verifica con EXPLAIN antes y después.


Errores típicos

    `EXPLAIN` sin `ANALYZE` en PostgreSQL → ves el plan estimado, no el real. Los dos pueden diferir. Crear índices innecesarios → cada índice añade overhead en INSERT/UPDATE. Un índice no usado es peso muerto. Monitorear solo CPU → un瓶颈 de disco no se ve en CPU. Alertar por todo → el operador se cansa y silencia las alertas. Solo alerta por lo accionable. No hacer baseline → si no sabes qué era "normal" antes, no sabes qué es "anormal" ahora. Confiar en `pg_stat_statements` sin normalizar queries → queries con literales distintos aparecen como entradas separadas. Usar `pg_stat_statements.track = top` y normalizar.

LAB práctico — Detectar y resolver una query lenta

Objetivo: introducir una query lenta intencionalmente, diagnosticarla con `EXPLAIN`, optimizarla con un índice, y medir la mejora.

Paso 1 — Generar volumen

-- Insertar 10,000 pedidos y 5,000 clientes adicionales
INSERT INTO clientes (nombre, email, telefono, fecha_registro)
SELECT
  &apos;Cliente &apos; || g,
  &apos;cliente&apos; || g || &apos;@test.com&apos;,
  &apos;+52 55 &apos; || lpad(g::text, 8, &apos;0&apos;),
  current_date - (random()  365)::int
FROM generate_series(1, 5000) g;

INSERT INTO pedidos (cliente_id, fecha_pedido, total, estado) SELECT (random() 5000 + 1)::int, current_date - (random() 180)::int, (random() 1000 + 50)::numeric(10,2), (ARRAY[&apos;pendiente&apos;,&apos;pagado&apos;,&apos;enviado&apos;,&apos;entregado&apos;,&apos;cancelado&apos;])[(random() 5)::int + 1] FROM generate_series(1, 10000) g;

Paso 2 — Query lenta

SELECT c.nombre, c.email, COUNT(p.pedido_id) AS total
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.cliente_id
WHERE p.fecha_pedido &gt;= &apos;2026-06-01&apos;
GROUP BY c.cliente_id
ORDER BY total DESC
LIMIT 20;

Medir tiempo:

\timing on
-- Correr la query
-- Esperado: 2-5 segundos con 10K pedidos

Paso 3 — Diagnóstico

EXPLAIN ANALYZE
SELECT c.nombre, c.email, COUNT(p.pedido_id) AS total
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.cliente_id
WHERE p.fecha_pedido &gt;= &apos;2026-06-01&apos;
GROUP BY c.cliente_id
ORDER BY total DESC
LIMIT 20;

Buscar: `Seq Scan on pedidos`, `rows=10000`, tiempo alto.

Paso 4 — Optimizar

-- Crear índice compuesto
CREATE INDEX idx_pedidos_cliente_fecha ON pedidos(cliente_id, fecha_pedido);
-- O simplemente sobre fecha_pedido
CREATE INDEX idx_pedidos_fecha ON pedidos(fecha_pedido);
ANALYZE pedidos;  -- Actualizar estadísticas para el planner

Paso 5 — Re-medir

EXPLAIN ANALYZE
-- Misma query
-- Esperado: &lt; 100 ms

Comparar antes/después. Anotar en el cuaderno.

Paso 6 — Verificar con pg_stat_statements

SELECT
  substring(query for 80) AS q,
  calls,
  round(mean_exec_time::numeric, 2) AS mean_ms
FROM pg_stat_statements
WHERE query LIKE &apos;%pedidos%&apos;
ORDER BY mean_exec_time DESC;

Entregables del LAB

  • [ ] Captura de la query lenta antes (con `\timing`).
  • [ ] EXPLAIN ANALYZE antes con cuello de botella identificado.
  • [ ] Índice creado.
  • [ ] EXPLAIN ANALYZE después, confirmando uso del índice.
  • [ ] Medición del speedup.
  • [ ] Entrada en `pg_stat_statements` mostrando la mejora.

Quest

Preguntas de opción múltiple (10)

1. ¿Qué mide el cache hit ratio?

A. La cantidad de CPU usada. B. El porcentaje de lecturas servidas desde memoria vs disco. C. El número de conexiones activas. D. El tiempo de respuesta promedio.

2. ¿Cuál es un valor saludable de cache hit ratio en PostgreSQL?

A. 50% B. 75% C. 95%+ D. 30%

3. ¿Qué hace `EXPLAIN ANALYZE` que `EXPLAIN` no hace?

A. Analiza la sintaxis. B. Ejecuta la query y mide tiempos reales. C. Genera el plan estimado. D. Cuenta las filas.

4. ¿Qué extensión PostgreSQL muestra las queries más lentas?

A. `pgcrypto` B. `pg_stat_statements` C. `uuid-ossp` D. `postgis`

5. ¿Qué comando MySQL muestra las queries lentas acumuladas?

A. `SHOW STATUS LIKE 'Slow_queries'` B. `SHOW SLAVE STATUS` C. `SHOW PROCESSLIST` D. `SHOW VARIABLES`

6. ¿Qué es un deadlock?

A. Una transacción que tarda mucho. B. Dos o más transacciones que se bloquean mutuamente, impidiendo avanzar. C. Un índice corrupto. D. Un error de sintaxis.

7. ¿Cómo se mata una query bloqueadora en PostgreSQL?

A. `KILL pid` B. `SELECT pg_terminate_backend(pid)` C. `DROP USER` D. `SHUTDOWN`

8. ¿Qué herramienta profesional se usa para visualizar métricas de base?

A. Excel. B. Grafana + Prometheus. C. Bloc de notas. D. PowerPoint.

9. ¿Qué pasa si `max_connections` se agota?

A. La base se cae. B. Las nuevas conexiones se rechazan con "too many connections". C. Se duplican las conexiones existentes. D. La base se reinicia.

10. ¿Por qué `log_min_duration_statement = 1000` es útil en PostgreSQL?

A. Hace la base más rápida. B. Loguea queries que tardan más de 1 segundo, ayudando a detectar lentitud. C. Bloquea queries lentas. D. Aumenta la memoria.

Preguntas abiertas (3)

A. Tu base de producción tiene cache hit ratio de 85% (debería ser >99%). Lista 3 acciones que tomarías para mejorarlo.

B. Encuentras una query en `pg_stat_statements` con `mean_exec_time = 800ms` y `calls = 50000`. ¿Cuánto tiempo total de CPU consume? Si la optimizas a 50 ms, ¿cuánto ahorras?

C. Diseña un sistema de alertas mínimo (sin Prometheus) para una base MySQL en producción. ¿Qué alertarías? ¿Con qué frecuencia? ¿A quién?


Respuestas modelo

1. B — Porcentaje de lecturas desde cache vs disco. La A es CPU. La C es conexiones. La D es latencia.

2. C — 95%+ es saludable. 75% indica buffer pool pequeño. 50% es señal de problemas graves.

3. B — `EXPLAIN ANALYZE` ejecuta la query y mide tiempos reales, mientras `EXPLAIN` solo estima. La A y C son del parser. La D es solo estimación.

4. B — `pg_stat_statements`. La A es cifrado. La C es UUID. La D es GIS.

5. A — `SHOW STATUS LIKE 'Slow_queries'`. La B es de réplica. La C muestra conexiones. La D muestra variables.

6. B — Dos transacciones bloqueándose mutuamente. MySQL/PostgreSQL detectan y abortan una automáticamente. La A es lentitud. La C es corrupción. La D es error de código.

7. B — `pg_terminate_backend(pid)`. La A es sintaxis de MySQL. La C es borrar usuario. La D apaga la base.

8. B — Grafana + Prometheus es la pila estándar. La A y D son ofimática. La C no es dashboard.

9. B — "Too many connections". La A es falso (la base sigue). La C es falso. La D no es automático.

10. B — Loguea queries > 1 segundo. La A es falso (tiene overhead mínimo). La C no bloquea. La D no es eso.


Respuestas a las preguntas abiertas

A. Tres acciones:

    Aumentar `shared_buffers` (PostgreSQL) o `innodb_buffer_pool_size` (MySQL) hasta 50-75% de la RAM disponible para la base. Identificar queries con `pg_stat_statements` que hacen `Seq Scan` y añadir índices. Activar `pg_prewarm` para cargar tablas frecuentes en cache tras reinicios.
B. 800 ms × 50000 = 40,000,000 ms = 40,000 s = 11 horas de CPU total. Si optimizas a 50 ms: 50 × 50000 = 2,500,000 ms = 2,500 s = 0.7 horas. Ahorras ~10.3 horas de CPU por ciclo de 50000 llamadas. Multiplicado por días, semanas, es enorme.

C. Alertas mínimas para MySQL en producción:

    Conexiones > 80% del máximo: cada 5 min, email al DBA de guardia. `Slow_queries` aumenta > 10% en 1 hora: cada hora, email. Disco > 80% lleno: cada 15 min, email + SMS. Replicación lag > 60 segundos: cada 1 min, email + Slack. Caída del servicio (no responde a `SELECT 1`): cada 1 min, SMS al celular del DBA.
  • Implementación: cron cada X minutos, script bash que ejecuta la métrica y envía email/SMS si supera umbral.
  • A quién: DBA primario primero, DBA secundario en copia, gerente solo para caídas.

Tras las huellas — Por Citlalli

El tlamatini no solo miraba el cielo. Anotaba lo que veía en el xiuhpohualli (cuenta de los años), un registro de 52 años donde llevaba patrones, eclipses, sequías y bonanzas. Cuando un nuevo síntoma aparecía en el cielo, lo comparaba con la historia: ¿esto se parecía a la sequía de 1455? ¿O al cometa que presagió la caída de Azcapotzalco?

Tu base de datos necesita su propio xiuhpohualli: un registro histórico de métricas. No basta con alertar en tiempo real. Necesitas poder decir "hace 6 meses, cuando hicimos el deploy del 15 de febrero, el cache hit ratio bajó del 99% al 92% y se mantuvo así 3 semanas. La causa fue el nuevo índice sobre `pedidos` que no anticipamos"*. Esa memoria operativa es la diferencia entre un DBA que reacciona y uno que anticipa.

Reflexión mexica: el tlamatini no era adivino, era observador disciplinado. La predicción viene del registro, no de la inspiración.

Cierre del cuaderno

    Las 4 métricas que vigilo: conexiones, cache hit, slow queries, replicación. Comando para ver queries más lentas en mi base actual. Última vez que añadí un índice: ¿medí antes/después? ____________. ¿Tengo alertas configuradas? Si no, mi primer paso mañana: ____________. ¿Dónde guardo mi "xiuhpohualli" (histórico de métricas)? ____________.