llaves foráneas en sql: de la tarea a la práctica
el puente que une a fabricantes y artículos
a mi hermano menor le dejaron una práctica sobre bases de datos en la prepa con dos tablas: fabricantes y articulos. me preguntó para qué sirven las llaves foráneas (foreign keys) y pos aquí está la explicación.
el problema de la tabla única
si metes todo en una sola tabla (articulos), terminas con algo así:
| id | descripcion | precio | fabricante_nombre | fabricante_pais |
|---|---|---|---|---|
| 1 | Teclado mecánico | 65.00 | Logitech | Suiza |
| 2 | Mouse óptico | 25.00 | Logitech | Suiza |
| 3 | Auriculares usb | 40.00 | logitech | suiza |
| 4 | Monitor 27” | 220.00 | Dell Inc. | EE.UU. |
cada vez que registres un producto de Logitech tienes que volver a escribir su nombre y su país. si tienes 500 artículos, escribes «Logitech» 500 veces.
pero lo peor no es el espacio desperdiciado, son las inconsistencias:
- si alguien escribe «Logitech» con minúscula o con un error de dedo en tres productos (como en la fila 3), para el sistema ya son fabricantes distintos.
- si la empresa cambia de razón social o de país, tienes que hacer un
UPDATEmasivo a cientos de filas. si una sola falla o se te pasa, la base de datos queda corrupta. - y si una marca pequeña tiene un solo producto y lo borras del catálogo, borraste también al fabricante del sistema. no queda rastro de que alguna vez existió.
separar y conectar
la solución es normalizar y partir la información en dos:
tabla fabricantes (la tabla padre):
| id | nombre | pais |
|---|---|---|
| 1 | Logitech | Suiza |
| 2 | Dell Inc. | EE.UU. |
tabla articulos (la tabla hija):
| id | descripcion | precio | fabricante_id |
|---|---|---|---|
| 101 | Teclado mecánico | 65.00 | 1 |
| 102 | Mouse óptico | 25.00 | 1 |
| 103 | Monitor 27” | 220.00 | 2 |
en fabricantes cada fila tiene un identificador único: su llave primaria (id).
y en articulos agregamos una columna para apuntar a ese identificador: fabricante_id. esa columna es la llave foránea.
en la notación de pata de gallo (crow’s foot), la relación es 1 a N: un fabricante puede tener cero o muchos artículos asociados, pero cada artículo pertenece a un único fabricante.
la restricción (constraint)
un detalle que confunde al inicio: llamar a la columna fabricante_id no la vuelve foránea por arte de magia. para el motor solo es un número entero cualquiera. si no defines la regla formal, puedes meter fabricante_id = 999 sin que exista el fabricante y la base de datos no va a decir nada: te queda un registro huérfano.
lo que activa la regla es la restricción (CONSTRAINT):
CREATE TABLE fabricantes (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nombre TEXT NOT NULL,
pais TEXT
);
CREATE TABLE articulos (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
descripcion TEXT NOT NULL,
precio NUMERIC(10, 2) NOT NULL CHECK (precio >= 0),
fabricante_id INTEGER NOT NULL,
CONSTRAINT articulos_fabricante_id_fk
FOREIGN KEY (fabricante_id)
REFERENCES fabricantes(id)
);
qué hace el motor
con la restricción activa, si intentas registrar un artículo con un fabricante que no existe:
INSERT INTO articulos (descripcion, precio, fabricante_id)
VALUES ('webcam', 50.00, 42);
el motor aborta la operación en seco:
ERROR: insert or update on table "articulos" violates foreign key constraint "articulos_fabricante_id_fk"
DETAIL: Key (fabricante_id)=(42) is not present in table "fabricantes".
no dependes de que tu backend o tu frontend recuerden validar si el fabricante existe; la integridad referencial la garantiza directamente el motor de la base de datos.
qué pasa al borrar un fabricante (ON DELETE)
la pregunta obligada: ¿qué pasa si borras un fabricante que ya tiene artículos registrados?
para eso existen las reglas ON DELETE:
ON DELETE RESTRICT(oNO ACTION): es el comportamiento por defecto. si intentas hacerDELETE FROM fabricantes WHERE id = 1;y ese fabricante tiene artículos, el motor bloquea la instrucción con un error. te obliga a decidir qué hacer con los artículos antes de tocar al padre. para catálogos y datos comerciales es la opción más sensata.ON DELETE CASCADE: si borras al fabricante, el motor elimina en automático todos sus artículos asociados. sirve para datos dependientes (como los renglones de una factura respecto a su cabecera), pero en catálogos principales es un peligro: un malDELETEte borra inventario sin avisar.ON DELETE SET NULL: borra al fabricante, pero conserva los artículos en la tabla cambiando su columnafabricante_idanull. para esto la columna debe permitir nulos (fabricante_id INTEGER NULL).
tres cosas que conviene tener presentes
-
las llaves foráneas no se indexan solas: en PostgreSQL (y en la mayoría de motores libres), la llave primaria crea un índice b-tree automático, pero la llave foránea en la tabla hija no. si la tabla de artículos crece y haces consultas con
JOINo borras filas enfabricantes, el motor va a tener que hacer un escaneo secuencial completo sobrearticulos. en producción casi siempre vas a querer crear el índice a mano:CREATE INDEX articulos_fabricante_id_idx ON articulos(fabricante_id); -
los tipos deben coincidir con exactitud: si
fabricantes.idesINTEGER,articulos.fabricante_idtiene que serINTEGER. si mezclasBIGINTen uno eINTen otro, la creación de la restricción va a fallar. -
el caso de sqlite: en SQLite las llaves foráneas vienen desactivadas por defecto por compatibilidad histórica. para que el motor aplique las restricciones tienes que activarlas en cada conexión:
PRAGMA foreign_keys = ON;si no lo haces, SQLite acepta la sintaxis de
REFERENCESsin marcar error, pero ignora las violaciones de integridad referencial.
documentación oficial de referencia
para revisar detalles de sintaxis y comportamiento específico de cada motor: