Saltar al contenido

007 — La clave primaria: cómo se distingue una fila de otra

🗂️ parte 🎚️ nivel ⏱️ duración 📗 clase

Programa · Parte 00 · ← Anterior · Siguiente →

Parte 00 — Primeros pasos: del archivo a la base de datos · Fundamentos · 2 horas estimadas · motores sqlite, duckdb, postgresql, mysql, mongodb · laboratorio labs/01-sql-foundations · 3 fuentes.

Conceptos centrales: clave primaria · clave natural · clave sustituta · clave compuesta · UNIQUE

En este caso se comparan 6 motores: 5 lo resuelven (5 con el resultado comprobado por máquina) y 1 no, con el motivo escrito.

De qué trata esta clase

Cómo se distingue una fila de otra. Presenta la clave primaria y el debate entre clave natural y sustituta con su criterio real —cuál de las dos sobrevive a los cambios del mundo— y la clave compuesta, que reaparecerá al hablar de índices y de particionado.

flowchart LR
    C["🗄️ Clase 007"]
    C --> K1["clave primaria"]
    C --> K2["clave natural"]
    C --> K3["clave sustituta"]
    C --> K4["clave compuesta"]
    C --> K5["UNIQUE"]
    classDef raiz fill:#0b3d2e,stroke:#3fb950,color:#fff
    class C raiz

Antes de empezar

Esta clase supone que ya trabajaste lo siguiente. Si algo de la última columna no te suena, vuelve a esa clase antes de seguir: aquí se usa sin volver a explicarlo.

# Clase previa Lo que se da por sabido
003 Tu primera base de datos: crear, insertar y leer CREATE TABLE · INSERT · SELECT · definición frente a manipulación · NULL
006 Tipos de datos: por qué un número no es un texto tipo · decimal exacto · coma flotante · fecha ISO-8601 · afinidad de tipos

Vocabulario de la clase

Los términos que siguen se usan más adelante con este significado exacto. La definición completa, con sus términos relacionados, está en el glosario del programa.

Término Qué significa Procedencia
clave primaria La clave candidata elegida para identificar cada fila: única, no nula y estable en el tiempo. Es la dirección por la que el resto del esquema se referirá a esa fila. se introduce aquí
clave natural Identificador que ya existe en el dominio —RUT, ISBN, matrícula—. Ventaja: significa algo. Riesgo: el mundo la cambia —una persona corrige su documento, un organismo reasigna códigos— y el cambio arrastra a todas las filas que la referencian. se introduce aquí
clave sustituta Identificador inventado por el sistema y sin significado externo: entero autoincremental, UUID. No cambia nunca porque no depende del mundo, a costa de necesitar además una restricción UNIQUE sobre la clave natural real. se introduce aquí
clave compuesta Clave primaria formada por dos o más columnas, típica de las tablas de relación: (estudiante_id, curso_id). Fija además el orden de las columnas del índice que la sostiene, y ese orden decide qué consultas se aceleran. se introduce aquí
UNIQUE Restricción que prohíbe valores repetidos en una columna o combinación de columnas. A diferencia de la clave primaria admite nulos —y cuántos admite depende del motor, que es una de las divergencias clásicas entre dialectos. se introduce aquí

Propósito

Contestar una pregunta que parece trivial y no lo es: ¿cómo distingue el sistema una fila de otra? De esa respuesta dependen las actualizaciones, los borrados, las relaciones entre tablas y la posibilidad misma de corregir un dato.

Resultados de aprendizaje

Al terminar podrás:

  1. Declarar una clave primaria y explicar qué garantiza.
  2. Distinguir clave natural de clave sustituta y elegir con criterio.
  3. Reconocer una clave compuesta y cuándo hace falta.
  4. Explicar por qué una clave primaria no puede ser nula ni cambiar a la ligera.
  5. Nombrar la fuente de cada afirmación anterior.

Fundamentos

Qué garantiza una clave primaria

Declarar PRIMARY KEY sobre un campo obliga a tres cosas:

  1. No se repite. Dos filas no pueden tener el mismo valor.
  2. No es nulo. Toda fila tiene que tenerlo.
  3. Identifica. Ese valor, y solo ese, señala a una fila concreta.

