Query Language

SQL

SQL es el lenguaje declarativo para realizar consultas sobre datos relacionales. Tú describes el resultado que deseas y la base de datos decide cómo computarlo.

beginner14 min readUpdated 16 sept 2026
report.sql
sql
-- report.sql
SELECT a.name          AS author,
       COUNT(p.id)     AS posts,
       MAX(p.created_at) AS latest
FROM authors a
LEFT JOIN posts p ON p.author_id = a.id
GROUP BY a.id, a.name
HAVING COUNT(p.id) > 0
ORDER BY posts DESC
LIMIT 10;
Primer estándar
1986
Paradigma
Declarativo
Funciona con
Bases de datos relacionales
Verbo principal
SELECT
Caja
Palabras clave case-insensitive
Usado por
PostgreSQL, MySQL, SQLite, SQL Server

Por que importa

Por qué SQL sobrevivió a todas las tendencias

Declarativo por diseño

Escribes cómo debe verse el resultado, no cómo iterar sobre las filas. El planificador elige los índices y el orden de los joins por ti.

Pensamiento basado en conjuntos

Cada sentencia opera sobre conjuntos completos de filas. Pensar en conjuntos, no en bucles, es el cambio mental que hace que SQL tenga sentido.

Restricciones como garantías

Las claves primarias, claves foráneas y checks son aplicados por la base de datos, por lo que es imposible escribir estados inválidos.

La imagen completa

Las tres ideas detrás de SQL

Describe el resultado en lugar de los pasos, piensa en conjuntos completos de filas y deja que las restricciones mantengan la integridad de los datos.

SELECT

Describir

Nombra las columnas y tablas que deseas; la base de datos devuelve un conjunto de resultados y decide el plan de ejecución.

Joins

Combinar

Las relaciones se entrelazan mediante claves coincidentes, donde los inner y outer joins deciden qué sucede con las filas que no coinciden.

Transactions

Proteger

BEGIN, COMMIT y ROLLBACK hacen que un grupo de sentencias sea atómico, para que las escrituras parciales nunca sobrevivan a un fallo.

HTML5 de un vistazo

Las cláusulas que usarás a diario

SELECT

Elige columnas y tablas para construir un conjunto de resultados.

WHERE

Mantén solo las filas que coincidan con una condición.

JOIN

Combina filas de dos tablas mediante una clave relacionada.

GROUP BY

Colapsa filas en grupos y agrégalas.

WITH

Nombra una subconsulta para que una consulta larga se lea de arriba hacia abajo.

COMMIT

Haz permanentes los cambios de una transacción, o usa ROLLBACK para deshacerlos.

Modelo de datos

Una fila por publicación

Una fila representa una única publicación de blog escrita por exactamente un autor.

La tabla de postsTabla relacional
  • idbigintClave primaria que identifica unívocamente la publicación
  • author_idbigintClave foránea a authors; quién la escribió
  • titletextTitular mostrado en los listados
  • bodytextEl contenido de la publicación en sí
  • published_attimestamptzNULL mientras la publicación sea un borrador
  • created_attimestamptzCuándo se insertó la fila por primera vez

Una fila representa una única publicación de blog escrita por exactamente un autor.

La guia completa

SQL: Todo lo que necesitas saber

¿Qué es SQL?

SQL, pronunciado “sequel” o “S-Q-L”, es el Structured Query Language (Lenguaje de Consulta Estructurado) utilizado para leer y escribir datos en bases de datos relacionales. Fue estandarizado en 1986, lo que lo hace más antiguo que la web, y sigue siendo la forma en que casi todas las aplicaciones se comunican con sus datos.

SQL se divide en dos partes. El DDL (lenguaje de definición de datos) crea y modifica la estructura: CREATE TABLE, ALTER TABLE, DROP INDEX. El DML (lenguaje de manipulación de datos) trabaja con los datos en sí: SELECT, INSERT, UPDATE, DELETE. Pasarás la mayor parte del tiempo usando DML, siendo SELECT la sentencia más común con diferencia.

