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
IN: l'alternativa llegible a una cadena d'ORNOT INi el seu comportament traïdor ambNULLINamb números, text i datesBETWEEN: sucre sintàctic per a un rang tancatNOT BETWEEN- El parany de
BETWEENamb dates i hores BETWEENamb text i la colacióBETWEEN SYMMETRICIN,ORiBETWEEN: rendiment i llegibilitat- Errors habituals i consells
- Exercicis
- Conclusió
IN: l'alternativa llegible a una cadena d'OR
IN: l'alternativa llegible a una cadena d'ORA 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:
INtambé 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 reprendremINamb tot el seu abast.
NOT IN i el seu comportament traïdor amb NULL
NOT IN i el seu comportament traïdor amb NULLNOT IN és la negació, i la seva equivalència és igual de mecànica:
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 sí que admet nuls. Volem les comandes que no van gestionar ni l'Óscar (4) ni la Laia (5):
| 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;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:
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:
| 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 INsempre que la llista pugui contenir unNULL. Si no ho pots garantir, fes servirNOT EXISTSo 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.
IN amb números, text i dates
IN amb números, text i datesIN 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:
Un error explícit, que és el millor que pot passar.
BETWEEN: sucre sintàctic per a un rang tancat
BETWEEN: sucre sintàctic per a un rang tancatBETWEEN substitueix dues comparacions per una:
Dues conseqüències immediates d'aquella equivalència:
- Inclou els dos extrems. És un interval tancat,
[a, b]. - L'ordre importa.
BETWEEN 10 AND 5no dona error: retorna zero files, perquè exigeixx >= 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.
NOT BETWEEN
NOT BETWEENRetorna 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.
- El parany de
BETWEEN amb dates i hores
BETWEEN amb dates i horesAquí 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:
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 ambDATE, ambTIMESTAMPi ambTIMESTAMPTZ; 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.
BETWEEN amb text i la colació
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):
| 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 laZ. La mateixa consulta pot retornar conjunts diferents en dos servidors. - Continua distingint majúscules, amb el mateix advertiment de 04-01.
BETWEEN SYMMETRIC
BETWEEN SYMMETRICCom que l'ordre dels extrems importa, BETWEEN 10 AND 5 retorna zero files. PostgreSQL ofereix una variant que els reordena automàticament:
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.
IN, OR i BETWEEN: rendiment i llegibilitat
IN, OR i BETWEEN: rendiment i llegibilitatLa 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:
- Els valors són consecutius? Aleshores és un rang:
BETWEEN. Escriurecategoria_id IN (1,2,3,4,5,6)sobre les sis categories és pitjor que no filtrar. - Els valors són arbitraris? Aleshores és una llista:
IN.estat IN ('pagat','pendent')no és un rang de res. - 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). UnINamb dos mil literals generat des de codi és un símptoma que hi falta unJOIN.
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 INamb unNULLa 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 INiNOT BETWEENexclouen 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 servirBETWEEN SYMMETRIC. - Suposar que
BETWEENexclou algun extrem. Els inclou tots dos. Si voliespreu >= 10 AND preu < 20,BETWEEN 10 AND 20no hi és equivalent. - Fer servir
BETWEENsobre una columnaTIMESTAMP. 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 unBETWEEN 1 AND 100mal 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
BETWEENper 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'
INen 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):
- Les comandes pagades amb targeta o PayPal, mostrant
id,client_id,data_comanda,estat,metode_pagamentidespeses_enviament. Quantes files? - 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).
- Escriu-lo amb
BETWEENi amb la convenció del curs, i comprova que donen el mateix. - Explica què passaria amb cadascuna de les dues versions si
data_comandafosTIMESTAMPi 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);- Explica què pretenia i què està passant realment.
- Corregeix-la perquè retorni "els clients referits per algú que no sigui la Lucía (1) ni en Carlos (2)".
- 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 |
Sí |
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:
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'ORiNOT 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 INamb unNULLa la llista retorna exactament zero files, perquèTRUE AND UNKNOWNésUNKNOWNi elWHEREnomés deixa passarTRUE.INamb el mateixNULLfunciona sense problema, perquèTRUE OR UNKNOWNésTRUE. Davant del dubte: anti-join oNOT 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; ambTIMESTAMPperd l'últim dia. La convenció>= inici AND < fifunciona 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'oLIKE. BETWEEN SYMMETRICreordena els extrems automàticament i només existeix a PostgreSQL.- Entre
INiORno 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 →JOINo 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
- Què és SQL?
- Configurar el teu entorn SQL
- Sintaxi bàsica de SQL
- Entendre bases de dades i taules
- El model relacional: claus primàries i foranes
- La base de dades del curs: BotigaVerda
Mòdul 2: Consultes bàsiques de SQL
- Instrucció SELECT
- Àlies, expressions i columnes calculades
- Filtrar dades amb WHERE
- DISTINCT i eliminació de duplicats
- Ordenar dades amb ORDER BY
- Limitar resultats amb LIMIT
Mòdul 3: Treballar amb múltiples taules
- Operacions JOIN
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN i CROSS JOIN
- Unions de conjunts: UNION, INTERSECT i EXCEPT
Mòdul 4: Filtratge avançat de dades
- Utilitzar LIKE per a coincidència de patrons
- Operadors IN i BETWEEN
- Valors NULL i IS NULL
- Funcions d'agregació: COUNT, SUM, AVG, MIN i MAX
- Agregar dades amb GROUP BY
- Clàusula HAVING
Mòdul 5: Manipulació de dades
- Crear taules i restriccions amb CREATE TABLE
- Instrucció INSERT
- Instrucció UPDATE
- Instrucció DELETE
- Instrucció UPSERT (MERGE)
- Modificar l'esquema: ALTER TABLE i migracions segures
Mòdul 6: Funcions avançades de SQL
- Funcions de cadena
- Funcions numèriques
- Funcions de data i hora
- Conversió de tipus i tractament dels NULL: CAST i COALESCE
- Expressions condicionals
Mòdul 7: Subconsultes i consultes imbricades
- Introducció a les subconsultes
- Subconsultes correlacionades
- EXISTS i NOT EXISTS
- Utilitzar subconsultes en les clàusules SELECT, FROM i WHERE
- Subconsultes o JOIN: quin triar
Mòdul 8: Índexs i optimització del rendiment
- Entendre els índexs
- Creació i gestió d'índexs
- Tipus d'índex i quan no indexar
- Tècniques d'optimització de consultes
- Anàlisi del rendiment de les consultes
Mòdul 9: Transaccions i concurrència
- Introducció a les transaccions
- Propietats ACID
- Instruccions de control de transaccions
- Nivells d'aïllament i anomalies de concurrència
- Gestió de la concurrència: bloquejos i interbloquejos
Mòdul 10: Temes avançats
- Vistes
- Expressions de taula comunes (CTE)
- Funcions de finestra
- Procediments emmagatzemats
- Disparadors (triggers)
- JSON i dades semiestructurades
Mòdul 11: SQL a la pràctica
- Casos d'ús al món real
- Bones pràctiques
- Seguretat: injecció SQL, permisos i rols
- SQL per a l'anàlisi de dades
- SQL en el desenvolupament web
