Hi ha dues maneres d'escriure el mateix filtre: una que s'entén d'un cop d'ull i una altra que cal desxifrar. IN i BETWEEN existeixen perquè puguis triar la primera. IN substitueix una cadena d'OR per una llista; BETWEEN substitueix dues comparacions per un rang. Cap dels dos no afegeix capacitat d'expressió —tot el que fan es podria escriure sense ells—, però tots dos redueixen el soroll i, amb això, la probabilitat d'equivocar-se.

Dit això, aquesta lliçó té dos avisos que valen més que la sintaxi. El primer és l'error més car de tot SQL: NOT IN amb una llista que conté un NULL no retorna "la resta de les files", retorna zero files, sense error i sense avís. El segon és més subtil però igual de freqüent: BETWEEN inclou tots dos extrems, i això converteix qualsevol rang de dates sobre una columna amb hora en una font silenciosa de dades perdudes.

Contingut

  1. IN: l'alternativa llegible a una cadena d'OR
  2. NOT IN i el seu comportament traïdor amb NULL
  3. IN amb números, text i dates
  4. BETWEEN: sucre sintàctic per a un rang tancat
  5. NOT BETWEEN
  6. El parany de BETWEEN amb dates i hores
  7. BETWEEN amb text i la colació
  8. BETWEEN SYMMETRIC
  9. IN, OR i BETWEEN: rendiment i llegibilitat
  10. Errors habituals i consells
  11. Exercicis
  12. Conclusió

  1. IN: l'alternativa llegible a una cadena d'OR

A 02-03 vas escriure això per localitzar les comandes que encara no s'havien enviat:

SELECT id, client_id, data_comanda, estat
FROM comandes
WHERE estat = 'pagat'
   OR estat = 'pendent'
ORDER BY id;

Amb dos valors és llegible. Amb cinc, deixa de ser-ho, i a més has de recordar els parèntesis tan bon punt aparegui un AND (el parany de precedència de 02-03). IN resol totes dues coses:

SELECT id,
       client_id,
       data_comanda,
       estat,
       metode_pagament
FROM comandes
WHERE estat IN ('pendent', 'pagat', 'enviat')
ORDER BY id;
id client_id data_comanda estat metode_pagament
16 4 2025-12-19 enviat targeta
17 7 2026-01-13 enviat paypal
18 5 2026-01-27 pagat transferencia
19 6 2026-02-09 pagat targeta
20 9 2026-02-21 pendent contrareemborsament

5 files: les comandes "vives" de BotigaVerda, les que encara tenen feina pendent al darrere. Les 14 lliurades i la cancel·lada en queden fora.

L'equivalència, demostrada

x IN (a, b, c) és exactament x = a OR x = b OR x = c. No és una aproximació: és la definició de l'operador a l'estàndard, i PostgreSQL la reescriu internament així.

-- Aquestes dues consultes són la mateixa
WHERE estat IN ('pendent', 'pagat', 'enviat')
WHERE estat = 'pendent' OR estat = 'pagat' OR estat = 'enviat'

D'aquella equivalència en surten tres propietats que convé tenir presents:

Propietat Conseqüència
L'ordre de la llista no importa IN ('a','b') i IN ('b','a') són idèntics
Els valors repetits no molesten IN ('a','a','b') dona el mateix que IN ('a','b')
IN () amb llista buida no és vàlid PostgreSQL dona error de sintaxi. Compte en generar la llista des de codi

Aquest últim punt és un clàssic de les aplicacions: si construeixes la consulta concatenant els ids seleccionats per l'usuari i l'usuari no en selecciona cap, generes IN () i la consulta peta. La solució habitual és no generar la condició quan la llista és buida, o fer servir IN (NULL)… que, com veuràs a la secció següent, té les seves pròpies sorpreses.

Un segon exemple: productes de diverses categories

SELECT id,
       nom,
       categoria_id,
       preu
FROM productes
WHERE categoria_id IN (1, 2, 4)
ORDER BY categoria_id, id;
id nom categoria_id preu
1 Oli d'oliva verge extra 500 ml 1 12.50
2 Arròs integral ecològic 1 kg 1 3.90
3 Mel de tarongina crua 500 g 1 9.75
4 Pasta d'espelta 500 g 1 2.80
5 Tomàquet triturat ecològic 400 g 1 1.95
6 Crema facial d'àloe vera 50 ml 2 18.90
7 Xampú sòlid de romaní 80 g 2 8.40
8 Oli corporal d'ametlles 200 ml 2 14.25
9 Bàlsam labial de calèndula 15 ml 2 4.60
14 Infusió de camamilla ecològica 20 u 4 3.25
15 Te verd matcha cerimonial 30 g 4 22.00
16 Kombutxa de gingebre 750 ml 4 4.95
17 Suc de taronja premsat en fred 1 L 4 5.40

