KriaturasBases de datos para quien empieza

Parte III · Diseñar bien

Capítulo 8 — El historial de partidas (datos históricos)

Hasta aquí tu base de datos de Kriaturas tiene jugadores, cartas bien organizadas por familias, mazos y colecciones. Tienes el catálogo del juego: lo que existe. Pero un juego no es lo que existe, sino lo que pasa cuando dos personas se sientan a jugar.

Y ahí tienes un agujero. Imagina que DragoFuego99 y LunaVerde se baten en duelo, una partida larguísima que gana DragoFuego99 en el último turno. Épico. Termina la partida y… no guardas nada. Al día siguiente nadie sabe que ese duelo existió. Ni quién ganó, ni cuándo, ni cuánto duró. Es como si no hubiera pasado.

En este capítulo le vas a dar memoria al juego: un sitio donde anotar cada partida para siempre. Por el camino vas a ver dos ideas nuevas e importantes: cómo se guardan los datos que ocurren a lo largo del tiempo (los datos históricos) y qué hacer con un dato que, en realidad, no hace falta guardar porque se puede calcular (el atributo derivado, como el rango de un jugador).

Al acabar sabrás diseñar la tabla partida, entender por qué una partida conecta la tabla jugador consigo misma, y tomar con criterio una de las decisiones más clásicas del diseño de bases de datos: ¿este dato lo guardo o lo calculo?


El juego solo guarda el presente (y eso es un problema)

Aquí hay algo que conviene entender pronto. Por defecto, una base de datos guarda el estado actual de las cosas. Cuando algo cambia, lo normal es actualizar el dato: el valor viejo desaparece y queda el nuevo.

Piensa en el email de un jugador. Si DragoFuego99 cambia su correo, actualizas la fila y ya está. A nadie le importa cuál era el correo antiguo. El presente basta.

Pero hay cosas en las que el pasado importa. Una partida no es un dato que se actualiza: es un hecho que ocurrió en un momento concreto y que quieres conservar tal cual, para siempre. La partida del martes no "se actualiza" a la del jueves. Son dos hechos distintos, y los quieres los dos.

A los datos que guardan hechos a lo largo del tiempo, en vez de pisar el valor anterior, se les llama datos históricos. La idea es sencilla y poderosa:

  • No sobrescribes. Añades una fila nueva por cada hecho.
  • Cada fila lleva una marca de tiempo: un dato (normalmente una fecha o una fecha y hora) que dice cuándo ocurrió.

En Kriaturas, el histórico natural son las partidas. Cada vez que se juega una, añades una fila a una tabla partida, con su fecha. Nunca borras ni pisas las anteriores. Así, con el tiempo, esa tabla se convierte en el diario completo del juego.

Un matiz, por si te lo cruzas: hay dos formas de marcar el tiempo. A veces te interesa un instante (cuándo pasó algo: la fecha de la partida) y a veces un periodo (desde cuándo hasta cuándo: por ejemplo, cuánto tiempo un jugador estuvo en cierto rango). En este libro nos basta con el instante: una fecha por partida.

Una partida enfrenta a dos jugadores… de la misma tabla

Vamos a diseñar la tabla. ¿Qué datos te interesan de una partida? Quién jugó, quién ganó, cuándo fue y cuánto duró. Algo así:

dato qué es
id identificador de la partida (clave primaria)
jugador1 uno de los dos jugadores
jugador2 el otro jugador
ganador quién ganó
fecha cuándo se jugó (la marca de tiempo)
duracion cuánto duró, en minutos

Fíjate en jugador1, jugador2 y ganador. Los tres son jugadores. Y los jugadores ya viven en una tabla: jugador, desde el capítulo 2. Así que no vas a copiar aquí el alias ni el email de nadie. Vas a hacer lo mismo que aprendiste con la clave foránea en el capítulo 4: guardar el id del jugador y dejar que ese número apunte a su fila en jugador.

La novedad es que aquí hay tres claves foráneas, y las tres apuntan a la misma tabla. jugador1 apunta a jugador, jugador2 apunta a jugador y ganador también. Es la primera vez que ves una tabla conectada consigo misma a través de otra. Tiene todo el sentido: una partida es, en el fondo, una relación entre dos filas de jugador.