Sin ella, «actualiza la fila de Ada» es una orden ambigua en cuanto haya dos Adas. Y las habrá.

Natural o sustituta

Una clave natural es un dato del propio dominio que ya identifica: el correo, el RUT, el ISBN de un libro. Una clave sustituta es un número inventado, sin significado, que solo existe para identificar: el típico id.

Natural Sustituta
Significado Tiene Ninguno
¿Puede cambiar? Sí, y pasa No, nunca
Legible No
Espacio Variable Pequeño y fijo
¿Sirve como referencia? Solo si no cambia Siempre

El argumento decisivo es el cambio. Un correo se cambia; un RUT se corrige porque estaba mal escrito; un código de producto se reorganiza. Cada vez que eso ocurre, toda tabla que lo hubiera copiado como referencia hay que actualizarla, y basta olvidar una para dejar datos huérfanos.

Con clave sustituta, cambiar el correo es un UPDATE de una fila y ninguna referencia se entera.

La recomendación práctica, y la que sigue este programa: clave sustituta para identificar y referenciar; clave natural declarada además como UNIQUE. Las dos, no una.

CREATE TABLE estudiantes (
    id     INTEGER PRIMARY KEY,       -- identidad estable
    correo TEXT NOT NULL UNIQUE,      -- identidad de negocio
    nombre TEXT NOT NULL
);

Sin ese UNIQUE, el sistema aceptaría dos personas con el mismo correo y nadie lo notaría hasta que una intentara recuperar su contraseña.

Claves compuestas

A veces lo que identifica es una pareja. En una tabla de inscripciones, la fila queda identificada por quién y en qué curso:

CREATE TABLE inscripciones (
    estudiante_id INTEGER NOT NULL,
    curso_id      INTEGER NOT NULL,
    PRIMARY KEY (estudiante_id, curso_id)
);

Esa clave compuesta hace algo más que identificar: impide que el mismo estudiante se inscriba dos veces en el mismo curso. La regla de negocio queda dentro del esquema, sin código.

Lo que una clave primaria no debe ser

flowchart TD
    A["¿Qué identifica esta fila?"] --> B{"¿Hay un dato del<br/>dominio único<br/>y estable?"}
    B -- "No" --> S["Clave sustituta"]
    B -- "Sí" --> C{"¿Puede cambiar<br/>alguna vez?"}
    C -- "Sí" --> S
    C -- "No" --> D["Puede ser natural...<br/>y aun así conviene<br/>la sustituta"]
    S --> E["Y la clave natural,<br/>declarada como UNIQUE"]

Ejemplo trabajado

Una academia usa el correo como clave primaria:

CREATE TABLE estudiantes (
    correo TEXT PRIMARY KEY,
    nombre TEXT NOT NULL
);
CREATE TABLE inscripciones (
    correo TEXT NOT NULL,
    curso  TEXT NOT NULL,
    PRIMARY KEY (correo, curso)
);

Funciona bien durante un año. Entonces Ada cambia de correo.

Lo que hay que hacer ahora. Actualizar estudiantes, actualizar inscripciones, y actualizar cualquier otra tabla que hubiera copiado el correo —pagos, certificados, registro de asistencia—. Si el motor tiene claves foráneas con ON UPDATE CASCADE, lo hace solo; si no, hay que acordarse de todas. Y si se olvida una, esas filas quedan apuntando a un correo que ya no existe.

Hay un problema peor: mientras dura la actualización, el sistema tiene el dato a medias. Con una sola tabla no importa; con cinco y sin transacción, sí.

Con clave sustituta.

CREATE TABLE estudiantes (
    id     INTEGER PRIMARY KEY,
    correo TEXT NOT NULL UNIQUE,
    nombre TEXT NOT NULL
);
CREATE TABLE inscripciones (
    estudiante_id INTEGER NOT NULL,
    curso         TEXT NOT NULL,
    PRIMARY KEY (estudiante_id, curso)
);

UPDATE estudiantes SET correo = 'ada@nuevo.org' WHERE id = 1;

Una fila. Ninguna referencia tocada. Y el UNIQUE sigue impidiendo dos estudiantes con el mismo correo, que era la parte útil de la clave natural.

