Curso SQL · Northwind
Tema 6 de 60
Bloque 2 · La consulta PostgreSQL 17 northwind

WHERE y los operadores

1. Qué es

WHERE descarta filas. Se ejecuta fila por fila, una a una, y hace una sola pregunta: ¿esta condición es verdadera para esta fila? Si la respuesta es sí, la fila sigue. Si es no —o si es desconocida, que es un caso aparte y tiene su propio tema— la fila desaparece.

Es el paso 2 del orden-logico-de-ejecucion y de lejos el más importante para el rendimiento: todo lo que descartes aquí es trabajo que no se hace después.

2. Sintaxis

SELECT columnas
FROM tabla
WHERE condición;

Operadores de comparación

Operador Significado
= Igual
<> o != Distinto
< > <= >= Menor, mayor, menor o igual, mayor o igual

<> es el del estándar SQL; != es un sinónimo que PostgreSQL acepta. Usa <>: funciona en todos los motores.

Operadores lógicos

Operador Significado Precedencia
NOT Niega 1ª (la más alta)
AND Ambas
OR Alguna 3ª (la más baja)

AND se evalúa antes que OR. Esa línea es la causa de más informes silenciosamente equivocados que ninguna otra en SQL. Se ve en § 3.

Atajos

Forma Equivale a
x IN (a, b, c) x = a OR x = b OR x = c
x BETWEEN a AND b x >= a AND x <= b
x NOT IN (...), x NOT BETWEEN ... Lo contrario

BETWEEN es inclusivo por los dos extremos. No es opinable, es la definición.

3. Ejemplos sobre Northwind

Comparación simple:

SELECT product_name, unit_price
FROM products
WHERE unit_price > 50
ORDER BY unit_price DESC;
      product_name       | unit_price
-------------------------+------------
 Côte de Blaye           |     263.50
 Thüringer Rostbratwurst |     123.79
 Mishi Kobe Niku         |      97.00
 Sir Rodney's Marmalade  |      81.00
 Carnarvon Tigers        |      62.50
 Raclette Courdavault    |      55.00
 Manjimup Dried Apples   |      53.00

7 productos de 77. El WHERE acaba de ahorrar el 91% del trabajo.

La trampa de la precedencia

Esta consulta parece decir "clientes de Alemania o de Francia, que estén en París":

SELECT count(*) FROM customers
WHERE country = 'Germany' OR country = 'France' AND city = 'Paris';
 count
-------
    13

Y esta, con paréntesis, dice lo mismo en español:

SELECT count(*) FROM customers
WHERE (country = 'Germany' OR country = 'France') AND city = 'Paris';
 count
-------
     2

13 contra 2. ¿Qué pasó? Como AND va primero, la primera consulta se lee en realidad:

WHERE country = 'Germany' OR (country = 'France' AND city = 'Paris')

Es decir: todos los alemanes (11) más los franceses de París (2) = 13. Ninguna de las dos consultas da error. Las dos devuelven un número creíble. Solo una responde la pregunta.

Regla sin excepción: si en un WHERE conviven AND y OR, pon paréntesis. Aunque sepas la precedencia. Los paréntesis no cuestan nada y quitan la duda al que lo lea, que puede ser tú dentro de un mes.

IN — la forma legible del OR

SELECT count(*) FROM customers
WHERE country IN ('Germany', 'France', 'Spain');
 count
-------
    27

Equivale a tres OR encadenados, pero no puede sufrir el problema de precedencia: IN es un solo operador, así que no hay nada que agrupar mal.

BETWEEN — y su orden obligatorio

SELECT count(*) FROM products WHERE unit_price BETWEEN 20 AND 50;   -- 31
SELECT count(*) FROM products WHERE unit_price >= 20 AND unit_price <= 50;  -- 31

Idénticas. Pero:

SELECT count(*) FROM products WHERE unit_price BETWEEN 50 AND 20;
 count
-------
     0

Cero, y sin ningún aviso. BETWEEN 50 AND 20 se traduce a >= 50 AND <= 20, que no puede ser cierto nunca. El menor va primero, siempre.

Booleanos: sin = true

discontinued es de tipo boolean, así que ya es una condición:

SELECT product_name, discontinued FROM products WHERE discontinued;
      product_name      | discontinued
------------------------+--------------
 Chai                   | t
 Chang                  | t
 Chef Anton's Gumbo Mix | t

10 productos descatalogados. Y el contrario:

SELECT count(*) FROM products WHERE NOT discontinued;   -- 67

WHERE discontinued = true funciona, pero es como decir "¿es verdad que es verdad?". WHERE discontinued y WHERE NOT discontinued es como se escribe.

El texto distingue mayúsculas

SELECT count(*) FROM customers WHERE country = 'germany';
 count
-------
     0

Once clientes alemanes, y la consulta devuelve cero. En PostgreSQL la comparación de texto es sensible a mayúsculas y a acentos. Las formas de no depender de eso están en buscar-texto-like-y-regex.

El aviso que hay que dar ya

SELECT count(*) FROM customers WHERE region = 'SP';    --  6
SELECT count(*) FROM customers WHERE region <> 'SP';   -- 25

6 + 25 = 31, y la tabla tiene 91 clientes. Faltan 60.

No es un error del motor: 60 clientes tienen region en NULL, y NULL no es igual ni distinto a nada. Ni entra en el =, ni entra en el <>. Es el tema siguiente y hay que leerlo entero: null-y-la-logica-de-tres-valores.

4. Errores comunes

  1. Mezclar AND y OR sin paréntesis. Ver arriba. No da error; da un número equivocado.
  2. BETWEEN con los extremos al revés. Devuelve 0 filas sin avisar.
  3. BETWEEN con fechas que llevan hora. order_date en Northwind es date, así que BETWEEN '1997-01-01' AND '1997-12-31' está bien (408 pedidos). Si la columna fuera timestamp, ese mismo BETWEEN perdería todo el 31 de diciembre a partir de las 00:00:01, porque '1997-12-31' significa 1997-12-31 00:00:00. La forma a prueba de balas es >= '1997-01-01' AND < '1998-01-01'.
  4. Comillas dobles en un valor de texto. WHERE country = "Germany" busca una columna llamada Germany y da column "Germany" does not exist. Simples para texto, dobles para identificadores.
  5. Comparar texto sin cuidar mayúsculas ni acentos. 'méxico', 'Mexico' y 'México' son tres valores distintos.
  6. Restar todo con <> y esperar el complemento. No lo es, por los NULL.

5. Diferencias con SQL Server

PostgreSQL SQL Server
Sensibilidad a mayúsculas Siempre sensible Depende del collation de la base — por defecto suele ser insensible
Tipo booleano boolean nativo, WHERE activo No hay boolean; se usa bit y hay que escribir WHERE activo = 1
Distinto <> o != <> o !=

La primera fila es la diferencia que más sorprende al venir de SQL Server: allí WHERE country = 'germany' normalmente sí devuelve los alemanes. En PostgreSQL nunca. No es que un motor esté mal: es que SQL Server esconde la decisión en una propiedad de la base y PostgreSQL te obliga a tomarla en la consulta.

6. Ejercicios

  1. Productos con precio entre 10 y 20, ambos incluidos, ordenados de más caro a más barato.
  2. Clientes de Alemania, Austria o Suiza (Switzerland), usando un solo operador.
  3. Pedidos de 1997 con un flete superior a 100. ¿Cuántos son?
  4. Productos no descatalogados con menos de 10 unidades en stock. Son los que hay que reponer.
  5. Sin ejecutarla, di qué devuelve y por qué: sql SELECT count(*) FROM products WHERE units_in_stock BETWEEN 100 AND 10;

7. Pregunta de negocio

Compras pide "el listado de productos en riesgo": los que se están quedando sin stock y todavía se venden. Ellos consideran riesgo si quedan menos unidades de las que marca el punto de reposición, o si hay pedidos pendientes de recibir.

La trampa está en el "o". Léela dos veces antes de escribir.

SolucionesResuélvelos antes de abrir

1.