Aquí tienes el plano. Primero en cajas y líneas, que se lee en cualquier sitio:

   ┌─────────────┐                      ┌──────────────────┐
   │   jugador    │                     │     partida       │
   ├─────────────┤  jugador1 (FK) ───▶ ├──────────────────┤
   │ id (PK)      │◀── jugador2 (FK) ──│ id (PK)           │
   │ alias        │                     │ jugador1 (FK)    │
   │ email        │◀── ganador  (FK) ──│ jugador2 (FK)    │
   │ fecha_alta   │                     │ ganador  (FK)    │
   └─────────────┘                      │ fecha            │
                                         │ duracion         │
                                         └──────────────────┘

Las tres flechas salen de partida y entran todas en jugador. Esa es la imagen de "una tabla relacionada consigo misma". Y aquí está la misma idea en formato Mermaid, que las herramientas web y GitHub dibujan solas:

Esta es una primera probada de "una tabla que se relaciona consigo misma". El caso puro y completo —cuando lo que conectas son dos filas del mismo tipo sin una tabla de por medio, como las amistades entre jugadores— se llama relación reflexiva, y es justo lo que viene en el capítulo 9. Aquí lo haces a través de partida, que es más fácil de ver.

Creando la tabla partida

Pasemos el plano a SQL. La única novedad respecto a lo que ya sabes es que declaras tres claves foráneas en lugar de una, y las tres con REFERENCES jugador(id):

SQL
CREATE TABLE partida (
    id        INTEGER PRIMARY KEY,
    jugador1  INTEGER,
    jugador2  INTEGER,
    ganador   INTEGER,
    fecha     DATE,
    duracion  INTEGER,            -- duración en minutos
    FOREIGN KEY (jugador1) REFERENCES jugador(id),
    FOREIGN KEY (jugador2) REFERENCES jugador(id),
    FOREIGN KEY (ganador)  REFERENCES jugador(id)
);

Léelo despacio. Le estás diciendo a la base de datos: "crea una tabla partida; sus columnas jugador1, jugador2 y ganador no son números cualesquiera, son id que tienen que existir en la tabla jugador". Gracias a eso, la base de datos no te dejará guardar una partida con un jugador inventado. Es la integridad referencial del capítulo 4, ahora trabajando tres veces a la vez.

Ahora metemos partidas. Recuerda el reparto: DragoFuego99 es el jugador 1, LunaVerde el 2, PixelPunk el 3 y ToxiRana el 4. Vamos a registrar cuatro duelos de marzo de 2026:

SQL
INSERT INTO partida (id, jugador1, jugador2, ganador, fecha, duracion) VALUES
    (1, 1, 2, 1, '2026-03-20', 12),
    (2, 3, 4, 4, '2026-03-22', 18),
    (3, 1, 3, 1, '2026-03-25',  9),
    (4, 2, 4, 2, '2026-03-28', 15);

Léelo fila a fila para no perderte entre tanto número:

  • Partida 1: jugó DragoFuego99 (1) contra LunaVerde (2); ganó DragoFuego99 (1); el 20 de marzo; duró 12 minutos.
  • Partida 2: PixelPunk (3) contra ToxiRana (4); ganó ToxiRana (4); el 22; 18 min.
  • Partida 3: DragoFuego99 (1) contra PixelPunk (3); ganó DragoFuego99 (1); el 25; 9 min.
  • Partida 4: LunaVerde (2) contra ToxiRana (4); ganó LunaVerde (2); el 28; 15 min.

Y aquí está la magia tranquila de los datos históricos: mañana habrá una partida 5, pasado una 6, y ninguna pisará a las anteriores. La tabla solo crece. Dentro de un año, esas filas son la historia entera de Kriaturas. Puedes preguntarle "¿qué partidas jugó DragoFuego99 en marzo?" y la respuesta seguirá ahí, intacta.

El dato que no se guarda: el rango del jugador

Ahora una pregunta con trampa. Cada jugador de Kriaturas tiene un rango (su nivel, su categoría) que sube cuando gana partidas. En la ficha del juego, el rango aparece como un dato del jugador. Entonces… ¿le añado una columna rango a la tabla jugador y la voy actualizando?

Podrías. Pero párate a pensar de dónde sale ese rango. Sale de las partidas ganadas. Cuantas más victorias, más alto el rango. O sea: el rango no es un dato independiente, es una consecuencia de algo que ya tienes guardado en otra tabla.