Errores frecuentes

  1. Tabla sin clave primaria. Parece funcionar hasta el primer duplicado, y entonces no hay forma de borrar solo uno de los dos.
  2. Usar como clave un dato que cambia. Correo, teléfono, nombre, cualquier código de negocio «que nunca cambia».
  3. Poner clave sustituta y olvidar el UNIQUE de la natural. Se admite el duplicado que se quería evitar.
  4. Clave compuesta de cinco campos porque «así es único». Cada tabla que la referencie tendrá que copiar los cinco.
  5. Reutilizar identificadores de filas borradas. Los datos históricos pasan a señalar a otra cosa.
  6. Creer que un identificador aleatorio es siempre mejor. Un UUID en texto engorda todos los índices de la tabla; conviene saber lo que cuesta.

Ejemplo de transferencia

Todos los almacenes tienen este problema y lo resuelven parecido: en MongoDB el _id es obligatorio y inmutable —si se usara el correo, cambiarlo obligaría a borrar y reinsertar el documento—; en Redis la clave es literalmente la ruta de acceso, así que nombrar por identificador y mantener un índice aparte es la única opción sensata; en Cassandra la clave primaria decide en qué nodo vive la fila y no se puede actualizar en absoluto.

Reto de transferencia

  1. Elige dos tablas reales y escribe cuál es su clave primaria.
  2. Para cada una, responde: ¿ese valor puede cambiar alguna vez? Si la respuesta es sí, cuenta cuántas tablas tendrían que actualizarse.
  3. Encuentra una tabla sin clave primaria y describe qué operación se vuelve imposible.
  4. Añade a una de tus tablas la pareja completa: sustituta como primaria y natural como UNIQUE. Intenta insertar un duplicado y guarda el error.

Preguntas de evaluación

  1. ¿Qué tres cosas garantiza una clave primaria?
  2. ¿Por qué el correo es mala clave primaria aunque sea único?
  3. ¿Qué regla de negocio impone una clave primaria compuesta en una tabla de inscripciones?
  4. Si eliges clave sustituta, ¿qué hay que declarar además y por qué?

🌐 El mismo problema en cada motor

Caso: Corregir el correo de una de las dos Ada

Dos estudiantes se llaman Ada. No es un caso rebuscado: es lo normal en cuanto hay más de cien personas. Hay que corregir el correo de una de las dos.

Con una clave primaria, la orden es exacta: WHERE id = 2. Sin ella, la única forma de señalar la fila sería por nombre, y WHERE nombre = 'Ada' cambiaría las dos —y además fallaría, porque dejaría dos correos iguales en una columna UNIQUE.

Eso es lo que garantiza una clave primaria: que existe una forma de referirse a una fila y solo a una. Sin ella, actualizar y borrar dejan de ser operaciones precisas.

Salida esperada, idéntica en todos los motores que lo resuelven:

id nombre correo
1 Ada ada@example.org
2 Ada nuevo@example.org
3 Linus linus@example.org

El contrato vive en motores.yaml y lo comprueba python scripts/verificar_equivalencia.py --clase 007: 5 de las 5 implementaciones se ejecutan de verdad y su resultado se compara con esa tabla; el resto se declara como material revisado, no ejecutado.

Motor ¿Resuelve el caso? Nivel de prueba Código Fuente
SQLite núcleo código doc oficial
DuckDB núcleo código doc oficial
PostgreSQL servicio código doc oficial
MySQL servicio código doc oficial
MongoDB servicio código doc oficial
Apache Cassandra no doc oficial

Los que resuelven el caso

SQLite · implementaciones/sqlite/consulta.sql

verificado — se ejecuta en CI sin servicios

-- motor: sqlite
-- doc: https://sqlite.org/lang_createtable.html
-- nota: INTEGER PRIMARY KEY es un alias del identificador interno de fila, asi
--       que la identidad estable no cuesta ni una columna adicional.

