El modelo relacional
"Un buen esquema es la diferencia entre un sistema que crece y uno que se hunde con su propio peso."
Qué vas a aprender en este capítulo
Antes de escribir SQL, necesitás entender el modelo que las bases relacionales usan: tablas con filas y columnas, conectadas por llaves. Vas a aprender el vocabulario formal (relación, tupla, atributo, dominio), los tipos de claves, las restricciones de integridad que el DBMS aplica automáticamente y por qué el modelo relacional ganó la partida sobre alternativas en los 70 y sigue siendo el estándar 50 años después.
1.1 La idea: datos en filas y columnas
💡 Intuición
¿Cómo ordenás los datos de la pupusería en papel?
+-----+------------------+--------+-------+
| ID | Nombre | Precio | Stock |
+-----+------------------+--------+-------+
| Q | Pupusa de queso | 0.50 | 100 |
| R | Pupusa revuelta | 0.60 | 80 |
| F | Pupusa de frijol | 0.55 | 60 |
+-----+------------------+--------+-------+
Una tabla: filas (productos) y columnas (atributos). Cada celda contiene un valor. Esa es la estructura básica de toda base de datos relacional.
El modelo relacional dice: "todos tus datos viven en tablas; las tablas se relacionan entre sí por valores compartidos (llaves)". Punto.
Esa simplicidad ganó porque:
- Es fácil de entender. Cualquiera con experiencia con Excel intuye lo básico.
- Es matemáticamente sólido. Edgar Codd, su inventor (1970), lo basó en teoría de conjuntos.
- Es flexible. Permite consultas no anticipadas — preguntas que no eran obvias al diseñar.
- Garantiza integridad. Reglas que protegen los datos automáticamente.
📜 Historia
Antes de los 70, las bases de datos eran jerárquicas (IMS de IBM, 1968) o de red (CODASYL, 1969). Datos almacenados como árboles o grafos, navegados con código procedural. Cambiar el esquema rompía todo.
Edgar F. Codd, matemático trabajando en IBM, publicó en 1970 "A Relational Model of Data for Large Shared Data Banks". La idea: separar qué querés (descripción declarativa) de cómo se obtiene (algoritmo). El usuario describe la consulta; el DBMS la traduce a operaciones físicas. Revolucionario.
IBM tardó casi una década en construir un producto basado en sus ideas (System R, después DB2). Mientras tanto, Larry Ellison leyó los papers, vio el potencial, fundó Oracle (1977) y le ganó a IBM en el mercado.
SQL (Structured Query Language) salió de System R, primero llamado SEQUEL, después SQL por razones de marca. Fue estandarizado por ANSI en 1986, y su sintaxis sigue siendo la misma hoy en día con extensiones.
Codd recibió el Premio Turing en 1981. Su modelo se enseña en cada universidad que tiene una carrera relacionada con software.
1.2 Vocabulario formal vs común
📐 Fundamento
El modelo relacional tiene términos formales y términos coloquiales. Conviene conocer ambos:
| Formal | Común | Significado |
|---|---|---|
| Relación | Tabla | Conjunto de tuplas |
| Tupla | Fila / registro | Una "línea" de la tabla |
| Atributo | Columna / campo | Una propiedad |
| Dominio | Tipo de dato | Conjunto de valores válidos |
| Cardinalidad | Cantidad de filas | |
| Grado | Cantidad de columnas | |
| Esquema | Definición de la tabla | Estructura, columnas, tipos |
Ejemplo:
- "Hay 3 tuplas en la relación Producto" = "Hay 3 filas en la tabla Producto".
- "El dominio del atributo precio es DECIMAL" = "El tipo de la columna precio es DECIMAL".
En conversación común hablás de tablas, filas, columnas. En documentos formales / exámenes, podés ver lo otro. Las dos formas valen.
Una tabla es un conjunto matemáticamente. Eso significa:
- Sin orden entre filas.
- Sin duplicados (en teoría — en la práctica los DBMS los permiten salvo que pidas lo contrario).
- Sin orden entre columnas (al usar la tabla; al definirla sí hay orden).
Por eso un SELECT * FROM productos puede devolver las filas en cualquier orden, salvo que uses ORDER BY.
1.3 Definir una tabla — DDL básico
📐 Fundamento
El DDL (Data Definition Language) es el subconjunto de SQL para definir esquemas:
CREATE TABLE producto (
codigo VARCHAR(10) PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
precio DECIMAL(10,2) NOT NULL CHECK (precio >= 0),
stock INTEGER NOT NULL DEFAULT 0,
activo BOOLEAN NOT NULL DEFAULT TRUE
);
Anatomía:
CREATE TABLE producto— crea la tabla.- Cada línea define una columna: nombre + tipo + restricciones.
VARCHAR(10)— texto de hasta 10 caracteres.DECIMAL(10,2)— número con 10 dígitos en total, 2 después del decimal. Ideal para dinero.INTEGER,BOOLEAN,DATE,TIMESTAMP— otros tipos comunes.PRIMARY KEY— esa columna es la llave primaria (más en sección siguiente).NOT NULL— el valor no puede ser nulo.CHECK (...)— restricción personalizada (precio no negativo).DEFAULT— valor si no se especifica al insertar.
Tipos comunes (SQL estándar):
| Categoría | Tipos típicos |
|---|---|
| Enteros | SMALLINT, INTEGER, BIGINT |
| Decimales | DECIMAL(p,s), NUMERIC(p,s), REAL, DOUBLE PRECISION |
| Texto | CHAR(n) (longitud fija), VARCHAR(n), TEXT |
| Fechas | DATE, TIME, TIMESTAMP |
| Booleano | BOOLEAN |
| Binario | BLOB, BYTEA |
Modificar:
ALTER TABLE producto ADD COLUMN descripcion VARCHAR(500);
ALTER TABLE producto DROP COLUMN activo;
ALTER TABLE producto ALTER COLUMN precio TYPE DECIMAL(12, 2);
Eliminar:
DROP TABLE producto; -- ¡cuidado, borra la tabla y los datos!
1.4 Llaves
📐 Fundamento
Las llaves son las herramientas que el modelo relacional usa para identificar filas y conectar tablas.
Llave primaria (Primary Key — PK)
Identifica de manera única cada fila. La PK es obligatoria (cada tabla seria tiene una) y única.
CREATE TABLE cliente (
id SERIAL PRIMARY KEY, -- entero auto-incremental
email VARCHAR(100) UNIQUE NOT NULL
);
SERIAL (Postgres) o AUTO_INCREMENT (MySQL): el DBMS asigna automáticamente 1, 2, 3, ... al insertar.
Reglas de la PK:
- No puede ser nula.
- No puede repetirse.
- Idealmente: estable (no cambia con el tiempo). Por eso preferimos un ID artificial sobre algo "natural" como el email (puede cambiar).
- Idealmente: chica (eficiencia de índices). Un INTEGER es mejor que un VARCHAR(100).
Llave candidata
Cualquier subconjunto de columnas que identifique únicamente. La PK se elige entre las llaves candidatas; las otras se mantienen como UNIQUE.
Ejemplo: en cliente, la PK es id. Pero email también identifica únicamente. email es llave candidata y se marca UNIQUE.
Llave compuesta
A veces una sola columna no alcanza. Ejemplo: tabla de inscripciones — un alumno se inscribe a una materia.
CREATE TABLE inscripcion (
alumno_id INTEGER,
materia_id INTEGER,
fecha DATE,
PRIMARY KEY (alumno_id, materia_id)
);
La PK son dos columnas juntas. Un alumno puede estar en muchas materias, una materia tiene muchos alumnos, pero cada combinación es única.
Llave foránea (Foreign Key — FK)
Una columna que referencia a la PK de otra tabla. Eso "conecta" dos tablas.
CREATE TABLE pedido (
id SERIAL PRIMARY KEY,
cliente_id INTEGER NOT NULL REFERENCES cliente(id),
fecha TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
total DECIMAL(10,2)
);
cliente_id REFERENCES cliente(id) significa: "este valor debe existir en la columna id de la tabla cliente".
Lo que el DBMS garantiza:
- No podés insertar un
pedidoconcliente_id = 999si no existe el cliente 999. - No podés borrar un cliente que tiene pedidos (a menos que cascadees).
Acciones en cascada (qué hacer al borrar el padre):
... REFERENCES cliente(id) ON DELETE CASCADE
... REFERENCES cliente(id) ON DELETE SET NULL
... REFERENCES cliente(id) ON DELETE RESTRICT -- default
CASCADE: borra los hijos también.SET NULL: poneNULLen los hijos.RESTRICT: rechaza el borrado mientras haya hijos.
1.5 Integridad
📐 Fundamento
El DBMS aplica automáticamente reglas que protegen los datos:
Integridad de entidad
La PK no puede ser nula. Cada fila debe tener identidad. Implementado por la cláusula PRIMARY KEY.
Integridad referencial
Las FK deben referenciar valores existentes (o ser nulas, si la columna lo permite).
Integridad de dominio
Cada columna acepta solo valores de su tipo y restricciones:
edad INTEGER CHECK (edad >= 0 AND edad <= 150)
estado VARCHAR(10) CHECK (estado IN ('activo', 'inactivo', 'baja'))
Integridad definida por usuario
UNIQUE, CHECK, triggers, stored procedures — reglas personalizadas.
NULL — el valor que no es
NULL significa "ausencia de valor / desconocido". No es lo mismo que 0 ni que "".
SELECT * FROM cliente WHERE telefono IS NULL; -- correcto
SELECT * FROM cliente WHERE telefono = NULL; -- ¡incorrecto! NULL nunca es igual ni siquiera a sí mismo
NULL propaga: 5 + NULL = NULL. NULL = NULL da NULL (no TRUE). En WHERE, NULL se trata como falso.
Cuándo usar NULL: valor genuinamente desconocido o no aplicable. Si "vacío" tiene significado (ej: balance = 0), usá ese valor en lugar de NULL.
1.6 Insertar, actualizar, eliminar — DML básico
📐 Fundamento
INSERT:
INSERT INTO producto (codigo, nombre, precio, stock)
VALUES ('Q', 'Pupusa de queso', 0.50, 100);
Múltiples filas:
INSERT INTO producto (codigo, nombre, precio, stock) VALUES
('Q', 'Pupusa de queso', 0.50, 100),
('R', 'Pupusa revuelta', 0.60, 80),
('F', 'Pupusa de frijol', 0.55, 60);
UPDATE:
UPDATE producto
SET precio = 0.55
WHERE codigo = 'Q';
WHERE es CRÍTICO. Sin él, actualizás TODAS las filas. Es uno de los errores más caros en bases reales.
DELETE:
DELETE FROM producto
WHERE stock = 0;
Misma advertencia. DELETE FROM producto; borra todo.
Buena costumbre: primero hacé el SELECT con tu WHERE para confirmar qué afectarías, después convertí a UPDATE o DELETE.
1.7 Tu primera mini-base con SQLite
🛠️ En la práctica
SQLite no necesita servidor — es un archivo. Ideal para empezar.
En Python:
import sqlite3
# Crea o abre el archivo
con = sqlite3.connect("pupuseria.db")
cur = con.cursor()
# Crear tabla
cur.execute("""
CREATE TABLE IF NOT EXISTS producto (
codigo TEXT PRIMARY KEY,
nombre TEXT NOT NULL,
precio REAL NOT NULL,
stock INTEGER NOT NULL DEFAULT 0
);
""")
# Insertar
cur.executemany(
"INSERT OR REPLACE INTO producto VALUES (?, ?, ?, ?)",
[
("Q", "Pupusa de queso", 0.50, 100),
("R", "Pupusa revuelta", 0.60, 80),
("F", "Pupusa de frijol", 0.55, 60),
]
)
# Consultar
cur.execute("SELECT codigo, nombre, precio FROM producto WHERE stock > 50;")
for fila in cur.fetchall():
print(fila)
con.commit()
con.close()
Notar:
?son placeholders: nunca inyectes datos del usuario directo en el SQL. Usá parámetros — previene SQL injection (lo veremos).executemanyes eficiente para varias filas.commitconfirma los cambios.closelibera el archivo.
Comando externo: sqlite3 pupuseria.db te da un shell SQL interactivo. Probá .tables, .schema producto, etc.
1.8 Por qué el modelo relacional ganó
📐 Fundamento
Hay alternativas modernas: NoSQL (MongoDB, Redis, Cassandra), NewSQL (Spanner, CockroachDB), grafos (Neo4j). Sin embargo, las relacionales siguen siendo dominantes. Por qué:
Ventajas:
- ACID. Atomicidad, Consistencia, Aislamiento, Durabilidad. Tus datos no se corrompen.
- Lenguaje universal. SQL: 50 años de estandarización. Aprendés una vez, usás en cualquier DBMS.
- Consultas declarativas. Vos decís qué querés, el DBMS decide cómo. El optimizador ha tenido 50 años de mejoras.
- Restricciones formales. Tus datos tienen forma garantizada.
- Madurez. PostgreSQL, MySQL, Oracle — décadas de uso, miles de horas de pruebas.
Cuándo NoSQL gana:
- Datos sin esquema fijo (logs, eventos, JSON variable).
- Escala horizontal masiva (millones de operaciones/seg distribuidas).
- Patrones de acceso muy específicos (cache, contador, cola).
Conclusión pragmática: empieza con relacional. Migrá a NoSQL solo cuando una métrica concreta lo justifique (latencia, throughput, escalabilidad). 90% de los proyectos no llegan a esa frontera.
1.9 Proyecto: tablas de la pupusería
🏗️ Avance del proyecto
Diseñamos el esquema inicial:
-- Tablas
CREATE TABLE cliente (
id SERIAL PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE,
desde DATE NOT NULL DEFAULT CURRENT_DATE
);
CREATE TABLE producto (
codigo VARCHAR(10) PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
precio DECIMAL(10,2) NOT NULL CHECK (precio >= 0),
stock INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE pedido (
id SERIAL PRIMARY KEY,
cliente_id INTEGER REFERENCES cliente(id),
fecha TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
total DECIMAL(10,2) NOT NULL DEFAULT 0
);
CREATE TABLE pedido_linea (
pedido_id INTEGER REFERENCES pedido(id) ON DELETE CASCADE,
producto VARCHAR(10) REFERENCES producto(codigo),
cantidad INTEGER NOT NULL CHECK (cantidad > 0),
importe DECIMAL(10,2) NOT NULL,
PRIMARY KEY (pedido_id, producto)
);
-- Datos iniciales
INSERT INTO producto VALUES
('Q', 'Pupusa de queso', 0.50, 100),
('R', 'Pupusa revuelta', 0.60, 80),
('F', 'Pupusa de frijol', 0.55, 60),
('CH', 'Pupusa de chicharron', 0.60, 50),
('CU', 'Curtido (extra)', 0.25, 200),
('RT', 'Refresco tamarindo', 1.00, 40);
INSERT INTO cliente (nombre, email) VALUES
('Maria Lopez', 'maria@example.com'),
('Juan Perez', 'juan@example.com'),
('Ana Rivera', NULL);
Observaciones:
cliente_idenpedidopuede ser NULL (clientes anónimos).pedido_lineatiene clave compuesta: un pedido no puede tener el mismo producto dos veces como línea separada.ON DELETE CASCADEenpedido_linea: si borrás un pedido, sus líneas desaparecen también.
Próximo capítulo: llenamos esto con datos y aprendemos a consultar con SQL — SELECT, JOIN, GROUP BY.
1.10 Resumen visual
| Concepto | Una línea |
|---|---|
| Tabla / relación | Filas y columnas con esquema fijo |
| Fila / tupla | Un registro |
| Columna / atributo | Una propiedad |
| Tipo / dominio | Conjunto de valores válidos |
| Primary Key | Identifica cada fila únicamente |
| Foreign Key | Referencia a la PK de otra tabla |
| NOT NULL, UNIQUE, CHECK | Restricciones |
NULL |
"Desconocido / no aplicable" |
| ACID | Atomicidad, Consistencia, Aislamiento, Durabilidad |
| DDL | CREATE / ALTER / DROP |
| DML | INSERT / UPDATE / DELETE |
1.11 Ejercicios
✏️ Ejercicio 1.1 — Diseñar tabla
Diseñá una tabla libro con: id auto-incremental, ISBN único, título, autor, año, precio (>=0), disponible (booleano). Escribí el CREATE TABLE.
Solución
CREATE TABLE libro (
id SERIAL PRIMARY KEY,
isbn VARCHAR(13) UNIQUE NOT NULL,
titulo VARCHAR(200) NOT NULL,
autor VARCHAR(100) NOT NULL,
anio INTEGER NOT NULL CHECK (anio >= 1500 AND anio <= 2100),
precio DECIMAL(8,2) NOT NULL CHECK (precio >= 0),
disponible BOOLEAN NOT NULL DEFAULT TRUE
);
✏️ Ejercicio 1.2 — Identificar problemas
¿Qué problemas tiene este diseño? Listá al menos 3.
CREATE TABLE pedidos (
id INTEGER,
cliente VARCHAR(100),
productos VARCHAR(500), -- "queso:5,frijol:3"
fecha VARCHAR(20),
total VARCHAR(20)
);
Solución
- Sin PRIMARY KEY — IDs pueden duplicarse.
clientees texto libre — escribir mal el nombre crea "clientes" diferentes; no hay relación con tabla cliente.productoses un string parseable — viola la 1FN; cada producto debería ser una fila aparte (enpedido_linea).fechaytotalcomo VARCHAR — sin tipo apropiado, no podés ordenar/comparar/calcular bien.- No hay restricciones —
totalpodría ser negativo, fechas malformadas.
Versión correcta:
CREATE TABLE pedido (
id SERIAL PRIMARY KEY,
cliente_id INTEGER REFERENCES cliente(id),
fecha TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
total DECIMAL(10,2) NOT NULL CHECK (total >= 0)
);
CREATE TABLE pedido_linea (
pedido_id INTEGER REFERENCES pedido(id) ON DELETE CASCADE,
producto VARCHAR(10) REFERENCES producto(codigo),
cantidad INTEGER NOT NULL CHECK (cantidad > 0),
PRIMARY KEY (pedido_id, producto)
);
✏️ Ejercicio 1.3 — Insertar y actualizar
Dado el esquema del proyecto:
a. Insertá un pedido del cliente Maria con 3 pupusas de queso y 2 revueltas. b. Calculá y actualizá el total.
Solución
-- 1. Crear pedido vacío y obtener id (Postgres)
INSERT INTO pedido (cliente_id) VALUES (1) RETURNING id;
-- supongamos que devuelve id = 5
-- 2. Líneas
INSERT INTO pedido_linea VALUES
(5, 'Q', 3, 0.50 * 3),
(5, 'R', 2, 0.60 * 2);
-- 3. Actualizar total
UPDATE pedido SET total = (
SELECT SUM(importe) FROM pedido_linea WHERE pedido_id = 5
) WHERE id = 5;
Mejor práctica: envolver en una transacción (BEGIN; ... COMMIT;) para que sea atómico. Lo veremos en transacciones.
1.12 Para profundizar
- Date, Introduction to Database Systems — clásico riguroso del modelo relacional.
- Codd, A Relational Model of Data... (1970) — el paper original, gratis online. Histórico.
postgresdocumentation: https://www.postgresql.org/docs/- Próximo capítulo: SQL básico — el lenguaje universal de las bases de datos.
Definiciones nuevas: tabla, fila, columna, dominio, esquema, primary key, foreign key, llave candidata, llave compuesta, integridad de entidad/referencial/dominio, NULL, DDL, DML, DBMS, ACID, SQLite.