SQL · Guías · 29 de octubre de 2026 · 11 min de lectura
Consultas SQL básicas: SELECT, WHERE, ORDER BY, GROUP BY y JOIN paso a paso

Una consulta SQL básica tiene siempre la misma forma: SELECT dice qué columnas quieres ver, FROM de qué tabla salen, WHERE filtra las filas, GROUP BY las agrupa para calcular totales, ORDER BY ordena el resultado y JOIN combina dos tablas por una columna común. Con estas seis piezas se resuelve la mayor parte del trabajo diario de un analista. Esta guía las recorre una por una, con consultas que funcionan de verdad: todas se ejecutan sobre la base de datos de una aerolínea que usamos en el curso de SQL de Nomto, y cada resultado que ves abajo es la salida real.
La base de datos de los ejemplos
Trabajaremos con cinco tablas de una aerolínea (los valores están en inglés, el dinero en dólares y las horas en la hora de Moscú, UTC+3):
| Tabla | Una fila es | Columnas que usaremos |
|---|---|---|
airports |
un aeropuerto (104) | airport_code, airport_name, city, timezone |
aircrafts |
un modelo de avión (9) | aircraft_code, model, range (alcance en km) |
flights |
una salida de un vuelo en un día (33 121) | flight_no, scheduled_departure, departure_airport, arrival_airport, status, aircraft_code, actual_departure |
bookings |
una reserva | book_ref, book_date, total_amount |
ticket_flights |
un tramo de vuelo en un billete | flight_id, fare_conditions (clase), amount (precio) |
Antes de escribir cualquier consulta conviene saber qué representa una fila de cada tabla. Casi todos los errores de principiante en SQL vienen de contar filas que no son lo que uno cree.
SELECT y FROM: elegir columnas
La consulta más simple pide columnas de una tabla:
SELECT model, range
FROM aircrafts;
Algunas reglas que conviene fijar desde el principio:
SELECT *devuelve todas las columnas. Sirve para echar un vistazo, pero en una consulta real nombra las columnas que necesitas: el resultado es más claro y no se rompe si alguien añade columnas a la tabla.- En
SELECTpuedes calcular. ConASle das nombre a la columna calculada. - El punto y coma al final separa consultas. En la sala de SQL de Nomto se ejecuta una sola consulta a la vez, así que puedes omitirlo.
El alcance está en kilómetros. Si lo queremos también en millas:
SELECT model,
range,
ROUND(range / 1.609) AS range_miles
FROM aircrafts
ORDER BY range DESC
LIMIT 5;
| model | range | range_miles |
|---|---|---|
| Boeing 777-300 | 11100 | 6899 |
| Boeing 767-300 | 7900 | 4910 |
| Airbus A319-100 | 6700 | 4164 |
| Airbus A320-200 | 5700 | 3543 |
| Airbus A321-200 | 5600 | 3480 |
Sin ROUND la columna saldría con decimales infinitos (6898.69484151647). ORDER BY y LIMIT los vemos más abajo.
WHERE: filtrar filas
WHERE deja pasar solo las filas que cumplen una condición. Los aeropuertos de Moscú:
SELECT airport_code, airport_name, city
FROM airports
WHERE city = 'Moscow'
ORDER BY airport_code;
| airport_code | airport_name | city |
|---|---|---|
| DME | Domodedovo | Moscow |
| SVO | Sheremetyevo | Moscow |
| VKO | Vnukovo | Moscow |
Fíjate en dos detalles: el texto va entre comillas simples (las dobles en PostgreSQL son para nombres de columnas) y la comparación distingue mayúsculas: 'moscow' no devuelve nada.
Combinar condiciones: AND, OR, IN, BETWEEN
Las salidas desde Sheremetyevo el 1 de agosto:
SELECT flight_no, scheduled_departure, arrival_airport, status
FROM flights
WHERE departure_airport = 'SVO'
AND scheduled_departure >= '2026-08-01'
AND scheduled_departure < '2026-08-02'
ORDER BY scheduled_departure, flight_no
LIMIT 5;
| flight_no | scheduled_departure | arrival_airport | status |
|---|---|---|---|
| PG0120 | 2026-08-01 09:00:00 | MJZ | Arrived |
| PG0591 | 2026-08-01 09:00:00 | TOF | Arrived |
| PG0241 | 2026-08-01 09:40:00 | CSY | Arrived |
| PG0677 | 2026-08-01 09:40:00 | MCX | Arrived |
| PG0041 | 2026-08-01 09:50:00 | NFG | Arrived |
Ese día salieron 50 vuelos de SVO. El filtro de fechas usa el patrón «mayor o igual que el inicio del día y menor que el inicio del día siguiente». Es más seguro que BETWEEN '2026-08-01' AND '2026-08-01', que con columnas de fecha y hora solo atrapa el instante exacto de medianoche.
IN sustituye a varios OR sobre la misma columna. Los vuelos que ya despegaron (en el aire o aterrizados):
SELECT COUNT(*)
FROM flights
WHERE status IN ('Departed', 'Arrived');
El resultado es 16 765. Cuando mezcles AND y OR, usa paréntesis: AND se evalúa primero, y a OR b AND c significa a OR (b AND c).
LIKE: buscar por patrón
% sustituye a cualquier cantidad de caracteres y _ a exactamente uno. Las ciudades que empiezan por «Novo»:
SELECT airport_code, airport_name, city
FROM airports
WHERE city LIKE 'Novo%'
ORDER BY city;
| airport_code | airport_name | city |
|---|---|---|
| NOZ | Spichenkovo | Novokuznetsk |
| OVB | Tolmachevo | Novosibirsk |
En PostgreSQL, ILIKE hace lo mismo sin distinguir mayúsculas.
IS NULL: valores vacíos
NULL significa «sin valor». Un vuelo que todavía no ha salido tiene actual_departure vacío. Aquí está la trampa más famosa de SQL:
SELECT COUNT(*) FROM flights WHERE actual_departure = NULL; -- 0
SELECT COUNT(*) FROM flights WHERE actual_departure IS NULL; -- 16348
La primera devuelve 0 aunque haya miles de vuelos sin salir: cualquier comparación con NULL da «desconocido», y WHERE solo deja pasar lo verdadero. Para vacíos usa siempre IS NULL o IS NOT NULL.
ORDER BY y LIMIT: ordenar y quedarse con las primeras filas
Sin ORDER BY la base de datos devuelve las filas en el orden que le resulta cómodo, y ese orden puede cambiar de una ejecución a otra. Las cinco reservas más caras:
SELECT book_ref, book_date, total_amount
FROM bookings
ORDER BY total_amount DESC
LIMIT 5;
| book_ref | book_date | total_amount |
|---|---|---|
| 3B54BB | 2026-07-01 17:08:00 | 12045.00 |
| 3AC131 | 2026-07-27 01:06:00 | 10871.00 |
| 65A6EA | 2026-06-29 06:28:00 | 10656.00 |
| D7E9AA | 2026-08-04 05:29:00 | 10628.00 |
| EF479E | 2026-07-29 15:58:00 | 10351.00 |
ASC(por defecto) ordena de menor a mayor;DESC, de mayor a menor.- Puedes ordenar por varias columnas:
ORDER BY scheduled_departure, flight_nodesempata por número de vuelo cuando dos salen a la misma hora, como en el ejemplo de SVO. LIMITsinORDER BYda «unas filas cualesquiera», no «las primeras». Si te importa cuáles salen, ordena.
Funciones de agregación: COUNT, SUM, AVG, MIN, MAX
Las funciones de agregación convierten muchas filas en un solo número. Una diferencia que conviene entender pronto:
SELECT COUNT(*) AS total,
COUNT(actual_departure) AS departed,
COUNT(*) - COUNT(actual_departure) AS not_yet
FROM flights;
| total | departed | not_yet |
|---|---|---|
| 33121 | 16773 | 16348 |
COUNT(*) cuenta filas; COUNT(columna) cuenta solo los valores no vacíos. COUNT(DISTINCT columna) cuenta valores distintos: SELECT COUNT(DISTINCT timezone) FROM airports devuelve 17 husos horarios.
GROUP BY: un total por grupo
GROUP BY parte la tabla en grupos y calcula el agregado dentro de cada uno. Cuántos vuelos hay en cada estado:
SELECT status, COUNT(*) AS flights
FROM flights
GROUP BY status
ORDER BY flights DESC;
| status | flights |
|---|---|
| Arrived | 16707 |
| Scheduled | 15383 |
| On Time | 518 |
| Cancelled | 414 |
| Departed | 58 |
| Delayed | 41 |
Varios agregados a la vez, por clase de tarifa:
SELECT fare_conditions,
COUNT(*) AS segments,
ROUND(AVG(amount), 2) AS avg_price,
MIN(amount) AS min_price,
MAX(amount) AS max_price
FROM ticket_flights
GROUP BY fare_conditions
ORDER BY avg_price DESC;
| fare_conditions | segments | avg_price | min_price | max_price |
|---|---|---|---|---|
| Business | 107642 | 511.43 | 91.00 | 2033.00 |
| Comfort | 17291 | 327.41 | 199.00 | 476.00 |
| Economy | 920793 | 159.6 | 30.00 | 745.00 |
La regla de oro: cada columna del SELECT tiene que estar en GROUP BY o dentro de una función de agregación. Si pones SELECT status, flight_no, COUNT(*) ... GROUP BY status, PostgreSQL da error, porque en un grupo hay cientos de números de vuelo y no sabe cuál mostrar.
HAVING: filtrar grupos
WHERE filtra filas antes de agrupar; HAVING filtra grupos después. Las ciudades con más de un aeropuerto:
SELECT city, COUNT(*) AS airports
FROM airports
GROUP BY city
HAVING COUNT(*) > 1
ORDER BY airports DESC;
| city | airports |
|---|---|
| Moscow | 3 |
| Ulyanovsk | 2 |
No puedes escribir WHERE COUNT(*) > 1: cuando se evalúa WHERE, los grupos todavía no existen.
JOIN: combinar tablas
En flights el avión aparece solo como código (321, SU9). El nombre del modelo está en aircrafts. JOIN une las dos tablas por la columna común:
SELECT a.model, COUNT(*) AS flights
FROM flights f
JOIN aircrafts a ON a.aircraft_code = f.aircraft_code
GROUP BY a.model
ORDER BY flights DESC;
| model | flights |
|---|---|
| Cessna 208 Caravan | 9273 |
| Bombardier CRJ-200 | 9048 |
| Sukhoi Superjet-100 | 8504 |
| Airbus A321-200 | 1952 |
| Boeing 737-300 | 1274 |
| Airbus A319-100 | 1239 |
| Boeing 767-300 | 1221 |
| Boeing 777-300 | 610 |
f y a son alias: nombres cortos para no repetir el nombre completo de la tabla. ON dice qué filas van juntas.
INNER JOIN y LEFT JOIN
La tabla anterior tiene 8 modelos, pero en aircrafts hay 9. JOIN (que es INNER JOIN) se queda solo con las filas que tienen pareja en ambas tablas, así que el modelo sin vuelos desaparece. LEFT JOIN conserva todas las filas de la tabla de la izquierda y rellena con NULL lo que no encuentra. Con eso se buscan «filas sin pareja»:
SELECT a.aircraft_code, a.model
FROM aircrafts a
LEFT JOIN flights f ON f.aircraft_code = a.aircraft_code
WHERE f.flight_id IS NULL;
| aircraft_code | model |
|---|---|
| 320 | Airbus A320-200 |
Si cuentas con LEFT JOIN, usa COUNT(f.flight_id) y no COUNT(*): así el A320 sale con 0 y no con 1.
Un aviso para cuando avances: si unes una tabla con otra que tiene varias filas por cada una de las suyas (una reserva con varios billetes), las filas se multiplican y las sumas se inflan. Lo explicamos con números en las preguntas de entrevista SQL.
Todo junto: una consulta completa
Las piezas se combinan en un orden fijo: SELECT … FROM … JOIN … WHERE … GROUP BY … HAVING … ORDER BY … LIMIT. Ciudades con al menos 300 vuelos aterrizados y su retraso medio de salida, en minutos:
SELECT ap.city,
COUNT(*) AS flights,
ROUND(AVG(EXTRACT(EPOCH FROM f.actual_departure - f.scheduled_departure) / 60), 1) AS avg_delay_min
FROM flights f
JOIN airports ap ON ap.airport_code = f.departure_airport
WHERE f.status = 'Arrived'
GROUP BY ap.city
HAVING COUNT(*) >= 300
ORDER BY avg_delay_min DESC;
| city | flights | avg_delay_min |
|---|---|---|
| Yekaterinburg | 348 | 14.7 |
| Bryansk | 306 | 13.2 |
| Krasnoyarsk | 358 | 13.1 |
| St. Petersburg | 956 | 12.6 |
| Moscow | 3990 | 12.4 |
| Novosibirsk | 536 | 11.8 |
| Perm | 311 | 10.5 |
| Rostov-on-Don | 310 | 9.2 |
La resta de dos fechas da un intervalo; EXTRACT(EPOCH FROM …) lo pasa a segundos y lo dividimos entre 60.
En qué orden se ejecuta una consulta
Escribimos SELECT primero, pero la base de datos lo procesa casi al final:
FROMyJOIN: arma la tabla de trabajo.WHERE: descarta filas.GROUP BY: forma los grupos.HAVING: descarta grupos.SELECT: calcula las columnas y los alias.ORDER BY: ordena (aquí ya puedes usar los alias del SELECT).LIMIT: corta.
Este orden explica por qué un alias del SELECT funciona en ORDER BY pero no en WHERE, y por qué los agregados van en HAVING.
Errores típicos de principiante
= NULLen vez deIS NULL. Devuelve cero filas sin avisar.- Comillas dobles para texto.
WHERE city = "Moscow"en PostgreSQL busca una columna llamada Moscow. - Columnas sin agrupar en el SELECT. Todo lo que no esté en
GROUP BYtiene que ir dentro de un agregado. LIMITsinORDER BY. El resultado no es «el top», es aleatorio.COUNT(*)después de unLEFT JOIN. Cuenta como 1 las filas sin pareja.- Fechas con
BETWEENsobre columnas con hora. Usa>=inicio y<día siguiente.
Cómo practicar
Leer consultas ayuda, pero se aprende escribiéndolas y equivocándose. Todos los ejemplos de esta guía se pueden ejecutar y modificar en la sala de SQL de Nomto, con la misma base de datos: cambia la ciudad, el estado o el orden y mira qué pasa. El primer módulo del curso empieza justo por SELECT y alias, y cada lección termina con tres tareas que se comprueban automáticamente. Cuando domines lo básico, sigue con los ejercicios SQL resueltos y con las funciones de ventana.
Preguntas frecuentes
¿Cuánto se tarda en aprender las consultas SQL básicas?
SELECT, WHERE, ORDER BY, GROUP BY y JOIN se pueden aprender en dos o tres semanas practicando una hora al día. Lo que lleva más tiempo es leer bien los datos: saber qué es una fila y cuándo un JOIN multiplica filas.
¿Qué diferencia hay entre WHERE y HAVING?
WHERE filtra filas antes de agrupar y no admite agregados. HAVING filtra los grupos ya formados y se usa con COUNT, SUM, AVG y compañía.
¿El SQL de esta guía sirve para MySQL o SQL Server?
Casi todo, sí. SELECT, WHERE, GROUP BY, HAVING, JOIN y ORDER BY son iguales en todos los sistemas. Cambian detalles: LIMIT es TOP en SQL Server, ILIKE no existe en MySQL ni en SQL Server y las funciones de fechas tienen nombres distintos.
¿Necesito instalar una base de datos para practicar?
No. En la sala de SQL de Nomto escribes la consulta en el navegador y la ejecutas sobre la base de la aerolínea, sin instalar nada.
También en English, Português (Brasil), Tiếng Việt, 简体中文