A un dato que se obtiene calculándolo a partir de otros datos, en lugar de guardarlo a mano, se le llama atributo derivado. La palabra "derivado" lo dice bien: deriva (se desprende) de otra cosa. El rango deriva de las victorias. El total de cartas de una colección derivaría de sumar las cantidades. La edad derivaría de la fecha de nacimiento. Son datos que la base de datos puede deducir sola en el momento en que se los pides.

Mira cuántas partidas ha ganado cada jugador, contando en la tabla partida:

jugador victorias (filas donde ganador = su id)
DragoFuego99 (1) 2 (partidas 1 y 3)
LunaVerde (2) 1 (partida 4)
PixelPunk (3) 0
ToxiRana (4) 1 (partida 2)

El rango de DragoFuego99 no necesita una columna propia: está escondido en esas dos filas de partida. Para conocerlo, cuentas. La idea, en lenguaje de todos los días, es "cuéntame las filas de partida donde el ganador sea este jugador". En SQL eso se hace con una función que cuenta filas, y lo vas a aprender de verdad en el capítulo 12, cuando veas cómo agrupar y contar. Por ahora quédate con el concepto, no con la sintaxis: el rango se calcula, no se almacena.

¿Guardar o calcular? La decisión de fondo

Esto abre una de las disyuntivas más típicas del diseño, y la vas a encontrar una y otra vez. Tienes dos caminos:

Calcularlo siempre que haga falta. No guardas el rango en ningún sitio; cada vez que alguien lo quiere ver, lo cuentas en el momento. Ventaja enorme: nunca se desincroniza. Es imposible que el rango "mienta", porque se calcula a partir de las partidas reales justo cuando lo pides. Inconveniente: cada consulta cuesta un poco de trabajo (hay que contar).

Guardarlo precalculado. Añades una columna rango a jugador y la actualizas cada vez que alguien gana. Ventaja: leerlo es instantáneo, no hay que contar nada. Inconveniente, y es serio: tienes dos sitios que dicen lo mismo (las partidas y la columna rango), y si se te olvida actualizar uno, se contradicen. El jugador ganó pero su rango sigue igual. Eso es un dato desfasado, y los datos desfasados son una fuente clásica de bugs.

¿Cuál es mejor? Depende, y esa es justo la respuesta que debe darte un buen diseño. Si el rango se consulta poco, calcularlo es lo más limpio y seguro. Si se consulta muchísimo (imagina un ranking que ve todo el mundo cada segundo), a veces compensa guardarlo precalculado y asumir el coste de mantenerlo al día. A ese dato repetido a propósito, para ganar velocidad, se le llama redundancia controlada: aceptas guardar algo dos veces sabiendo lo que haces y teniendo un plan para mantenerlo sincronizado.

Para saber más: cuando se opta por guardar el dato precalculado, existe una forma de que la base de datos lo actualice sola cada vez que cambian las partidas: son los disparadores (triggers), pequeñas órdenes que se ejecutan automáticamente ante un cambio. Quedan fuera de este libro, pero te suena el nombre por si algún día lo necesitas. Y la regla general de "no guardar dos veces lo mismo sin motivo" tiene su propio capítulo: la normalización, que llega en el capítulo 10.

La regla práctica para empezar es sencilla: por defecto, calcula. Guarda precalculado solo cuando midas que lo necesitas. Es más fácil empezar limpio y optimizar después que arrastrar un dato desfasado desde el principio.

Resumen

En este capítulo le has dado memoria a Kriaturas. Has visto que una base de datos guarda por defecto solo el presente, y que para conservar lo que pasa con el tiempo se usan datos históricos: una fila nueva por cada hecho, con su marca de tiempo, sin pisar las anteriores. Has diseñado y poblado la tabla partida, que conecta la tabla jugador consigo misma mediante tres claves foráneas (jugador1, jugador2, ganador). Y has conocido el atributo derivado: el rango del jugador no se guarda, se calcula contando victorias, lo que te ha llevado a la gran pregunta del diseño —¿guardar o calcular?— y a la idea de redundancia controlada.

Ya guardas lo que pasa entre dos jugadores dentro del combate. Pero los jugadores también se relacionan fuera de la partida: se hacen amigos e intercambian cartas entre ellos. Eso es otro tipo de vínculo, en el que una tabla se relaciona consigo misma de forma pura. Es la relación reflexiva, y es lo que vas a modelar en el próximo capítulo.