Hasta ahora todas tus consultas han devuelto todas las filas de la tabla. Con 20 productos eso es manejable; con 20 millones, ni tu terminal ni tu paciencia lo aguantarían. La cláusula WHERE es la que convierte una consulta en una pregunta concreta: no "dame los productos", sino "dame los productos de cosmética que cuestan más de 10 euros". En esta lección aprenderás a filtrar filas con comparaciones sobre números, texto y fechas, a combinarlas con AND, OR y NOT, a manejar columnas booleanas, y a reconocer dos trampas que producen resultados equivocados sin dar ningún error: la precedencia de operadores y las comparaciones con valores nulos.
Contenido
WHEREcomo filtro fila a fila- Dónde encaja
WHEREen el orden lógico de ejecución - Comparar números
- Comparar texto
- Comparar fechas
- Comparar booleanos
- Combinar condiciones:
AND,OR,NOTy los paréntesis - Expresiones calculadas dentro de
WHERE - El síntoma de los nulos
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
WHERE como filtro fila a fila
WHERE como filtro fila a filaWHERE se escribe después de FROM y contiene una condición: una expresión que, para cada fila, se evalúa como verdadera, falsa o desconocida.
| id | nombre | precio |
|---|---|---|
| 1 | Aceite de oliva virgen extra 500 ml | 12.50 |
| 6 | Crema facial de aloe vera 50 ml | 18.90 |
| 8 | Aceite corporal de almendras 200 ml | 14.25 |
| 10 | Detergente ecológico concentrado 1 L | 11.20 |
| 13 | Velas de cera de soja (pack 2) | 13.75 |
| 15 | Té verde matcha ceremonial 30 g | 22.00 |
| 20 | Cápsulas de espirulina 120 uds | 16.40 |
7 filas de 20. El mecanismo mental es simple: PostgreSQL recorre las filas, evalúa precio > 10 en cada una y se queda solo con las que dan verdadero.
Y aquí está la clave que resuelve la mitad de los problemas con nulos: solo pasan las filas cuya condición es verdadera. Una condición falsa descarta la fila, y una condición desconocida (NULL) también la descarta. WHERE no es "descarta las falsas": es "conserva las verdaderas". Volveremos a ello en la sección 9.
En el modelo relacional de 01-05, esto es una selección (elegir filas), frente a la proyección que hace SELECT (elegir columnas). Una consulta con SELECT y WHERE hace las dos cosas: recorta la tabla a lo alto y a lo ancho.
- Dónde encaja
WHERE en el orden lógico de ejecución
WHERE en el orden lógico de ejecuciónYa puedes activar el paso 2 del diagrama de 02-01:
flowchart LR
A["1 · FROM<br/>productos<br/>20 filas"] --> B["2 · WHERE<br/>precio > 10<br/>7 filas"]
B --> C["3 · SELECT<br/>id, nombre, precio"]
C --> D["4 · ORDER BY<br/>(02-05)"]
D --> E["5 · LIMIT<br/>(02-06)"]
De ese orden se deducen tres consecuencias prácticas:
| Consecuencia | Explicación |
|---|---|
WHERE no puede usar alias del SELECT |
Cuando se evalúa, el SELECT aún no se ha ejecutado (lo viste en 02-02) |
WHERE sí puede usar columnas que no proyectas |
WHERE coste > 5 funciona aunque coste no aparezca en el SELECT: la columna existe en la fila que sale de FROM |
| Filtrar pronto es filtrar barato | Cuanto antes descartes filas, menos trabajo hay que hacer después. Es la base de la optimización del módulo 8 |
Ejemplo del segundo punto:
| id | nombre | precio |
|---|---|---|
| 6 | Crema facial de aloe vera 50 ml | 18.90 |
| 15 | Té verde matcha ceremonial 30 g | 22.00 |
| 20 | Cápsulas de espirulina 120 uds | 16.40 |
La columna coste no está en el resultado, pero sí participa en el filtro. Perfectamente válido.
- Comparar números
Los operadores de comparación que viste en 01-03, ahora aplicados de verdad:
| Operador | Significado | Ejemplo | Filas devueltas sobre productos |
|---|---|---|---|
= |
Igual | WHERE categoria_id = 2 |
4 |
<> o != |
Distinto | WHERE categoria_id <> 1 |
15 |
> |
Mayor | WHERE precio > 10 |
7 |
>= |
Mayor o igual | WHERE stock >= 100 |
8 |
< |
Menor | WHERE precio < 5 |
7 |
<= |
Menor o igual | WHERE stock <= 50 |
3 |
| id | nombre | categoria_id | precio | stock |
|---|---|---|---|---|
| 6 | Crema facial de aloe vera 50 ml | 2 | 18.90 | 60 |
| 7 | Champú sólido de romero 80 g | 2 | 8.40 | 95 |
| 8 | Aceite corporal de almendras 200 ml | 2 | 14.25 | 45 |
| 9 | Bálsamo labial de caléndula 15 ml | 2 | 4.60 | 130 |
Toda la categoría "Cosmética natural". Fíjate en que has tenido que escribir 2 y no 'Cosmética natural': en productos solo está el identificador. Traducir el 2 a su nombre exige la tabla categorias y por tanto un JOIN (módulo 3).
Un filtro operativo típico: productos agotados.
| id | nombre | stock | activo |
|---|---|---|---|
| 13 | Velas de cera de soja (pack 2) | 0 | true |
Solo el producto 13, tal como quedó fijado al diseñar el conjunto de datos.
Números sin comillas.
WHERE precio > '10'funciona en PostgreSQL porque el motor convierte la cadena, pero es una mala costumbre: en comparaciones más complejas puede impedir que se use un índice y, en otros motores, produce comparaciones alfabéticas donde'9' > '10'. EscribeWHERE precio > 10.
- Comparar texto
Las cadenas se comparan con los mismos operadores, entre comillas simples:
| id | nombre | apellidos | ciudad | pais |
|---|---|---|---|---|
| 7 | Sofia | Moreira Costa | Lisboa | Portugal |
| 8 | Tiago | Almeida Nunes | Oporto | Portugal |
| id | nombre | apellidos | ciudad |
|---|---|---|---|
| 9 | Camille | Dubois | Lyon |
| 10 | Julien | Moreau | París |
4.1. Las comparaciones de texto distinguen mayúsculas
Esta es la trampa número uno con datos de texto:
Devuelve 0 filas. No hay ningún error, no hay ningún aviso: simplemente no hay ningún cliente cuyo país sea exactamente la cadena 'españa' en minúsculas. Los 11 clientes españoles tienen 'España'.
| id | nombre | apellidos | ciudad |
|---|---|---|---|
| 1 | Lucía | Martínez Soler | Valencia |
| 2 | Carlos | Ferrer Ibáñez | Valencia |
| 3 | Marta | Sanchis Gil | Castellón |
| 4 | Javier | Ortega Ruiz | Madrid |
| 5 | Ana | Belmonte Roca | Barcelona |
| 6 | Pau | Llorens Vidal | Valencia |
| 11 | Elena | Navarro Puig | Alicante |
| 12 | Diego | Ramos Herrera | Sevilla |
| 13 | Núria | Bosch Ferrer | Barcelona |
| 14 | Hugo | Iglesias Pardo | Zaragoza |
| 15 | Inés | Carrasco Vega | Valencia |
11 filas, que con las 2 de Portugal y las 2 de Francia suman los 15 clientes.
Recuerda la distinción de 01-03: la sintaxis de SQL no distingue mayúsculas (select = SELECT), pero los datos sí. La forma robusta de comparar ignorando mayúsculas usa funciones de cadena o el operador ILIKE, y ambos son materia de los módulos 4 y 6:
-- Anticipos, no los desarrollamos aquí
WHERE LOWER(pais) = 'españa' -- función de cadena, lección 06-01
WHERE pais ILIKE 'españa' -- comparación insensible, lección 04-014.2. Los espacios cuentan (y son invisibles)
0 filas. Hay un espacio al final del literal y 'España' ≠ 'España '. Este error es especialmente traicionero porque no se ve al leer la consulta, y aparece constantemente cuando los datos llegan de un CSV o de un formulario web mal saneado.
Dos matices que conviene conocer:
- Los espacios iniciales también cuentan:
' España'tampoco coincide. - Con el tipo
CHAR(n)(de longitud fija) el estándar SQL rellena con espacios y los ignora al comparar, así que'España'y'España 'sí serían iguales. Es una de las razones por las que TiendaVerde usaVARCHARyTEXT, nuncaCHAR.
Si sospechas de espacios sobrantes, un truco rápido para verlos:
| pais_delimitado |
|---|
| [España] |
| [Portugal] |
| [Francia] |
Los corchetes delatarían cualquier espacio parásito. (DISTINCT es el tema de la lección siguiente; aquí solo sirve para no repetir quince veces los mismos tres países.) La limpieza definitiva de espacios se hace con TRIM, en el módulo 6.
4.3. Comparaciones de orden sobre texto
Los operadores < y > también funcionan con cadenas y usan el orden alfabético de la colación de la base de datos:
| id | nombre | apellidos |
|---|---|---|
| 8 | Tiago | Almeida Nunes |
| 5 | Ana | Belmonte Roca |
| 13 | Núria | Bosch Ferrer |
Los tres clientes cuyos apellidos empiezan por A o B. Es un filtro poco frecuente en la práctica —para buscar por prefijo se usa LIKE, en la lección 04-01—, pero conviene saber que existe. El detalle de cómo la colación afecta al orden se trata en la lección 02-05.
- Comparar fechas
Las fechas se escriben como literales de texto en formato ISO ('AAAA-MM-DD') y PostgreSQL las convierte al tipo DATE de la columna:
| id | cliente_id | fecha_pedido | estado |
|---|---|---|---|
| 17 | 7 | 2026-01-13 | enviado |
| 18 | 5 | 2026-01-27 | pagado |
| 19 | 6 | 2026-02-09 | pagado |
| 20 | 9 | 2026-02-21 | pendiente |
Los cuatro pedidos de 2026. Un rango se construye con dos comparaciones unidas por AND:
SELECT id, cliente_id, fecha_pedido, estado, gastos_envio
FROM pedidos
WHERE fecha_pedido >= '2025-06-01'
AND fecha_pedido < '2025-09-01';| id | cliente_id | fecha_pedido | estado | gastos_envio |
|---|---|---|---|---|
| 7 | 6 | 2025-06-11 | entregado | 6.50 |
| 8 | 7 | 2025-06-28 | entregado | 9.90 |
| 9 | 8 | 2025-07-15 | entregado | 9.90 |
| 10 | 9 | 2025-08-03 | entregado | 12.50 |
Los pedidos del verano de 2025: junio, julio y agosto.
Fíjate en el patrón >= inicio AND < fin, con el límite superior excluido. Es la forma profesional de escribir rangos de fechas, por dos motivos:
- No tienes que saber si el mes tiene 28, 30 o 31 días: pones el día 1 del mes siguiente.
- Si algún día esas columnas pasaran de
DATEaTIMESTAMP,<= '2025-08-31'dejaría fuera todo lo ocurrido ese día después de medianoche, mientras que< '2025-09-01'seguiría siendo correcto.
En el módulo 4 verás BETWEEN, que escribe rangos de forma más compacta pero incluye siempre los dos extremos, con esa misma trampa para las fechas con hora.
Errores típicos con fechas, ya avisados en 01-03:
| Escritura | Qué pasa |
|---|---|
'2026-01-01' |
Correcto, ISO 8601, sin ambigüedad |
'03/04/2026' |
Depende de la configuración DateStyle del servidor: puede ser el 3 de abril o el 4 de marzo... o fallar |
'2026-13-01' |
ERROR: date/time field value out of range |
2026-01-01 (sin comillas) |
Se interpreta como la resta 2026 - 1 - 1 = 2024 |
Esa última merece verse:
Menos mal que da error: si la columna fuera numérica, habrías filtrado por 2024 sin enterarte.
- Comparar booleanos
productos.activo y proveedores.activo son de tipo BOOLEAN. Una condición booleana ya es una condición: no hace falta compararla con nada.
-- Estas tres consultas son equivalentes
SELECT id, nombre FROM productos WHERE activo;
SELECT id, nombre FROM productos WHERE activo = TRUE;
SELECT id, nombre FROM productos WHERE activo IS TRUE;Las tres devuelven 19 filas: los 20 productos menos el 20, que está descatalogado.
Y para el caso contrario:
| id | nombre | precio | activo |
|---|---|---|---|
| 20 | Cápsulas de espirulina 120 uds | 16.40 | false |
| id | nombre | pais | activo |
|---|---|---|---|
| 5 | EcoNordic Supplies | Alemania | false |
¿Cuál escribir? Comparativa:
| Forma | Legibilidad | Comportamiento con NULL |
Recomendación |
|---|---|---|---|
WHERE activo |
Muy alta, se lee como inglés | La fila con NULL no pasa |
Preferida cuando la columna es NOT NULL |
WHERE activo = TRUE |
Alta, explícita | La fila con NULL no pasa |
Aceptable; útil si el lector no domina SQL |
WHERE activo IS TRUE |
Media | La fila con NULL no pasa (devuelve false, no NULL) |
Solo si la columna admite nulos y quieres una condición que nunca dé NULL |
WHERE NOT activo |
Alta | La fila con NULL no pasa |
Preferida para el caso negativo |
WHERE activo = FALSE |
Alta | Igual que la anterior | Aceptable |
En TiendaVerde las dos columnas activo son NOT NULL, así que las diferencias son teóricas. En bases de datos reales, una columna booleana que admita nulos tiene tres estados posibles (true, false, NULL) y ahí WHERE NOT activo y WHERE activo IS NOT TRUE dejan de ser lo mismo.
Nota de dialecto: MySQL y SQLite no tienen un tipo booleano real: guardan 1 y 0.
WHERE activofunciona igual porque cualquier valor distinto de 0 es verdadero, peroWHERE activo = TRUEallí significa literalmenteactivo = 1. SQL Server no admiteWHERE columnaa secas: exigeWHERE columna = 1sobre unBIT.
- Combinar condiciones:
AND, OR, NOT y los paréntesis
AND, OR, NOT y los paréntesis7.1. AND: se deben cumplir todas
| id | nombre | ciudad | pais |
|---|---|---|---|
| 1 | Lucía | Valencia | España |
| 2 | Carlos | Valencia | España |
| 6 | Pau | Valencia | España |
| 15 | Inés | Valencia | España |
Cuatro clientes valencianos. Cada AND adicional reduce o mantiene el número de filas, nunca lo aumenta.
7.2. OR: basta con una
SELECT id, cliente_id, fecha_pedido, estado
FROM pedidos
WHERE estado = 'pagado'
OR estado = 'pendiente';| id | cliente_id | fecha_pedido | estado |
|---|---|---|---|
| 18 | 5 | 2026-01-27 | pagado |
| 19 | 6 | 2026-02-09 | pagado |
| 20 | 9 | 2026-02-21 | pendiente |
Los tres pedidos aún no enviados. Cada OR adicional aumenta o mantiene el número de filas.
(En 04-02 escribirás esta misma condición como estado IN ('pagado', 'pendiente'), que es más corta y más legible cuando la lista crece.)
7.3. NOT: niega una condición
| id | cliente_id | fecha_pedido | estado |
|---|---|---|---|
| 6 | 5 | 2025-05-23 | cancelado |
| 16 | 4 | 2025-12-19 | enviado |
| 17 | 7 | 2026-01-13 | enviado |
| 18 | 5 | 2026-01-27 | pagado |
| 19 | 6 | 2026-02-09 | pagado |
| 20 | 9 | 2026-02-21 | pendiente |
6 filas, que con las 14 entregadas suman los 20 pedidos. WHERE estado <> 'entregado' da exactamente el mismo resultado y suele leerse mejor; NOT brilla cuando lo que niegas es una condición compuesta: WHERE NOT (a AND b).
7.4. Los paréntesis: el error que no da error
AND tiene más prioridad que OR. Ya lo viste en la tabla de precedencia de 01-03; ahora vas a ver el daño que causa con datos reales.
La pregunta de negocio: "dame los productos de las categorías 1 (Alimentación) o 2 (Cosmética natural) que cuesten más de 10 €".
Escrita ingenuamente, sin paréntesis:
-- ⚠️ INCORRECTA
SELECT id, nombre, categoria_id, precio
FROM productos
WHERE categoria_id = 1
OR categoria_id = 2 AND precio > 10;| id | nombre | categoria_id | precio |
|---|---|---|---|
| 1 | Aceite de oliva virgen extra 500 ml | 1 | 12.50 |
| 2 | Arroz integral ecológico 1 kg | 1 | 3.90 |
| 3 | Miel de azahar cruda 500 g | 1 | 9.75 |
| 4 | Pasta de espelta 500 g | 1 | 2.80 |
| 5 | Tomate triturado ecológico 400 g | 1 | 1.95 |
| 6 | Crema facial de aloe vera 50 ml | 2 | 18.90 |
| 8 | Aceite corporal de almendras 200 ml | 2 | 14.25 |
7 filas, y cuatro de ellas cuestan menos de 3 €. El motor ha leído:
Es decir: toda la categoría 1, sin mirar el precio, más los productos caros de la categoría 2.
Con paréntesis:
-- ✅ CORRECTA
SELECT id, nombre, categoria_id, precio
FROM productos
WHERE (categoria_id = 1 OR categoria_id = 2)
AND precio > 10;| id | nombre | categoria_id | precio |
|---|---|---|---|
| 1 | Aceite de oliva virgen extra 500 ml | 1 | 12.50 |
| 6 | Crema facial de aloe vera 50 ml | 2 | 18.90 |
| 8 | Aceite corporal de almendras 200 ml | 2 | 14.25 |
3 filas. Esta es la respuesta correcta.
Compara el impacto:
| Versión | Filas | ¿Responde a la pregunta? |
|---|---|---|
| Sin paréntesis | 7 | No: incluye 4 productos baratos |
| Con paréntesis | 3 | Sí |
Ninguna de las dos da error. La primera devuelve un informe con datos incorrectos que nadie detecta hasta que alguien pregunta por qué la pasta de espelta de 2.80 € aparece en el listado de productos de más de 10 €.
Regla del curso: siempre que en un
WHEREconvivanANDyOR, pon paréntesis. Aunque conozcas la precedencia de memoria, el paréntesis documenta tu intención para quien lea la consulta dentro de seis meses (que probablemente serás tú).
7.5. Un filtro de tres condiciones
SELECT id, nombre, categoria_id, precio, stock
FROM productos
WHERE activo
AND precio < 10
AND stock > 100;| id | nombre | categoria_id | precio | stock |
|---|---|---|---|---|
| 2 | Arroz integral ecológico 1 kg | 1 | 3.90 | 200 |
| 4 | Pasta de espelta 500 g | 1 | 2.80 | 150 |
| 5 | Tomate triturado ecológico 400 g | 1 | 1.95 | 300 |
| 9 | Bálsamo labial de caléndula 15 ml | 2 | 4.60 | 130 |
| 11 | Estropajo vegetal de luffa (pack 3) | 3 | 5.50 | 110 |
| 14 | Infusión de manzanilla ecológica 20 uds | 4 | 3.25 | 180 |
| 18 | Cepillo de dientes de bambú | 5 | 3.50 | 240 |
Siete productos baratos, activos y con stock de sobra: candidatos perfectos para una promoción.
- Expresiones calculadas dentro de
WHERE
WHEREPuedes usar en WHERE las mismas expresiones que aprendiste en 02-02, escribiéndolas completas (recuerda: el alias no existe todavía):
| id | nombre | precio | coste | margen |
|---|---|---|---|---|
| 6 | Crema facial de aloe vera 50 ml | 18.90 | 9.50 | 9.40 |
| 8 | Aceite corporal de almendras 200 ml | 14.25 | 7.10 | 7.15 |
| 10 | Detergente ecológico concentrado 1 L | 11.20 | 6.00 | 5.20 |
| 12 | Bolsas reutilizables de algodón (pack 5) | 9.90 | 4.30 | 5.60 |
| 13 | Velas de cera de soja (pack 2) | 13.75 | 6.90 | 6.85 |
| 15 | Té verde matcha ceremonial 30 g | 22.00 | 12.50 | 9.50 |
| 20 | Cápsulas de espirulina 120 uds | 16.40 | 8.70 | 7.70 |
Los siete productos que dejan más de 5 € de margen por unidad.
Y sobre lineas_pedido, las ventas de más de 30 €:
SELECT id,
pedido_id,
producto_id,
cantidad,
ROUND(cantidad * precio_unitario * (1 - descuento), 2) AS importe
FROM lineas_pedido
WHERE cantidad * precio_unitario * (1 - descuento) > 30;| id | pedido_id | producto_id | cantidad | importe |
|---|---|---|---|---|
| 18 | 8 | 1 | 3 | 35.63 |
| 24 | 10 | 6 | 2 | 34.02 |
| 28 | 12 | 15 | 2 | 44.00 |
Solo tres líneas de las 47 superan los 30 €. Nótese la duplicación de la expresión entre SELECT y WHERE: es fea pero necesaria. Las formas de evitarla —subconsultas o CTE— llegan en los módulos 7 y 10.
Aviso de rendimiento: un filtro como
WHERE precio - coste > 5aplica una operación a cada fila y por tanto no puede aprovechar un índice normal sobreprecioni sobrecoste. Con 20 filas da igual; con 20 millones, no. La solución (índices sobre expresiones) es materia del módulo 8.
- El síntoma de los nulos
Diez de los veinte pedidos de TiendaVerde entraron por la web y no tienen comercial asignado: su empleado_id es NULL. Vamos a ver qué ocurre al filtrar por esa columna.
Intento 1: buscar los pedidos sin comercial.
Cero filas, cuando sabemos que hay diez. Sin error, sin aviso.
Intento 2: los pedidos que NO gestionó el comercial 4.
El comercial 4 (Óscar Peris) gestionó los pedidos 2, 6, 10 y 16. Cabría esperar que los "no suyos" fueran 20 − 4 = 16.
| id | cliente_id | empleado_id |
|---|---|---|
| 4 | 4 | 5 |
| 8 | 7 | 5 |
| 12 | 10 | 5 |
| 14 | 12 | 6 |
| 18 | 5 | 5 |
| 20 | 9 | 6 |
6 filas, no 16. Han desaparecido los diez pedidos con empleado_id nulo.
Por qué ocurre. Recuerda la regla de la sección 1: WHERE conserva las filas cuya condición es verdadera. Y cualquier comparación con NULL no da verdadero ni falso: da desconocido.
| Fila | Condición | Se evalúa a | ¿Pasa el filtro? |
|---|---|---|---|
Pedido 2 (empleado_id = 4) |
4 <> 4 |
false |
No |
Pedido 4 (empleado_id = 5) |
5 <> 4 |
true |
Sí |
Pedido 1 (empleado_id = NULL) |
NULL <> 4 |
NULL (desconocido) |
No |
"No sé quién gestionó el pedido 1" no permite concluir que fuera distinto del comercial 4. SQL, con buen criterio lógico, prefiere no afirmarlo, y la fila se cae del resultado.
Cómo reconocer el síntoma. Sospecha de los nulos cuando:
- Una consulta devuelve menos filas de las que esperabas y la columna del filtro puede ser nula.
- Un filtro y su negación no suman el total: aquí
empleado_id = 4da 4 filas yempleado_id <> 4da 6; 4 + 6 = 10, no 20. Las diez que faltan son las nulas.
Esta comprobación —"¿el filtro y su contrario suman el total?"— es una de las mejores costumbres que puedes adquirir.
Cómo se arregla. Con el operador IS NULL / IS NOT NULL, que ya asomó en 01-03:
Devuelve las 10 filas esperadas: los pedidos 1, 3, 5, 7, 9, 11, 13, 15, 17 y 19.
No lo desarrollamos más aquí: la lógica de tres valores, IS DISTINCT FROM, el comportamiento de los nulos dentro de AND/OR y las funciones para tratarlos son el contenido completo de la lección 04-03. Por ahora quédate con dos cosas:
= NULLy<> NULLnunca devuelven filas. Si las escribes, has cometido un error.- Si una columna del
WHEREadmite nulos, pregúntate qué quieres que ocurra con esas filas antes de dar la consulta por buena.
Errores Comunes y Consejos
- Usar un alias del
SELECTen elWHERE.column "..." does not exist. Repite la expresión completa:WHEREse ejecuta antes queSELECT. - Mezclar
ANDyORsin paréntesis. No da error; da un resultado incorrecto. Es el error más caro de esta lección. - Comparar texto sin respetar mayúsculas o con espacios sobrantes. Cero filas y ninguna pista. Comprueba los valores reales antes de acusar a la consulta.
- Escribir fechas en formato local.
'01/03/2026'es ambiguo;'2026-03-01'no lo es nunca. - Escribir fechas sin comillas.
WHERE fecha_pedido >= 2026-01-01compara con el entero 2024. - Usar
<=para el final de un rango de fechas. Correcto hoy conDATE, peligroso el día que la columna seaTIMESTAMP. Usa>= inicio AND < fin. - Comparar con
= NULL. Siempre cero filas.IS NULL(lección 04-03). - Olvidar que un filtro con
<>excluye los nulos. Comprueba que el filtro y su negación sumen el total de filas. - Consejo: construye el
WHEREpor capas. Empieza con una condición, mira cuántas filas salen, añade la siguiente. Si un salto no cuadra, ya sabes qué condición revisar. - Consejo: cuenta siempre las filas. El
(N filas)depsqles tu mejor detector de errores lógicos. - Consejo: escribe una condición por línea, con
AND/ORal principio de la línea e indentados bajoWHERE. Añadir, quitar o comentar una condición se vuelve trivial.
Ejercicios
Ejercicio 1
El equipo de compras quiere revisar el catálogo de proveedores caros. Escribe una consulta que devuelva id, nombre, precio y stock de los productos activos cuyo precio supere los 9 € y cuyo stock sea inferior a 100 unidades. ¿Cuántas filas esperas y cuántas salen?
Ejercicio 2
Dirección pide "los pedidos de 2025 que estén entregados o cancelados y cuyos gastos de envío superen los 5 €". Escribe la consulta correcta, y escribe también la versión sin paréntesis explicando cuántas filas de más devolvería y por qué.
Ejercicio 3
Sobre empleados, escribe una consulta que devuelva los empleados cuyo jefe no sea la empleada 1 (Rosa Alcázar Vives), mostrando id, nombre, apellidos y jefe_id. Después comprueba si el número de filas encaja con el total de la tabla y explica qué ha pasado.
Soluciones
Solución 1
| id | nombre | precio | stock |
|---|---|---|---|
| 3 | Miel de azahar cruda 500 g | 9.75 | 80 |
| 6 | Crema facial de aloe vera 50 ml | 18.90 | 60 |
| 8 | Aceite corporal de almendras 200 ml | 14.25 | 45 |
| 10 | Detergente ecológico concentrado 1 L | 11.20 | 70 |
| 12 | Bolsas reutilizables de algodón (pack 5) | 9.90 | 85 |
| 13 | Velas de cera de soja (pack 2) | 13.75 | 0 |
| 15 | Té verde matcha ceremonial 30 g | 22.00 | 40 |
7 filas. El razonamiento, condición a condición, sobre los nueve productos que cuestan más de 9 €:
| id | precio > 9 | stock < 100 | activo | ¿Pasa? |
|---|---|---|---|---|
| 1 | 12.50 ✓ | 120 ✗ | ✓ | No |
| 3 | 9.75 ✓ | 80 ✓ | ✓ | Sí |
| 6 | 18.90 ✓ | 60 ✓ | ✓ | Sí |
| 8 | 14.25 ✓ | 45 ✓ | ✓ | Sí |
| 10 | 11.20 ✓ | 70 ✓ | ✓ | Sí |
| 12 | 9.90 ✓ | 85 ✓ | ✓ | Sí |
| 13 | 13.75 ✓ | 0 ✓ | ✓ | Sí |
| 15 | 22.00 ✓ | 40 ✓ | ✓ | Sí |
| 20 | 16.40 ✓ | 55 ✓ | ✗ | No |
La lección del ejercicio es doble. Primero, el producto 13 cuela porque "poco stock" incluye "ningún stock": tiene 0 unidades y aun así cumple stock < 100. Si lo que buscas son productos reponibles, el filtro correcto sería stock > 0 AND stock < 100. Segundo, el producto 20 queda fuera solo gracias a la condición activo: cumple las otras dos, y si hubieras olvidado ese filtro habrías propuesto reponer un artículo descatalogado.
Fíjate también en que el producto 3 entra con 9.75 €: precio > 9 es estricto, pero 9.75 es mayor que 9. Si hubieras querido "a partir de 10 €" tendrías que haber escrito precio >= 10, y entonces el 3 y el 12 se caerían.
Solución 2
Versión correcta:
SELECT id,
cliente_id,
fecha_pedido,
estado,
gastos_envio
FROM pedidos
WHERE fecha_pedido >= '2025-01-01'
AND fecha_pedido < '2026-01-01'
AND (estado = 'entregado' OR estado = 'cancelado')
AND gastos_envio > 5;| id | cliente_id | fecha_pedido | estado | gastos_envio |
|---|---|---|---|---|
| 7 | 6 | 2025-06-11 | entregado | 6.50 |
| 8 | 7 | 2025-06-28 | entregado | 9.90 |
| 9 | 8 | 2025-07-15 | entregado | 9.90 |
| 10 | 9 | 2025-08-03 | entregado | 12.50 |
| 12 | 10 | 2025-10-01 | entregado | 12.50 |
5 filas. El pedido 6 (cancelado, 4.95 € de portes) no llega a 5 €, y los pedidos con portes de 4.95 € o gratuitos quedan fuera.
Versión sin paréntesis:
-- ⚠️ INCORRECTA
WHERE fecha_pedido >= '2025-01-01'
AND fecha_pedido < '2026-01-01'
AND estado = 'entregado' OR estado = 'cancelado'
AND gastos_envio > 5;Como AND se evalúa antes que OR, el motor la agrupa así:
(fecha >= '2025-01-01' AND fecha < '2026-01-01' AND estado = 'entregado') OR (estado = 'cancelado' AND gastos_envio > 5)
Es decir: todos los pedidos entregados de 2025 sin mirar los portes (14 de los 14 entregados están en 2025), más los cancelados con portes superiores a 5 € (ninguno, porque el único cancelado tiene 4.95 €). Total: 14 filas en lugar de 5. Nueve filas de más, todas ellas con gastos de envío que no cumplen el requisito. Y, de nuevo, sin ningún mensaje de error.
Solución 3
| id | nombre | apellidos | jefe_id |
|---|---|---|---|
| 4 | Óscar | Peris Blasco | 2 |
| 5 | Laia | Puig Sanchis | 2 |
| 6 | Marc | Estévez Roig | 2 |
| 7 | Irene | Salvador Mira | 3 |
4 filas. Comprobemos el encaje: la tabla tiene 8 empleados; con jefe_id = 1 hay 3 (Andrés, Beatriz y Daniel); con jefe_id <> 1 hay 4. Y 3 + 4 = 7, no 8.
Falta una fila: Rosa Alcázar Vives, la directora general, cuyo jefe_id es NULL. Su condición NULL <> 1 se evalúa a desconocido, no a verdadero, así que WHERE la descarta. Es exactamente el síntoma de la sección 9.
Si lo que querías era "todo el mundo salvo quienes dependen directamente de Rosa", incluida la propia Rosa, la consulta correcta usa IS NULL (lección 04-03):
Y esa sí devuelve 5 filas: los cuatro anteriores más Rosa. La prueba de que la lógica cierra: 3 (dependen de Rosa) + 5 (el resto) = 8 empleados.
Conclusión
Ya sabes hacer preguntas concretas a TiendaVerde:
WHEREfiltra fila a fila y conserva solo aquellas cuya condición es verdadera; las falsas y las desconocidas se descartan por igual.- Ocupa el paso 2 del orden lógico, antes de
SELECT: puede usar cualquier columna de la tabla, pero nunca un alias de la proyección. - Comparas números sin comillas, texto entre comillas simples y respetando mayúsculas y espacios, y fechas en formato ISO con el patrón
>= inicio AND < fin. - Con booleanos basta
WHERE activoyWHERE NOT activo; comparar con= TRUEes correcto pero redundante. - Combinas condiciones con
AND,ORyNOT, y sabes que mezclarANDyORsin paréntesis produce resultados incorrectos sin dar ningún error. - Puedes filtrar por expresiones calculadas, repitiéndolas enteras porque el alias aún no existe.
- Reconoces el síntoma de los nulos: menos filas de las esperadas, y un filtro y su negación que no suman el total. La cura,
IS NULL, llega en la lección 04-03.
En la siguiente lección, DISTINCT y eliminación de duplicados, atacarás un problema distinto: cuando proyectas solo unas pocas columnas, aparecen filas repetidas que en la tabla original no lo eran. Verás cómo eliminarlas, por qué DISTINCT actúa sobre la combinación completa de columnas del SELECT (y no sobre la primera, como mucha gente cree) y dónde se sitúa exactamente en el orden lógico de ejecución.
Curso de SQL
Módulo 1: Introducción a SQL
- ¿Qué es SQL?
- Configurando tu entorno SQL
- Sintaxis básica de SQL
- Entendiendo bases de datos y tablas
- El modelo relacional: claves primarias y foráneas
- La base de datos del curso: TiendaVerde
Módulo 2: Consultas básicas de SQL
- Instrucción SELECT
- Alias, expresiones y columnas calculadas
- Filtrando datos con WHERE
- DISTINCT y eliminación de duplicados
- Ordenando datos con ORDER BY
- Limitando resultados con LIMIT
Módulo 3: Trabajando con múltiples tablas
- Operaciones JOIN
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN y CROSS JOIN
- Uniones de conjuntos: UNION, INTERSECT y EXCEPT
Módulo 4: Filtrado avanzado de datos
- Usando LIKE para coincidencia de patrones
- Operadores IN y BETWEEN
- Valores NULL y IS NULL
- Funciones de agregación: COUNT, SUM, AVG, MIN y MAX
- Agregando datos con GROUP BY
- Cláusula HAVING
Módulo 5: Manipulación de datos
- Creando tablas y restricciones con CREATE TABLE
- Instrucción INSERT
- Instrucción UPDATE
- Instrucción DELETE
- Instrucción UPSERT (MERGE)
- Modificando el esquema: ALTER TABLE y migraciones seguras
Módulo 6: Funciones avanzadas de SQL
- Funciones de cadena
- Funciones numéricas
- Funciones de fecha y hora
- Conversión de tipos y manejo de NULL: CAST y COALESCE
- Expresiones condicionales
Módulo 7: Subconsultas y consultas anidadas
- Introducción a subconsultas
- Subconsultas correlacionadas
- EXISTS y NOT EXISTS
- Usando subconsultas en cláusulas SELECT, FROM y WHERE
- Subconsultas o JOIN: cuál elegir
Módulo 8: Índices y optimización de rendimiento
- Entendiendo los índices
- Creación y gestión de índices
- Tipos de índice y cuándo no indexar
- Técnicas de optimización de consultas
- Análisis del rendimiento de consultas
Módulo 9: Transacciones y concurrencia
- Introducción a las transacciones
- Propiedades ACID
- Instrucciones de control de transacciones
- Niveles de aislamiento y anomalías de concurrencia
- Manejo de concurrencia: bloqueos e interbloqueos
Módulo 10: Temas avanzados
- Vistas
- Expresiones de tabla comunes (CTE)
- Funciones de ventana
- Procedimientos almacenados
- Triggers
- JSON y datos semiestructurados
Módulo 11: SQL en la práctica
- Casos de uso en el mundo real
- Mejores prácticas
- Seguridad: inyección SQL, permisos y roles
- SQL para análisis de datos
- SQL en desarrollo web