13 files: 5 d'Alimentació, 4 de Cosmètica natural i 4 de Begudes.

Nota: IN també admet una subconsulta en lloc d'una llista literal —WHERE categoria_id IN (SELECT id FROM categories WHERE ...)—, i aquesta és de fet la seva forma més potent. Però les subconsultes són el mòdul 7: aquí ens quedem amb les llistes explícites, i a 07-01 reprendrem IN amb tot el seu abast.

  1. NOT IN i el seu comportament traïdor amb NULL

NOT IN és la negació, i la seva equivalència és igual de mecànica:

x NOT IN (a, b)   ≡   NOT (x = a OR x = b)   ≡   x <> a AND x <> b

Fixa't bé en aquesta última forma, perquè hi ha el problema.

El cas normal

SELECT id,
       client_id,
       data_comanda,
       estat
FROM comandes
WHERE estat NOT IN ('lliurat', 'cancellat')
ORDER BY id;
id client_id data_comanda estat
16 4 2025-12-19 enviat
17 7 2026-01-13 enviat
18 5 2026-01-27 pagat
19 6 2026-02-09 pagat
20 9 2026-02-21 pendent

5 files. Funciona perfectament. I funciona perquè estat és NOT NULL i perquè la llista no conté cap NULL.

El cas que arruïna informes

Ara la mateixa idea sobre comandes.empleat_id, que que admet nuls. Volem les comandes que no van gestionar ni l'Óscar (4) ni la Laia (5):

SELECT id, client_id, empleat_id, estat
FROM comandes
WHERE empleat_id NOT IN (4, 5)
ORDER BY id;
id client_id empleat_id estat
14 12 6 lliurat
20 9 6 pendent

2 files. Les deu comandes web amb empleat_id nul han desaparegut: és el mateix símptoma de <> que vas veure a 02-03, ni més ni menys.

Però ara ve el cas greu. Imagina't que la llista la genera la teva aplicació a partir d'una consulta prèvia, i que un dels valors que porta és NULL:

-- ⚠️ INCORRECTA: la llista conté un NULL
SELECT id, client_id, empleat_id, estat
FROM comandes
WHERE empleat_id NOT IN (4, 5, NULL)
ORDER BY id;
(0 files)

Zero files. No dues, no divuit: zero. Sense error, sense avís, sense res. I no és un cas rar: és el que passa cada vegada que algú escriu WHERE id NOT IN (SELECT columna_que_admet_nuls FROM una_altra_taula), que és una construcció habitualíssima.

Per què passa

Desenvolupa l'equivalència:

empleat_id NOT IN (4, 5, NULL)
≡ empleat_id <> 4 AND empleat_id <> 5 AND empleat_id <> NULL

El tercer factor, empleat_id <> NULL, mai no és cert. Dona UNKNOWN per a qualsevol valor d'empleat_id, fins i tot per a la comanda 14, l'empleat de la qual és el 6. I en la lògica d'SQL:

Fila <> 4 <> 5 <> NULL AND dels tres Passa el WHERE?
Comanda 14 (empleat_id = 6) true true UNKNOWN UNKNOWN No
Comanda 2 (empleat_id = 4) false true UNKNOWN false No
Comanda 1 (empleat_id = NULL) UNKNOWN UNKNOWN UNKNOWN UNKNOWN No

TRUE AND TRUE AND UNKNOWN dona UNKNOWN, i el WHERE només deixa passar TRUE (regla de 02-03, secció 1). Cap fila no pot sobreviure: és matemàticament impossible que la condició sigui certa mentre hi hagi un NULL a la llista.

flowchart TD
    A["WHERE x NOT IN (4, 5, NULL)"] --> B["x <> 4 AND x <> 5 AND x <> NULL"]
    B --> C["El tercer factor SEMPRE és UNKNOWN"]
    C --> D["TRUE AND TRUE AND UNKNOWN = UNKNOWN"]
    D --> E["❌ WHERE només deixa passar TRUE<br/>→ 0 files, sempre"]

I per què IN sí que funciona

L'asimetria és el més desconcertant de l'assumpte. IN amb la mateixa llista sí que retorna files:

SELECT id, client_id, empleat_id
FROM comandes
WHERE empleat_id IN (4, 5, NULL)
ORDER BY id;
id client_id empleat_id
2 2 4
4 4 5
6 5 4
8 7 5
10 9 4
12 10 5
16 4 4
18 5 5

