Tabla puente
También llamada tabla intermedia, junction table o entidad asociativa. Considere que los ejemplos descritos parten del caso de Universidad que hemos trabajo en clase.
Qué es y para qué sirve
- Es una tabla que resuelve una relación muchos-a-muchos (N:M) entre dos (o más) entidades del modelo.
- Convierte esa relación N:M en dos relaciones 1:N, garantizando integridad y evitando duplicación.
- Además de enlazar, puede almacenar atributos propios de la relación (p. ej., fecha de inscripción, rol, nota).
Cuándo la necesitas
- Cuando A ↔ B tiene cardinalidad N:M:
- Student ↔ Course (un estudiante cursa muchos cursos y un curso tiene muchos estudiantes) → Enrollment.
- Libro ↔ Autor → BookAuthor.
- Auto-relación N:M (seguidores en redes): User ↔ User → UserFollow.
- En relaciones ternarias (A–B–C) que requieren mantener una combinación única de tres claves (p. ej., Student–Course–Semester).
Estructura típica
Una tabla puente suele contener:
- Claves foráneas a las entidades que vincula (NOT NULL).
- Clave primaria que garantiza unicidad de la pareja (o tercia) vinculada.
- Atributos de la relación (opcionales).
- Restricciones para reflejar reglas de negocio (UNIQUE, CHECK, ON DELETE…).
Dos patrones de clave primaria
- PK compuesta (recomendada si solo enlazas y no necesitas referenciar la fila desde otros lados):
- PK =
(fk_a, fk_b)yUNIQUEimplícita por la PK.
- PK =
- PK surrogate (id autoincremental) +
UNIQUE(fk_a, fk_b):- Útil si la fila tendrá muchos atributos, si habrá FK externas apuntando a este enlace, o historial.
Ejemplo (MySQL/MariaDB) en el caso de Universdiad:
Student ↔ Course → Enrollment
-- Opción A: PK compuesta
CREATE TABLE enrollment (
student_id INT NOT NULL,
course_id INT NOT NULL,
enrolled_at DATETIME DEFAULT CURRENT_TIMESTAMP,
grade TINYINT,
PRIMARY KEY (student_id, course_id), -- unicidad del par
CONSTRAINT fk_enr_student FOREIGN KEY (student_id) REFERENCES student(student_id)
ON DELETE CASCADE,
CONSTRAINT fk_enr_course FOREIGN KEY (course_id) REFERENCES course(course_id)
ON DELETE RESTRICT
);
-- Opción B: surrogate key + UNIQUE
CREATE TABLE enrollment (
enrollment_id INT AUTO_INCREMENT PRIMARY KEY,
student_id INT NOT NULL,
course_id INT NOT NULL,
enrolled_at DATETIME DEFAULT CURRENT_TIMESTAMP,
grade TINYINT,
UNIQUE (student_id, course_id), -- evita duplicados
FOREIGN KEY (student_id) REFERENCES student(student_id),
FOREIGN KEY (course_id) REFERENCES course(course_id)
);
-- Índices útiles (según consultas más frecuentes)
CREATE INDEX idx_enr_course_student ON enrollment (course_id, student_id);
CREATE INDEX idx_enr_student_course ON enrollment (student_id, course_id);Cómo llega desde el E-R al relacional
- En el modelo E-R trazas una relación N:M entre STUDENT y COURSE.
- En el modelo relacional, esa relación se mapea a una tabla asociativa con las FKs a cada entidad.
- Si la relación tiene atributos (p. ej., grade), van en la tabla puente (no en student ni course).
Reglas de negocio típicas (y cómo implementarlas)
- “Un estudiante no puede inscribirse dos veces al mismo curso” → PK compuesta o
UNIQUE(student_id, course_id). - “Si borro un estudiante, borro sus inscripciones” →
ON DELETE CASCADEenfk_enr_student. - “Si un curso tiene historial, no se puede borrar” →
ON DELETE RESTRICTenfk_enr_course. - “La nota debe ser 0–100” →
CHECK (grade BETWEEN 0 AND 100).
Buenas prácticas
- Nombre descriptivo:
enrollment,book_author,user_follow(ostudent_coursesi prefieres compuesto). - FKs NOT NULL (la relación no existe sin ambos lados).
- Unicidad del par/terna con PK compuesta o
UNIQUE. - Índices alineados a los JOIN y filtros más usados.
- Evitar datos derivados en la puente (p. ej., totales que se pueden calcular).
- Transacciones al insertar desde múltiples sesiones para que el
UNIQUEevite “doble vínculo” por carrera de concurrencia.
¿Es una entidad débil?
- No por definición. Es una entidad asociativa.
- Puede comportarse como débil si su identidad depende completamente de las FKs y no tiene clave propia; aun así, en la práctica la nombramos tabla puente o entidad asociativa para N:M.
Constraint
Qué es un constraint
Un constraint (restricción) es una regla que el SGBD hace cumplir automáticamente para proteger la integridad de los datos.
Si una instrucción INSERT/UPDATE viola la regla, la operación falla y la base no queda en un estado inválido.
Tres grandes objetivos que cubren los constraints:
- Dominio: que cada columna tenga valores válidos (tipo, rango, obligatoriedad).
- Entidad: que cada fila sea identificable de forma única.
- Referencia: que las relaciones entre tablas se mantengan coherentes.
Tipos de constraints (estándar SQL)
- NOT NULL – prohíbe valores nulos en una columna.
- UNIQUE – asegura que no se repitan valores (puede ser de una o varias columnas).
- PRIMARY KEY – identifica de forma única a cada fila (implícitamente
NOT NULL+UNIQUE). - FOREIGN KEY – obliga a que un valor exista previamente en la tabla referenciada (integridad referencial).
- CHECK – valida una condición booleana (rango, patrón, consistencia simple).
Nota de versiones: CHECK se enfuerza en MySQL ≥ 8.0.16 y MariaDB ≥ 10.2.x. En versiones más antiguas se ignoraba o requería alternativas (triggers).
Ejemplos rápidos (MariaDB/MySQL)
A. Definir constraints al crear tablas
CREATE TABLE student (
student_id INT AUTO_INCREMENT,
student_code VARCHAR(20) NOT NULL, -- NOT NULL
email VARCHAR(255) UNIQUE, -- UNIQUE
PRIMARY KEY (student_id), -- PRIMARY KEY
CONSTRAINT uq_student_code UNIQUE (student_code), -- UNIQUE nombrado
CONSTRAINT chk_code_len CHECK (CHAR_LENGTH(student_code) >= 3) -- CHECK
) ENGINE=InnoDB;
CREATE TABLE course (
course_id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(20) NOT NULL UNIQUE,
credits TINYINT NOT NULL,
CONSTRAINT chk_credits CHECK (credits BETWEEN 1 AND 10)
) ENGINE=InnoDB;
CREATE TABLE enrollment (
student_id INT NOT NULL,
course_id INT NOT NULL,
enrolled_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (student_id, course_id), -- UNIQUE del par
CONSTRAINT fk_enr_student FOREIGN KEY (student_id)
REFERENCES student(student_id) ON DELETE CASCADE,
CONSTRAINT fk_enr_course FOREIGN KEY (course_id)
REFERENCES course(course_id) ON DELETE RESTRICT
) ENGINE=InnoDB;B. Añadir o quitar constraints después (ALTER TABLE)
-- Agregar UNIQUE compuesto
ALTER TABLE student
ADD CONSTRAINT uq_student_name_email UNIQUE (full_name, email);
-- Agregar FOREIGN KEY
ALTER TABLE enrollment
ADD CONSTRAINT fk_enr_course FOREIGN KEY (course_id)
REFERENCES course(course_id) ON DELETE RESTRICT;
-- Quitar una FOREIGN KEY (usa su nombre)
ALTER TABLE enrollment DROP FOREIGN KEY fk_enr_course;
-- Quitar un UNIQUE en MySQL/MariaDB se hace como índice:
ALTER TABLE student DROP INDEX uq_student_name_email;
-- Quitar un CHECK (MySQL 8+)
ALTER TABLE course DROP CHECK chk_credits;¿Por qué usar constraints y no sólo validación en la app?
- Consistencia centralizada: protegen los datos aunque haya múltiples aplicaciones o usuarios conectados.
- Atómicas: se aplican dentro de la transacción; no hay “ventanas” para estados inválidos.
- Performance:
PRIMARY KEYyUNIQUEcrean índices útiles; las FK suelen beneficiarse de índices en las columnas referenciadas.
Buenas prácticas
- Nombra tus constraints (
fk_enr_course,chk_credits): facilita depurar y alterar. - Usa
NOT NULLpor defecto salvo que nulo tenga un significado claro. - Prefiere
UNIQUEmulticolumna para reglas como “no hay dos matrículas del mismo estudiante en el mismo curso”. - Define
ON DELETE/UPDATEen FKs de acuerdo al negocio (CASCADE,RESTRICT,SET NULL). - CHECK para reglas de dominio (rangos, formatos). Para reglas complejas que cruzan varias filas/ tablas, evalúa triggers.
- Índices para FKs: añade un índice en la columna que referencia; mejora
JOINy borrados/actualizaciones en cascada.
Errores típicos
- Confiar sólo en la app: otro cliente puede saltarse la validación.
CHECKen versiones antiguas de MySQL/MariaDB → no se aplicaba (ver tu versión).- Falta de índices en FKs →
JOINlentos y cascadas costosas. PRIMARY KEYno estable (p. ej., usar un dato que cambia). Mejor surrogate key (AUTO_INCREMENT) y UNIQUE para la llave natural.
📘 Guía práctica: Organización de scripts SQL en Ubuntu (LAMP con MariaDB/MySQL)
0) (Opcional) Instalar LAMP con MariaDB
Si todavía no tienes tu entorno:
sudo apt update
sudo apt install -y apache2 mariadb-server php libapache2-mod-php php-mysql
sudo systemctl enable --now mariadb
sudo mysql_secure_installation # asegura root, elimina test DB, etc.Nota: en MariaDB puedes entrar al cliente con
sudo mysqlosudo mariadb.
1) Crear base de datos y usuario de trabajo
Abre el cliente como root:
sudo mysqlEn el prompt SQL:
-- Crea la BD (utf8mb4 = Unicode completo)
CREATE DATABASE IF NOT EXISTS uni_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
-- Usuario para base de datos
CREATE USER IF NOT EXISTS 'dev'@'localhost' IDENTIFIED BY 'Dev.2025!';
-- OJO: Aquí según se requiera puede dar privilegios
-- Todos los privilegios para el usuario
GRANT ALL ON uni_db.* TO 'dev'@'localhost';
-- Privilegios (no root, sólo lo necesario)
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX
ON uni_db.* TO 'dev'@'localhost';
FLUSH PRIVILEGES;
EXIT;2) Estructura de carpetas y archivos /sql
Dentro de tu proyecto:
Considera que podría estár en cualquier carpeta, pero, para ser ordenado/a, podría ser recomendable ubicarlos en la carpeta html:
cd /var/www/html
Luego de ello:
cd /var/www/html
mkdir -p proyecto/sql
cd proyecto/sql
touch 01_schema.sql 02_seed.sql 03_queries.sql3) Contenido de ejemplo (Universidad)
📂 01_schema.sql — Definición de tablas (DDL)
-- 01_schema.sql
SET NAMES utf8mb4;
SET time_zone = '+00:00';
CREATE DATABASE IF NOT EXISTS uni_db
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE uni_db;
-- Tabla STUDENT
CREATE TABLE IF NOT EXISTS student (
student_id INT AUTO_INCREMENT PRIMARY KEY,
student_code VARCHAR(20) NOT NULL UNIQUE,
full_name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- Tabla COURSE
CREATE TABLE IF NOT EXISTS course (
course_id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(20) NOT NULL UNIQUE,
title VARCHAR(120) NOT NULL,
credits TINYINT NOT NULL CHECK (credits BETWEEN 1 AND 10)
) ENGINE=InnoDB;
-- Relación N:M → ENROLLMENT
CREATE TABLE IF NOT EXISTS enrollment (
enrollment_id INT AUTO_INCREMENT PRIMARY KEY,
student_id INT NOT NULL,
course_id INT NOT NULL,
enrolled_at DATETIME DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_enr_student FOREIGN KEY (student_id)
REFERENCES student(student_id) ON DELETE CASCADE,
CONSTRAINT fk_enr_course FOREIGN KEY (course_id)
REFERENCES course(course_id) ON DELETE RESTRICT,
CONSTRAINT uq_enr UNIQUE (student_id, course_id)
) ENGINE=InnoDB;
-- Índice para consultas frecuentes
CREATE INDEX idx_enr_course_student ON enrollment (course_id, student_id);📂 02_seed.sql — Datos iniciales (DML)
-- 02_seed.sql
USE uni_db;
START TRANSACTION;
-- Estudiantes
INSERT INTO student (student_code, full_name, email) VALUES
('S-001','Ana Pérez','ana.perez@example.edu'),
('S-002','Luis Díaz','luis.diaz@example.edu'),
('S-003','Marta Ríos','marta.rios@example.edu');
-- Cursos
INSERT INTO course (code, title, credits) VALUES
('CS101','Fundamentos de Programación',4),
('DB201','Bases de Datos I',3),
('MA110','Cálculo I',4);
-- Matrículas (usando SELECT para obtener IDs)
INSERT INTO enrollment (student_id, course_id)
SELECT s.student_id, c.course_id
FROM student s JOIN course c
WHERE s.student_code='S-001' AND c.code='CS101';
INSERT INTO enrollment (student_id, course_id)
SELECT s.student_id, c.course_id
FROM student s JOIN course c
WHERE s.student_code='S-001' AND c.code='DB201';
INSERT INTO enrollment (student_id, course_id)
SELECT s.student_id, c.course_id
FROM student s JOIN course c
WHERE s.student_code='S-002' AND c.code='DB201';
COMMIT;
-- Validaciones rápidas
SELECT COUNT(*) AS total_students FROM student;
SELECT COUNT(*) AS total_courses FROM course;
SELECT COUNT(*) AS total_enrollments FROM enrollment;📂 03_queries.sql — Consultas representativas
-- 03_queries.sql
USE uni_db;
-- Q1: Filtrar por nombre
SELECT student_id, full_name, email
FROM student
WHERE full_name LIKE 'Ana%';
-- Q2: JOIN → lista inscripciones con nombre de alumno y curso
SELECT e.enrollment_id, s.full_name, c.title, e.enrolled_at
FROM enrollment e
JOIN student s ON s.student_id = e.student_id
JOIN course c ON c.course_id = e.course_id
ORDER BY e.enrolled_at DESC;
-- Q3: Agregación → número de estudiantes por curso
SELECT c.code, c.title, COUNT(*) AS enrolled_students
FROM enrollment e
JOIN course c ON c.course_id = e.course_id
GROUP BY c.course_id, c.code, c.title
HAVING COUNT(*) >= 1
ORDER BY enrolled_students DESC;
-- Q4: Subconsulta → estudiantes SIN matrícula en DB201
SELECT s.full_name
FROM student s
WHERE s.student_id NOT IN (
SELECT e.student_id
FROM enrollment e
JOIN course c ON c.course_id = e.course_id
WHERE c.code='DB201'
);
-- Q5: Vista de resumen
CREATE OR REPLACE VIEW vw_enroll_summary AS
SELECT s.student_code, s.full_name, c.code AS course_code, c.title, e.enrolled_at
FROM enrollment e
JOIN student s ON s.student_id = e.student_id
JOIN course c ON c.course_id = e.course_id;
-- Consultar la vista
SELECT * FROM vw_enroll_summary;4) Ejecutar los scripts en orden
#Ubiquese en en la carpeta de trabajo.
cd var/www/html/proyecto/sql
# 1) Esquema (DDL)
mysql -u root -p < 01_schema.sql
# 2) Datos iniciales (DML)
mysql -u dev -p --database=uni_db < 02_seed.sql
# 3) Consultas y vista
mysql -u dev -p --database=uni_db < 03_queries.sql⚠️ Recuerda: el orden importa → primero el esquema, luego los datos, después las consultas.
5) Integración con proyecto LAMP
Si tu aplicación web está en /var/www/html/miapp, puedes copiar los scripts allí:
sudo mkdir -p /var/www/html/miapp/sql
sudo cp var/www/html/proyecto/sql/*.sql /var/www/html/miapp/sql/
sudo chown -R $USER:www-data /var/www/html/miapp/sql
sudo chmod -R 750 /var/www/html/miapp/sqlApache no ejecuta SQL: estos archivos se usan desde la consola o desde tu app PHP.
🚀 Consejos finales
- Valida siempre: después de correr
02_seed.sql, usaSELECT COUNT(*)para comprobar registros. - Evita sorpresas: si necesitas rehacer, crea un
00_reset.sqlconDROP TABLE ...en orden inverso. - Documenta: pon comentarios (
-- ...) en cada bloque de tus scripts para que sepas qué hace cada parte. - Engine: mantén
InnoDBpara soportar claves foráneas y transacciones.