Fundamentos de SQL: las queries que resuelven el 90% del trabajo
Por Equipo Hexadevs · 28 jul 2026 · 4 min de lectura
SQL es uno de los lenguajes más antiguos que sigue siendo relevante. La mayoría de las aplicaciones reales pasan más tiempo leyendo y escribiendo datos que ejecutando lógica compleja. Si te manejas con confianza en SQL, vas a ser más rápido y vas a meter menos bugs que el dev que hace todo desde la aplicación.
El SELECT bien armado
SELECT
u.id,
u.nombre,
u.email,
COUNT(o.id) AS total_ordenes
FROM usuarios u
LEFT JOIN ordenes o ON o.usuario_id = u.id
WHERE u.activo = true
AND u.created_at >= '2026-01-01'
GROUP BY u.id, u.nombre, u.email
HAVING COUNT(o.id) > 5
ORDER BY total_ordenes DESC
LIMIT 50;
Esta query te trae “los 50 usuarios activos con más de 5 órdenes desde enero”. Leéla en voz alta: FROM, JOIN, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT. Ese es el orden de ejecución lógico de SQL, no el orden sintáctico.
Los JOIN bien entendidos
-- INNER JOIN: solo los que tienen match en ambos lados
SELECT u.nombre, o.total
FROM usuarios u
INNER JOIN ordenes o ON o.usuario_id = u.id;
-- LEFT JOIN: todos los usuarios, tengan órdenes o no
SELECT u.nombre, COALESCE(o.total, 0) AS total
FROM usuarios u
LEFT JOIN ordenes o ON o.usuario_id = u.id;
Regla práctica: si querés “todos los X, con sus Y si existen” → LEFT JOIN. Si querés “solo los X que tienen Y” → INNER JOIN. Los RIGHT JOIN casi nunca se necesitan; reescribí la query con LEFT JOIN cambiando el orden de las tablas.
GROUP BY: el patrón que más se rompe
SELECT
DATE_TRUNC('month', created_at) AS mes,
COUNT(*) AS total_ordenes,
SUM(total) AS revenue,
AVG(total) AS ticket_promedio
FROM ordenes
WHERE created_at >= '2026-01-01'
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY mes DESC;
GROUP BY colapsa filas por un criterio y te deja aplicar funciones de agregación: COUNT, SUM, AVG, MIN, MAX. Si querés filtrar después del agrupamiento, usás HAVING. Si querés filtrar antes, WHERE. Esa distinción resuelve el 90% de las dudas con GROUP BY.
Índices: lo que separa una query rápida de una lenta
-- Crear un índice simple
CREATE INDEX idx_ordenes_usuario_id ON ordenes(usuario_id);
-- Índice compuesto (respetar orden: de más selectivo a menos)
CREATE INDEX idx_ordenes_usuario_estado ON ordenes(usuario_id, estado);
En psql, \d ordenes muestra la estructura de la tabla y los índices existentes.
Reglas:
- Indexá las columnas de los
WHEREy losJOIN ON. Esas son las que más se filtran. - El orden en un índice compuesto importa.
(a, b)sirve paraWHERE a = ?pero no paraWHERE b = ?. - No indexés todo. Cada índice hace más lentos los
INSERT/UPDATEy consume espacio.
INSERT, UPDATE, DELETE: el ABC
-- Insertar uno
INSERT INTO usuarios (nombre, email)
VALUES ('Ada Lovelace', 'ada@example.com')
RETURNING id, created_at;
-- Actualizar con cuidado
UPDATE usuarios
SET activo = false
WHERE last_login_at < NOW() - INTERVAL '1 year'
RETURNING id;
-- Borrar con cuidado
DELETE FROM ordenes
WHERE created_at < NOW() - INTERVAL '5 years'
RETURNING id;
El RETURNING (Postgres) te devuelve las filas afectadas sin un SELECT posterior. Pequeño detalle que ahorra una query.
Regla sagrada: las queries destructivas (
UPDATEyDELETE) van siempre conWHERE. Si vas a hacer un cambio masivo, primeroSELECTcon la misma condición para verificar qué vas a tocar.
Subqueries y CTEs: orden mental
-- Subquery en WHERE
SELECT nombre
FROM usuarios
WHERE id IN (SELECT usuario_id FROM ordenes WHERE total > 1000);
-- CTE: más legible cuando hay varias capas
WITH ordenes_grandes AS (
SELECT usuario_id, total
FROM ordenes
WHERE total > 1000
)
SELECT u.nombre, COUNT(og.total) AS cantidad
FROM usuarios u
JOIN ordenes_grandes og ON og.usuario_id = u.id
GROUP BY u.nombre;
Las CTEs (WITH) son legibles pero no siempre son más rápidas que las subqueries. Postgres las optimiza por igual. Usalas cuando la legibilidad mejore, no pensando en performance.
Transacciones: el superpoder del ACID
BEGIN;
UPDATE cuentas SET balance = balance - 100 WHERE id = 1;
UPDATE cuentas SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- Si algo falla:
ROLLBACK;
Si una de las dos updates falla, las dos se deshacen. Sin esto, podrías debitarle a alguien sin acreditar al otro. Casi cualquier ORM moderno te permite hacer esto con sintaxis más amigable, pero entendé qué pasa por debajo.
Cuando SQL no alcanza: los límites
SQL no es la herramienta correcta para todo:
- Búsqueda full-text compleja: mirá Postgres
tsvectoro un motor dedicado (Meilisearch, Elasticsearch). - Grafos con muchas relaciones profundas: una base de grafos (Neo4j) o un join manual con cuidado.
- Series temporales masivas: TimescaleDB, InfluxDB.
Para casi todo lo que vas a construir, una base relacional bien usada es la respuesta.
SQL se aprende resolviendo problemas reales. Cada vez que tengas que tocar tres tablas en una sola operación, escribí la query en SQL puro antes de salir a buscar un ORM. Esa disciplina te hace mejor dev en cualquier stack.