Notas de estudio — Parcial 2
Sistemas de Gestión de Datos 2026-1 — EAFIT
1. Conceptos clave
- Base de datos — coleccion estructurada de datos que modela un mini-mundo, con proposito y usuarios definidos
- Esquema vs Estado — esquema = estructura (DDL, no cambia frecuentemente); estado = datos actuales (cambia con cada INSERT/UPDATE/DELETE)
- DBMS — software intermediario: define, construye, manipula, comparte, protege y mantiene la BD
- Grado = numero de columnas (fijo, definido por esquema) / Cardinalidad = numero de filas (variable, cambia con los datos)
- Independencia logica — cambiar esquema logico sin afectar programas externos
- Independencia fisica — cambiar almacenamiento sin afectar esquema logico
- Modelo de 3 esquemas (ANSI/SPARC): nivel externo (vistas) —> nivel conceptual (logico) —> nivel interno (fisico)
Tipos de sentencias SQL:
| Categoria | Significa | Comandos | Para que sirve |
|---|---|---|---|
| DDL | Data Definition Language | CREATE, ALTER, DROP, TRUNCATE | Definir y modificar la estructura de la BD |
| DML | Data Manipulation Language | INSERT, UPDATE, DELETE | Manipular los datos (agregar, cambiar, borrar) |
| DQL | Data Query Language | SELECT | Consultar datos (leer informacion) |
| DCL | Data Control Language | GRANT, REVOKE | Controlar permisos de acceso |
| TCL | Transaction Control Language | COMMIT, ROLLBACK, SAVEPOINT | Controlar transacciones (confirmar o deshacer) |
En el parcial, la mayoria de preguntas seran DQL (SELECT) y algo de DDL (CREATE/ALTER). Pero es importante saber la diferencia entre todas.
2. Modelo relacional
Terminologia: Relacion = tabla, Tupla = fila, Atributo = columna, Dominio = tipo de dato
Claves
| Tipo | Definicion |
|---|---|
| Superclave | Conjunto de atributos que identifica unicamente cada tupla (puede sobrar atributos) |
| Clave candidata | Superclave minimal — quitar cualquier atributo rompe la unicidad |
| Clave primaria (PK) | La candidata elegida. Una sola por tabla. Implica NOT NULL + UNIQUE |
| Clave alternativa | Candidatas no elegidas como PK |
| Clave foranea (FK) | Atributo que referencia la PK de otra tabla (o la misma) |
Encontrar candidatas: probar subconjuntos de menor a mayor, verificar unicidad en los datos, quedarse con los minimales.
Ejercicio: encontrar claves candidatas
MATRICULA(estudiante_id, curso_id, semestre, nota, profesor_id)
Datos:
| est_id | curso_id | semestre | nota | prof_id |
|--------|----------|----------|------|---------|
| E1 | C1 | 2026-1 | 4.2 | P1 |
| E1 | C2 | 2026-1 | 3.8 | P2 |
| E2 | C1 | 2026-1 | 3.5 | P1 |
| E1 | C1 | 2025-2 | 2.9 | P3 |
Analisis:
- est_id solo? NO — E1 aparece 3 veces
- (est_id, curso_id) solo? NO — (E1, C1) aparece 2 veces
- (est_id, curso_id, semestre)? SI — cada combinacion es unica
- Es minimal? SI — quitar cualquiera de los 3 rompe unicidad
- Clave candidata: (estudiante_id, curso_id, semestre)
Nota: prof_id no es parte de la clave porque depende del curso+semestre, no identifica la tupla.
Reglas de integridad
| Regla | Que dice |
|---|---|
| Entidad | Ningun componente de la PK puede ser NULL |
| Referencial | FK debe existir como PK en tabla referenciada, o ser NULL |
| Dominio/Negocio | Valores dentro del tipo permitido + reglas de aplicacion (CHECK) |
Opciones ON DELETE / ON UPDATE
| Opcion | Efecto |
|---|---|
| RESTRICT | Rechaza la operacion si hay referencias |
| CASCADE | Propaga (borra/actualiza en cascada) |
| SET NULL | Pone NULL en la FK |
| SET DEFAULT | Pone valor por defecto en la FK |
Regla rapida: FK que es parte de PK —> NUNCA puede ser NULL (integridad de entidad). FK que NO es parte de PK —> puede ser NULL si no hay NOT NULL explicito.
CASCADE vs RESTRICT vs SET NULL — ejemplo concreto
DEPARTAMENTO(dep_id PK, nombre)
EMPLEADO(emp_id PK, nombre, dep_id FK -> DEPARTAMENTO)
Estado:
DEP: (1, 'Ventas'), (2, 'RRHH'), (3, 'IT')
EMP: (10, 'Ana', 1), (20, 'Luis', 1), (30, 'Maria', 2)
DELETE FROM DEPARTAMENTO WHERE dep_id = 1 (Ana y Luis referencian dep_id=1):
| Opcion en FK | Resultado |
|---|---|
| RESTRICT | ERROR — hay empleados referenciando dep_id=1 |
| CASCADE | Borra dep 1 Y borra Ana y Luis automaticamente |
| SET NULL | Borra dep 1. Ana y Luis quedan con dep_id = NULL |
| SET DEFAULT | Borra dep 1. Ana y Luis quedan con dep_id = valor DEFAULT |
Para UPDATE es analogo: CASCADE propaga el nuevo valor, RESTRICT rechaza, SET NULL pone NULL.
3. Algebra relacional
Propiedad de clausura: toda operacion produce una relacion, por lo que se pueden componer.
Operadores
| Operador | Simbolo | Tipo | Que hace | SQL equivalente |
|---|---|---|---|---|
| Seleccion | Unaria | Filtra filas por condicion | WHERE | |
| Proyeccion | Unaria | Elige columnas, elimina duplicados | SELECT DISTINCT | |
| Producto cartesiano | Binaria | Cada fila de R con cada fila de S | FROM R, S | |
| Union | Binaria | Filas de R o S (sin duplicados). Requiere union-compatible | UNION | |
| Diferencia | Binaria | Filas en R pero no en S. NO conmutativa | EXCEPT | |
| Interseccion | Binaria | Filas en R y en S. Equivale a | INTERSECT | |
| Join natural | Binaria | Combina filas con igual valor en atributos comunes | NATURAL JOIN | |
| Theta-join | Binaria | Producto cartesiano + seleccion por | JOIN ON | |
| Division | Binaria | Valores de R asociados con TODOS los de S | NOT EXISTS(NOT EXISTS) | |
| Renombramiento | Unaria | Renombra relacion o atributos | AS |
Ejemplo por operador
R (nombre, dep) S (nombre, dep)
| nombre | dep | | nombre | dep |
|--------|-------| |--------|-------|
| Ana | IT | | Ana | IT |
| Luis | RRHH | | Pedro | IT |
| Maria | IT | | Maria | Vtas |
- Seleccion —> (Ana,IT), (Maria,IT) — filtra filas
- Proyeccion —> IT, RRHH — elimina duplicados (IT aparecia 2x)
- Union —> 5 filas: las de R + las de S, sin duplicados. (Ana,IT) aparece 1 vez
- Diferencia —> (Luis,RRHH), (Maria,IT) — filas de R no en S
- Interseccion —> (Ana,IT) — unica tupla identica en ambas
Division — algoritmo paso a paso
- — todos los valores distintos de A
- — cada A combinado con cada B de S (lo que “deberia existir”)
- Paso 2 — combinaciones que faltan en R
- (Paso 3) — valores de A que les falta algun B
- Paso 1 Paso 4 — valores de A que NO les falta nada = resultado
Division — ejemplo completo trazado
R (est, curso) S (curso)
| est | curso | | curso |
|------|-------| |-------|
| Ana | BD | | BD |
| Ana | Redes | | Redes |
| Luis | BD |
| Luis | Redes |
| Luis | SO |
| Maria| BD |
Paso 1 — : Ana, Luis, Maria
Paso 2 — (3 est x 2 cursos = 6 filas): (Ana,BD), (Ana,Redes), (Luis,BD), (Luis,Redes), (Maria,BD), (Maria,Redes)
Paso 3 — Paso 2 (lo que falta): (Maria,Redes) — Maria no tiene Redes en R
Paso 4 — (Paso 3): Maria
Paso 5 — Paso 1 Paso 4 = RESULTADO: Ana, Luis
Ana y Luis estan inscritos en TODOS los cursos de S. Maria no (le falta Redes).
Algebra relacional —> SQL — equivalencias
| Algebra | SQL |
|---|---|
| SELECT * FROM R WHERE cond | |
| SELECT DISTINCT A, B FROM R | |
| SELECT * FROM R, S | |
| SELECT * FROM R NATURAL JOIN S | |
| SELECT * FROM R JOIN S ON cond | |
| … UNION … | |
| … EXCEPT … | |
| … INTERSECT … | |
| Doble NOT EXISTS |
4. SQL DDL — CREATE TABLE y ALTER TABLE
Tipos de datos principales en PostgreSQL
| Tipo | Descripcion | Ejemplo |
|---|---|---|
| INTEGER / INT | Numero entero | 42, -10, 0 |
| SERIAL | Entero autoincremental | 1, 2, 3… (se genera solo) |
| BIGINT | Entero grande | Para IDs de millones de registros |
| NUMERIC(p,s) | Decimal exacto (p digitos, s decimales) | NUMERIC(10,2) —> 12345678.99 |
| REAL / FLOAT | Decimal aproximado | 3.14159 |
| VARCHAR(n) | Texto de largo variable (max n) | VARCHAR(50) —> ‘Hola mundo’ |
| TEXT | Texto sin limite de largo | Descripcion larga… |
| CHAR(n) | Texto de largo fijo (rellena con espacios) | CHAR(3) —> ‘AB ‘ |
| BOOLEAN | Verdadero o falso | TRUE, FALSE |
| DATE | Solo fecha | ’2024-03-17’ |
| TIMESTAMP | Fecha + hora | ’2024-03-17 14:30:00’ |
| TIME | Solo hora | ’14:30:00’ |
Constraints (restricciones)
| Constraint | Que hace | Ejemplo |
|---|---|---|
| PRIMARY KEY | Identifica cada fila de forma unica. No permite NULL ni duplicados | id INT PRIMARY KEY |
| NOT NULL | La columna no puede quedar vacia | nombre VARCHAR(100) NOT NULL |
| UNIQUE | No permite valores duplicados (pero si permite NULL) | email VARCHAR(200) UNIQUE |
| DEFAULT | Valor por defecto si no se especifica | activo BOOLEAN DEFAULT TRUE |
| CHECK | Valida una condicion | edad INT CHECK (edad >= 18) |
| FOREIGN KEY | Referencia a otra tabla. Asegura que el valor exista alla | REFERENCES otra_tabla(id) |
Ejemplo completo — crear 3 tablas relacionadas (tienda)
-- =============================================================
-- 1) Primero creamos la tabla "categorias"
-- porque "productos" va a referenciarla con FK
-- =============================================================
CREATE TABLE categorias (
-- SERIAL = entero que se auto-incrementa (1, 2, 3...)
-- PRIMARY KEY = identificador unico de cada fila
id SERIAL PRIMARY KEY,
-- VARCHAR(100) = texto de hasta 100 caracteres
-- NOT NULL = este campo es obligatorio
nombre VARCHAR(100) NOT NULL,
-- TEXT = texto sin limite de largo
descripcion TEXT
);
-- =============================================================
-- 2) Ahora creamos "productos" que referencia a "categorias"
-- =============================================================
CREATE TABLE productos (
id SERIAL PRIMARY KEY, -- PK autoincremental
nombre VARCHAR(200) NOT NULL, -- nombre obligatorio
-- NUMERIC(10,2) = hasta 10 digitos, 2 decimales
-- CHECK = valida que el precio sea positivo
precio NUMERIC(10,2) NOT NULL CHECK (precio > 0),
-- DEFAULT 0 = si no se pone stock, queda en 0
stock INTEGER DEFAULT 0,
-- FOREIGN KEY: categoria_id debe existir en categorias.id
-- REFERENCES crea la relacion entre tablas
categoria_id INTEGER REFERENCES categorias(id),
-- TIMESTAMP con valor por defecto = hora actual
creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- =============================================================
-- 3) Tabla de ventas con dos foreign keys
-- =============================================================
CREATE TABLE ventas (
id SERIAL PRIMARY KEY, -- PK
producto_id INTEGER NOT NULL REFERENCES productos(id), -- FK obligatoria
cantidad INTEGER NOT NULL CHECK (cantidad > 0), -- debe ser positiva
fecha DATE NOT NULL DEFAULT CURRENT_DATE, -- fecha por defecto = hoy
-- UNIQUE en dos columnas = no se puede repetir
-- la misma combinacion producto+fecha
UNIQUE (producto_id, fecha)
);
-- IMPORTANTE: el orden de creacion importa.
-- Si tabla B tiene FK que apunta a tabla A, hay que crear A primero.
ALTER TABLE — modificar estructura existente
-- =============================================================
-- AGREGAR una columna nueva
-- =============================================================
-- ALTER TABLE tabla ADD COLUMN nombre_col TIPO;
ALTER TABLE productos
ADD COLUMN color VARCHAR(50);
-- Los registros existentes tendran NULL en "color"
-- Agregar con DEFAULT para que no quede NULL:
ALTER TABLE productos
ADD COLUMN activo BOOLEAN DEFAULT TRUE;
-- =============================================================
-- ELIMINAR una columna
-- =============================================================
-- DROP COLUMN elimina la columna y todos sus datos
ALTER TABLE productos
DROP COLUMN color;
-- Si otras tablas dependen de esta columna, usar CASCADE:
ALTER TABLE productos
DROP COLUMN color CASCADE;
-- =============================================================
-- RENOMBRAR una columna
-- =============================================================
-- RENAME COLUMN viejo TO nuevo
ALTER TABLE productos
RENAME COLUMN nombre TO nombre_producto;
-- =============================================================
-- CAMBIAR el tipo de dato
-- =============================================================
-- ALTER COLUMN ... TYPE nuevo_tipo
ALTER TABLE productos
ALTER COLUMN precio TYPE NUMERIC(12,2);
-- Si el cambio no es directo, necesitas USING:
ALTER TABLE productos
ALTER COLUMN stock TYPE VARCHAR(20)
USING stock::VARCHAR;
-- USING indica como convertir los datos existentes
-- =============================================================
-- AGREGAR y ELIMINAR constraints
-- =============================================================
-- Agregar NOT NULL a una columna existente
ALTER TABLE productos
ALTER COLUMN nombre SET NOT NULL;
-- Quitar NOT NULL
ALTER TABLE productos
ALTER COLUMN nombre DROP NOT NULL;
-- Agregar una constraint con nombre
ALTER TABLE productos
ADD CONSTRAINT precio_positivo CHECK (precio > 0);
-- Eliminar una constraint por nombre
ALTER TABLE productos
DROP CONSTRAINT precio_positivo;
-- Agregar FOREIGN KEY a tabla existente
ALTER TABLE ventas
ADD CONSTRAINT fk_producto
FOREIGN KEY (producto_id) REFERENCES productos(id);
-- =============================================================
-- RENOMBRAR la tabla
-- =============================================================
ALTER TABLE productos
RENAME TO inventario;
-- =============================================================
-- CAMBIAR valor por defecto
-- =============================================================
-- Poner un nuevo DEFAULT
ALTER TABLE productos
ALTER COLUMN stock SET DEFAULT 10;
-- Quitar el DEFAULT
ALTER TABLE productos
ALTER COLUMN stock DROP DEFAULT;
5. SQL DML — INSERT, UPDATE, DELETE
INSERT INTO — insertar datos
-- Sintaxis: INSERT INTO tabla (col1, col2) VALUES (val1, val2);
-- Insertar un registro:
INSERT INTO categorias (nombre, descripcion) -- columnas destino
VALUES ('Electronica', 'Dispositivos electronicos');
-- 'Electronica' va a nombre, 'Dispositivos...' va a descripcion
-- id se genera solo porque es SERIAL
-- Insertar varios registros a la vez:
INSERT INTO categorias (nombre, descripcion) VALUES
('Ropa', 'Prendas de vestir'), -- registro 1
('Alimentos', 'Comida y bebidas'), -- registro 2
('Deportes', 'Articulos deportivos'); -- registro 3
-- Mas eficiente que 3 INSERTs separados
-- RETURNING muestra lo que se inserto:
INSERT INTO categorias (nombre) VALUES ('Hogar')
RETURNING id, nombre;
-- Resultado: id = 5, nombre = 'Hogar'
-- Util para obtener el id generado por SERIAL
UPDATE — actualizar datos existentes
-- Sintaxis: UPDATE tabla SET col = valor WHERE condicion;
-- Actualizar un registro especifico:
UPDATE productos
SET precio = 29999.99, stock = 50 -- cambiar precio y stock
WHERE id = 1; -- solo el producto con id = 1
-- Actualizar basado en condicion:
UPDATE productos
SET precio = precio * 1.10 -- sube 10% el precio
WHERE categoria_id = 1; -- solo los de categoria 1
-- IMPORTANTE: sin WHERE se actualizan TODAS las filas
SIEMPRE usar WHERE con UPDATE. Sin WHERE, se actualizan TODAS las filas de la tabla.
DELETE — eliminar datos
-- Sintaxis: DELETE FROM tabla WHERE condicion;
-- Eliminar un registro:
DELETE FROM productos WHERE id = 5; -- borra solo el producto 5
-- Eliminar todos los productos sin stock:
DELETE FROM productos WHERE stock = 0; -- borra los que tienen stock = 0
-- TRUNCATE borra TODOS los registros (mas rapido que DELETE):
TRUNCATE TABLE ventas;
-- No se puede usar WHERE con TRUNCATE
-- Reinicia el SERIAL a 1
SIEMPRE usar WHERE con DELETE. Sin WHERE, se borran TODAS las filas.
DELETE vs TRUNCATE: DELETE borra fila por fila (se puede deshacer con ROLLBACK). TRUNCATE borra todo de una, es mas rapido, pero no se puede deshacer.
6. SQL SELECT — consultas basicas
Sintaxis general y orden de ejecucion
-- El orden en que ESCRIBIS la consulta:
SELECT columnas -- 1. Que columnas queres ver
FROM tabla -- 2. De que tabla
WHERE condicion -- 3. Filtro de filas
GROUP BY columnas -- 4. Agrupar resultados
HAVING cond_grupo -- 5. Filtro de grupos
ORDER BY columna -- 6. Ordenar resultados
LIMIT n; -- 7. Limitar cantidad
-- IMPORTANTE: El orden de EJECUCION es diferente:
-- FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> LIMIT
-- Esto es clave para entender errores.
-- Por eso no podes usar un alias de SELECT en WHERE
-- (SELECT se ejecuta despues de WHERE).
SELECT basico (tabla customer de dvdrental)
-- Seleccionar todas las columnas de todos los clientes:
SELECT * FROM customer; -- * = todas las columnas
-- En la practica, es mejor nombrar las columnas:
SELECT
customer_id, -- ID del cliente
first_name, -- nombre
last_name -- apellido
FROM customer; -- tabla de clientes de dvdrental
WHERE — filtrar filas
-- Operadores de comparacion: =, !=, <, >, <=, >=
SELECT first_name, last_name -- columnas a mostrar
FROM customer -- tabla fuente
WHERE active = 1; -- solo clientes activos
-- AND, OR para combinar condiciones:
SELECT first_name, last_name
FROM customer
WHERE active = 1 -- condicion 1: activo
AND store_id = 1; -- condicion 2: tienda 1
-- Ambas deben ser TRUE para que la fila pase
-- BETWEEN para rangos (inclusivo ambos extremos):
SELECT title, rental_rate -- titulo y precio
FROM film -- tabla de peliculas
WHERE rental_rate BETWEEN 2.99 AND 4.99;
-- Equivale a: rental_rate >= 2.99 AND rental_rate <= 4.99
-- IN para lista de valores:
SELECT title FROM film
WHERE rating IN ('PG', 'PG-13', 'G');
-- Equivale a: rating = 'PG' OR rating = 'PG-13' OR rating = 'G'
-- LIKE para patrones de texto:
SELECT first_name FROM customer
WHERE first_name LIKE 'A%';
-- % = cualquier cantidad de caracteres
-- _ = un solo caracter
-- 'A%' = empieza con A
-- '%son' = termina en 'son'
-- '%an%' = contiene 'an'
-- IS NULL / IS NOT NULL:
SELECT * FROM customer
WHERE email IS NOT NULL; -- solo clientes con email
-- NUNCA usar = NULL (siempre da UNKNOWN, nunca TRUE)
ORDER BY y LIMIT
-- ORDER BY ordena los resultados
-- ASC = ascendente (por defecto), DESC = descendente
SELECT first_name, last_name
FROM customer
ORDER BY last_name ASC; -- ordenar por apellido A-Z
-- Ordenar por multiples columnas:
SELECT first_name, last_name
FROM customer
ORDER BY last_name ASC, first_name DESC;
-- Primero por apellido A-Z, desempata por nombre Z-A
-- LIMIT para limitar resultados:
SELECT title, rental_rate -- titulo y precio
FROM film -- tabla peliculas
ORDER BY rental_rate DESC -- las mas caras primero
LIMIT 10; -- solo las 10 primeras
-- Resultado: las 10 peliculas mas caras
DISTINCT — valores unicos
-- DISTINCT elimina filas duplicadas del resultado
SELECT DISTINCT rating FROM film;
-- Muestra cada rating una sola vez: PG, G, R, etc.
SELECT DISTINCT store_id, active FROM customer;
-- Combinaciones unicas de store_id + active
Alias — renombrar columnas y tablas
-- AS da un nombre temporal a una columna o tabla
SELECT
first_name AS nombre, -- alias de columna
last_name AS apellido -- alias de columna
FROM customer AS c; -- alias de tabla
-- El AS es opcional (pero es mas legible con el):
SELECT first_name nombre, last_name apellido
FROM customer c;
-- Util en calculos:
SELECT title, rental_rate * 100 AS precio_en_centavos
FROM film;
-- La columna calculada se llama "precio_en_centavos"
7. JOINs — unir tablas
Los JOINs combinan filas de dos o mas tablas basandose en una columna relacionada (normalmente una llave foranea). Son el tema mas importante del parcial.
| Tipo de JOIN | Que retorna |
|---|---|
| INNER JOIN | Solo las filas que tienen coincidencia en AMBAS tablas |
| LEFT JOIN | TODAS las filas de la tabla izquierda + las coincidencias de la derecha (NULL si no hay coincidencia a la derecha) |
| RIGHT JOIN | TODAS las filas de la tabla derecha + las coincidencias de la izquierda (NULL si no hay coincidencia a la izquierda) |
| FULL OUTER JOIN | TODAS las filas de AMBAS tablas (NULL donde no hay coincidencia en algun lado) |
INNER JOIN
-- INNER JOIN: Solo muestra filas donde HAY coincidencia
-- en ambas tablas.
-- Ejemplo: Clientes con sus pagos (dvdrental)
SELECT
c.customer_id, -- ID del cliente
-- c. es el alias de customer
c.first_name, -- nombre del cliente
c.last_name, -- apellido del cliente
p.amount -- monto del pago
-- p. es el alias de payment
FROM customer c -- tabla izquierda
INNER JOIN payment p -- tabla derecha
ON c.customer_id = p.customer_id;
-- condicion de union: la columna que las conecta
-- Solo aparecen clientes que han hecho al menos un pago.
-- Si un cliente nunca pago, NO aparece en el resultado.
-- Resultado: multiples filas por cliente (una por cada pago)
LEFT JOIN (LEFT OUTER JOIN)
-- LEFT JOIN: Muestra TODAS las filas de la tabla izquierda,
-- y si hay coincidencia a la derecha, la incluye.
-- Si NO hay coincidencia -> pone NULL en las columnas derechas.
-- Ejemplo: Todos los clientes con su rental_id (si existe)
SELECT
c.customer_id, -- ID del cliente
c.first_name, -- nombre
c.last_name, -- apellido
r.rental_id -- ID del alquiler (sera NULL si no tiene)
FROM customer c -- tabla IZQUIERDA (todos aparecen)
LEFT JOIN rental r -- tabla derecha (solo las que coincidan)
ON c.customer_id = r.customer_id;
-- condicion: unir por customer_id
-- Resultado: TODOS los clientes aparecen.
-- Si un cliente nunca alquilo -> rental_id = NULL
-- Ejemplo: customer_id 599 (Austin Cintron) -> rental_id = NULL
Patron clave: LEFT JOIN + IS NULL para encontrar huerfanos:
-- Clientes que NUNCA han alquilado
-- Patron: LEFT JOIN + WHERE col_derecha IS NULL
-- Este patron es SUPER comun en parciales
SELECT
c.customer_id, -- ID del cliente
c.first_name, -- nombre
c.last_name -- apellido
FROM customer c -- tabla principal: todos los clientes
LEFT JOIN rental r -- LEFT JOIN: mantener clientes SIN rental
ON c.customer_id = r.customer_id
-- condicion de union por customer_id
WHERE r.rental_id IS NULL; -- CLAVE: filtra solo los que NO tienen coincidencia
-- Patron: LEFT JOIN + WHERE col_derecha IS NULL
-- = "encontrar los que NO estan en la otra tabla"
-- Resultado: cliente 599 (Austin Cintron) — nunca alquilo
RIGHT JOIN (RIGHT OUTER JOIN)
-- RIGHT JOIN: Igual que LEFT pero al reves.
-- Muestra TODAS las filas de la tabla DERECHA,
-- y las coincidencias de la izquierda.
-- Ejemplo: Todos los alquileres con datos del cliente
SELECT
c.first_name, -- nombre (NULL si no hay match)
c.last_name, -- apellido
r.rental_id, -- ID del alquiler
r.rental_date -- fecha del alquiler
FROM customer c -- tabla izquierda (solo las que coincidan)
RIGHT JOIN rental r -- tabla DERECHA (todas sus filas aparecen)
ON c.customer_id = r.customer_id;
-- condicion de union
-- Resultado: TODOS los alquileres aparecen.
-- Si un alquiler no tiene cliente -> first_name = NULL
-- Equivalente con LEFT JOIN (invirtiendo tablas):
-- FROM rental r LEFT JOIN customer c ON ...
En la practica, LEFT JOIN se usa mucho mas que RIGHT JOIN. Un RIGHT JOIN se puede reescribir como LEFT JOIN invirtiendo las tablas. Pero el parcial puede pedir explicitamente RIGHT JOIN.
FULL OUTER JOIN
-- FULL OUTER JOIN: Combina LEFT + RIGHT.
-- Muestra TODAS las filas de AMBAS tablas.
-- NULL donde no hay coincidencia en algun lado.
SELECT
c.customer_id, -- ID del cliente (NULL si no tiene match)
c.first_name, -- nombre del cliente
r.rental_id -- ID del alquiler (NULL si no tiene match)
FROM customer c -- tabla izquierda
FULL OUTER JOIN rental r -- tabla derecha
ON c.customer_id = r.customer_id;
-- condicion de union
-- Aparecen clientes sin alquiler (rental_id = NULL)
-- Y alquileres sin cliente (customer_id = NULL)
-- Combina lo que falta de LEFT y RIGHT JOIN
Multi-table JOIN — 3 tablas
-- Clientes con sus pedidos y los productos de cada pedido
-- Patron: cadena de JOINs para navegar relaciones N:M
SELECT
c.nombre, -- nombre del cliente
p.pid, -- ID del pedido
pr.producto, -- nombre del producto
dp.cantidad -- cantidad del producto en ese pedido
FROM cliente c -- punto de partida: clientes
JOIN pedido p -- 1er salto: cliente -> sus pedidos
ON c.cid = p.cid
JOIN detalle_pedido dp -- 2do salto: pedido -> lineas de detalle
ON p.pid = dp.pid
JOIN producto pr -- 3er salto: detalle -> nombre producto
ON dp.prod_id = pr.prod_id;
-- Cada JOIN reduce filas (INNER = solo con match)
-- Traza: cliente(base) -> pedido(solo con pedidos) -> detalle -> producto
COUNT con LEFT JOIN — la trampa clasica
-- MAL: COUNT(*) cuenta la fila NULL como 1
SELECT
c.nombre,
COUNT(*) AS pedidos -- ERROR: cuenta TODAS las filas, incluyendo NULLs
FROM CLIENTE c
LEFT JOIN PEDIDO p ON c.cid = p.cid
GROUP BY c.nombre;
-- Cliente sin pedidos -> 1 (INCORRECTO: deberia ser 0)
-- LEFT JOIN crea 1 fila con NULLs, y COUNT(*) la cuenta
-- BIEN: COUNT(columna_derecha) ignora NULLs
SELECT
c.nombre,
COUNT(p.pid) AS pedidos -- CORRECTO: COUNT(col) ignora NULLs -> da 0
FROM CLIENTE c
LEFT JOIN PEDIDO p ON c.cid = p.cid
GROUP BY c.nombre;
-- Cliente sin pedidos -> 0 (CORRECTO: p.pid es NULL -> no se cuenta)
8. Funciones de agregacion
Las funciones de agregacion calculan un valor a partir de un conjunto de filas. Son esenciales para hacer resumenes de datos.
| Funcion | Que hace | Ejemplo (dvdrental) |
|---|---|---|
| COUNT(*) | Cuenta TODAS las filas (incluyendo NULL) | COUNT(*) —> 599 |
| COUNT(col) | Cuenta filas donde col NO es NULL | COUNT(email) —> 598 |
| SUM(col) | Suma los valores | SUM(amount) —> 67416.51 |
| AVG(col) | Promedio de los valores | AVG(amount) —> 4.20 |
| MIN(col) | Valor minimo | MIN(amount) —> 0.00 |
| MAX(col) | Valor maximo | MAX(amount) —> 11.99 |
OJO: COUNT(*) vs COUNT(columna). COUNT(*) cuenta todas las filas, incluso si tienen NULL. COUNT(columna) solo cuenta las filas donde esa columna NO es NULL. En un parcial, esto es trampa comun.
GROUP BY — agrupar resultados
GROUP BY agrupa las filas que tienen el mismo valor en una columna, y luego se aplican funciones de agregacion a cada grupo.
-- Cuantos alquileres tiene cada cliente?
SELECT
customer_id, -- columna por la que agrupamos
COUNT(*) AS total_rentals -- cuenta las filas en cada grupo
FROM rental -- tabla de alquileres
GROUP BY customer_id; -- "agrupa todas las filas con el mismo customer_id"
-- REGLA: toda columna en SELECT que NO sea funcion
-- de agregacion, DEBE estar en GROUP BY
-- Resultado:
-- customer_id | total_rentals
-- 1 | 32
-- 2 | 27
-- 3 | 26
HAVING — filtrar grupos
WHERE filtra filas individuales ANTES de agrupar. HAVING filtra grupos DESPUES de agrupar. Esta es una diferencia fundamental.
-- Clientes con 30 o mas alquileres
SELECT
c.customer_id, -- ID del cliente
c.first_name, -- nombre
c.last_name, -- apellido
COUNT(*) AS total_rentals -- total de alquileres por cliente
FROM customer c -- tabla de clientes
JOIN rental r -- JOIN = INNER JOIN (son sinonimos)
ON c.customer_id = r.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name
-- agrupamos por las 3 columnas no-agregadas del SELECT
HAVING COUNT(*) >= 30; -- HAVING filtra los GRUPOS donde el conteo es >= 30
-- No podes usar WHERE COUNT(*) >= 30
-- porque WHERE se ejecuta ANTES de GROUP BY
-- y COUNT no existe todavia en ese momento
-- Resultado:
-- customer_id | first_name | last_name | total_rentals
-- 148 | Eleanor | Hunt | 46
-- 526 | Karl | Seal | 45
-- 236 | Marcia | Dean | 42
Regla de oro: si necesitas filtrar por el resultado de COUNT, SUM, AVG, etc. —> usa HAVING. Si filtras por un valor directo de la fila —> usa WHERE.
GROUP BY + HAVING — ejemplo trazado completo
-- Vendedores con mas de 2 ventas y monto total > 1000
-- Tabla VENTA: (Ana,Laptop,2000), (Ana,Mouse,50), (Ana,Laptop,2000),
-- (Luis,Teclado,100), (Luis,Mouse,50),
-- (Maria,Laptop,2000), (Maria,Monitor,800), (Maria,Teclado,100), (Maria,Mouse,50)
SELECT
vendedor, -- columna de agrupacion (debe estar en GROUP BY)
COUNT(*) AS num_ventas, -- agregado: cuantas ventas por vendedor
SUM(monto) AS total -- agregado: suma de montos por vendedor
FROM VENTA -- tabla fuente con todas las ventas
GROUP BY vendedor -- agrupar filas por vendedor
HAVING COUNT(*) > 2 -- filtrar grupos: al menos 3 ventas
AND SUM(monto) > 1000; -- filtrar grupos: total mayor a 1000
-- Traza: FROM (9 filas) -> WHERE (todas pasan) -> GROUP BY vendedor:
-- | Grupo | COUNT | SUM | HAVING (>2 AND >1000) |
-- |-------|-------|------|-----------------------|
-- | Ana | 3 | 4050 | PASA |
-- | Luis | 2 | 150 | FILTRADO (COUNT no >2)|
-- | Maria | 4 | 2950 | PASA |
-- Resultado: Ana (3, 4050) y Maria (4, 2950)
9. Subconsultas (Subqueries)
Una subconsulta es un SELECT dentro de otro SELECT. Es como hacer una consulta primero para obtener un valor, y luego usar ese valor en la consulta principal.
Subconsulta en WHERE (valor escalar)
-- Peliculas con precio mayor al promedio
SELECT
title, -- titulo de la pelicula
rental_rate -- precio de alquiler
FROM film -- tabla de peliculas
WHERE rental_rate > ( -- comparar precio individual vs...
SELECT AVG(rental_rate) -- ...el promedio de TODAS las peliculas
FROM film -- la subconsulta calcula AVG(rental_rate) = 2.98
);
-- Paso 1: La subconsulta calcula AVG(rental_rate) = 2.98
-- Paso 2: La consulta principal filtra WHERE rental_rate > 2.98
-- Es como si escribieras: WHERE rental_rate > 2.98
Subconsulta con IN
-- Clientes que han alquilado la pelicula con film_id = 1:
SELECT
first_name, -- nombre del cliente
last_name -- apellido del cliente
FROM customer -- tabla de clientes
WHERE customer_id IN ( -- el customer_id debe estar en la lista
-- Esta subconsulta retorna una LISTA de customer_ids
SELECT DISTINCT r.customer_id -- IDs unicos de clientes
FROM rental r -- tabla de alquileres
JOIN inventory i -- unir con inventario
ON r.inventory_id = i.inventory_id
WHERE i.film_id = 1 -- solo la pelicula 1
);
-- IN funciona como: WHERE customer_id = 1 OR customer_id = 5 OR ...
-- La subconsulta genera la lista de IDs
Subconsulta en FROM (tabla derivada)
-- Primero calculamos el total por cliente,
-- luego sacamos el promedio de esos totales:
SELECT
AVG(sub.total_paid) AS promedio_total -- promedio de los totales
FROM (
-- Esta subconsulta genera una "tabla virtual"
SELECT
customer_id, -- ID del cliente
SUM(amount) AS total_paid -- total pagado por cada cliente
FROM payment -- tabla de pagos
GROUP BY customer_id -- un total por cliente
) AS sub; -- OBLIGATORIO: alias para subconsulta en FROM
-- Paso 1: La subconsulta calcula el total pagado por cliente
-- Paso 2: La consulta principal calcula el promedio de esos totales
-- La subconsulta en FROM SIEMPRE DEBE tener un alias (sub)
Subconsultas anidadas (2 niveles)
-- Clientes cuyo total pagado es mayor al promedio
-- del total pagado entre todos los clientes:
SELECT
c.customer_id, -- ID del cliente
c.first_name, -- nombre
c.last_name, -- apellido
SUM(p.amount) AS total_paid -- total pagado por este cliente
FROM customer c -- tabla de clientes
JOIN payment p -- unir con pagos
ON c.customer_id = p.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name
-- agrupar por cliente
HAVING SUM(p.amount) > ( -- filtrar: total del grupo mayor que...
-- Subconsulta nivel 1: promedio de los totales
SELECT AVG(sub.total) -- promedio de todos los totales
FROM (
-- Subconsulta nivel 2: total por cliente
SELECT SUM(amount) AS total -- total pagado por cada cliente
FROM payment -- tabla de pagos
GROUP BY customer_id -- agrupar por cliente
) AS sub -- alias obligatorio para subconsulta en FROM
);
-- Orden de ejecucion:
-- 1) Subconsulta nivel 2: total de cada cliente -> [120, 80, 150, ...]
-- 2) Subconsulta nivel 1: promedio de esos totales -> AVG = ~112
-- 3) Consulta principal: filtra clientes cuyo total > 112
Subconsulta correlacionada
-- Empleados que ganan mas que el promedio de SU departamento
-- Patron: subconsulta correlacionada (se re-ejecuta por cada fila)
SELECT
e1.nombre, -- nombre del empleado evaluado
e1.sal, -- salario del empleado
e1.dep -- departamento
FROM EMPLEADO e1 -- consulta externa: recorre cada empleado
WHERE e1.sal > ( -- comparar salario individual vs...
SELECT AVG(e2.sal) -- ...promedio de su departamento
FROM EMPLEADO e2 -- recorrer empleados del mismo dep
WHERE e2.dep = e1.dep -- correlacion: e1.dep viene de la consulta EXTERNA
); -- se recalcula para cada fila de e1
-- Diferencia con subconsulta normal:
-- Normal: la subconsulta se ejecuta 1 sola vez
-- Correlacionada: se re-ejecuta por CADA fila de la consulta externa
-- Porque la subconsulta depende de e1.dep (cambia por fila)
10. EXISTS y NOT EXISTS
EXISTS verifica si una subconsulta retorna al menos una fila. Retorna TRUE si hay resultados, FALSE si esta vacia. Es muy eficiente para verificar existencia.
EXISTS — los que SI tienen
-- Clientes que han hecho al menos un pago
SELECT
c.customer_id, -- ID del cliente
c.first_name, -- nombre
c.last_name -- apellido
FROM customer c -- tabla de clientes
WHERE EXISTS ( -- para cada cliente, verifica si hay algun pago
SELECT 1 -- no importa que seleccionas, solo importa
-- si retorna filas o no
FROM payment p -- tabla de pagos
WHERE p.customer_id = c.customer_id
-- conecta la subconsulta con la consulta principal
);
-- Funciona asi:
-- Para el cliente 1: hay pagos con customer_id = 1? -> Si -> incluir
-- Para el cliente 2: hay pagos con customer_id = 2? -> Si -> incluir
-- Para el cliente 599: hay pagos con customer_id = 599? -> NO -> excluir
NOT EXISTS — los que NO tienen
-- Peliculas que NUNCA han sido alquiladas:
SELECT
f.film_id, -- ID de la pelicula
f.title -- titulo
FROM film f -- tabla de peliculas
WHERE NOT EXISTS ( -- verifica si NO hay ningun alquiler para esta pelicula
SELECT 1 -- SELECT 1 es convencion en EXISTS
FROM inventory i -- tabla de inventario
JOIN rental r -- unir con alquileres
ON i.inventory_id = r.inventory_id
WHERE i.film_id = f.film_id -- conecta con la consulta principal (film)
);
-- NOT EXISTS = incluir solo si la subconsulta NO retorna filas
-- Logica: para cada pelicula, buscar si hay al menos
-- una copia (inventory) que haya sido alquilada (rental)
EXISTS vs IN: ambos pueden lograr lo mismo, pero EXISTS suele ser mas rapido cuando la tabla externa es pequena y la interna es grande, porque EXISTS para de buscar apenas encuentra la primera coincidencia.
OJO: En la subconsulta de EXISTS: SELECT 1, SELECT *, SELECT columna… da lo mismo. Solo importa si retorna filas o no. Convencionalmente se usa SELECT 1.
Doble NOT EXISTS = division en SQL (“para todo”)
-- Clientes que han rentado TODAS las peliculas de accion
-- Patron: doble NOT EXISTS = division relacional
-- Lectura: "no existe pelicula de accion que este cliente NO haya rentado"
SELECT c.first_name, c.last_name -- datos del cliente
FROM customer c -- tabla de clientes
WHERE NOT EXISTS ( -- NOT EXISTS externo: "no hay pelicula tal que..."
SELECT 1 -- recorrer cada pelicula de accion
FROM film f
JOIN film_category fc ON f.film_id = fc.film_id
JOIN category cat ON fc.category_id = cat.category_id
WHERE cat.name = 'Action' -- solo peliculas de accion
AND NOT EXISTS ( -- NOT EXISTS interno: "...el cliente NO la rento"
SELECT 1
FROM rental r
JOIN inventory i ON r.inventory_id = i.inventory_id
WHERE i.film_id = f.film_id -- misma pelicula
AND r.customer_id = c.customer_id -- mismo cliente (correlacion)
)
);
-- Lectura: "cliente c tal que NO EXISTE pelicula de accion
-- para la cual NO EXISTA rental de c por esa pelicula"
-- = c rento TODAS las de accion
Patron “nunca hizo X” — 3 formas
-- Clientes que NUNCA han rentado -- 3 formas equivalentes
-- Opcion 1: LEFT JOIN + IS NULL (mas comun)
SELECT c.customer_id, c.first_name, c.last_name
FROM customer c
LEFT JOIN rental r -- LEFT: mantener clientes sin rental
ON c.customer_id = r.customer_id
WHERE r.rental_id IS NULL; -- filtrar: solo los que NO tienen match
-- Opcion 2: NOT EXISTS (generalmente mas eficiente)
SELECT c.customer_id, c.first_name, c.last_name
FROM customer c
WHERE NOT EXISTS ( -- TRUE si la subconsulta retorna 0 filas
SELECT 1 FROM rental r
WHERE r.customer_id = c.customer_id -- correlacion: para este cliente
);
-- Opcion 3: NOT IN (cuidado con NULLs!)
SELECT * FROM customer
WHERE customer_id NOT IN (
SELECT customer_id FROM rental
WHERE customer_id IS NOT NULL -- OBLIGATORIO: filtrar NULLs
);
-- Si hay NULLs en la sublista y no los filtras,
-- NOT IN retorna SIEMPRE vacio (ver seccion de NULLs)
11. Ejercicios resueltos (base dvdrental)
Ejercicio 1: Clientes con o sin alquileres (LEFT JOIN)
Enunciado: Listar todos los clientes mostrando customer_id, first_name, last_name y rental_id (si existe), incluyendo clientes sin registros en rental.
-- Mostrar TODOS los clientes, tengan o no alquileres
-- Patron: LEFT JOIN para incluir registros sin match
SELECT
c.customer_id, -- ID del cliente
c.first_name, -- nombre del cliente
c.last_name, -- apellido del cliente
r.rental_id -- ID del alquiler (sera NULL si no tiene)
FROM customer c -- tabla principal: customer (todos aparecen)
LEFT JOIN rental r -- LEFT JOIN: mantiene TODOS los clientes
ON c.customer_id = r.customer_id;
-- condicion: unir por customer_id
-- Por que LEFT JOIN y no INNER JOIN?
-- Porque queremos ver INCLUSO los clientes que nunca alquilaron.
-- INNER JOIN los excluiria.
-- Resultado: customer_id 599 (Austin Cintron) -> rental_id = NULL
Ejercicio 2: Clientes que NUNCA han alquilado (LEFT JOIN + IS NULL)
Enunciado: Mostrar clientes que no tienen ningun alquiler asociado.
-- Encontrar clientes SIN alquileres
-- Patron: LEFT JOIN + WHERE col_derecha IS NULL
SELECT
c.customer_id, -- ID del cliente
c.first_name, -- nombre
c.last_name -- apellido
FROM customer c -- tabla principal
LEFT JOIN rental r -- LEFT JOIN: mantener todos los clientes
ON c.customer_id = r.customer_id
WHERE r.rental_id IS NULL; -- CLAVE: filtra solo los que NO tienen coincidencia
-- Patron: LEFT JOIN + WHERE col_derecha IS NULL
-- = "encontrar los que NO estan en la otra tabla"
-- Es la forma clasica de encontrar registros huerfanos
Ejercicio 3: Todos los alquileres con datos del cliente (RIGHT JOIN)
Enunciado: Listar todos los registros de rental mostrando rental_id, customer_id, y first_name, last_name del cliente cuando haya coincidencia.
-- Mostrar TODOS los alquileres con datos del cliente
-- Patron: RIGHT JOIN para mantener todos los registros de rental
SELECT
r.rental_id, -- ID del alquiler
r.customer_id, -- ID del cliente en rental
c.first_name, -- nombre (NULL si no hay match)
c.last_name -- apellido (NULL si no hay match)
FROM customer c -- tabla izquierda
RIGHT JOIN rental r -- RIGHT JOIN: mantiene TODOS los registros de rental
ON c.customer_id = r.customer_id;
-- Equivalente con LEFT JOIN (invirtiendo tablas):
-- FROM rental r LEFT JOIN customer c ON ...
Ejercicio 4: Total de alquileres por cliente (RIGHT JOIN + COUNT)
Enunciado: Calcular cuantos alquileres tiene cada customer_id usando RIGHT JOIN.
-- Contar alquileres por cliente usando RIGHT JOIN
SELECT
r.customer_id, -- ID del cliente (de la tabla rental)
COUNT(*) AS total_rentals -- COUNT(*) cuenta todas las filas en cada grupo
FROM customer c -- tabla izquierda
RIGHT JOIN rental r -- RIGHT JOIN: todos los registros de rental se mantienen
ON c.customer_id = r.customer_id
GROUP BY r.customer_id; -- agrupa por customer_id para contar por cliente
-- Resultado:
-- customer_id | total_rentals
-- 1 | 32
-- 2 | 27
Ejercicio 5: Total pagado por cliente (JOIN + SUM)
Enunciado: Mostrar customer_id, first_name, last_name y la suma total de pagos de cada cliente.
-- Calcular cuanto ha pagado cada cliente en total
-- Patron: JOIN + funcion de agregacion + GROUP BY
SELECT
c.customer_id, -- ID del cliente
c.first_name, -- nombre
c.last_name, -- apellido
SUM(p.amount) AS total_paid -- SUM suma todos los montos de pago del cliente
FROM customer c -- tabla de clientes
JOIN payment p -- JOIN = INNER JOIN (solo clientes con pagos)
ON c.customer_id = p.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name;
-- REGLA: todo lo que no sea SUM/COUNT/etc
-- debe estar en GROUP BY
-- Resultado:
-- customer_id | first_name | last_name | total_paid
-- 148 | Eleanor | Hunt | 211.55
-- 526 | Karl | Seal | 208.58
Ejercicio 6: Cantidad de peliculas por categoria (JOIN + COUNT)
Enunciado: Mostrar el nombre de la categoria y la cantidad de peliculas en cada una.
-- Contar peliculas por categoria
-- Patron: JOIN a traves de tabla intermedia + COUNT + GROUP BY
SELECT
c.name AS category, -- c.name es el nombre de la categoria
COUNT(fc.film_id) AS total_films
-- cuenta cuantas peliculas hay en cada categoria
FROM category c -- tabla de categorias
JOIN film_category fc -- film_category es la tabla intermedia
-- que conecta film con category (relacion N:N)
ON c.category_id = fc.category_id
GROUP BY c.name; -- agrupa por nombre de categoria
-- Resultado:
-- category | total_films
-- Action | 64
-- Animation | 66
-- Children | 60
-- TIP: cuando dos tablas tienen relacion muchos-a-muchos (N:N),
-- existe una tabla intermedia que las conecta.
-- film_category conecta film con category.
Ejercicio 7: Categorias con mas de 60 peliculas (HAVING)
Enunciado: Solo las categorias cuyo numero de peliculas sea mayor a 60.
-- Categorias con mas de 60 peliculas
-- Patron: GROUP BY + HAVING para filtrar grupos
SELECT
c.name AS category, -- nombre de la categoria
COUNT(fc.film_id) AS total_films
-- total de peliculas por categoria
FROM category c -- tabla categorias
JOIN film_category fc -- tabla intermedia
ON c.category_id = fc.category_id
GROUP BY c.name -- agrupar por nombre
HAVING COUNT(fc.film_id) > 60; -- HAVING filtra DESPUES de agrupar
-- No podes usar WHERE aqui porque COUNT
-- se calcula DESPUES del agrupamiento
Ejercicio 8: Clientes con 30+ alquileres (JOIN + COUNT + HAVING)
Enunciado: Clientes con 30 o mas alquileres.
-- Clientes frecuentes: 30+ alquileres
-- Patron: JOIN + COUNT + GROUP BY + HAVING
SELECT
c.customer_id, -- ID del cliente
c.first_name, -- nombre
c.last_name, -- apellido
COUNT(r.rental_id) AS total_rentals
-- COUNT de rental_id (no COUNT(*) para ser preciso)
FROM customer c -- tabla de clientes
JOIN rental r -- unir con alquileres
ON c.customer_id = r.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name
-- agrupar por las columnas no-agregadas
HAVING COUNT(r.rental_id) >= 30; -- solo muestra clientes con 30 o mas alquileres
Ejercicio 9: Clientes que han pagado al menos una vez (EXISTS)
Enunciado: Usar EXISTS para encontrar clientes con al menos un pago.
-- Clientes con al menos un pago
-- Patron: EXISTS para verificar existencia
SELECT
c.customer_id, -- ID del cliente
c.first_name, -- nombre
c.last_name -- apellido
FROM customer c -- tabla de clientes
WHERE EXISTS ( -- verifica: hay al menos un pago para este cliente?
SELECT 1 -- SELECT 1 por convencion (da igual que pongas)
FROM payment p -- tabla de pagos
WHERE p.customer_id = c.customer_id
-- correlaciona la subconsulta con la consulta principal
);
-- EXISTS retorna TRUE si la subconsulta tiene al menos 1 fila
-- Es mas eficiente que IN cuando hay muchos registros
Ejercicio 10: Peliculas NUNCA alquiladas (LEFT JOIN doble + IS NULL)
Enunciado: Peliculas que no tienen ningun alquiler registrado.
-- Peliculas que NUNCA fueron alquiladas
-- Patron: doble LEFT JOIN + IS NULL
SELECT
f.film_id, -- ID de la pelicula
f.title -- titulo
FROM film f -- tabla de peliculas
LEFT JOIN inventory i -- unir con inventario (una pelicula puede no tener copia)
ON f.film_id = i.film_id
LEFT JOIN rental r -- unir con alquileres
ON i.inventory_id = r.inventory_id
WHERE r.rental_id IS NULL; -- filtrar las que NO tienen ningun alquiler
-- Esto captura dos casos:
-- 1) Peliculas sin copia en inventory
-- 2) Peliculas con copia pero nunca alquiladas
-- Ambos casos resultan en rental_id = NULL
Ejercicio 11: Clientes por encima del promedio de pago total (subconsultas anidadas)
Enunciado: Clientes cuyo total pagado sea mayor que el promedio del total pagado entre todos los clientes.
-- Clientes que pagaron mas que el promedio total
-- Patron: subconsultas anidadas (2 niveles en HAVING)
SELECT
c.customer_id, -- ID del cliente
c.first_name, -- nombre
c.last_name, -- apellido
SUM(p.amount) AS total_paid -- total pagado por este cliente
FROM customer c -- tabla de clientes
JOIN payment p -- unir con pagos
ON c.customer_id = p.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name
-- agrupar por cliente
HAVING SUM(p.amount) > ( -- filtrar: total del grupo mayor que...
-- Subconsulta nivel 1:
-- calcula el promedio de los totales por cliente
SELECT AVG(sub.total) -- promedio de todos los totales
FROM (
-- Subconsulta nivel 2:
-- calcula el total pagado por CADA cliente
SELECT SUM(amount) AS total -- total por cliente
FROM payment -- tabla de pagos
GROUP BY customer_id -- un total por cliente
) AS sub -- alias obligatorio para subconsulta en FROM
);
-- Orden de ejecucion:
-- 1) Subconsulta nivel 2: total de cada cliente -> [120, 80, 150, ...]
-- 2) Subconsulta nivel 1: promedio de esos totales -> AVG = ~112
-- 3) Consulta principal: filtra los que sumaron > 112
12. Patrones de examen
| Patron | Tecnica SQL | Ejemplo |
|---|---|---|
| Encontrar huerfanos (“los que NO tienen…”) | LEFT JOIN + WHERE col IS NULL | Clientes sin alquileres |
| Verificar existencia | EXISTS / NOT EXISTS | Clientes con al menos un pago |
| Para todo / division | Doble NOT EXISTS | Clientes que rentaron TODAS las de accion |
| Por encima del promedio global | WHERE col > (SELECT AVG…) | Peliculas mas caras que el promedio |
| Por encima del promedio de su grupo | Subconsulta correlacionada en WHERE | Empleado que gana mas que su dep |
| Top N general | ORDER BY col DESC LIMIT N | Las 10 peliculas mas caras |
| Contar por grupo | GROUP BY + COUNT | Alquileres por cliente |
| Filtrar grupos | GROUP BY + HAVING | Clientes con 30+ alquileres |
| Subconsulta anidada en HAVING | HAVING SUM > (SELECT AVG(SELECT SUM…)) | Total pagado > promedio de totales |
| Navegacion N:M | JOIN tabla_intermedia | Peliculas por categoria (via film_category) |
| Self-join | tabla t1 JOIN tabla t2 ON t1.col = t2.pk | Empleado y su supervisor |
| Comparar con promedio en HAVING | HAVING AVG(col) > (SELECT AVG…) | Departamentos con salario promedio > global |
Errores comunes en parciales:
- Olvidar GROUP BY cuando usas COUNT/SUM
- Poner COUNT en WHERE en vez de HAVING
- Olvidar el alias en subconsulta de FROM
- Confundir LEFT y RIGHT JOIN
- Usar = NULL en vez de IS NULL
- COUNT(*) en LEFT JOIN cuenta NULLs como 1
13. NULL y logica de 3 valores
Cualquier comparacion con NULL da UNKNOWN. WHERE solo pasa filas con TRUE (no UNKNOWN, no FALSE).
Tabla de verdad completa
AND — F gana siempre, U gana sobre T:
| AND | T | F | U |
|---|---|---|---|
| T | T | F | U |
| F | F | F | F |
| U | U | F | U |
OR — T gana siempre, U gana sobre F:
| OR | T | F | U |
|---|---|---|---|
| T | T | T | T |
| F | T | F | U |
| U | T | U | U |
NOT: NOT T = F, NOT F = T, NOT U = U
Ejemplos de 3-valued logic
NULL = NULL—> UNKNOWN (no TRUE)NULL <> NULL—> UNKNOWN (no TRUE)NULL > 5—> UNKNOWNNOT (NULL > 5)—> NOT UNKNOWN —> UNKNOWNNULL = 5 OR TRUE—> UNKNOWN OR TRUE —> TRUE (T gana en OR)NULL = 5 AND TRUE—> UNKNOWN AND TRUE —> UNKNOWNIS NULL/IS NOT NULL— unica forma correcta de testear NULLs
AVG y NULLs — comportamiento importante
-- AVG ignora NULLs al calcular el promedio
-- Datos: 4, 3, NULL, 5
-- AVG = (4 + 3 + 5) / 3 = 4.0 (NO divide por 4)
-- Si queres que NULL cuente como 0:
SELECT AVG(COALESCE(col, 0)) FROM tabla;
-- COALESCE reemplaza NULL por 0 antes de promediar
-- Resultado: (4 + 3 + 0 + 5) / 4 = 3.0
NOT IN con NULLs — la trampa
-- TRAMPA: NOT IN con NULLs en la lista -> resultado SIEMPRE vacio
-- Supongamos: SELECT dep_id FROM departamento -> (1, 2, NULL)
-- Esta consulta PARECE correcta pero SIEMPRE retorna 0 filas:
SELECT * FROM empleado WHERE dep_id NOT IN (1, 2, NULL);
-- NOT IN se expande internamente a AND de desigualdades:
-- WHERE dep_id <> 1 AND dep_id <> 2 AND dep_id <> NULL
-- ^^^^^^^^^^^^^^
-- SIEMPRE = UNKNOWN
-- UNKNOWN AND cualquier_cosa nunca es TRUE
-- Resultado: 0 filas SIEMPRE
-- SOLUCION 1: filtrar NULLs en la subconsulta
SELECT * FROM empleado
WHERE dep_id NOT IN (
SELECT dep_id FROM departamento
WHERE dep_id IS NOT NULL -- quitar NULLs de la lista
);
-- SOLUCION 2: usar NOT EXISTS (inmune a NULLs)
SELECT * FROM empleado e
WHERE NOT EXISTS (
SELECT 1 FROM departamento d
WHERE d.dep_id = e.dep_id -- NULL = NULL -> UNKNOWN -> no retorna fila -> OK
);
-- NOT EXISTS es mas seguro que NOT IN porque
-- no tiene el problema de NULLs en la sublista
COUNT(*) vs COUNT(col) con NULLs
-- Tabla ejemplo: (1, 'Ana', NULL), (2, 'Luis', 'luis@mail.com'), (3, 'Maria', 'maria@mail.com')
SELECT COUNT(*) FROM customer; -- 3 (cuenta TODAS las filas)
SELECT COUNT(email) FROM customer; -- 2 (ignora la fila donde email es NULL)
-- Diferencia critica en LEFT JOIN:
-- LEFT JOIN produce filas con NULL en la tabla derecha
-- COUNT(*) las cuenta como 1 (incorrecto si queres contar matches)
-- COUNT(col_derecha) las ignora (correcto)
14. Cheat sheet — referencia rapida de sintaxis
| Accion | Sintaxis |
|---|---|
| Crear schema | CREATE SCHEMA nombre; |
| Eliminar schema | DROP SCHEMA nombre CASCADE; |
| Crear tabla | CREATE TABLE t (col TIPO CONSTRAINT, …); |
| Agregar columna | ALTER TABLE t ADD COLUMN col TIPO; |
| Eliminar columna | ALTER TABLE t DROP COLUMN col; |
| Renombrar columna | ALTER TABLE t RENAME COLUMN viejo TO nuevo; |
| Cambiar tipo | ALTER TABLE t ALTER COLUMN col TYPE nuevo_tipo; |
| Agregar NOT NULL | ALTER TABLE t ALTER COLUMN col SET NOT NULL; |
| Quitar NOT NULL | ALTER TABLE t ALTER COLUMN col DROP NOT NULL; |
| Agregar constraint | ALTER TABLE t ADD CONSTRAINT nombre CHECK (cond); |
| Eliminar constraint | ALTER TABLE t DROP CONSTRAINT nombre; |
| Agregar FK | ALTER TABLE t ADD CONSTRAINT nombre FOREIGN KEY (col) REFERENCES otra(id); |
| Cambiar default | ALTER TABLE t ALTER COLUMN col SET DEFAULT valor; |
| Quitar default | ALTER TABLE t ALTER COLUMN col DROP DEFAULT; |
| Renombrar tabla | ALTER TABLE t RENAME TO nuevo_nombre; |
| Insertar datos | INSERT INTO t (c1, c2) VALUES (v1, v2); |
| Insertar multiples | INSERT INTO t (c1, c2) VALUES (v1, v2), (v3, v4); |
| Actualizar datos | UPDATE t SET col = val WHERE condicion; |
| Eliminar datos | DELETE FROM t WHERE condicion; |
| Truncar tabla | TRUNCATE TABLE t; |
| SELECT basico | SELECT cols FROM t WHERE cond ORDER BY col; |
| INNER JOIN | FROM a JOIN b ON a.id = b.a_id |
| LEFT JOIN | FROM a LEFT JOIN b ON a.id = b.a_id |
| RIGHT JOIN | FROM a RIGHT JOIN b ON a.id = b.a_id |
| FULL OUTER JOIN | FROM a FULL OUTER JOIN b ON a.id = b.a_id |
| Encontrar huerfanos | LEFT JOIN b ON … WHERE b.id IS NULL |
| Contar por grupo | SELECT col, COUNT(*) FROM t GROUP BY col |
| Filtrar grupos | GROUP BY col HAVING COUNT(*) > n |
| EXISTS | WHERE EXISTS (SELECT 1 FROM t2 WHERE …) |
| Subconsulta escalar | WHERE col > (SELECT AVG(col) FROM t2) |
| Subconsulta anidada | HAVING SUM(x) > (SELECT AVG(sub.y) FROM (SELECT …) AS sub) |
| Subconsulta en FROM | FROM (SELECT … GROUP BY …) AS sub |
| Division (para todo) | WHERE NOT EXISTS (SELECT … WHERE NOT EXISTS (…)) |
Recordar para el parcial:
- WHERE filtra filas, HAVING filtra grupos
- Toda columna en SELECT que no sea agregacion debe ir en GROUP BY
- LEFT JOIN + IS NULL = encontrar los que NO estan
- EXISTS es para verificar existencia (usa SELECT 1)
- Las subconsultas en FROM necesitan alias obligatorio
- Orden de ejecucion: FROM —> WHERE —> GROUP BY —> HAVING —> SELECT —> ORDER BY —> LIMIT
Errores comunes:
- Olvidar GROUP BY cuando usas COUNT/SUM
- Poner COUNT en WHERE en vez de HAVING
- Olvidar el alias en subconsulta de FROM
- Confundir LEFT y RIGHT JOIN
- Usar = NULL en vez de IS NULL