Volver a los posts
•
bases de datossqltutoriales

llaves foráneas en sql: de la tarea a la práctica

el puente que une a fabricantes y artículos

#sql#postgresql#mariadb#sqlite#bases-de-datos

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 UPDATE masivo 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.

Diagrama relacional entre fabricantes y artículos

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 (o NO ACTION): es el comportamiento por defecto. si intentas hacer DELETE 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 mal DELETE te borra inventario sin avisar.
  • ON DELETE SET NULL: borra al fabricante, pero conserva los artículos en la tabla cambiando su columna fabricante_id a null. para esto la columna debe permitir nulos (fabricante_id INTEGER NULL).

tres cosas que conviene tener presentes

  1. 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 JOIN o borras filas en fabricantes, el motor va a tener que hacer un escaneo secuencial completo sobre articulos. en producción casi siempre vas a querer crear el índice a mano:

    CREATE INDEX articulos_fabricante_id_idx ON articulos(fabricante_id);
  2. los tipos deben coincidir con exactitud: si fabricantes.id es INTEGER, articulos.fabricante_id tiene que ser INTEGER. si mezclas BIGINT en uno e INT en otro, la creación de la restricción va a fallar.

  3. 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 REFERENCES sin 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: