SQL básico — consultas

"SQL es el inglés de los datos. Si lo manejás bien, podés trabajar en cualquier compañía del planeta."

Qué vas a aprender en este capítulo

SQL es el lenguaje para hablar con bases relacionales. En este capítulo vas a aprender la consulta básica SELECT, los JOIN que conectan tablas, las funciones de agregación que resumen datos (COUNT, SUM, AVG), la cláusula GROUP BY para agrupar, las subconsultas y al final vas a ver el ataque más famoso contra bases de datos: SQL injection, y cómo prevenirlo.

2.1 La idea: declarar qué, no cómo

💡 Intuición

En Python, si querés filtrar productos con stock > 50 escribirías:

caros = []
for p in productos:
    if p["stock"] > 50:
        caros.append(p)

Tres líneas, dos para la lógica y una para el resultado. Es imperativo: vos decís paso a paso cómo hacerlo.

En SQL:

SELECT * FROM producto WHERE stock > 50;

Una línea. Y vos no decís cómo: decís qué querés. El DBMS decide cómo. Si la tabla tiene índice por stock, lo usa. Si no, recorre. Si tiene 10 millones de filas, paraleliza. Vos solo describís el resultado.

SQL es declarativo. Esa es la diferencia más profunda con los lenguajes de programación que ya conocés.

Esa propiedad permite que el DBMS optimice consultas que un humano nunca optimizaría a mano. Es por eso que SQL escala a millones y miles de millones de filas, mientras que un loop Python explota.

2.2 SELECT básico

📐 Fundamento

Sintaxis general:

SELECT columnas
FROM tabla
WHERE condicion
ORDER BY columna [ASC|DESC]
LIMIT n;

Todas las columnas

SELECT * FROM producto;

* = "todas las columnas". OK para exploración. Mal en producción — devuelve más datos que los necesarios y se rompe si cambia el esquema.

Columnas específicas

SELECT codigo, nombre, precio FROM producto;

Renombrar (alias)

SELECT nombre AS producto, precio * 1.13 AS precio_con_iva
FROM producto;

Condiciones — WHERE

SELECT * FROM producto WHERE precio > 0.50;
SELECT * FROM producto WHERE stock = 0;
SELECT * FROM producto WHERE nombre LIKE 'Pupusa%';     -- empieza con "Pupusa"
SELECT * FROM producto WHERE codigo IN ('Q', 'R', 'F');
SELECT * FROM producto WHERE precio BETWEEN 0.40 AND 0.60;
SELECT * FROM cliente WHERE email IS NULL;

Operadores:

=, <> (o !=), <, >, <=, >= Comparación
AND, OR, NOT Lógicos
IN (lista) Está en el conjunto
BETWEEN a AND b Rango (inclusivo)
LIKE 'patrón' Texto con % (cualquier secuencia) y _ (un carácter)
IS NULL, IS NOT NULL Para nulos

Ordenar

SELECT * FROM producto ORDER BY precio DESC, nombre ASC;

DESC descendente, ASC ascendente (default).

Limitar

SELECT * FROM producto ORDER BY precio DESC LIMIT 5;     -- los 5 más caros
SELECT * FROM producto ORDER BY id LIMIT 10 OFFSET 20;   -- página 3 si páginas son de 10

OFFSET salta filas. Combinado con LIMIT da paginación.

DISTINCT

Eliminar duplicados:

SELECT DISTINCT cliente_id FROM pedido;

2.3 JOIN — unir tablas

📐 Fundamento

El JOIN combina filas de tablas distintas que cumplen una condición. Es el corazón del modelo relacional.

INNER JOIN

SELECT p.id, c.nombre AS cliente, p.fecha, p.total
FROM pedido AS p
INNER JOIN cliente AS c ON p.cliente_id = c.id;

Devuelve filas donde AMBAS tablas tienen match. Pedidos sin cliente o clientes sin pedidos quedan fuera.