Algo importante que debes entender desde el principio es que SQL no es un lenguaje de programación en el sentido habitual. No existen los bucles en el núcleo del lenguaje ni variables en una consulta simple. Tú describes el resultado que deseas y la base de datos determina cómo generarlo.

Declarativo y basado en conjuntos

En un lenguaje como JavaScript o Python, calcularías un reporte iterando:

const result = [];
for (const author of authors) {
  let count = 0;
  for (const post of posts) {
    if (post.author_id === author.id) count++;
  }
  if (count > 0) result.push({ author, count });
}

En SQL, expresas la misma intención en una sola sentencia:

SELECT a.name, COUNT(p.id) AS posts
FROM authors a
LEFT JOIN posts p ON p.author_id = a.id
GROUP BY a.id, a.name
HAVING COUNT(p.id) > 0;

No hay bucles. Declaraste que quieres cada autor con su recuento de posts. La base de datos puede resolver esto mediante un index scan, un hash join o cualquier otra técnica; y si mañana añades un índice, la misma consulta puede volverse más rápida sin cambiar un solo carácter.

Esta es la mentalidad basada en conjuntos (set-based): las sentencias operan sobre conjuntos completos de filas a la vez. Una vez que comprendes este concepto, SQL se vuelve conciso de una manera que los bucles rara vez logran.

SELECT: eligiendo columnas

Cada lectura comienza con SELECT, donde se enumeran las columnas que deseas obtener.

SELECT id, title, published_at
FROM posts;

SELECT * devuelve todas las columnas y es útil mientras exploras los datos, pero especifica el nombre de tus columnas en las consultas de la aplicación. Las listas explícitas son estables cuando el esquema cambia, evitan la transferencia de columnas grandes que no se utilizan y hacen que la intención sea obvia. Puedes calcular nuevas columnas y renombrarlas:

SELECT title,
       LENGTH(body) AS body_length,
       COALESCE(published_at, created_at) AS visible_at
FROM posts;

AS le asigna un alias a una columna. Úsalo para que los resultados sean legibles y para dar nombre a las columnas calculadas, lo cual es importante cuando una librería de cliente mapea las filas a objetos.

Filtrado con WHERE

WHERE mantiene únicamente las filas que cumplen una condición. Soporta las comparaciones habituales además de AND, OR, NOT, IN, BETWEEN y LIKE.

SELECT id, title
FROM posts
WHERE author_id = 7
  AND published_at IS NOT NULL
  AND title ILIKE '%sql%';

Hay dos detalles que vale la pena recordar. Primero, LIKE distingue entre mayúsculas y minúsculas en muchas bases de datos, mientras que ILIKE (Postgres) no lo hace; el LIKE de MySQL tampoco distingue entre mayúsculas y minúsculas con las collations habituales. Segundo, un comodín al inicio como '%sql' impide que un índice ordinario sea de ayuda, ya que no hay un prefijo al cual buscar. Para búsquedas de texto reales, utiliza en su lugar un índice de texto completo (full-text index).

Ordenación y paginación

ORDER BY ordena el resultado, y LIMIT junto con OFFSET lo recorta.

SELECT id, title
FROM posts
WHERE published_at IS NOT NULL
ORDER BY published_at DESC
LIMIT 20 OFFSET 40;

Sin ORDER BY, la base de datos no garantiza el orden de las filas. Una consulta que “casualmente” devuelve los resultados ordenados hoy puede cambiar cuando el planificador elija un plan diferente, por lo que siempre debes ordenar explícitamente cuando el orden sea importante.

OFFSET omite filas, lo cual se vuelve costoso en tablas grandes cuando se avanza mucho en los resultados, ya que la base de datos sigue teniendo que recorrerlas. La paginación basada en claves (keyset pagination) evita este coste recordando la última fila vista:

SELECT id, title
FROM posts
WHERE published_at < :last_published_at
ORDER BY published_at DESC
LIMIT 20;

NULL y la lógica trivalente

NULL no significa cero ni una cadena vacía. Significa desconocido, y SQL utiliza una lógica trivalente: cada condición se evalúa como verdadera, falsa o desconocida. Las filas solo pasan una cláusula WHERE cuando la condición es verdadera, por lo que el valor desconocido se trata como falso para el filtrado.

SELECT id FROM posts WHERE published_at = NULL;     -- never matches
SELECT id FROM posts WHERE published_at IS NULL;    -- correct
SELECT id FROM posts WHERE published_at IS NOT NULL;

Consecuencias a tener en cuenta:

  • NULL = NULL es desconocido, no verdadero. Usa IS NULL o IS NOT DISTINCT FROM.
  • NULL + 1 es NULL; usa COALESCE(x, 0) para sustituirlo por un valor predeterminado.
  • NOT IN (1, 2, NULL) nunca es verdadero, ya que comparar con un elemento desconocido da como resultado desconocido. Prefiere NOT EXISTS cuando la lista pueda contener NULL.
  • Los agregados ignoran los NULL: COUNT(column) cuenta los valores que no son nulos, mientras que COUNT(*) cuenta las filas.

La nulidad es una decisión de diseño. Declara las columnas como NOT NULL a menos que los valores ausentes tengan un significado real.

JOINs, con un diagrama

Un join combina filas de dos tablas basándose en una columna relacionada. Supongamos que authors tiene a Ada, Grace y Linus, y posts tiene dos filas de Ada y una de Grace. Los tipos de join producen resultados diferentes.

authors            posts
-------            ----------------------------
id name            id author_id title
1  Ada             10 1         Joins
2  Grace           11 1         Indexes
3  Linus           12 2         NULLs

INNER JOIN  ->  posts 10, 11, 12        (only matches)
LEFT JOIN   ->  posts 10, 11, 12, Linus (all authors, NULL post)
RIGHT JOIN  ->  posts 10, 11, 12        (all posts)
FULL JOIN   ->  posts 10, 11, 12, Linus (both sides)
-- Every author, including those with no posts.
SELECT a.name, p.title
FROM authors a
LEFT JOIN posts p ON p.author_id = a.id
ORDER BY a.name;

La condición del join se define en ON. En el caso de los outer joins, añadir un filtro de la tabla derecha en WHERE convierte silenciosamente el outer join en un inner join, ya que las filas NULL no pasan el filtro. Coloca dichas condiciones en la cláusula ON cuando quieras que las filas sin coincidencia se mantengan.

-- Keeps authors with no published posts.
SELECT a.name, p.title
FROM authors a
LEFT JOIN posts p
  ON p.author_id = a.id
 AND p.published_at IS NOT NULL;

Un self join utiliza la misma tabla dos veces con diferentes alias, lo cual es útil para jerarquías. Un cross join empareja cada fila con todas las demás y rara vez es lo que buscas por accidente.

GROUP BY y HAVING

Los agregados colapsan muchas filas en una sola: COUNT, SUM, AVG, MIN, MAX. GROUP BY define los grupos.

SELECT a.name          AS author,
       COUNT(p.id)     AS post_count,
       MAX(p.created_at) AS latest
FROM authors a
LEFT JOIN posts p ON p.author_id = a.id
GROUP BY a.id, a.name
HAVING COUNT(p.id) > 0
ORDER BY post_count DESC;

La regla es que cada columna seleccionada debe estar en el GROUP BY o envuelta en un agregado. Las bases de datos que aplican esto (Postgres, y MySQL 8 por defecto) te protegen de obtener resultados arbitrarios.

WHERE filtra las filas antes del agrupamiento, y HAVING filtra los grupos después. Por lo tanto, una condición sobre un agregado pertenece en HAVING, y una condición sobre una columna simple generalmente pertenece en WHERE por eficiencia.

SELECT author_id, COUNT(*) AS posts
FROM posts
WHERE published_at IS NOT NULL   -- filter rows
GROUP BY author_id
HAVING COUNT(*) > 5;             -- filter groups

Subconsultas y CTEs

Una subconsulta es una consulta anidada dentro de otra. Puede aparecer en SELECT, FROM o WHERE.

SELECT title
FROM posts
WHERE author_id IN (
  SELECT id FROM authors WHERE name = 'Ada'
);

Una common table expression (CTE) le asigna un nombre a una subconsulta mediante WITH, lo cual suele leerse mejor que el anidamiento y permite reutilizar el resultado.

WITH published AS (
  SELECT id, author_id, title
  FROM posts
  WHERE published_at IS NOT NULL
)
SELECT a.name, COUNT(p.id) AS posts
FROM authors a
LEFT JOIN published p ON p.author_id = a.id
GROUP BY a.id, a.name;

Las CTEs pueden encadenarse, y una recursive CTE se referencia a sí misma para recorrer un árbol, como categorías o hilos de comentarios. Opta por una CTE siempre que una consulta comience a anidarse a más de un nivel de profundidad; la legibilidad también es una característica de rendimiento.

Escritura de filas

INSERT añade filas, UPDATE las modifica y DELETE las elimina.

INSERT INTO posts (author_id, title, body)
VALUES (7, 'Learning SQL', 'SQL is declarative.');

UPDATE posts
SET published_at = now()
WHERE id = 42;

DELETE FROM posts
WHERE id = 42;

Asigna siempre una cláusula WHERE a UPDATE y DELETE. Sin ella, todas las filas se verán afectadas. Un buen hábito es ejecutar primero el SELECT correspondiente y confirmar el número de filas. Un UPDATE también puede hacer referencia a valores existentes:

UPDATE posts
SET title = title || ' (updated)'
WHERE author_id = 7;

INSERT puede añadir varias filas a la vez y puede alimentarse a partir de una consulta:

INSERT INTO posts (author_id, title)
SELECT id, 'Welcome' FROM authors;

Transacciones

Una transacción agrupa sentencias para que todas tengan éxito o todas fallen. Esto es lo que mantiene la consistencia en un cambio de varios pasos.

BEGIN;

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

COMMIT;

Si una sentencia falla, o decides abortar, ROLLBACK deshace cada cambio realizado desde BEGIN. Sin una transacción, un fallo entre las dos actualizaciones provocaría que faltara dinero. Agrupa las escrituras relacionadas, mantén la transacción corta y nunca esperes la respuesta de un usuario o una llamada de red mientras tengas una abierta.

Claves y restricciones

Las restricciones son reglas que la base de datos aplica por ti:

  • PRIMARY KEY — identifica de forma única cada fila; también crea un índice e implica NOT NULL.
  • FOREIGN KEY — requiere que el valor exista en otra tabla, preservando la integridad referencial.
  • UNIQUE — prohíbe duplicados en una columna o combinación de columnas.
  • NOT NULL — requiere un valor.
  • CHECK — requiere que se cumpla una condición, como price >= 0.
  • DEFAULT — proporciona un valor cuando no se indica ninguno.
CREATE TABLE posts (
  id           bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  author_id    bigint NOT NULL REFERENCES authors (id) ON DELETE CASCADE,
  title        text NOT NULL,
  body         text NOT NULL,
  published_at timestamptz,
  created_at   timestamptz NOT NULL DEFAULT now()
);

Una clave primaria subrogada, como una columna de identidad, es estable y compacta. Una clave natural, como una dirección de correo electrónico, tiene un significado pero puede cambiar, por lo que es preferible usar una clave subrogada con una restricción UNIQUE en la clave natural.

Los índices a nivel conceptual

Un índice es una estructura separada y ordenada que mapea los valores de las columnas con la ubicación de las filas. Sin uno, encontrar filas implica escanear la tabla; con uno, la base de datos puede saltar directamente a las coincidencias.