8 files, exactament les mateixes que donaria IN (4, 5). La raó és a la taula de veritat d'OR: TRUE OR UNKNOWN és TRUE, mentre que TRUE AND UNKNOWN és UNKNOWN. Amb IN (que és una cadena d'OR) el NULL és inofensiu; amb NOT IN (que és una cadena d'AND) ho destrueix tot.

Operador S'expandeix a Efecte d'un NULL a la llista
IN Cadena d'OR Cap. TRUE OR UNKNOWN = TRUE
NOT IN Cadena d'AND Devastador. TRUE AND UNKNOWN = UNKNOWN → 0 files

Com protegir-se'n

Quatre mesures, de la més simple a la més robusta:

Mesura Com
Excloure els nuls de la llista Si la llista ve d'una consulta, afegeix-hi WHERE columna IS NOT NULL
Fer servir l'anti-join de 03-03 LEFT JOIN ... WHERE dreta.id IS NULL és immune als nuls
Fer servir NOT EXISTS Semànticament correcte davant dels nuls. Mòdul 7
Declarar la columna NOT NULL La solució d'arrel, si el model ho permet (mòdul 5)

La regla que cal gravar: desconfia de NOT IN sempre que la llista pugui contenir un NULL. Si no ho pots garantir, fes servir NOT EXISTS o un anti-join. Aquest error ha arribat a producció a totes les empreses del món almenys una vegada, i el seu símptoma —un informe que de sobte apareix buit— sempre s'atribueix primer a les dades i no a la consulta.

La lliçó 04-03 desenvolupa la lògica de tres valors que hi ha a sota de tot això, amb les taules de veritat completes.

  1. IN amb números, text i dates

IN no està limitat a enters. Funciona amb qualsevol tipus comparable, sempre que tots els elements de la llista siguin del mateix tipus que l'expressió de l'esquerra.

Amb text, l'ús més freqüent després dels identificadors:

SELECT id,
       nom,
       cognoms,
       ciutat,
       pais
FROM clients
WHERE pais IN ('Portugal', 'França')
ORDER BY pais, id;
id nom cognoms ciutat pais
9 Camille Dubois Lió França
10 Julien Moreau París França
7 Sofia Moreira Costa Lisboa Portugal
8 Tiago Almeida Nunes Porto Portugal

4 files: la clientela internacional. Recorda de 02-03 que el text distingeix majúscules: IN ('portugal') donaria zero files.

Amb dates, per a dies concrets i no per a rangs:

SELECT id,
       client_id,
       data_comanda,
       estat
FROM comandes
WHERE data_comanda IN (DATE '2025-03-04', DATE '2025-12-02', DATE '2026-02-21')
ORDER BY id;
id client_id data_comanda estat
1 1 2025-03-04 lliurat
15 1 2025-12-02 lliurat
20 9 2026-02-21 pendent

3 files. El prefix DATE davant del literal no és obligatori (PostgreSQL converteix la cadena per context), però documenta el tipus i evita ambigüitats. Per a dies consecutius, IN és l'eina equivocada: allà toca un rang, que és la segona meitat de la lliçó.

Amb llistes mixtes de tipus, PostgreSQL intenta convertir i de vegades falla:

SELECT id FROM productes WHERE categoria_id IN (1, 'dos');
ERROR:  invalid input syntax for type integer: "dos"

Un error explícit, que és el millor que pot passar.

  1. BETWEEN: sucre sintàctic per a un rang tancat

BETWEEN substitueix dues comparacions per una:

x BETWEEN a AND b   ≡   x >= a AND x <= b

Dues conseqüències immediates d'aquella equivalència:

  1. Inclou els dos extrems. És un interval tancat, [a, b].
  2. L'ordre importa. BETWEEN 10 AND 5 no dona error: retorna zero files, perquè exigeix x >= 10 AND x <= 5, que és impossible.

Vegem-ho amb un rang triat perquè tots dos extrems existeixin a les dades:

SELECT id,
       nom,
       categoria_id,
       preu
FROM productes
WHERE preu BETWEEN 3.50 AND 5.50
ORDER BY preu, id;
id nom categoria_id preu
18 Raspall de dents de bambú 5 3.50
2 Arròs integral ecològic 1 kg 1 3.90
9 Bàlsam labial de calèndula 15 ml 2 4.60
16 Kombutxa de gingebre 750 ml 4 4.95
17 Suc de taronja premsat en fred 1 L 4 5.40
11 Fregall vegetal de lufa (pack 3) 3 5.50

6 files, i les dues que interessen són la primera i l'última: el raspall de bambú costa exactament 3,50 € i el fregall de lufa exactament 5,50 €, i tots dos apareixen. Si BETWEEN fos obert, aquesta consulta retornaria 4 files.

La forma llarga dona el mateix, lletra per lletra:

SELECT id, nom, categoria_id, preu
FROM productes
WHERE preu >= 3.50
  AND preu <= 5.50
ORDER BY preu, id;

Idèntiques 6 files. BETWEEN no és més ràpid ni més lent: el planificador de PostgreSQL l'expandeix a les dues comparacions abans de decidir el pla. El guany és de llegibilitat i de no repetir el nom de la columna, cosa que evita l'error clàssic d'escriure WHERE preu >= 3.50 AND cost <= 5.50 per descuit.

  1. NOT BETWEEN

x NOT BETWEEN a AND b   ≡   x < a OR x > b
SELECT id,
       nom,
       preu
FROM productes
WHERE preu NOT BETWEEN 3.50 AND 5.50
ORDER BY preu, id;

Retorna 14 files, que amb les 6 anteriors sumen els 20 productes. Que sumin és la comprovació de sempre —i aquí funciona perquè preu és NOT NULL. Si admetés nuls, aquelles files no apareixerien ni a un costat ni a l'altre i el compte no tancaria; el mecanisme és idèntic al de <> i al de NOT LIKE.

  1. El parany de BETWEEN amb dates i hores

Aquí hi ha el motiu pel qual aquest curs va fixar des de 02-03 la convenció >= inici AND < fi i no BETWEEN.

A BotigaVerda totes les columnes de data són DATE: desen un dia de calendari, sense hora. Amb DATE, BETWEEN és perfectament segur:

SELECT id,
       client_id,
       data_comanda,
       estat,
       despeses_enviament
FROM comandes
WHERE data_comanda BETWEEN DATE '2025-06-01' AND DATE '2025-08-31'
ORDER BY data_comanda, id;
id client_id data_comanda estat despeses_enviament
7 6 2025-06-11 lliurat 6.50
8 7 2025-06-28 lliurat 9.90
9 8 2025-07-15 lliurat 9.90
10 9 2025-08-03 lliurat 12.50

4 files: l'estiu del 2025, les mateixes comandes que a 02-03 vas obtenir amb >= '2025-06-01' AND < '2025-09-01'.

Què passaria si la columna fos TIMESTAMP

Suposa que demà l'equip decideix desar també l'hora de la comanda i data_comanda passa a ser TIMESTAMP. Un valor com 2025-08-31 14:20:00 deixa de ser "el 31 d'agost" per al motor: és un instant.

I BETWEEN '2025-06-01' AND '2025-08-31' es converteix en:

data_comanda >= 2025-06-01 00:00:00  AND  data_comanda <= 2025-08-31 00:00:00

Perquè la cadena '2025-08-31' es converteix a l'instant mitjanit del 31 d'agost. Resultat: es perden totes les comandes del 31 d'agost llevat de les fetes exactament a les 00:00:00. Un dia sencer de facturació desapareix de l'informe, cada mes, sense que ningú no ho noti fins al tancament del trimestre.

Les quatre escriptures possibles i el seu veredicte:

Escriptura Amb DATE Amb TIMESTAMP Veredicte
BETWEEN '2025-06-01' AND '2025-08-31' ✅ Correcta ❌ Perd l'últim dia Fràgil
>= '2025-06-01' AND <= '2025-08-31' ✅ Correcta ❌ Problema idèntic Fràgil
BETWEEN '2025-06-01' AND '2025-08-31 23:59:59' ✅ Correcta ⚠️ Perd els microsegons finals del dia Nyap clàssic
>= '2025-06-01' AND < '2025-09-01' ✅ Correcta Correcta La del curs

La tercera fila mereix un comentari, perquè és la que més es veu en codi real. '2025-08-31 23:59:59' deixa fora l'interval entre 23:59:59.000001 i 23:59:59.999999. Amb TIMESTAMP de precisió de microsegons, això és gairebé un segon de dades perdudes per cada rang. Sembla menyspreable fins que comptes transaccions d'un sistema de pagaments.

Regla del curs, ara justificada del tot: els rangs de dates s'escriuen >= inici AND < fi, amb el límit superior exclusiu i expressat com el primer instant del període següent. Funciona amb DATE, amb TIMESTAMP i amb TIMESTAMPTZ; no depèn de la precisió del tipus; i no t'obliga a saber si el mes té 28, 30 o 31 dies.

Aleshores BETWEEN no serveix per a dates? Sí que serveix, amb dues condicions: que la columna sigui DATE (no TIMESTAMP) i que qui llegeixi la consulta d'aquí a un any ho continuï sabent. Com que el segon no es pot garantir, la convenció uniforme surt més barata. Per a números i per a text, en canvi, BETWEEN és l'escriptura idiomàtica i no té cap pega.

  1. BETWEEN amb text i la colació

BETWEEN també funciona amb cadenes, comparant-les en l'ordre que defineix la colació de la base de dades (ho vas veure a 02-05):

SELECT id,
       nom,
       cognoms
FROM clients
WHERE cognoms BETWEEN 'A' AND 'C'
ORDER BY cognoms, id;
id nom cognoms
8 Tiago Almeida Nunes
5 Ana Belmonte Roca
13 Núria Bosch Ferrer

3 files. I aquí hi ha una sorpresa que atrapa gairebé tothom: Inés Carrasco Vega no hi apareix, tot i que el seu cognom comença per C.

El motiu és que BETWEEN 'A' AND 'C' exigeix cognoms <= 'C', i 'Carrasco Vega' és més gran que 'C' a seques: comparteixen el primer caràcter i la primera cadena continua, així que va després en l'ordre alfabètic. El límit superior 'C' només inclouria algú el cognom del qual fos exactament "C".

Per a "tots els cognoms que comencen per A, B o C" hi ha dues escriptures correctes:

-- Opció 1: pujar el límit superior a la lletra següent, exclusiu
WHERE cognoms >= 'A' AND cognoms < 'D'

-- Opció 2: fer servir LIKE (04-01)
WHERE cognoms LIKE 'A%' OR cognoms LIKE 'B%' OR cognoms LIKE 'C%'

La primera retorna 4 files (les tres anteriors més Carrasco Vega) i és el mateix patró >= inici AND < fi de les dates. No és casualitat: és la forma robusta d'expressar un rang sobre qualsevol tipus ordenat.

Dos avisos més sobre text:

  • El resultat depèn de la colació. En una colació lingüística catalana, 'à' s'ordena al costat de 'a'; en la colació C (binària per bytes), va després de tota la Z. La mateixa consulta pot retornar conjunts diferents en dos servidors.
  • Continua distingint majúscules, amb el mateix advertiment de 04-01.

  1. BETWEEN SYMMETRIC

Com que l'ordre dels extrems importa, BETWEEN 10 AND 5 retorna zero files. PostgreSQL ofereix una variant que els reordena automàticament:

SELECT id, nom, preu
FROM productes
WHERE preu BETWEEN SYMMETRIC 5.50 AND 3.50
ORDER BY preu, id;

Retorna les mateixes 6 files de la secció 4. BETWEEN SYMMETRIC a AND b equival a BETWEEN LEAST(a,b) AND GREATEST(a,b).

Per a què serveix? Sobretot quan els dos extrems són paràmetres i no controles en quin ordre arriben: un formulari amb dues caselles "preu des de" i "preu fins a" que l'usuari omple a l'inrevés. Amb SYMMETRIC la consulta retorna alguna cosa sensata en lloc d'un resultat buit.

Nota de dialecte: BETWEEN SYMMETRIC és SQL estàndard, però a la pràctica només PostgreSQL l'implementa. MySQL, SQLite, SQL Server i Oracle no el reconeixen. Si necessites portabilitat, BETWEEN LEAST(:a, :b) AND GREATEST(:a, :b) fa el mateix en gairebé tots ells.

  1. IN, OR i BETWEEN: rendiment i llegibilitat

La pregunta raonable és si alguna d'aquestes escriptures és més ràpida. La resposta curta: entre IN i OR, no; entre llistes i rangs, depèn de què estiguis expressant.

Escriptura Llegibilitat Rendiment Quan fer-la servir
x = a OR x = b Baixa a partir de 3 valors Idèntic a IN Mai, llevat que siguin dos valors i ja siguis dins d'una condició més gran
x IN (a, b, c) Alta Es reescriu a = ANY(ARRAY[...]); amb llistes llargues PostgreSQL fa servir una taula hash Valors discrets que no formen un rang
x >= a AND x <= b Mitjana (repeteix la columna) Pot fer servir un índex B-tree per rang Quan vulguis deixar explícit quin extrem s'inclou
x BETWEEN a AND b Alta Idèntic a l'anterior: el planificador l'expandeix Rangs continus de números o de text
x IN (1,2,3,4,5,…,100) Baixa Funciona, però és una llista on hi hauria d'haver un rang ⚠️ Senyal que volies BETWEEN 1 AND 100

Tres criteris pràctics per decidir:

  1. Els valors són consecutius? Aleshores és un rang: BETWEEN. Escriure categoria_id IN (1,2,3,4,5,6) sobre les sis categories és pitjor que no filtrar.
  2. Els valors són arbitraris? Aleshores és una llista: IN. estat IN ('pagat','pendent') no és un rang de res.
  3. La llista és llarguíssima i surt d'una altra taula? Aleshores no és ni una cosa ni l'altra: és una subconsulta o un JOIN (mòdul 7). Un IN amb dos mil literals generat des de codi és un símptoma que hi falta un JOIN.

Sobre el rendiment hi ha un matís que veuràs al mòdul 8: PostgreSQL tracta IN amb llista curta com una sèrie de comparacions i, a partir de certa mida, construeix una taula hash. Un IN amb milers d'elements pot continuar sent eficient, però el temps d'anàlisi de la consulta creix, i aquella consulta no es reutilitza bé des de la memòria cau de plans perquè cada crida té una llista diferent. És una altra raó per preferir el JOIN quan la llista ve de dades.

Errors habituals i consells

  • NOT IN amb un NULL a la llista. Zero files, sempre, sense cap avís. És l'error més car d'aquesta lliçó i probablement del curs.
  • Oblidar que NOT IN i NOT BETWEEN exclouen les files nul·les de la columna. Igual que <> a 02-03: comprova que la condició i la seva negació sumin el total.
  • Escriure BETWEEN 10 AND 5. Zero files, sense error. El primer extrem ha de ser el menor, o fes servir BETWEEN SYMMETRIC.
  • Suposar que BETWEEN exclou algun extrem. Els inclou tots dos. Si volies preu >= 10 AND preu < 20, BETWEEN 10 AND 20 no hi és equivalent.
  • Fer servir BETWEEN sobre una columna TIMESTAMP. Perds l'últim dia. Fes servir >= inici AND < fi.
  • Fer servir '…23:59:59' com a límit superior. Perds gairebé un segon per rang, i amb precisió de microsegons això és un forat real.
  • BETWEEN 'A' AND 'C' esperant tots els cognoms amb C. Només arriba fins a la cadena "C" exacta. Fes servir >= 'A' AND < 'D'.
  • Generar IN () amb una llista buida des de codi. Error de sintaxi en temps d'execució. Contempla el cas abans de concatenar.
  • Convertir un rang en una llista. IN (1,2,3,…,100) és un BETWEEN 1 AND 100 mal escrit.
  • Consell: en escriure NOT IN, digues en veu alta "hi pot haver un nul, aquí?". Si la resposta no és un no rotund, canvia d'estratègia.
  • Consell: fes servir BETWEEN per a números i text, i >= … AND < … per a dates. És una regla simple, uniforme i que no et trairà mai.
  • Consell: posa la llista d'IN en ordre alfabètic o numèric encara que no importi per al resultat. Detectar un valor duplicat o absent en una llista ordenada és trivial; en una de desordenada, no.

Exercicis

Exercici 1

Atenció al client necessita dos llistats. Escriu-los fent servir IN (no OR):

  1. Les comandes pagades amb targeta o PayPal, mostrant id, client_id, data_comanda, estat, metode_pagament i despeses_enviament. Quantes files?
  2. Els productes de les categories 3 (Llar sostenible) i 5 (Higiene personal) que a més estiguin actius, mostrant id, nom, categoria_id, preu i stock.

Exercici 2

Direcció demana el detall del quart trimestre del 2025 (octubre, novembre i desembre).

  1. Escriu-lo amb BETWEEN i amb la convenció del curs, i comprova que donen el mateix.
  2. Explica què passaria amb cadascuna de les dues versions si data_comanda fos TIMESTAMP i hi hagués una comanda registrada el 31 de desembre del 2025 a les 18:40.

Exercici 3

Un company t'ensenya aquesta consulta i et diu que "no retorna res i no entén per què":

-- ⚠️ INCORRECTA
SELECT id, nom, cognoms, referit_per_id
FROM clients
WHERE referit_per_id NOT IN (1, 2, NULL);
  1. Explica què pretenia i què està passant realment.
  2. Corregeix-la perquè retorni "els clients referits per algú que no sigui la Lucía (1) ni en Carlos (2)".
  3. Corregeix-la perquè retorni "els clients que no van ser referits ni per la Lucía ni per en Carlos", incloent-hi els que van arribar pel seu compte. Indica quantes files dona cada versió.

Solucions

Solució 1

1.

SELECT id,
       client_id,
       data_comanda,
       estat,
       metode_pagament,
       despeses_enviament
FROM comandes
WHERE metode_pagament IN ('targeta', 'paypal')
ORDER BY id;
id client_id data_comanda estat metode_pagament despeses_enviament
1 1 2025-03-04 lliurat targeta 4.95
3 3 2025-04-02 lliurat targeta 4.95
4 4 2025-04-19 lliurat paypal 4.95
5 1 2025-05-07 lliurat targeta 0.00
6 5 2025-05-23 cancellat targeta 4.95
8 7 2025-06-28 lliurat targeta 9.90
9 8 2025-07-15 lliurat paypal 9.90
10 9 2025-08-03 lliurat targeta 12.50
11 2 2025-09-09 lliurat targeta 0.00
13 11 2025-10-22 lliurat targeta 4.95
14 12 2025-11-14 lliurat paypal 4.95
15 1 2025-12-02 lliurat targeta 0.00
16 4 2025-12-19 enviat targeta 4.95
17 7 2026-01-13 enviat paypal 9.90
19 6 2026-02-09 pagat targeta 4.95

15 files: 11 amb targeta i 4 amb PayPal. Les 5 restants es van pagar per transferència (3) o contrareemborsament (2).

2.

SELECT id,
       nom,
       categoria_id,
       preu,
       stock
FROM productes
WHERE categoria_id IN (3, 5)
  AND actiu
ORDER BY categoria_id, id;
id nom categoria_id preu stock
10 Detergent ecològic concentrat 1 L 3 11.20 70
11 Fregall vegetal de lufa (pack 3) 3 5.50 110
12 Bosses reutilitzables de cotó (pack 5) 3 9.90 85
13 Espelmes de cera de soja (pack 2) 3 13.75 0
18 Raspall de dents de bambú 5 3.50 240
19 Desodorant natural en barra 50 g 5 7.80 75

6 files: els 4 de Llar sostenible i els 2 d'Higiene personal, tots actius. Fixa't que el producte 13 hi apareix tot i tenir stock 0: està actiu, i actiu i stock són coses diferents. És el mateix matís de l'exercici 1 de 02-03.

Solució 2

1. Les dues versions:

-- Amb BETWEEN (vàlida perquè data_comanda és DATE)
SELECT id, client_id, data_comanda, estat, despeses_enviament
FROM comandes
WHERE data_comanda BETWEEN DATE '2025-10-01' AND DATE '2025-12-31'
ORDER BY data_comanda, id;
-- ✅ Convenció del curs
SELECT id, client_id, data_comanda, estat, despeses_enviament
FROM comandes
WHERE data_comanda >= DATE '2025-10-01'
  AND data_comanda <  DATE '2026-01-01'
ORDER BY data_comanda, id;

Totes dues retornen el mateix:

id client_id data_comanda estat despeses_enviament
12 10 2025-10-01 lliurat 12.50
13 11 2025-10-22 lliurat 4.95
14 12 2025-11-14 lliurat 4.95
15 1 2025-12-02 lliurat 0.00
16 4 2025-12-19 enviat 4.95

5 files. Observa que la comanda 12 és exactament de l'1 d'octubre: entra per l'extrem inferior, que totes dues versions inclouen.

2. Amb TIMESTAMP i una comanda del 31 de desembre a les 18:40:

Versió Què avalua Inclou la comanda de les 18:40?
BETWEEN '2025-10-01' AND '2025-12-31' <= 2025-12-31 00:00:00 No. Es perd
>= '2025-10-01' AND < '2026-01-01' < 2026-01-01 00:00:00

La primera versió perdria totes les comandes del 31 de desembre posteriors a mitjanit, és a dir, pràcticament totes. I com que l'error no dona cap missatge, el trimestre es tancaria amb una xifra baixa que ningú no sabria explicar. Aquest és el motiu exacte de la convenció del curs: la segona versió no depèn del tipus de la columna, i per tant no es trenca el dia que algú canviï aquell tipus.

Solució 3

1. Què pretenia i què passa. Pretenia excloure els clients referits per la Lucía i per en Carlos. El que passa és que la llista conté un NULL, així que la condició s'expandeix a:

referit_per_id <> 1 AND referit_per_id <> 2 AND referit_per_id <> NULL

i aquest tercer factor és UNKNOWN per a tota fila. TRUE AND TRUE AND UNKNOWN = UNKNOWN, que el WHERE descarta. Resultat: 0 files, garantides.

2. Referits per algú que no sigui la Lucía ni en Carlos:

-- ✅ CORRECTA
SELECT id,
       nom,
       cognoms,
       referit_per_id
FROM clients
WHERE referit_per_id NOT IN (1, 2)
ORDER BY id;
id nom cognoms referit_per_id
8 Tiago Almeida Nunes 7
10 Julien Moreau 9
11 Elena Navarro Puig 6
13 Núria Bosch Ferrer 5

4 files. N'hi ha prou de treure el NULL de la llista. Els 7 clients amb referit_per_id nul continuen sense aparèixer, i això és correcte per a aquesta pregunta: qui no va ser referit per ningú no va ser referit per "algú que no sigui la Lucía ni en Carlos".

3. Els que no van ser referits ni per la Lucía ni per en Carlos, inclosos els que van arribar sols:

-- ✅ CORRECTA
SELECT id,
       nom,
       cognoms,
       referit_per_id
FROM clients
WHERE referit_per_id NOT IN (1, 2)
   OR referit_per_id IS NULL
ORDER BY id;
id nom cognoms referit_per_id
1 Lucía Martínez Soler (null)
4 Javier Ortega Ruiz (null)
6 Pau Llorens Vidal (null)
7 Sofia Moreira Costa (null)
8 Tiago Almeida Nunes 7
9 Camille Dubois (null)
10 Julien Moreau 9
11 Elena Navarro Puig 6
12 Diego Ramos Herrera (null)
13 Núria Bosch Ferrer 5
14 Hugo Iglesias Pardo (null)

11 files. La comprovació tanca: 4 clients van ser referits per la Lucía (2, 3, 15) o per en Carlos (5) —quatre en total— i 15 − 4 = 11.

Resum de les tres versions:

Versió Files Pregunta que respon
NOT IN (1, 2, NULL) 0 Cap: està trencada
NOT IN (1, 2) 4 "Referits per algú diferent de la Lucía i en Carlos"
NOT IN (1, 2) OR ... IS NULL 11 "No referits ni per la Lucía ni per en Carlos"

Les dues últimes són legítimes i responen preguntes diferents. Triar malament entre elles és un error d'anàlisi; la primera, en canvi, és un error d'SQL. IS NULL, que aquí ha aparegut com a pedaç, és el tema complet de la lliçó següent.

Conclusió

Ja escrius filtres de llistes i de rangs amb l'escriptura correcta:

  • IN és una cadena d'OR i NOT IN és una cadena d'AND. Tota la resta se'n dedueix: l'ordre no importa, els duplicats són indiferents, i la llista buida no és sintaxi vàlida.
  • NOT IN amb un NULL a la llista retorna exactament zero files, perquè TRUE AND UNKNOWN és UNKNOWN i el WHERE només deixa passar TRUE. IN amb el mateix NULL funciona sense problema, perquè TRUE OR UNKNOWN és TRUE. Davant del dubte: anti-join o NOT EXISTS.
  • BETWEEN és >= a AND <= b: interval tancat, amb tots dos extrems inclosos, i sensible a l'ordre dels límits. NOT BETWEEN és < a OR > b, amb la mateixa ceguesa davant dels nuls.
  • Amb columnes DATE, BETWEEN és segur; amb TIMESTAMP perd l'últim dia. La convenció >= inici AND < fi funciona amb qualsevol tipus i és la que fa servir el curs.
  • Amb text, BETWEEN 'A' AND 'C' no inclou "Carrasco Vega": el rang tancat sobre cadenes gairebé mai no significa el que sembla. Fes servir >= 'A' AND < 'D' o LIKE.
  • BETWEEN SYMMETRIC reordena els extrems automàticament i només existeix a PostgreSQL.
  • Entre IN i OR no hi ha diferència de rendiment; l'elecció entre llista i rang ha de seguir la naturalesa de la dada: valors discrets → IN, valors consecutius → BETWEEN, valors que surten d'una altra taula → JOIN o subconsulta (mòdul 7).

A la lliçó següent, valors NULL i IS NULL, se salda per fi el deute. Has vist treure el cap al mateix mecanisme al WHERE de 02-03, als LEFT JOIN de tot el mòdul 3 i al NOT IN d'avui, sempre amb la mateixa promesa de "ho explicarem a 04-03". Allà arriben les taules de veritat completes de la lògica de tres valors, IS NULL i IS NOT NULL, l'IS DISTINCT FROM que tracta el nul com un valor més, i la incoherència de l'estàndard per la qual dos NULL no són iguals en un WHERE però sí que s'agrupen junts en un GROUP BY.

Curs de SQL

Mòdul 1: Introducció a SQL

Mòdul 2: Consultes bàsiques de SQL

Mòdul 3: Treballar amb múltiples taules

Mòdul 4: Filtratge avançat de dades

Mòdul 5: Manipulació de dades

Mòdul 6: Funcions avançades de SQL

Mòdul 7: Subconsultes i consultes imbricades

Mòdul 8: Índexs i optimització del rendiment

Mòdul 9: Transaccions i concurrència

Mòdul 10: Temes avançats

Mòdul 11: SQL a la pràctica

Mòdul 12: Projecte final

© Copyright 2026. Tots els drets reservats