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:

  1. Es fácil de entender. Cualquiera con experiencia con Excel intuye lo básico.
  2. Es matemáticamente sólido. Edgar Codd, su inventor (1970), lo basó en teoría de conjuntos.
  3. Es flexible. Permite consultas no anticipadas — preguntas que no eran obvias al diseñar.
  4. 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:

  1. No puede ser nula.
  2. No puede repetirse.
  3. Idealmente: estable (no cambia con el tiempo). Por eso preferimos un ID artificial sobre algo "natural" como el email (puede cambiar).
  4. 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 pedido con cliente_id = 999 si 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: pone NULL en 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).
  • executemany es eficiente para varias filas.
  • commit confirma los cambios.
  • close libera 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:

  1. ACID. Atomicidad, Consistencia, Aislamiento, Durabilidad. Tus datos no se corrompen.
  2. Lenguaje universal. SQL: 50 años de estandarización. Aprendés una vez, usás en cualquier DBMS.
  3. Consultas declarativas. Vos decís qué querés, el DBMS decide cómo. El optimizador ha tenido 50 años de mejoras.
  4. Restricciones formales. Tus datos tienen forma garantizada.
  5. 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_id en pedido puede ser NULL (clientes anónimos).
  • pedido_linea tiene clave compuesta: un pedido no puede tener el mismo producto dos veces como línea separada.
  • ON DELETE CASCADE en pedido_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.

✏️ 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)
);

✏️ 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.

1.12 Para profundizar


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.