CREATE INDEX posts_author_published_idx
  ON posts (author_id, published_at DESC);

Conceptualmente, piensa en una guía telefónica ordenada por apellido. Buscar un nombre es rápido porque el libro está ordenado; filtrar por una columna que no es la clave de ordenación implica leer cada página. Los índices sacrifican velocidad de escritura y almacenamiento a cambio de velocidad de lectura, ya que cada insert y update debe mantenerlos.

Crea índices en las columnas que utilizas para filtrar y hacer join, prefiere los índices compuestos que coincidan con la estructura real de tus consultas, y verifica con EXPLAIN si el planificador realmente los está utilizando. Tener más índices no es mejor; lo importante es tener los índices correctos.

Funciones de ventana en una sola sesión

Una función de ventana realiza cálculos sobre un conjunto de filas relacionadas con la fila actual, sin colapsarlas como lo hace GROUP BY. Esto facilita la creación de totales acumulados, rankings y comparaciones por grupo.

SELECT author_id,
       title,
       published_at,
       ROW_NUMBER() OVER (
         PARTITION BY author_id
         ORDER BY published_at DESC
       ) AS rank_in_author
FROM posts
WHERE published_at IS NOT NULL;

La cláusula OVER define la ventana: PARTITION BY divide las filas en grupos y ORDER BY las ordena dentro de cada grupo. Algunas funciones comunes incluyen ROW_NUMBER, RANK, LAG, LEAD y SUM(...) OVER (...). La diferencia clave con la agregación es que cada fila original se mantiene.

Normalización: 1NF, 2NF, 3NF

La normalización es el proceso de eliminar la redundancia para que cada dato se almacene una sola vez. Las primeras tres formas normales son las que más utilizarás:

Primera forma normal (1NF) — sin grupos repetidos ni columnas con múltiples valores. Una columna tags que contenga "sql,indexes" la incumple; una tabla post_tags separada lo soluciona.

Segunda forma normal (2NF) — 1NF más la ausencia de dependencias parciales de parte de una clave compuesta. Si una línea de pedido tiene como clave (order_id, product_id) y almacena product_name, ese nombre depende solo de product_id, por lo que pertenece a products.

Tercera forma normal (3NF) — 2NF más la ausencia de dependencias transitivas entre columnas que no son claves. Si posts almacenara tanto author_id como author_email, el email dependería del autor y no del post, por lo que pertenece a authors.

Imagina una tabla que repite el email del autor en cada post. Si cambias el email, debes actualizar muchas filas; si olvidas una, los datos se vuelven contradictorios. Divídela en authors y posts y el dato existirá una sola vez. Desnormaliza más adelante, de forma deliberada, cuando un problema de rendimiento medible lo justifique.

Mejores prácticas

  • Nombra las columnas explícitamente en lugar de usar SELECT * en las consultas de la aplicación.
  • Usa siempre ORDER BY cuando el orden de los resultados sea importante.
  • Declara NOT NULL por defecto y gestiona los valores ausentes con COALESCE.
  • Utiliza IS NULL en lugar de = NULL, y prefiere NOT EXISTS sobre NOT IN con listas que admitan nulos.
  • Filtra las filas en WHERE y los grupos en HAVING.
  • Usa CTEs para mantener la legibilidad de las consultas complejas.
  • Asigna a UPDATE y DELETE una cláusula WHERE y confirma las filas objetivo primero.
  • Envuelve las escrituras relacionadas en una transacción y mantenla breve.
  • Añade índices para patrones de consulta reales y verifícalos con EXPLAIN.
  • Almacena cada dato una sola vez y busca la tercera forma normal antes de desnormalizar.

