> ## Documentation Index
> Fetch the complete documentation index at: https://apuntes.simulab.es/llms.txt
> Use this file to discover all available pages before exploring further.

# SQL desde cero: consultas SELECT, JOIN, GROUP BY y diseño de tablas

> Introducción a SQL y bases de datos relacionales: tablas, claves primarias y foráneas, CREATE TABLE, INSERT, UPDATE y DELETE, consultas SELECT con WHERE, ORDER BY y LIMIT, funciones de agregado, GROUP BY y HAVING, JOIN (INNER, LEFT), subconsultas, índices, normalización y transacciones, con ejemplos.

Casi todas las aplicaciones guardan sus datos en una **base de datos relacional**: la tienda online, el banco, la red social o la secretaría de la universidad. Los datos se organizan en **tablas** relacionadas entre sí, y se consultan con **SQL** (*Structured Query Language*), un lenguaje declarativo: se dice **qué** datos se quieren y el gestor decide **cómo** obtenerlos. SQL funciona casi igual en MySQL, PostgreSQL, SQLite, Oracle o SQL Server.

## Lo que vas a aprender

* Qué son las tablas, las filas, las columnas y las claves.
* Cómo crear tablas y añadir, modificar y borrar datos.
* Cómo hacer consultas con filtros, orden y límites.
* Las funciones de agregado y la agrupación.
* Cómo combinar tablas con JOIN.
* Subconsultas, índices, normalización y transacciones.

## Fundamentos

### El modelo relacional

| Concepto | Qué es |
| - | - |
| **Tabla** (relación) | Conjunto de datos de un tipo: alumnos, asignaturas, notas |
| **Fila** (registro, tupla) | Un elemento: un alumno |
| **Columna** (atributo) | Una propiedad, con un tipo: nombre, edad |
| **Clave primaria** (PK) | Columna (o columnas) que identifica cada fila de forma única |
| **Clave foránea** (FK) | Columna que apunta a la clave primaria de otra tabla |

Usaremos este esquema de ejemplo:

```sql theme={null}
CREATE TABLE alumnos (
    id      INTEGER PRIMARY KEY,
    nombre  VARCHAR(50) NOT NULL,
    grado   VARCHAR(30),
    edad    INTEGER CHECK (edad > 0)
);

CREATE TABLE asignaturas (
    id        INTEGER PRIMARY KEY,
    nombre    VARCHAR(50) NOT NULL,
    creditos  INTEGER
);

CREATE TABLE notas (
    alumno_id      INTEGER REFERENCES alumnos(id),
    asignatura_id  INTEGER REFERENCES asignaturas(id),
    nota           DECIMAL(4, 2),
    PRIMARY KEY (alumno_id, asignatura_id)
);
```

### Insertar, modificar y borrar

```sql theme={null}
INSERT INTO alumnos (id, nombre, grado, edad)
VALUES (1, 'Ana', 'Informática', 20),
       (2, 'Luis', 'Industrial', 22),
       (3, 'Eva', 'Informática', 19);

UPDATE alumnos SET edad = 21 WHERE id = 1;

DELETE FROM alumnos WHERE id = 3;
```

Un `UPDATE` o `DELETE` **sin `WHERE`** afecta a **todas** las filas.

### Consultar: SELECT

```sql theme={null}
SELECT nombre, edad
FROM alumnos
WHERE grado = 'Informática' AND edad >= 20
ORDER BY edad DESC, nombre
LIMIT 10;
```

| Cláusula | Para qué |
| - | - |
| `SELECT` | Columnas que se muestran (`*` para todas, `DISTINCT` para quitar repetidos) |
| `FROM` | Tabla de origen |
| `WHERE` | Filtrar filas: `=`, `<>`, `<`, `BETWEEN`, `IN (...)`, `LIKE 'A%'`, `IS NULL` |
| `ORDER BY` | Ordenar (`ASC` por defecto, `DESC`) |
| `LIMIT` | Máximo de filas (`TOP` en SQL Server) |

### Agregados y agrupación

```sql theme={null}
SELECT grado, COUNT(*) AS num_alumnos, AVG(edad) AS edad_media
FROM alumnos
GROUP BY grado
HAVING COUNT(*) >= 2
ORDER BY num_alumnos DESC;
```

* Funciones de agregado: `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`.
* `GROUP BY` agrupa las filas con el mismo valor y calcula los agregados por grupo.
* `WHERE` filtra **filas antes** de agrupar; `HAVING` filtra **grupos después**.

### Combinar tablas: JOIN

```sql theme={null}
SELECT a.nombre, s.nombre AS asignatura, n.nota
FROM notas n
JOIN alumnos a     ON a.id = n.alumno_id
JOIN asignaturas s ON s.id = n.asignatura_id
WHERE n.nota >= 5;
```

| Tipo | Devuelve |
| - | - |
| `INNER JOIN` (o `JOIN`) | Solo las filas con pareja en las dos tablas |
| `LEFT JOIN` | Todas las de la izquierda; con `NULL` si no tienen pareja |
| `RIGHT JOIN` | Todas las de la derecha |
| `FULL JOIN` | Todas las de las dos |
| `CROSS JOIN` | Todas las combinaciones (producto cartesiano) |

### Orden lógico de ejecución

Aunque se escribe empezando por `SELECT`, el gestor lo evalúa así:

`FROM` → `JOIN` → `WHERE` → `GROUP BY` → `HAVING` → `SELECT` → `ORDER BY` → `LIMIT`

Por eso no se puede usar en `WHERE` un alias definido en `SELECT`, y por eso los agregados van en `HAVING`.

### Subconsultas

```sql theme={null}
-- Alumnos con alguna nota por encima de la media general
SELECT nombre FROM alumnos
WHERE id IN (SELECT alumno_id FROM notas
             WHERE nota > (SELECT AVG(nota) FROM notas));
```

### Índices

Un **índice** es una estructura auxiliar (normalmente un árbol B, primo de los [árboles AVL](https://apuntes.simulab.es/programacion/algoritmos/arboles-avl.md)) que permite encontrar filas sin recorrer toda la tabla: de $O(n)$ a $O(\log n)$. Acelera las búsquedas y los JOIN, pero ocupa espacio y ralentiza las inserciones.

```sql theme={null}
CREATE INDEX idx_notas_alumno ON notas(alumno_id);
```

### Normalización

Diseñar las tablas para **no repetir información** y evitar incoherencias:

| Forma normal | Regla |
| - | - |
| 1FN | Cada celda tiene un único valor (nada de listas en una columna) |
| 2FN | 1FN, y cada columna depende de **toda** la clave primaria |
| 3FN | 2FN, y ninguna columna depende de otra columna que no sea clave |

Si en la tabla `notas` se guardara también el nombre del alumno, al cambiarlo habría que actualizarlo en muchas filas: eso se evita llevándolo a la tabla `alumnos`.

### Transacciones

Un grupo de operaciones que se ejecutan **todas o ninguna**:

```sql theme={null}
BEGIN;
UPDATE cuentas SET saldo = saldo - 100 WHERE id = 1;
UPDATE cuentas SET saldo = saldo + 100 WHERE id = 2;
COMMIT;      -- o ROLLBACK para deshacer
```

Cumplen las propiedades **ACID**: atomicidad, consistencia, aislamiento y durabilidad.

## Ejemplos resueltos

> [accordion] Ejemplo 1 · Nota media de cada alumno
>
> ```sql theme={null}
> SELECT a.nombre, ROUND(AVG(n.nota), 2) AS media
> FROM alumnos a
> JOIN notas n ON n.alumno_id = a.id
> GROUP BY a.id, a.nombre
> ORDER BY media DESC;
> ```

> [accordion] Ejemplo 2 · Alumnos sin ninguna nota (LEFT JOIN)
>
> ```sql theme={null}
> SELECT a.nombre
> FROM alumnos a
> LEFT JOIN notas n ON n.alumno_id = a.id
> WHERE n.alumno_id IS NULL;
> ```
>
> Con un `INNER JOIN` estos alumnos desaparecerían del resultado.

> [accordion] Ejemplo 3 · Asignaturas con más de 10 aprobados
>
> ```sql theme={null}
> SELECT s.nombre, COUNT(*) AS aprobados
> FROM notas n
> JOIN asignaturas s ON s.id = n.asignatura_id
> WHERE n.nota >= 5
> GROUP BY s.id, s.nombre
> HAVING COUNT(*) > 10;
> ```

> [accordion] Ejemplo 4 · Créditos aprobados por alumno
>
> ```sql theme={null}
> SELECT a.nombre, COALESCE(SUM(s.creditos), 0) AS creditos
> FROM alumnos a
> LEFT JOIN notas n       ON n.alumno_id = a.id AND n.nota >= 5
> LEFT JOIN asignaturas s ON s.id = n.asignatura_id
> GROUP BY a.id, a.nombre;
> ```
>
> La condición `nota >= 5` va en el `ON` y no en el `WHERE` para no perder a los alumnos sin aprobados. `COALESCE` cambia el `NULL` por 0.

## Errores frecuentes

* **`UPDATE` o `DELETE` sin `WHERE`.** Prueba antes la condición con un `SELECT`.
* **Comparar con `= NULL`.** Se usa `IS NULL`: `NULL = NULL` no es verdadero.
* **Poner un agregado en `WHERE`.** Va en `HAVING`.
* **Seleccionar columnas que no están en el `GROUP BY`** ni en un agregado.
* **Construir consultas concatenando texto del usuario.** Abre la puerta a la **inyección SQL**; usa siempre consultas parametrizadas.
* **Olvidar la condición del JOIN:** sale el producto cartesiano.

## Preguntas frecuentes

> [accordion] ¿SQL o NoSQL?
>
> Las bases de datos relacionales son la opción por defecto cuando los datos tienen estructura y relaciones, y hace falta consistencia (bancos, gestión, comercio). Las NoSQL (MongoDB, Redis, Cassandra) se usan para datos sin esquema fijo, cachés o volúmenes enormes repartidos en muchas máquinas, a cambio de menos garantías.

> [accordion] ¿Con qué practico?
>
> SQLite viene incluido en Python (`import sqlite3`) y no necesita instalar un servidor. Para algo más completo, PostgreSQL es gratuito y el más fiel al estándar.

## Temas relacionados

> [card] [Fundamentos de Python](https://apuntes.simulab.es/programacion/python/fundamentos-python.md)
>
> Usar SQLite desde Python.

> [card] [Tablas hash](https://apuntes.simulab.es/programacion/java/tablas-hash.md)
>
> Índices hash para buscar por igualdad.

> [card] [Lógica proposicional](https://apuntes.simulab.es/matematicas/logica/logica-proposicional.md)
>
> AND, OR y NOT en las condiciones.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.