-- === preparacion ===
-- Dos estudiantes se llaman igual. No es un caso raro: es lo normal en
-- cuanto hay mas de cien personas.
CREATE TABLE estudiantes (
    id     INTEGER PRIMARY KEY,
    nombre TEXT NOT NULL,
    correo TEXT NOT NULL UNIQUE
);
INSERT INTO estudiantes (id, nombre, correo) VALUES
    (1, 'Ada',   'ada@example.org'),
    (2, 'Ada',   'ada2@example.org'),
    (3, 'Linus', 'linus@example.org');

-- Corregir el correo de la SEGUNDA Ada. Con el id se puede senalar a una fila
-- concreta; con el nombre no:
--   UPDATE estudiantes SET correo = ... WHERE nombre = 'Ada';
-- habria cambiado las dos, y ademas habria fallado por violar el UNIQUE.
UPDATE estudiantes SET correo = 'nuevo@example.org' WHERE id = 2;

-- === consulta ===
SELECT id, nombre, correo FROM estudiantes ORDER BY id;

DuckDB · implementaciones/duckdb/consulta.sql

verificado — se ejecuta en CI sin servicios

-- motor: duckdb
-- doc: https://duckdb.org/docs/stable/sql/constraints
-- nota: la comprobacion que hay que hacer ANTES de declarar una clave sobre
--       datos que ya existen:
--         SELECT correo, COUNT(*) FROM estudiantes
--         GROUP BY correo HAVING COUNT(*) > 1;
--       Si devuelve filas, la clave no se puede crear todavia.

-- === preparacion ===
-- Dos estudiantes se llaman igual. No es un caso raro: es lo normal en
-- cuanto hay mas de cien personas.
CREATE TABLE estudiantes (
    id     INTEGER PRIMARY KEY,
    nombre VARCHAR NOT NULL,
    correo VARCHAR NOT NULL UNIQUE
);
INSERT INTO estudiantes (id, nombre, correo) VALUES
    (1, 'Ada',   'ada@example.org'),
    (2, 'Ada',   'ada2@example.org'),
    (3, 'Linus', 'linus@example.org');

-- Corregir el correo de la SEGUNDA Ada. Con el id se puede senalar a una fila
-- concreta; con el nombre no:
--   UPDATE estudiantes SET correo = ... WHERE nombre = 'Ada';
-- habria cambiado las dos, y ademas habria fallado por violar el UNIQUE.
UPDATE estudiantes SET correo = 'nuevo@example.org' WHERE id = 2;

-- === consulta ===
SELECT id, nombre, correo FROM estudiantes ORDER BY id;

PostgreSQL · implementaciones/postgresql/consulta.sql

verificado — se ejecuta contra el motor real levantado con docker compose

-- motor: postgresql
-- doc: https://www.postgresql.org/docs/current/ddl-identity-columns.html
-- nota: las dos identidades conviven y las dos hacen falta:
--         id     integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY  -- referencias
--         correo text NOT NULL UNIQUE                              -- negocio
--       Aqui el id se escribe a mano para que las filas sean comparables con
--       las de los demas motores.

-- === preparacion ===
DROP TABLE IF EXISTS estudiantes;

-- Dos estudiantes se llaman igual. No es un caso raro: es lo normal en
-- cuanto hay mas de cien personas.
CREATE TABLE estudiantes (
    id     integer PRIMARY KEY,
    nombre text NOT NULL,
    correo text NOT NULL UNIQUE
);
INSERT INTO estudiantes (id, nombre, correo) VALUES
    (1, 'Ada',   'ada@example.org'),
    (2, 'Ada',   'ada2@example.org'),
    (3, 'Linus', 'linus@example.org');

-- Corregir el correo de la SEGUNDA Ada. Con el id se puede senalar a una fila
-- concreta; con el nombre no:
--   UPDATE estudiantes SET correo = ... WHERE nombre = 'Ada';
-- habria cambiado las dos, y ademas habria fallado por violar el UNIQUE.
UPDATE estudiantes SET correo = 'nuevo@example.org' WHERE id = 2;

-- === consulta ===
SELECT id, nombre, correo FROM estudiantes ORDER BY id;

MySQL · implementaciones/mysql/consulta.sql

verificado — se ejecuta contra el motor real levantado con docker compose

-- motor: mysql
-- doc: https://dev.mysql.com/doc/refman/8.4/en/example-auto-increment.html
-- nota: InnoDB organiza FISICAMENTE la tabla por la clave primaria, asi que una
--       clave ancha —un UUID en texto— engorda todos los indices secundarios a
--       la vez. Aqui la eleccion de clave tiene un costo de almacenamiento que
--       en otros motores no tiene.

-- === preparacion ===
DROP TABLE IF EXISTS estudiantes;

-- Dos estudiantes se llaman igual. No es un caso raro: es lo normal en
-- cuanto hay mas de cien personas.
CREATE TABLE estudiantes (
    id     INT PRIMARY KEY,
    nombre VARCHAR(50) NOT NULL,
    correo VARCHAR(50) NOT NULL UNIQUE
);
INSERT INTO estudiantes (id, nombre, correo) VALUES
    (1, 'Ada',   'ada@example.org'),
    (2, 'Ada',   'ada2@example.org'),
    (3, 'Linus', 'linus@example.org');

-- Corregir el correo de la SEGUNDA Ada. Con el id se puede senalar a una fila
-- concreta; con el nombre no:
--   UPDATE estudiantes SET correo = ... WHERE nombre = 'Ada';
-- habria cambiado las dos, y ademas habria fallado por violar el UNIQUE.
UPDATE estudiantes SET correo = 'nuevo@example.org' WHERE id = 2;

-- === consulta ===
SELECT id, nombre, correo FROM estudiantes ORDER BY id;

MongoDB · implementaciones/mongodb/consulta.js

verificado — se ejecuta contra el motor real levantado con docker compose

// motor: mongodb
// doc: https://www.mongodb.com/docs/manual/core/document/
// nota: el _id es obligatorio e INMUTABLE. Si se hubiera usado el correo como
//       _id, esta correccion no seria una actualizacion: habria que borrar el
//       documento y crear otro, con todo lo que apuntara a el.

// === preparacion ===
db.estudiantes.drop();
db.estudiantes.insertMany([
  { _id: 1, nombre: "Ada", correo: "ada@example.org" },
  { _id: 2, nombre: "Ada", correo: "ada2@example.org" },
  { _id: 3, nombre: "Linus", correo: "linus@example.org" },
]);
db.estudiantes.createIndex({ correo: 1 }, { unique: true });

db.estudiantes.updateOne({ _id: 2 }, { $set: { correo: "nuevo@example.org" } });

// === consulta ===
db.estudiantes
  .find()
  .sort({ _id: 1 })
  .forEach((d) => print(d._id + "|" + d.nombre + "|" + d.correo));

Los que no resuelven este caso — y qué se hace en su lugar

Descartar un motor con un argumento es tan formativo como usarlo. Ninguna de estas filas dice que el motor sea peor: dice que este problema no es el suyo.

Motor Por qué no Qué se hace en su lugar Fuente
Apache Cassandra Aquí la clave primaria decide en qué nodo vive la fila, así que no se puede actualizar; y la comprobación de unicidad sobre otra columna no existe: un INSERT con una clave que ya está sobrescribe en silencio. Un identificador estable (UUID) como clave de partición y una tabla aparte estudiante_por_correo que haga de índice, mantenida por la aplicación. doc

Laboratorio

python scripts/validate_repository.py
python labs/01-sql-foundations/run_lab.py

Guarda como evidencia la salida completa, la versión del motor y la semilla o los parámetros usados. Una captura sin comando no es evidencia: no se puede repetir.

Evaluación

Criterio Peso Qué se comprueba
Comprensión conceptual 25 % Explica el mecanismo, no solo el resultado
Ejecución reproducible 25 % Otra persona obtiene lo mismo con las instrucciones dadas
Interpretación basada en evidencia 25 % Cada conclusión se apoya en una salida o una medición
Límites y riesgos declarados 25 % Dice qué no demuestra el ejercicio y qué faltaría en producción

La clase se da por superada cuando la respuesta explica el mecanismo, muestra la salida que la respalda y declara al menos un límite del ejercicio.

Fuentes de esta clase

Todo lo afirmado arriba procede de estas obras. Los identificadores viven en catalog/sources.json y el estado de los enlaces se comprueba con python scripts/check_external_links.py.


Programa · Parte 00 · ← Anterior · Siguiente →