Errores comunes

  • Escribir WHERE column = NULL y obtener cero filas.
  • Usar NOT IN contra una lista que contiene NULL y que no devuelva nada silenciosamente.
  • Olvidar WHERE en un UPDATE o DELETE y modificar todas las filas.
  • Seleccionar columnas no agrupadas y obtener valores arbitrarios.
  • Asumir que las filas regresan en un orden útil sin ORDER BY.
  • Usar LIMIT ... OFFSET para paginación profunda y pagar un costo creciente.
  • Filtrar la tabla correcta en WHERE y convertir accidentalmente un LEFT JOIN en un inner join.
  • Indexar cada columna y ralentizar las escrituras sin obtener ningún beneficio en las lecturas.
  • Concatenar la entrada del usuario en cadenas SQL en lugar de usar parámetros, lo que invita a inyecciones.
  • Almacenar datos repetidos en una sola tabla ancha en lugar de normalizarlos.

Próximos pasos

SQL es la base de cualquier base de datos relacional, por lo que el siguiente paso es elegir un motor y aprender su dialecto y particularidades. Comienza con PostgreSQL por su cumplimiento de los estándares y sus tipos enriquecidos, o con MySQL por su ubicuidad y su sistema de replicación. A partir de ahí, descubre cómo los resultados de las consultas se convierten en respuestas HTTP en la guía de REST, y cómo un servicio de Node.js envía esas consultas mediante una conexión agrupada (pooled connection) en Node.js basics.

En la practica

Leer, unir, agregar, escribir

Cuatro sentencias que cubren la mayoría del SQL cotidiano.

list-posts.sql
SELECT id, title, published_at
FROM posts
WHERE published_at IS NOT NULL
  AND title ILIKE '%sql%'
ORDER BY published_at DESC
LIMIT 20 OFFSET 0;

Filtrado de grupos

WHERE se ejecuta antes de la agrupación y HAVING después, por lo que una condición agregada debe usar HAVING.

Preferir
SELECT author_id, COUNT(*) AS posts
FROM posts
GROUP BY author_id
HAVING COUNT(*) > 5;
Evitar
SELECT author_id, COUNT(*) AS posts
FROM posts
WHERE COUNT(*) > 5   -- aggregates are not
GROUP BY author_id;  -- allowed in WHERE

Pruebas de valores ausentes

NULL significa desconocido, por lo que la igualdad contra él nunca coincide. Usa IS NULL e IS NOT NULL.

Preferir
SELECT id, title
FROM posts
WHERE published_at IS NULL;
Evitar
SELECT id, title
FROM posts
WHERE published_at = NULL;
-- always returns zero rows

Compromisos

Dónde encaja SQL y dónde tiene límites

SQL es la herramienta adecuada para preguntas relacionales. Conocer sus límites evita que luches contra el lenguaje.

Strengths

  • Un lenguaje, muchas bases de datos

    El núcleo de SQL está estandarizado, por lo que las habilidades son transferibles entre PostgreSQL, MySQL, SQLite, SQL Server y el resto.

  • El optimizador hace la parte difícil

    Tú describes el resultado y el planificador elige los índices y las estrategias de join, superando a menudo a los bucles escritos a mano.

  • La integridad reside en el esquema

    Las claves, restricciones y transacciones hacen que la corrección sea una propiedad de la base de datos en lugar de una promesa en el código de la aplicación.

Trade-offs

  • Los dialectos divergen

    El SQL estándar es una base, no una garantía. La paginación, los upserts, las funciones de fecha y los operadores JSON varían según el proveedor.

  • NULL es sutil

    La lógica trivalente confunde tanto a principiantes como a expertos, y un solo NULL mal manejado puede eliminar filas silenciosamente.

  • El rendimiento no es automático

    El optimizador necesita buenos índices y estadísticas actualizadas. Una consulta que funciona bien con mil filas puede colapsar con un millón.

Preguntas frecuentes

Preguntas frecuentes

Keep learning

Related topics from the roadmap.

$ comienza a aprender

Listo para aprender SQL Fundamentals?

Nuestro tutorial interactivo te guia a traves de SQL Fundamentals paso a paso — con quizzes y codigo real que puedes ejecutar en el navegador.