Curso de SQL desde cero, en español

SQL es el idioma en el que se le piden datos a una base de datos. Lo hablan los bancos, las tiendas en línea, la app de tu móvil y casi cualquier programa que guarde información, y es de los lenguajes que más se usan en el mundo: lo necesitan quienes programan, quienes analizan datos y quienes solo quieren sacar un informe sin esperar a nadie. Este curso empieza desde cero y no necesitas instalar nada: todas las consultas se pueden ejecutar en el editor de SQL del navegador, que ya trae cargada la base de ejemplo.

La ruta: ocho lecciones

Están pensadas para leerse en orden. Las dos primeras están en esta misma página y las demás tienen la suya. Todas usan la misma base de ejemplo, una pequeña tienda de informática, así que los resultados que ves en el texto son los que obtendrás tú. Debajo de casi cada consulta hay un enlace que la abre en el editor para que la cambies, y al final de cada lección hay retos con la solución escondida. Cuando el motor importa, un recuadro «En PostgreSQL y MySQL» muestra qué cambia en los otros dos motores más usados.

Lección 1. Primeros pasos

Tablas, filas y claves

Una base de datos relacional guarda la información en tablas, como hojas de cálculo con reglas estrictas: cada tabla tiene unas columnas fijas, con un nombre y un tipo, y cada fila es un registro. Una tabla clientes tiene una fila por cliente y columnas como nombre o pais.

Dos ideas sostienen todo lo demás. La clave primaria es la columna que identifica cada fila sin repetirse, casi siempre un id numérico. La clave foránea es una columna que guarda la clave primaria de otra tabla: cada pedido no repite el nombre y la ciudad del cliente, solo guarda su cliente_id. Así cada dato está en un solo sitio, y las tablas se relacionan por esas claves. De ahí el nombre.

Esquema de la base tienda: clientes, pedidos, detalle, productos, categorias y empleados, con flechas de cada clave foránea a la clave primaria a la que apuntaLa base «tienda»: seis tablas unidas por sus clavesclientesidPKnombreciudadpaisemailaltapedidosidPKcliente_idFKfechaestadodetallepedido_idPK FKproducto_idPK FKcantidadprecio_unitarioproductosidPKnombrecategoria_idFKpreciostockcategoriasidPKnombreempleadosidPKnombrepuestojefe_idFKsalarioPK: clave primaria (identifica cada fila). FK: clave foránea (apunta a la PK de otra tabla). Las flechas van de la FK a su PK.

La base de ejemplo del curso. Un cliente tiene pedidos; cada pedido tiene líneas de detalle, y cada línea apunta a un producto, que pertenece a una categoría. Los empleados apuntan a su jefe, que es otro empleado. Esquema: decodigo.com.

Esta es la base que usaremos en todo el curso: 12 clientes de seis países, 15 productos en 5 categorías, 22 pedidos con 41 líneas de detalle y 8 empleados. Tiene algunos huecos puestos a propósito, porque en los datos reales siempre los hay: un cliente sin ciudad, otro sin correo, uno que nunca ha comprado, un producto sin categoría, otro que nunca se ha vendido y un pedido cancelado sin líneas. Irán saliendo a lo largo de las lecciones.

SQLite, PostgreSQL y MySQL

SQL es un estándar, pero cada motor de base de datos lo implementa con su propio dialecto. Los tres más usados son estos, y lo que aprendas aquí sirve en los tres:

MotorCómo funcionaDónde lo encontrarás
SQLiteUna biblioteca: la base entera es un archivo, sin servidorMóviles, navegadores, aplicaciones de escritorio, prototipos
PostgreSQLUn servidor al que se conectan los programasAplicaciones web y empresas; el más completo de los libres
MySQLUn servidor, como PostgreSQLMuchísima web clásica, WordPress incluido

El curso usa SQLite, porque no hay que instalar ningún servidor y es el que corre dentro del editor del navegador. Todas las consultas se comprobaron con SQLite 3.46.1, y las diferencias con PostgreSQL 17 y MySQL 8.4 se comprobaron en esos motores.

Dónde escribir las consultas

La forma más rápida es el editor de SQL: escribes la consulta, pulsas «Ejecutar» y ves la tabla de resultados. Si prefieres trabajar en tu equipo, instala la consola sqlite3: en macOS ya viene instalada; en Linux está en los paquetes de cualquier distribución (en Debian y Ubuntu, sudo apt install sqlite3), y para Windows se descarga de sqlite.org. Guarda el script de la base, que tienes completo al final de esta lección, como tienda.sql, y crea con él la base:

sqlite3 tienda.db < tienda.sql
sqlite3 tienda.db

La segunda orden abre la consola sobre el archivo tienda.db. Dentro, las órdenes que empiezan por punto son de la consola, no de SQL: .tables lista las tablas y .mode table hace que los resultados salgan en una tabla dibujada.

sqlite> .tables
sqlite> .mode table
sqlite> SELECT nombre, pais FROM clientes LIMIT 3;
sqlite3 3.46.1categorias  clientes    detalle     empleados   pedidos     productos 

+----------------+----------+
|     nombre     |   pais   |
+----------------+----------+
| Ana Torres     | México   |
| Luis Fernández | España   |
| Camila Rojas   | Colombia |
+----------------+----------+

La primera consulta

Pedir datos se hace con SELECT: qué columnas quieres y de qué tabla. El asterisco significa «todas las columnas»:

SELECT * FROM categorias;

Ábrela en el editor y cámbiala →

resultado+----+----------------+
| id |     nombre     |
+----+----------------+
| 1  | Portátiles     |
| 2  | Monitores      |
| 3  | Periféricos    |
| 4  | Almacenamiento |
| 5  | Redes          |
+----+----------------+

En la práctica casi nunca se usa *: se piden las columnas que hacen falta, en el orden que conviene. El resultado de una consulta es siempre otra tabla, aunque tenga una sola fila o una sola columna.

SELECT nombre, ciudad, pais
FROM clientes;

Ábrela en el editor y cámbiala →

resultado+----------------+------------------+-----------+
|     nombre     |      ciudad      |   pais    |
+----------------+------------------+-----------+
| Ana Torres     | Ciudad de México | México    |
| Luis Fernández | Madrid           | España    |
| Camila Rojas   | Bogotá           | Colombia  |
| Diego Martínez | Guadalajara      | México    |
| Valentina Díaz | Buenos Aires     | Argentina |
| Jorge Ramírez  | Monterrey        | México    |
| Lucía Gómez    | Barcelona        | España    |
| Mateo Herrera  | NULL             | Chile     |
| Sofía Castro   | Lima             | Perú      |
| Andrés Navarro | Ciudad de México | México    |
| Paula Morales  | Madrid           | España    |
| Tomás Ortega   | Santiago         | Chile     |
+----------------+------------------+-----------+

Fíjate en Mateo Herrera: su ciudad sale como NULL. NULL no es un texto vacío ni un cero, sino la ausencia de dato: «no sabemos». Se comporta de una forma muy particular, que veremos en la lección 2.

Las cuatro partes de una consulta, SELECT, FROM, WHERE y ORDER BY, y el resultado: los cuatro clientes de México ordenados por nombreUna consulta dice qué quieres, no cómo buscarloSELECT nombre, paisqué columnasFROM clientesde qué tablaWHERE pais = 'México'qué filasORDER BY nombre;en qué ordennombrepaisAna TorresMéxicoAndrés NavarroMéxicoDiego MartínezMéxicoJorge RamírezMéxicoel motor decide cómoencontrarlas

SQL es declarativo: describes el resultado que quieres y el motor elige cómo buscarlo. No hay bucles ni índices que recorrer a mano. Esquema: decodigo.com.

Cuatro reglas de escritura

  • Mayúsculas: las palabras de SQL (SELECT, FROM) se escriben en mayúsculas por costumbre, para distinguirlas de los nombres de tablas y columnas, pero da igual: select funciona exactamente igual.
  • Punto y coma: cierra cada sentencia. En el editor y en la consola es lo que separa una consulta de la siguiente.
  • Comillas: los textos van entre comillas simples, 'México'. Las dobles se reservan para nombres de columnas o tablas raros, con espacios o tildes.
  • Comentarios: todo lo que va detrás de -- hasta el final de la línea se ignora.
Ver el script completo de la base «tienda» (para crearla en tu equipo)
CREATE TABLE categorias (
  id     INTEGER PRIMARY KEY,
  nombre VARCHAR(40) NOT NULL UNIQUE
);

CREATE TABLE clientes (
  id     INTEGER PRIMARY KEY,
  nombre VARCHAR(60) NOT NULL,
  ciudad VARCHAR(40),
  pais   VARCHAR(40) NOT NULL,
  email  VARCHAR(80) UNIQUE,
  alta   DATE NOT NULL
);

CREATE TABLE productos (
  id           INTEGER PRIMARY KEY,
  nombre       VARCHAR(60) NOT NULL,
  categoria_id INTEGER REFERENCES categorias(id),
  precio       NUMERIC(10,2) NOT NULL CHECK (precio > 0),
  stock        INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE pedidos (
  id         INTEGER PRIMARY KEY,
  cliente_id INTEGER NOT NULL REFERENCES clientes(id),
  fecha      DATE NOT NULL,
  estado     VARCHAR(20) NOT NULL
);

CREATE TABLE detalle (
  pedido_id       INTEGER NOT NULL REFERENCES pedidos(id),
  producto_id     INTEGER NOT NULL REFERENCES productos(id),
  cantidad        INTEGER NOT NULL CHECK (cantidad > 0),
  precio_unitario NUMERIC(10,2) NOT NULL,
  PRIMARY KEY (pedido_id, producto_id)
);

CREATE TABLE empleados (
  id      INTEGER PRIMARY KEY,
  nombre  VARCHAR(60) NOT NULL,
  puesto  VARCHAR(40) NOT NULL,
  jefe_id INTEGER REFERENCES empleados(id),
  salario NUMERIC(10,2) NOT NULL
);

INSERT INTO categorias (id, nombre) VALUES
  (1, 'Portátiles'), (2, 'Monitores'), (3, 'Periféricos'), (4, 'Almacenamiento'), (5, 'Redes');

INSERT INTO clientes (id, nombre, ciudad, pais, email, alta) VALUES
  (1,  'Ana Torres',      'Ciudad de México', 'México',    'ana.torres@ejemplo.com',  '2025-01-14'),
  (2,  'Luis Fernández',  'Madrid',           'España',    'luis.f@ejemplo.com',      '2025-02-03'),
  (3,  'Camila Rojas',    'Bogotá',           'Colombia',  'camila.r@ejemplo.com',    '2025-02-27'),
  (4,  'Diego Martínez',  'Guadalajara',      'México',    'diego.m@ejemplo.com',     '2025-04-09'),
  (5,  'Valentina Díaz',  'Buenos Aires',     'Argentina', 'vale.diaz@ejemplo.com',   '2025-05-21'),
  (6,  'Jorge Ramírez',   'Monterrey',        'México',    'jorge.r@ejemplo.com',     '2025-06-30'),
  (7,  'Lucía Gómez',     'Barcelona',        'España',    'lucia.g@ejemplo.com',     '2025-08-12'),
  (8,  'Mateo Herrera',   NULL,               'Chile',     'mateo.h@ejemplo.com',     '2025-09-05'),
  (9,  'Sofía Castro',    'Lima',             'Perú',      NULL,                      '2025-11-18'),
  (10, 'Andrés Navarro',  'Ciudad de México', 'México',    'andres.n@ejemplo.com',    '2026-01-07'),
  (11, 'Paula Morales',   'Madrid',           'España',    'paula.m@ejemplo.com',     '2026-03-02'),
  (12, 'Tomás Ortega',    'Santiago',         'Chile',     'tomas.o@ejemplo.com',     '2026-05-16');

INSERT INTO productos (id, nombre, categoria_id, precio, stock) VALUES
  (1,  'Portátil Aero 14',          1, 1299.99, 8),
  (2,  'Portátil Nova 16',          1, 1849.00, 3),
  (3,  'Monitor 27" 4K',            2, 429.90, 12),
  (4,  'Monitor 24" FHD',           2, 159.00, 20),
  (5,  'Teclado mecánico',          3, 89.50, 35),
  (6,  'Ratón inalámbrico',         3, 24.99, 60),
  (7,  'Auriculares con micrófono', 3, 59.00, 0),
  (8,  'SSD 1 TB',                  4, 94.90, 40),
  (9,  'Disco externo 4 TB',        4, 119.00, 15),
  (10, 'Memoria USB 128 GB',        4, 14.50, 100),
  (11, 'Router Wi-Fi 7',            5, 249.00, 6),
  (12, 'Switch 8 puertos',          5, 39.90, 18),
  (13, 'Base para portátil',        NULL, 29.00, 25),
  (14, 'Webcam 4K',                 3, 139.00, 9),
  (15, 'Cable HDMI 2 m',            NULL, 9.99, 200);

INSERT INTO pedidos (id, cliente_id, fecha, estado) VALUES
  (1,  1,  '2026-01-10', 'entregado'),
  (2,  2,  '2026-01-15', 'entregado'),
  (3,  3,  '2026-01-28', 'entregado'),
  (4,  1,  '2026-02-04', 'entregado'),
  (5,  4,  '2026-02-19', 'entregado'),
  (6,  5,  '2026-03-03', 'entregado'),
  (7,  2,  '2026-03-11', 'devuelto'),
  (8,  6,  '2026-03-25', 'entregado'),
  (9,  7,  '2026-04-08', 'entregado'),
  (10, 3,  '2026-04-22', 'entregado'),
  (11, 8,  '2026-05-06', 'entregado'),
  (12, 1,  '2026-05-19', 'entregado'),
  (13, 9,  '2026-06-02', 'cancelado'),
  (14, 10, '2026-06-17', 'entregado'),
  (15, 4,  '2026-07-01', 'entregado'),
  (16, 11, '2026-07-14', 'entregado'),
  (17, 5,  '2026-08-05', 'entregado'),
  (18, 2,  '2026-08-21', 'entregado'),
  (19, 6,  '2026-09-09', 'enviado'),
  (20, 10, '2026-09-23', 'enviado'),
  (21, 7,  '2026-10-02', 'pendiente'),
  (22, 1,  '2026-10-12', 'pendiente');

INSERT INTO detalle (pedido_id, producto_id, cantidad, precio_unitario) VALUES
  (1, 1, 1, 1299.99), (1, 6, 1, 24.99),
  (2, 3, 2, 429.90),
  (3, 5, 1, 89.50), (3, 6, 2, 24.99), (3, 10, 3, 14.50),
  (4, 8, 2, 94.90),
  (5, 2, 1, 1849.00), (5, 4, 1, 159.00),
  (6, 11, 1, 249.00), (6, 12, 2, 39.90),
  (7, 14, 1, 139.00),
  (8, 1, 2, 1299.99), (8, 3, 2, 429.90), (8, 5, 2, 89.50),
  (9, 9, 1, 119.00), (9, 10, 2, 14.50),
  (10, 7, 2, 59.00), (10, 12, 1, 39.90),
  (11, 4, 2, 159.00), (11, 6, 2, 24.99), (11, 13, 1, 29.00),
  (12, 3, 1, 429.90),
  (14, 8, 4, 94.90), (14, 9, 1, 119.00),
  (15, 5, 1, 89.50), (15, 6, 1, 24.99),
  (16, 1, 1, 1249.99), (16, 14, 1, 139.00),
  (17, 12, 1, 39.90), (17, 10, 2, 14.50),
  (18, 2, 1, 1799.00), (18, 3, 1, 429.90),
  (19, 11, 1, 249.00),
  (20, 4, 3, 159.00), (20, 10, 5, 14.50),
  (21, 8, 1, 94.90), (21, 7, 1, 59.00),
  (22, 5, 1, 89.50), (22, 6, 1, 24.99), (22, 12, 1, 39.90);

INSERT INTO empleados (id, nombre, puesto, jefe_id, salario) VALUES
  (1, 'Elena Vargas',    'Directora',            NULL, 6200.00),
  (2, 'Raúl Peña',       'Jefe de ventas',       1,    4100.00),
  (3, 'Marta Ruiz',      'Jefa de almacén',      1,    3900.00),
  (4, 'Iván Soto',       'Vendedor',             2,    2400.00),
  (5, 'Carla Medina',    'Vendedora',            2,    2550.00),
  (6, 'Hugo Salas',      'Vendedor',             2,    2300.00),
  (7, 'Nadia Fuentes',   'Mozo de almacén',      3,    1900.00),
  (8, 'Óscar Lara',      'Mozo de almacén',      3,    1850.00);

Lección 2. Consultar: elegir, filtrar y ordenar

Columnas calculadas y alias

En el SELECT no solo van columnas: también expresiones. Con AS se le da nombre a la columna del resultado, el alias. Aquí, el precio con un 16 % de IVA, redondeado a dos decimales con ROUND:

SELECT nombre,
       precio,
       ROUND(precio * 1.16, 2) AS precio_con_iva
FROM productos;

Ábrela en el editor y cámbiala →

resultado+---------------------------+---------+----------------+
|          nombre           | precio  | precio_con_iva |
+---------------------------+---------+----------------+
| Portátil Aero 14          | 1299.99 | 1507.99        |
| Portátil Nova 16          | 1849    | 2144.84        |
| Monitor 27" 4K            | 429.9   | 498.68         |
| Monitor 24" FHD           | 159     | 184.44         |
| Teclado mecánico          | 89.5    | 103.82         |
| Ratón inalámbrico         | 24.99   | 28.99          |
| Auriculares con micrófono | 59      | 68.44          |
| SSD 1 TB                  | 94.9    | 110.08         |
| Disco externo 4 TB        | 119     | 138.04         |
| Memoria USB 128 GB        | 14.5    | 16.82          |
| Router Wi-Fi 7            | 249     | 288.84         |
| Switch 8 puertos          | 39.9    | 46.28          |
| Base para portátil        | 29      | 33.64          |
| Webcam 4K                 | 139     | 161.24         |
| Cable HDMI 2 m            | 9.99    | 11.59          |
+---------------------------+---------+----------------+

Hay un detalle de SQLite en la columna precio: el portátil Nova 16 sale a 1849 y el monitor de 27 pulgadas a 429.9, aunque en el script se escribieron como 1849.00 y 429.90. SQLite guarda el primero como el entero 1849, porque no pierde nada al hacerlo, y el segundo como un número decimal, que se muestra sin el cero final. Los tipos de SQLite son más flexibles que los de otros motores, y lo veremos en la lección 6. Para mostrar siempre dos decimales, printf('%.2f', precio).

WHERE: qué filas

WHERE se queda con las filas que cumplen una condición. Se pueden comparar números, textos y fechas con =, <> (distinto), <, >, <= y >=, y combinar condiciones con AND, OR y NOT:

SELECT nombre, precio, stock
FROM productos
WHERE precio < 100 AND stock > 30;

Ábrela en el editor y cámbiala →

resultado+--------------------+--------+-------+
|       nombre       | precio | stock |
+--------------------+--------+-------+
| Teclado mecánico   | 89.5   | 35    |
| Ratón inalámbrico  | 24.99  | 60    |
| SSD 1 TB           | 94.9   | 40    |
| Memoria USB 128 GB | 14.5   | 100   |
| Cable HDMI 2 m     | 9.99   | 200   |
+--------------------+--------+-------+

Cuidado al mezclar AND y OR: AND se evalúa primero, igual que la multiplicación antes que la suma. Si quieres los clientes de México o de España que se dieron de alta en 2026, los paréntesis no son opcionales:

-- Sin paréntesis: México (de cualquier año) O España de 2026
SELECT nombre, pais, alta FROM clientes
WHERE pais = 'México' OR pais = 'España' AND alta >= '2026-01-01';

-- Con paréntesis: (México o España) Y de 2026
SELECT nombre, pais, alta FROM clientes
WHERE (pais = 'México' OR pais = 'España') AND alta >= '2026-01-01';

Ábrela en el editor y cámbiala →

sin paréntesis+----------------+--------+------------+
|     nombre     |  pais  |    alta    |
+----------------+--------+------------+
| Ana Torres     | México | 2025-01-14 |
| Diego Martínez | México | 2025-04-09 |
| Jorge Ramírez  | México | 2025-06-30 |
| Andrés Navarro | México | 2026-01-07 |
| Paula Morales  | España | 2026-03-02 |
+----------------+--------+------------+
con paréntesis+----------------+--------+------------+
|     nombre     |  pais  |    alta    |
+----------------+--------+------------+
| Andrés Navarro | México | 2026-01-07 |
| Paula Morales  | España | 2026-03-02 |
+----------------+--------+------------+

Las fechas de la base están en el formato AAAA-MM-DD, el del estándar ISO. Con ese formato, comparar fechas como textos da el orden correcto, que es lo que hace SQLite, porque no tiene un tipo de fecha propio.

IN, BETWEEN y LIKE

Tres atajos para condiciones frecuentes: IN para una lista de valores, BETWEEN para un intervalo (con los dos extremos incluidos) y LIKE para buscar dentro de un texto, donde % significa «cualquier cosa» y _ «un carácter cualquiera»:

SELECT nombre, pais FROM clientes WHERE pais IN ('Chile', 'Perú', 'Argentina');

SELECT nombre, precio FROM productos WHERE precio BETWEEN 50 AND 150;

SELECT nombre FROM productos WHERE nombre LIKE 'Monitor%';

Ábrela en el editor y cámbiala →

IN+----------------+-----------+
|     nombre     |   pais    |
+----------------+-----------+
| Valentina Díaz | Argentina |
| Mateo Herrera  | Chile     |
| Sofía Castro   | Perú      |
| Tomás Ortega   | Chile     |
+----------------+-----------+
BETWEEN+---------------------------+--------+
|          nombre           | precio |
+---------------------------+--------+
| Teclado mecánico          | 89.5   |
| Auriculares con micrófono | 59     |
| SSD 1 TB                  | 94.9   |
| Disco externo 4 TB        | 119    |
| Webcam 4K                 | 139    |
+---------------------------+--------+
LIKE+-----------------+
|     nombre      |
+-----------------+
| Monitor 27" 4K  |
| Monitor 24" FHD |
+-----------------+

En SQLite, LIKE no distingue mayúsculas de minúsculas, pero solo en las letras sin tilde: '%torres%' encuentra a Ana Torres, y '%DÍAZ%' no encuentra a Valentina Díaz, porque para SQLite la Í y la í son letras distintas.

SELECT nombre FROM clientes WHERE nombre LIKE '%torres%';
SELECT nombre FROM clientes WHERE nombre LIKE '%DÍAZ%';

Ábrela en el editor y cámbiala →

'%torres%'+------------+
|   nombre   |
+------------+
| Ana Torres |
+------------+
'%DÍAZ%'(sin filas)

En PostgreSQL y MySQL

Aquí cada motor hace una cosa distinta. PostgreSQL distingue siempre mayúsculas y minúsculas en LIKE (para no distinguirlas tiene ILIKE), así que '%torres%' no encuentra nada:

SELECT nombre FROM clientes WHERE nombre LIKE '%torres%';
PostgreSQL 17 nombre 
--------
(0 rows)

MySQL, con su configuración de fábrica (la intercalación utf8mb4_0900_ai_ci: ai es «sin tildes», ci, «sin mayúsculas»), no distingue ni mayúsculas ni tildes: '%diaz%' encuentra a Valentina Díaz.

SELECT nombre FROM clientes WHERE nombre LIKE '%diaz%';
MySQL 8.4+-----------------+
| nombre          |
+-----------------+
| Valentina Díaz  |
+-----------------+

NULL: el dato que falta

Para buscar los NULL hay que escribir IS NULL (o IS NOT NULL). Con el signo igual no funciona, y no da ningún error: simplemente no devuelve nada.

SELECT nombre, email FROM clientes WHERE email = NULL;
SELECT nombre, email FROM clientes WHERE email IS NULL;

Ábrela en el editor y cámbiala →

= NULL(sin filas)
IS NULL+--------------+-------+
|    nombre    | email |
+--------------+-------+
| Sofía Castro | NULL  |
+--------------+-------+

La razón es que NULL significa «desconocido», y comparar algo con un desconocido da otro desconocido, ni verdadero ni falso. WHERE solo se queda con las filas en las que la condición es verdadera. Esta lógica de tres valores tiene consecuencias sorprendentes, y varias están en las rarezas de SQL.

ORDER BY, LIMIT y DISTINCT

Sin ORDER BY, el orden de las filas no está garantizado: el motor las devuelve como le resulte cómodo. Para ordenar, ORDER BY con una o varias columnas, cada una con ASC (ascendente, la opción por omisión) o DESC. LIMIT corta el resultado, y con OFFSET se salta las primeras filas:

-- Los tres productos más caros
SELECT nombre, precio FROM productos
ORDER BY precio DESC
LIMIT 3;

-- Clientes por país y, dentro de cada país, por nombre
SELECT pais, nombre FROM clientes
ORDER BY pais, nombre
LIMIT 5 OFFSET 2;

Ábrela en el editor y cámbiala →

los más caros+------------------+---------+
|      nombre      | precio  |
+------------------+---------+
| Portátil Nova 16 | 1849    |
| Portátil Aero 14 | 1299.99 |
| Monitor 27" 4K   | 429.9   |
+------------------+---------+
LIMIT 5 OFFSET 2+----------+----------------+
|   pais   |     nombre     |
+----------+----------------+
| Chile    | Tomás Ortega   |
| Colombia | Camila Rojas   |
| España   | Lucía Gómez    |
| España   | Luis Fernández |
| España   | Paula Morales  |
+----------+----------------+

Con LIMIT y OFFSET se hace la paginación de una web («página 2, de 10 en 10»), aunque con tablas grandes hay formas más eficientes, que están en el ejemplo de paginación. Por último, DISTINCT quita las filas repetidas del resultado:

SELECT DISTINCT pais FROM clientes ORDER BY pais;

Ábrela en el editor y cámbiala →

resultado+-----------+
|   pais    |
+-----------+
| Argentina |
| Chile     |
| Colombia  |
| España    |
| México    |
| Perú      |
+-----------+

Resumen de las lecciones 1 y 2

ParaEscribeRecuerda
Elegir columnasSELECT nombre, pais FROM clientes* trae todas; mejor pedir las que hacen falta
Calcular y renombrarprecio * 1.16 AS con_ivael alias es el nombre de la columna del resultado
FiltrarWHERE pais = 'México' AND alta >= '2026-01-01'AND antes que OR: usa paréntesis
Una lista, un intervaloIN ('Chile', 'Perú'), BETWEEN 50 AND 150BETWEEN incluye los extremos
Buscar en un textoLIKE 'Monitor%'cada motor trata distinto mayúsculas y tildes
El dato que faltaIS NULL, IS NOT NULL= NULL nunca es verdadero
Ordenar y cortarORDER BY precio DESC LIMIT 3sin ORDER BY no hay orden garantizado
Quitar repetidosSELECT DISTINCT paisse aplica a la fila entera del resultado

Retos

Reto 1. Muestra el nombre y el stock de los productos de los que no queda ninguna unidad o quedan menos de cinco, del que menos stock tiene al que más.

Ver la solución
SELECT nombre, stock
FROM productos
WHERE stock < 5
ORDER BY stock;

Ábrela en el editor y cámbiala →

resultado+---------------------------+-------+
|          nombre           | stock |
+---------------------------+-------+
| Auriculares con micrófono | 0     |
| Portátil Nova 16          | 3     |
+---------------------------+-------+

«Ninguna o menos de cinco» es lo mismo que stock < 5. Los auriculares están agotados: buen candidato para un aviso de reposición.

Reto 2. ¿Qué clientes se dieron de alta en el segundo semestre de 2025? Muestra su nombre y la fecha de alta.

Ver la solución
SELECT nombre, alta
FROM clientes
WHERE alta BETWEEN '2025-07-01' AND '2025-12-31'
ORDER BY alta;

Ábrela en el editor y cámbiala →

resultado+---------------+------------+
|    nombre     |    alta    |
+---------------+------------+
| Lucía Gómez   | 2025-08-12 |
| Mateo Herrera | 2025-09-05 |
| Sofía Castro  | 2025-11-18 |
+---------------+------------+

Como las fechas son textos AAAA-MM-DD, BETWEEN funciona. Si la columna guardara también la hora ('2025-12-31 18:30'), ese último día se quedaría fuera: es una de las rarezas.

Reto 3. Lista los productos cuyo nombre contiene «4K» o «TB», con su precio, del más barato al más caro.

Ver la solución
SELECT nombre, precio
FROM productos
WHERE nombre LIKE '%4K%' OR nombre LIKE '%TB%'
ORDER BY precio;

Ábrela en el editor y cámbiala →

resultado+--------------------+--------+
|       nombre       | precio |
+--------------------+--------+
| SSD 1 TB           | 94.9   |
| Disco externo 4 TB | 119    |
| Webcam 4K          | 139    |
| Monitor 27" 4K     | 429.9  |
+--------------------+--------+

Cómo está hecho este curso

Cada consulta se ejecutó en SQLite 3.46.1 sobre la base «tienda» recién creada, y los resultados que ves están copiados de esa ejecución, en el formato de la consola sqlite3 con .mode table. Las diferencias con PostgreSQL 17 y MySQL 8.4 se comprobaron en esos motores, con sus mensajes literales. Debajo de casi cada consulta hay un enlace para abrirla en el editor de SQL y cambiarla.

¿Buscas una receta concreta en vez de una lección? En Ejemplos de SQL hay tareas resueltas: los N primeros de cada grupo, buscar duplicados, totales acumulados, recorrer jerarquías o consultar desde Python. Y si algo te da un resultado que no esperabas, mira las rarezas de SQL.