Regla de oro: siempre usá alias (p, c). Hace el SQL legible y permite columnas con mismo nombre.

LEFT JOIN (LEFT OUTER JOIN)

SELECT c.nombre, p.id AS pedido_id, p.fecha
FROM cliente AS c
LEFT JOIN pedido AS p ON c.id = p.cliente_id;

Devuelve todas las filas de la izquierda (cliente). Si no hay match en la derecha, las columnas derechas son NULL. Ejemplo: clientes que aún no pidieron nada.

RIGHT JOIN

Espejo del anterior. Casi nadie lo usa — siempre podés convertir un RIGHT JOIN a LEFT cambiando el orden de las tablas.

FULL OUTER JOIN

Todas las filas de ambos lados, NULL donde no hay match.

CROSS JOIN

Todas las combinaciones (producto cartesiano):

SELECT * FROM cliente CROSS JOIN producto;
-- N clientes × M productos filas

Útil rara vez. Cuidado: explota rápido.

Diagrama mental

Diagrama de Venn de los JOINs: INNER es la intersección, LEFT añade el resto de izquierda, RIGHT el resto de derecha, FULL ambos lados. INNER LEFT RIGHT FULL

JOIN de varias tablas

SELECT c.nombre AS cliente, pr.nombre AS producto, pl.cantidad
FROM pedido AS p
INNER JOIN cliente AS c ON p.cliente_id = c.id
INNER JOIN pedido_linea AS pl ON pl.pedido_id = p.id
INNER JOIN producto AS pr ON pl.producto = pr.codigo
WHERE p.fecha >= '2026-01-01';

Tres JOINs encadenados. Una consulta así te dice quién compró qué en un período.

2.4 Agregación

📐 Fundamento

Las funciones de agregación resumen muchos valores en uno:

Función Qué hace
COUNT(*) Cuenta filas
COUNT(columna) Cuenta no-NULL
SUM(columna) Suma
AVG(columna) Promedio
MIN, MAX Extremos
SELECT COUNT(*) AS total_clientes FROM cliente;
SELECT AVG(precio), MIN(precio), MAX(precio) FROM producto;
SELECT SUM(total) AS ingreso_total FROM pedido WHERE fecha >= '2026-01-01';

GROUP BY

Para agrupar y agregar por categoría:

SELECT cliente_id, COUNT(*) AS pedidos, SUM(total) AS gastado
FROM pedido
GROUP BY cliente_id;

Devuelve una fila por cliente, con el conteo y la suma.

Regla: las columnas en SELECT deben estar en GROUP BY o ser agregaciones. Sino, error.

-- Mal:
SELECT cliente_id, fecha, COUNT(*) FROM pedido GROUP BY cliente_id;
-- ERROR: fecha no está en GROUP BY ni es agregación

-- Bien:
SELECT cliente_id, COUNT(*), MAX(fecha) AS ultima FROM pedido GROUP BY cliente_id;

HAVING

WHERE filtra antes de agrupar, HAVING filtra después:

SELECT cliente_id, COUNT(*) AS pedidos
FROM pedido
GROUP BY cliente_id
HAVING COUNT(*) > 5;       -- solo clientes con más de 5 pedidos

HAVING es para condiciones sobre agregaciones.

Orden de ejecución (lógico)

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

Aunque el SQL se escribe en otro orden, se evalúa así. Por eso podés usar alias del SELECT en ORDER BY pero no en WHERE.

2.5 Subconsultas

📐 Fundamento

Una subconsulta es un SELECT dentro de otro:

-- Productos con precio mayor que el promedio
SELECT * FROM producto
WHERE precio > (SELECT AVG(precio) FROM producto);
-- Clientes que pidieron pupusas de queso
SELECT * FROM cliente
WHERE id IN (
    SELECT cliente_id FROM pedido WHERE id IN (
        SELECT pedido_id FROM pedido_linea WHERE producto = 'Q'
    )
);