SELECT product_name, unit_price
FROM products
WHERE unit_price BETWEEN 10 AND 20
ORDER BY unit_price DESC;

29 productos. El más caro es Maxilaku, justo en el límite de 20.00 — y ahí se ve que BETWEEN incluye el extremo. Abajo empatan tres a 10.00: Longlife Tofu, Sir Rodney's Scones y Aniseed Syrup.

2.

SELECT company_name, country
FROM customers
WHERE country IN ('Germany', 'Austria', 'Switzerland');

15 clientes — 11 alemanes, 2 austriacos, 2 suizos. Con OR daría lo mismo, pero IN es la respuesta que pedía el enunciado y la que no se puede romper por precedencia.

3.

SELECT count(*)
FROM orders
WHERE order_date >= '1997-01-01'
  AND order_date <  '1998-01-01'
  AND freight > 100;
 count
-------
    94

Escrito con >= y < a propósito: es la forma que seguiría siendo correcta si mañana order_date pasara a guardar la hora.

4.

SELECT product_name, units_in_stock, units_on_order
FROM products
WHERE NOT discontinued
  AND units_in_stock < 10
ORDER BY units_in_stock;
        product_name        | units_in_stock | units_on_order
----------------------------+----------------+----------------
 Gorgonzola Telino          |              0 |             70
 Sir Rodney's Scones        |              3 |             40
 Louisiana Hot Spiced Okra  |              4 |            100
 Longlife Tofu              |              4 |             20
 Rogede sild                |              5 |             70
 Northwoods Cranberry Sauce |              6 |              0
 Scottish Longbreads        |              6 |             10
 Mascarpone Fabioli         |              9 |             40

8 productos. Fíjate en que el NOT discontinued importa: sin él entrarían descatalogados sin stock, que no hay que reponer. Y fíjate también en units_on_order: casi todos ya tienen reposición en camino. El único que no la tiene —Northwoods Cranberry Sauce— es el que hay que mirar hoy.

5. Devuelve 0. Los extremos están al revés: >= 100 AND <= 10 es imposible. Lo peligroso no es el cero, es que no hay error: un informe con este BETWEEN sale en blanco y parece que no hay datos.

Pregunta de negocio.

La lectura literal es "en riesgo = por debajo del punto de reposición o con pedidos en camino", y esa es una condición con AND y OR conviviendo. Sin paréntesis:

-- MAL
SELECT product_name FROM products
WHERE NOT discontinued AND units_in_stock < reorder_level OR units_on_order > 0;

Devuelve 18 productos, e incluye descatalogados — porque AND se agrupó primero y el OR units_on_order > 0 se aplicó por su cuenta, saltándose el NOT discontinued.

Con paréntesis:

SELECT product_name, units_in_stock, reorder_level, units_on_order
FROM products
WHERE NOT discontinued
  AND (units_in_stock < reorder_level OR units_on_order > 0)
ORDER BY units_in_stock;
       product_name        | units_in_stock | reorder_level | units_on_order
---------------------------+----------------+---------------+----------------
 Gorgonzola Telino         |              0 |            20 |             70
 Sir Rodney's Scones       |              3 |             5 |             40
 Longlife Tofu             |              4 |             5 |             20
 Louisiana Hot Spiced Okra |              4 |            20 |            100
 Rogede sild               |              5 |            15 |             70

Devuelve 17. La diferencia es un solo producto: Chang, que está descatalogado (discontinued) y tiene 40 unidades en camino. Sin paréntesis se colaba en la lista de "productos que hay que reponer" un producto que la empresa ya decidió dejar de vender.

Un solo producto de diferencia es justo lo que hace peligroso este error. Si fueran 200 filas de más, alguien lo notaría. Una sola pasa desapercibida hasta que Compras hace un pedido de algo que no se vende.

Y la parte que no es SQL: el "o" del enunciado probablemente no es lo que Compras quiere. Tener pedidos en camino no es riesgo — es lo contrario, es que alguien ya reaccionó. Lo más útil que puedes hacer con esta petición es devolver las 21 filas y preguntar si units_on_order > 0 no debería ser al revés. Esa pregunta vale más que la consulta.