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 persona en un escritorio arma una consulta con bloques que filtran una tabla grande hasta dejar una tabla pequeña y ordenada

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:

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

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:

  1. FROM y JOIN: arma la tabla de trabajo.
  2. WHERE: descarta filas.
  3. GROUP BY: forma los grupos.
  4. HAVING: descarta grupos.
  5. SELECT: calcula las columnas y los alias.
  6. ORDER BY: ordena (aquí ya puedes usar los alias del SELECT).
  7. 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

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, 简体中文