Tipos:

  • Escalar: devuelve un solo valor.
  • De fila: devuelve una fila.
  • De tabla: devuelve varias filas.
  • Correlacionada: referencia columnas de la consulta exterior — se evalúa por cada fila del exterior.
-- Subquery correlacionada: clientes con su pedido más reciente
SELECT c.nombre, (
    SELECT MAX(fecha)
    FROM pedido p
    WHERE p.cliente_id = c.id
) AS ultima_compra
FROM cliente c;

CTE — Common Table Expression (WITH)

WITH ventas_por_cliente AS (
    SELECT cliente_id, COUNT(*) AS n_pedidos, SUM(total) AS gastado
    FROM pedido
    GROUP BY cliente_id
)
SELECT c.nombre, vc.n_pedidos, vc.gastado
FROM cliente c
JOIN ventas_por_cliente vc ON c.id = vc.cliente_id
WHERE vc.gastado > 100;

WITH te deja nombrar una subconsulta. Hace queries complejas mucho más legibles. Casi todo SQL moderno lo soporta.

2.6 SQL Injection — el ataque que NO querés

⚠️ Trampa común

El error #1 de aplicaciones que usan SQL. Si construís SQL concatenando strings con input del usuario, sos vulnerable.

Mal código (Python):

nombre = input("Nombre: ")
cur.execute(f"SELECT * FROM cliente WHERE nombre = '{nombre}'")

Si el usuario escribe: ' OR '1'='1

El SQL resultante es:

SELECT * FROM cliente WHERE nombre = '' OR '1'='1';

'1'='1' siempre es verdadero — devuelve todos los clientes. El atacante acaba de leer toda la tabla.

Peor: '; DROP TABLE cliente; --

SELECT * FROM cliente WHERE nombre = ''; DROP TABLE cliente; --';

Te dropean la tabla. Game over. (El comic XKCD "Bobby Tables" lo ilustra.)

La solución: parámetros

nombre = input("Nombre: ")
cur.execute("SELECT * FROM cliente WHERE nombre = ?", (nombre,))

? (o %s en MySQL/Postgres con DBAPI) es un placeholder. La biblioteca de la DB escapa el valor — el atacante no puede romper el SQL.

Regla absoluta: NUNCA concates input del usuario en SQL. Siempre parámetros.

En el ORM

Frameworks como SQLAlchemy, Django ORM, Laravel Eloquent generan SQL parametrizado automáticamente. Por eso son la opción default en aplicaciones serias.

# SQLAlchemy
clientes = session.query(Cliente).filter_by(nombre=nombre).all()

A prueba de injection.

2.7 Índices — por qué consultas rápidas

📐 Fundamento

Buscar WHERE codigo = 'Q' en una tabla con 10 millones de filas. Sin índice, el DBMS escanea todas — segundos. Con índice sobre codigo, lo encuentra en microsegundos.

Crear:

CREATE INDEX idx_pedido_cliente ON pedido(cliente_id);
CREATE INDEX idx_pedido_fecha ON pedido(fecha);
CREATE UNIQUE INDEX idx_cliente_email ON cliente(email);

Cuándo crear índices:

  • Columnas usadas frecuentemente en WHERE, JOIN, ORDER BY.
  • PK e UNIQUE ya están indexadas automáticamente.

Cuándo NO:

  • Tabla chica (decenas de filas). El índice no compensa.
  • Columna con pocos valores distintos (booleano). Índice no ayuda.
  • Tabla con muchísimas escrituras y pocas lecturas — los índices se mantienen al insertar/actualizar.

Cómo el DBMS los usa. El optimizador lee tu query, mira las estadísticas y decide qué índices usar. Para inspeccionar:

EXPLAIN ANALYZE SELECT * FROM pedido WHERE cliente_id = 5;

Te muestra el plan de ejecución: cómo el DBMS va a resolver la consulta. Es la herramienta clave para optimizar.

Tipos de índice:

  • B-tree (default): bueno para igualdad y rangos.
  • Hash: igualdad pura, rápido.
  • GiST, GIN: para búsqueda full-text, geográfica.

Profundizamos más en la materia siguiente.

2.8 Proyecto: consultas a la pupusería

🏗️ Avance del proyecto

Suponiendo el esquema del cap 1 con datos:

Reportes típicos

1. Producto más vendido del mes:

SELECT pr.nombre, SUM(pl.cantidad) AS unidades
FROM pedido_linea pl
JOIN producto pr ON pl.producto = pr.codigo
JOIN pedido p ON pl.pedido_id = p.id
WHERE p.fecha >= DATE_TRUNC('month', CURRENT_DATE)
GROUP BY pr.nombre
ORDER BY unidades DESC
LIMIT 5;

2. Top clientes (los 10 que más gastaron):

SELECT c.nombre, COUNT(p.id) AS pedidos, SUM(p.total) AS total_gastado
FROM cliente c
LEFT JOIN pedido p ON c.id = p.cliente_id
GROUP BY c.id, c.nombre
ORDER BY total_gastado DESC NULLS LAST
LIMIT 10;

3. Productos sin stock que se vendieron en el último mes (señal de demanda perdida):

SELECT pr.nombre
FROM producto pr
WHERE pr.stock = 0
  AND pr.codigo IN (
      SELECT pl.producto
      FROM pedido_linea pl
      JOIN pedido p ON pl.pedido_id = p.id
      WHERE p.fecha >= CURRENT_DATE - INTERVAL '30 days'
  );

4. Ingreso total por mes:

SELECT
    DATE_TRUNC('month', fecha) AS mes,
    COUNT(*) AS pedidos,
    SUM(total) AS ingreso,
    AVG(total) AS ticket_promedio
FROM pedido
GROUP BY mes
ORDER BY mes DESC;

Estas son consultas reales del tipo que un dueño usa para tomar decisiones: qué producto reponer, qué cliente atender mejor, cómo va el negocio.

2.9 Resumen visual

Cláusula Para qué
SELECT cols FROM tabla Especifica qué columnas
WHERE cond Filtra filas
JOIN ... ON ... Une tablas
GROUP BY cols Agrupa
HAVING cond Filtra grupos
ORDER BY Ordena
LIMIT n OFFSET m Pagina
Subquery SELECT dentro de otro
WITH (CTE) Subquery con nombre
EXPLAIN Plan de ejecución
JOINs
INNER JOIN: solo coincidencias
LEFT JOIN: todas las de izquierda + match
RIGHT JOIN: todas las de derecha + match
FULL OUTER JOIN: ambos lados

2.10 Ejercicios

✏️ Ejercicio 2.1 — SELECT simple

Escribí consultas para:

a. Productos con precio entre 0.50 y 0.70. b. Productos cuyo nombre contiene "queso". c. Top 3 productos más caros.

✏️ Ejercicio 2.2 — JOIN

Listá todos los pedidos con el nombre del cliente y el total.

✏️ Ejercicio 2.3 — Agregación

¿Cuántos pedidos hizo cada cliente y cuánto gastó? Solo los que hicieron al menos 1.

✏️ Ejercicio 2.4 — Subconsulta

Productos que nunca se han vendido.

✏️ Ejercicio 2.5 — SQL Injection

Identificá la vulnerabilidad y arregla el código:

import sqlite3

con = sqlite3.connect("clientes.db")
cur = con.cursor()

email = input("Email: ")
cur.execute(f"DELETE FROM cliente WHERE email = '{email}'")
con.commit()

2.11 Para profundizar


Definiciones nuevas: SELECT, WHERE, ORDER BY, LIMIT, JOIN (INNER/LEFT/RIGHT/FULL), GROUP BY, HAVING, agregación, subconsulta, CTE (WITH), índice, optimizador, EXPLAIN, SQL injection, parametrización.