El mòdul 7 es va tancar amb un canvi de pregunta. Durant set mòduls la pregunta ha estat això retorna el que vull?; a partir d'aquí és quant triga?. I la resposta gairebé sempre passa pel mateix objecte: l'índex, una estructura de dades auxiliar que el motor manté al costat de la taula i que converteix "mira totes les files fins a trobar-la" en "ves directament on és".
En aquesta lliçó no crearàs cap índex encara. Primer cal entendre quin problema resolen i com funcionen per dins, perquè gairebé tots els errors que es cometen amb índexs —crear els que no serveixen, no crear els que fan falta, estranyar-se que el motor els ignori— vénen de no tenir el model mental correcte. En acabar sabràs per què buscar entre 20 milions de files pot costar quatre accessos a disc, per què LIKE '%oli%' no en pot aprofitar cap, i —el que més et sorprendrà— quins índexs té ja BotigaVerda sense que ningú els hagi creat i quins li falten.
Contingut
- L'analogia de l'índex d'un llibre, i on es trenca
- Com llegeix el motor una taula sense índex
- L'estructura B-tree
- Què desa realment un índex: la clau i el punter
Index Scan,Index Only Scani índexs cobridors- Què accelera un B-tree i què no
- Els índexs que BotigaVerda ja té
- El forat: PostgreSQL no indexa les claus foranes
- El preu d'un índex
- Errors habituals i consells
- Exercicis
- Conclusió
- L'analogia de l'índex d'un llibre, i on es trenca
Tens un manual de 800 pàgines i vols saber on es parla de HAVING. Hi ha dues maneres:
- Fullejar el llibre sencer fins a trobar-ho. Funciona sempre, i costa 800 pàgines.
- Anar a l'índex alfabètic del final, buscar "HAVING" —que està ordenat, així que el localitzes en segons— i llegir "pàg. 412".
L'índex del llibre és exactament un índex de base de dades: una còpia ordenada d'una part de la informació (els termes) juntament amb un punter a on és la resta (la pàgina). No conté el llibre; conté el just per saltar-hi.
L'analogia és bona, però convé marcar on es trenca, perquè és justament allà on comencen les decisions interessants:
| L'índex d'un llibre | Un índex de base de dades |
|---|---|
| N'hi ha un, al final | N'hi pot haver molts sobre la mateixa taula, cadascun per columnes diferents |
| Es compon una vegada, en imprimir | Es manté viu: cada INSERT, UPDATE i DELETE l'actualitza |
| Ocupa 10 pàgines de 800 | Pot ocupar tant com la mateixa taula, o més |
| Sempre l'uses tu | L'usa el planificador, i de vegades decideix que no compensa |
| Només serveix per buscar termes | Serveix per buscar, per ordenar i per agrupar |
Aquestes cinc diferències són, en realitat, el guió del mòdul sencer. La quarta és la més difícil d'acceptar: crear un índex no garanteix que s'utilitzi. Ho veuràs demostrat sobre BotigaVerda a la lliçó 08-05.
- Com llegeix el motor una taula sense índex
Sense índex només hi ha una estratègia possible: el recorregut seqüencial (sequential scan, o Seq Scan en l'argot de PostgreSQL). El motor llegeix la taula bloc a bloc des del principi, comprova el WHERE a cada fila i descarta les que no compleixen.
| id | nom | preu |
|---|---|---|
| 15 | Te verd matcha cerimonial 30 g | 22.00 |
| 6 | Crema facial d'àloe vera 50 ml | 18.90 |
| 20 | Càpsules d'espirulina 120 u | 16.40 |
| 8 | Oli corporal d'ametlles 200 ml | 14.25 |
| 13 | Espelmes de cera de soja (pack 2) | 13.75 |
| 1 | Oli d'oliva verge extra 500 ml | 12.50 |
| 10 | Detergent ecològic concentrat 1 L | 11.20 |
Set files de vint. Per donar-te-les, el motor n'ha llegit vint. Amb 20 files això és gratis. La clau és com escala:
| Files de la taula | Files llegides per un Seq Scan |
Files llegides amb un índex B-tree | Ordre de magnitud del temps |
|---|---|---|---|
| 20 | 20 | ~2 | Imperceptible en tots dos casos |
| 1.000 | 1.000 | ~3 | Imperceptible |
| 100.000 | 100.000 | ~3 | Dècimes de segon → microsegons |
| 10.000.000 | 10.000.000 | ~4 | Segons → microsegons |
| 1.000.000.000 | 1.000.000.000 | ~5 | Minuts → microsegons |
(Xifres d'intuïció, no de laboratori: el nombre exacte depèn de l'amplada de les files, de la memòria cau i del disc. El que importa és la forma de les dues columnes.)
La columna del Seq Scan creix linealment: el doble de files, el doble de temps. La de l'índex creix logarítmicament: multiplicar les files per mil hi afegeix un accés. Aquesta diferència entre O(n) i O(log n) és tot el mòdul en una línia, i explica per què una consulta que va perfecta amb les dades de desenvolupament pot tombar producció sis mesos després.
Un
Seq Scanno és un error. És l'estratègia correcta quan la taula és petita o quan retornaràs bona part d'ella. Ho veuràs amb detall a 08-03 i demostrat a 08-05.
- L'estructura B-tree
L'índex per defecte a PostgreSQL —i a tots els motors relacionals— és el B-tree (balanced tree, arbre equilibrat). És un arbre ordenat amb tres tipus de node:
- Arrel: un únic node, punt d'entrada de tota cerca.
- Nodes interns: contenen valors separadors i punters a nodes del nivell inferior. Diuen "els valors menors que 5,50 són per aquí".
- Fulles: contenen els valors reals de la clau, cadascun amb el punter a la fila de la taula. A més, les fulles estan enllaçades entre si en ordre, cosa que permet recórrer un rang sense tornar a pujar a l'arbre.
Així es veuria un índex B-tree sobre productes.preu, amb els 20 preus del catàleg repartits en cinc fulles de quatre entrades:
flowchart TD
R["<b>ARREL</b><br/>3,90 · 5,50 · 9,90 · 14,25"]
H1["<b>Fulla 1</b><br/>1,95 → prod 5<br/>2,80 → prod 4<br/>3,25 → prod 14<br/>3,50 → prod 18"]
H2["<b>Fulla 2</b><br/>3,90 → prod 2<br/>4,60 → prod 9<br/>4,95 → prod 16<br/>5,40 → prod 17"]
H3["<b>Fulla 3</b><br/>5,50 → prod 11<br/>7,80 → prod 19<br/>8,40 → prod 7<br/>9,75 → prod 3"]
H4["<b>Fulla 4</b><br/>9,90 → prod 12<br/>11,20 → prod 10<br/>12,50 → prod 1<br/>13,75 → prod 13"]
H5["<b>Fulla 5</b><br/>14,25 → prod 8<br/>16,40 → prod 20<br/>18,90 → prod 6<br/>22,00 → prod 15"]
R --> H1
R --> H2
R --> H3
R --> H4
R --> H5
H1 -.-> H2 -.-> H3 -.-> H4 -.-> H5
Busca el producte de 12,50 €: entres per l'arrel, veus que 12,50 està entre 9,90 i 14,25, baixes a la fulla 4 i el trobes. Dos accessos, no vint. I per a WHERE preu BETWEEN 9 AND 15 hi entres una vegada per 9,00 i després segueixes les fletxes puntejades entre fulles fins a passar-te de 15: això és el que fa que un B-tree serveixi igual de bé per a rangs que per a igualtats.
Per què l'alçada creix tan a poc a poc
A PostgreSQL cada node de l'arbre és una pàgina de 8 kB. En aquesta pàgina hi caben moltes entrades: per a una clau entera de 4 bytes, de l'ordre de 250 entrades útils un cop descomptades les capçaleres i el marge que el motor deixa lliure. Aquest número s'anomena factor de ramificació (fanout), i és el que fa que l'arbre sigui ample i baix en lloc d'estret i alt.
| Alçada de l'arbre | Files que pot indexar (fanout ≈ 250) | Accessos per localitzar una fila |
|---|---|---|
| 1 (només arrel) | 250 | 1 |
| 2 | 62.500 | 2 |
| 3 | ~15,6 milions | 3 |
| 4 | ~3.900 milions | 4 |
Llegeix-ho a poc a poc, perquè és la dada que convé memoritzar del mòdul: una taula de quinze milions de files es recorre amb tres accessos. I d'aquests tres, els nivells superiors són gairebé sempre a la memòria cau perquè totes les consultes hi passen, així que a la pràctica el cost real acostuma a ser un accés a disc, o cap.
El fanout depèn de l'amplada de la clau: indexar un INTEGER dóna arbres molt amples; indexar un VARCHAR(150) amb noms llargs dóna entrades cinc vegades més grans, menys entrades per pàgina i, amb les mateixes files, un nivell més d'alçada. És el primer motiu pel qual indexar columnes estretes surt més barat.
L'"equilibrat" del nom significa que totes les fulles són a la mateixa profunditat, i el motor ho manté així dividint i fusionant pàgines en inserir i esborrar. Per això no existeixen "índexs desequilibrats" que calgui reconstruir a mà: el cost de mantenir-los equilibrats es paga a cada escriptura, i d'aquí ve bona part del preu del qual parla la secció 9.
- Què desa realment un índex: la clau i el punter
Una entrada de fulla no conté la fila. Conté dues coses:
- El valor de la clau (
12.50). - Un punter físic a la fila, que a PostgreSQL s'anomena
ctidi és un parell(bloc, posició dins del bloc).
El ctid és una columna de sistema que pots consultar:
| ctid | id | nom | preu |
|---|---|---|---|
| (0,1) | 1 | Oli d'oliva verge extra 500 ml | 12.50 |
| (0,5) | 5 | Tomàquet triturat ecològic 400 g | 1.95 |
| (0,15) | 15 | Te verd matcha cerimonial 30 g | 22.00 |
Els vint productes són al bloc 0: hi caben de sobres en una sola pàgina de 8 kB. Aquest detall, aparentment anecdòtic, és la raó que cap índex no serveixi de res en aquesta taula, i tornarà a la secció 7 i a 08-05.
Compte: el
ctidno és un identificador estable. Canvia quan la fila s'actualitza, perquè PostgreSQL n'escriu una versió nova en un altre lloc. No el desis mai en una columna ni el facis servir com a clau: per a això hi haid. Aquí només l'utilitzem per veure la maquinària per dins.
La conseqüència que l'índex desi un punter i no la fila és important: una cerca per índex implica dos passos. Primer es baixa per l'arbre fins a la fulla i s'obté el ctid; després cal anar a la taula (el heap) a llegir la fila i recollir les columnes que has demanat. Aquest segon pas s'anomena heap fetch, i si la teva consulta retorna mil files, són mil salts a posicions possiblement disperses del disc.
Index Scan, Index Only Scan i índexs cobridors
Index Scan, Index Only Scan i índexs cobridorsD'aquí surten dos dels nodes de pla que veuràs constantment a 08-05:
| Node | Què fa | Quan apareix |
|---|---|---|
Index Scan |
Recorre l'índex i va a la taula per cada fila trobada | El cas normal: necessites columnes que no són a l'índex |
Index Only Scan |
Recorre l'índex i no toca la taula | Totes les columnes que demana la consulta són a l'índex |
El segon és la grossa: s'estalvia el heap fetch sencer. I s'aconsegueix amb el que s'anomena un índex cobridor (covering index): un índex que cobreix totes les columnes que la consulta necessita, tant per filtrar com per mostrar.
-- Si existeix un índex sobre (preu), aquesta consulta necessita anar a la taula
-- a buscar el nom: Index Scan.
SELECT p.nom, p.preu FROM productes AS p WHERE p.preu > 15;
-- Aquesta altra només demana la columna indexada: es pot resoldre
-- enterament dins de l'índex, sense tocar la taula: Index Only Scan.
SELECT p.preu FROM productes AS p WHERE p.preu > 15;A la lliçó 08-02 veuràs com fabricar índexs cobridors expressament amb la clàusula INCLUDE, que afegeix columnes a l'índex només per llegir-les, sense fer-les servir per ordenar.
Matís honest: a PostgreSQL l'
Index Only Scanno sempre evita el 100 % dels accessos a la taula. L'índex no sap si una fila és visible per a la teva sessió, així que consulta un mapa auxiliar (visibility map) i, per als blocs marcats com a no del tot nets, sí que va al heap. Això apareix al pla com aHeap Fetches: N. Si aquest número és alt, la taula necessita manteniment (VACUUM), i això es tracta a 08-05.
- Què accelera un B-tree i què no
Un B-tree està ordenat. Tot el que es pugui expressar com "ves a un punt de l'ordre i avança" ho resol; tota la resta, no. Aquesta taula tanca tres promeses que el curs arrossega des dels mòduls 2, 4 i 6:
| Operació | Aprofita un B-tree? | Per què |
|---|---|---|
preu = 12.50 |
✅ Sí | Igualtat: es baixa al punt exacte |
preu > 10, preu BETWEEN 5 AND 15 |
✅ Sí | Rang: un punt d'entrada i es recorren les fulles enllaçades |
estat IN ('pagat','enviat') |
✅ Sí | Equival a diverses cerques d'igualtat |
nom LIKE 'Oli%' |
✅ Sí | Un prefix és un rang: d''Oli' a 'Olj' |
ORDER BY preu |
✅ Sí | L'índex ja està ordenat: es llegeix en ordre i s'estalvia el Sort |
MIN(preu), MAX(preu) |
✅ Sí | Són la primera i l'última entrada de l'índex |
ORDER BY preu DESC LIMIT 5 |
✅ Sí | Es llegeixen 5 entrades des del final i s'atura |
nom LIKE '%oli%' |
❌ No | Sense prefix no hi ha punt d'entrada: podria ser a qualsevol fulla |
LOWER(email) = '[email protected]' |
❌ No | L'índex desa email, no LOWER(email): són valors diferents |
EXTRACT(YEAR FROM data_comanda) = 2025 |
❌ No | Mateixa raó: l'índex no conté el resultat de la funció |
estat <> 'lliurat' |
❌ Gairebé mai | La negació descriu gairebé tota la taula; no és un rang útil |
preu + 2 > 15 |
❌ No | Hi ha una operació sobre la columna filtrada |
Les quatre files vermelles del final tenen la mateixa causa: per fer servir un índex, la columna ha d'aparèixer nua a un costat de la comparació. Tan bon punt l'emboliques en una funció, en un càlcul o en un comodí inicial, el motor ja no pot traduir la teva condició a "un punt de l'ordre i avança".
Dues conseqüències pràctiques que resoldràs a les properes lliçons, no ara:
- Les condicions de les files vermelles es poden reescriure gairebé sempre.
EXTRACT(YEAR FROM data_comanda) = 2025és idèntic adata_comanda >= '2025-01-01' AND data_comanda < '2026-01-01', i aquesta segona versió sí que fa servir l'índex: són les mateixes 16 comandes del 2025. Aquesta reescriptura té nom —sargabilitat— i és el cor de la lliçó 08-04. - Quan no es poden reescriure, hi ha eines específiques: un índex sobre l'expressió per a
LOWER(email)(08-02) i l'extensiópg_trgmamb un índex GIN per alLIKE '%oli%'que 04-01 va deixar pendent (08-03).
- Els índexs que BotigaVerda ja té
Aquí arriba la sorpresa: mai no has escrit un CREATE INDEX i BotigaVerda ja té onze índexs. A 05-01 es va dir de passada que PRIMARY KEY i UNIQUE "s'implementen creant un índex per sota, i les estructures es veuen al mòdul 8". Ha arribat el moment.
Table "public.productes"
Column | Type | Nullable | Default
--------------+------------------------+----------+-------------------------------------
id | integer | not null | generated by default as identity
nom | character varying(150) | not null |
categoria_id | integer | |
proveidor_id | integer | |
preu | numeric(10,2) | not null |
cost | numeric(10,2) | |
stock | integer | not null | 0
actiu | boolean | not null | true
data_alta | date | not null | CURRENT_DATE
Indexes:
"productes_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"productes_categoria_id_fkey" FOREIGN KEY (categoria_id) REFERENCES categories(id) ON DELETE RESTRICT
"productes_proveidor_id_fkey" FOREIGN KEY (proveidor_id) REFERENCES proveidors(id) ON DELETE RESTRICT
Referenced by:
TABLE "linies_comanda" CONSTRAINT "linies_comanda_producte_id_fkey" FOREIGN KEY (producte_id) REFERENCES productes(id) ON DELETE RESTRICT
...Un sol índex, productes_pkey, i btree escrit explícitament: és l'estructura de la secció 3. Per veure'ls tots de cop, la vista de catàleg pg_indexes:
SELECT tablename, indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'public'
ORDER BY tablename, indexname;| tablename | indexname | indexdef |
|---|---|---|
| categories | categories_nom_key | CREATE UNIQUE INDEX ... ON public.categories USING btree (nom) |
| categories | categories_pkey | CREATE UNIQUE INDEX ... USING btree (id) |
| clients | clients_email_key | CREATE UNIQUE INDEX ... USING btree (email) |
| clients | clients_pkey | CREATE UNIQUE INDEX ... USING btree (id) |
| comandes | comandes_pkey | CREATE UNIQUE INDEX ... USING btree (id) |
| devolucions | devolucions_pkey | CREATE UNIQUE INDEX ... USING btree (id) |
| empleats | empleats_pkey | CREATE UNIQUE INDEX ... USING btree (id) |
| linies_comanda | linies_comanda_pkey | CREATE UNIQUE INDEX ... USING btree (id) |
| productes | productes_pkey | CREATE UNIQUE INDEX ... USING btree (id) |
| proveidors | proveidors_pkey | CREATE UNIQUE INDEX ... USING btree (id) |
| ressenyes | ressenyes_pkey | CREATE UNIQUE INDEX ... USING btree (id) |
Onze índexs: nou de les claus primàries i dos dels UNIQUE de categories.nom i clients.email. Tots són UNIQUE INDEX ... USING btree, i tots existeixen perquè una restricció els necessita: l'única manera raonable de comprovar "aquest valor no està repetit" abans d'acceptar un INSERT és tenir els valors ordenats. Per això la restricció i l'índex són a PostgreSQL el mateix objecte físic, i per això un INSERT a clients amb un email duplicat falla en microsegons i no llegint les 15 files.
Això explica també per què WHERE c.id = 7 o WHERE c.email = '[email protected]' han estat sempre ràpides al curs, encara que ningú no hagués parlat d'índexs.
- El forat: PostgreSQL no indexa les claus foranes
I ara el punt més valuós de la lliçó. Mira un altre cop la llista: productes.categoria_id no hi apareix. Ni comandes.client_id. Ni linies_comanda.comanda_id.
PostgreSQL crea un índex automàticament per a
PRIMARY KEYi per aUNIQUE, però NO per a les claus foranes. L'índex existeix al costat referenciat (la PK del pare), mai al costat referenciant (la columna filla).
Aquestes són les onze claus foranes de BotigaVerda, totes sense índex:
| Taula | Columna FK | Referència | Acció | Consultes que en pateixen |
|---|---|---|---|---|
productes |
categoria_id |
categories(id) |
RESTRICT |
Productes d'una categoria |
productes |
proveidor_id |
proveidors(id) |
RESTRICT |
Productes d'un proveïdor |
clients |
referit_per_id |
clients(id) |
SET NULL |
Referits d'un client |
empleats |
cap_id |
empleats(id) |
SET NULL |
Subordinats d'un cap |
comandes |
client_id |
clients(id) |
RESTRICT |
Comandes d'un client |
comandes |
empleat_id |
empleats(id) |
SET NULL |
Comandes d'un comercial |
linies_comanda |
comanda_id |
comandes(id) |
CASCADE |
Línies d'una comanda |
linies_comanda |
producte_id |
productes(id) |
RESTRICT |
Vendes d'un producte |
ressenyes |
producte_id |
productes(id) |
CASCADE |
Ressenyes d'un producte |
ressenyes |
client_id |
clients(id) |
CASCADE |
Ressenyes d'un client |
devolucions |
comanda_id |
comandes(id) |
CASCADE |
Devolucions d'una comanda |
Per què fa mal, en dos fronts.
Primer, els JOIN. La consulta canònica del curs uneix linies_comanda amb comandes, clients i productes. Cada ON lc.comanda_id = co.id es pot resoldre en dues direccions: buscant a comandes per id (indexat, ràpid) o buscant a linies_comanda per comanda_id (no indexat, recorregut complet). Amb 47 línies tant és; amb 20 milions, "les línies de la comanda 8.412.006" sense índex significa llegir els 20 milions.
Segon, i menys conegut: els esborrats al pare. Quan executes DELETE FROM comandes WHERE id = 5, PostgreSQL està obligat a comprovar totes les taules que referencien comandes —linies_comanda i devolucions— per aplicar el CASCADE. Si aquestes columnes no tenen índex, cada esborrat d'una comanda provoca un recorregut complet de les taules filles. És una de les causes clàssiques d'"esborrar una fila triga 40 segons" en bases de dades grans, i el motor no t'avisa: la consulta que va lenta no és la que has escrit.
Nota de dialecte: això no és així a tots els motors. MySQL amb InnoDB crea un índex automàticament sobre cada columna de clau forana si no n'existeix ja un que serveixi, i de fet ho exigeix. Oracle i SQL Server es comporten com PostgreSQL: no el creen, i la seva documentació oficial recomana crear-lo a mà. Si véns de MySQL, aquesta és probablement la diferència de rendiment que més et mossegarà en migrar.
La regla pràctica que t'endus —i que aplicaràs a la lliçó següent— és curta: gairebé tota columna de clau forana vol el seu índex. "Gairebé", perquè hi ha excepcions (una taula filla diminuta, o una FK per la qual mai no es filtra ni s'esborra), i aquestes excepcions són el tema de 08-03.
- El preu d'un índex
Si els índexs fossin gratis, la resposta seria indexar-ho tot. No ho són, i convé tenir el cost present des del primer minut:
| Cost | En què consisteix |
|---|---|
| Espai en disc | Un índex sobre una columna entera pot ocupar entre el 10 % i el 40 % de la mida de la taula. Cinc índexs poden ocupar més que les dades |
| Escriptures més lentes | Cada INSERT i cada DELETE actualitzen tots els índexs de la taula. Un UPDATE actualitza els de les columnes que toca (i, a PostgreSQL, sovint tots) |
| Feina del planificador | Més camins possibles per avaluar abans de decidir el pla |
| Manteniment | VACUUM i les còpies de seguretat tenen més objectes per recórrer |
La conseqüència és que un índex és una aposta: acceleres les lectures que l'utilitzen a canvi d'encarir totes les escriptures de la taula. En una taula que es llegeix mil vegades per cada escriptura, l'aposta és excel·lent. En una taula de registre d'esdeveniments que s'escriu constantment i es consulta un cop al mes, és un mal negoci. Quantificar aquesta aposta —i la llista de casos en què no s'ha d'indexar— és tota la lliçó 08-03.
Errors habituals i consells
- Creure que crear un índex garanteix que s'utilitzi. El planificador decideix. Amb taules petites, amb filtres poc selectius o amb estadístiques desfasades, triarà el
Seq Scani farà bé. - Pensar que l'índex conté la fila. Conté la clau i un punter. D'aquí el segon accés a la taula, l'
Index Scanenfront de l'Index Only Scani l'existència mateixa dels índexs cobridors. - Donar per fet que les claus foranes estan indexades. A PostgreSQL no ho estan. És el descobriment més rendible d'aquesta lliçó.
- Indexar una columna i continuar filtrant amb una funció al damunt.
LOWER(email)no fa servir l'índex sobreemail. O reescrius la consulta, o crees un índex sobre l'expressió (08-02). - Esperar miracles de
LIKE '%text%'. Cap B-tree no et pot ajudar sense prefix. La solució existeix, però és una altra família d'índexs (08-03). - Confondre el
ctidamb un identificador. Canvia a cadaUPDATE. Per identificar una fila hi ha la seva clau primària. - Consell: pensa en "un punt de l'ordre i avançar". Si la teva condició es pot traduir a això, l'índex serveix. Si no, no. És el millor filtre mental que existeix per predir un pla sense executar-lo.
- Consell: mira sempre
\d taulaabans de crear un índex. És la manera més ràpida de descobrir que ja n'existeix un d'equivalent. - Consell: memoritza la taula d'alçades. Saber que 15 milions de files són 3 accessos t'estalvia discussions senceres sobre si "la taula és massa gran per buscar-hi".
Exercicis
Exercici 1
Per a cada condició, digues si un índex B-tree sobre la columna implicada es podria fer servir, i justifica-ho en una frase.
-- a)
WHERE p.preu BETWEEN 5 AND 15
-- b)
WHERE UPPER(c.ciutat) = 'VALÈNCIA'
-- c)
WHERE p.nom LIKE '%ecològic%'
-- d)
WHERE co.data_comanda >= '2025-01-01' AND co.data_comanda < '2026-01-01'
-- e)
WHERE lc.quantitat * lc.preu_unitari > 50
-- f)
ORDER BY p.preu DESC LIMIT 5Exercici 2
Sense executar res, respon:
- Quants índexs té avui la taula
linies_comandai sobre quines columnes? - Quines de les seves columnes són claus foranes i quines d'aquestes estan indexades?
DELETE FROM comandes WHERE id = 5va esborrar en cascada tres línies a la lliçó 01-06. Descriu què ha de fer el motor alinies_comandaper localitzar-les, i com canviaria amb 20 milions de línies.
Exercici 3
Un company proposa: "Com que els índexs acceleren les consultes, crearem un índex sobre cada columna de comandes: client_id, empleat_id, data_comanda, estat, metode_pagament i despeses_enviament."
- Dóna dos arguments tècnics en contra, fent servir el que has vist a les seccions 6 i 9.
- Quines d'aquestes sis columnes et semblen candidates raonables i quines no? Justifica-ho amb la naturalesa de les dades de BotigaVerda.
Solucions
Solució 1
| # | Fa servir l'índex? | Per què |
|---|---|---|
a) preu BETWEEN 5 AND 15 |
✅ Sí | Un rang és un punt d'entrada més un recorregut de les fulles enllaçades. Retornaria 10 dels 20 productes, així que el planificador podria preferir el Seq Scan de tota manera: utilitzable no és el mateix que utilitzat |
b) UPPER(c.ciutat) = 'VALÈNCIA' |
❌ No | L'índex desa ciutat, no UPPER(ciutat). Amb un índex sobre l'expressió, sí (08-02) |
c) nom LIKE '%ecològic%' |
❌ No | Sense prefix fix no hi ha punt d'entrada en l'ordre. Necessita pg_trgm + GIN (08-03) |
| d) Rang de dates del 2025 | ✅ Sí | La columna apareix nua i la condició és un rang. És la versió sargable d'EXTRACT(YEAR ...) = 2025, i retorna les mateixes 16 comandes |
e) quantitat * preu_unitari > 50 |
❌ No | Hi ha un càlcul entre dues columnes: no és un rang sobre cap d'elles |
f) ORDER BY preu DESC LIMIT 5 |
✅ Sí | Es llegeixen les 5 últimes entrades de l'índex en ordre invers i s'atura, sense ordenar res |
Solució 2
1. Un de sol: linies_comanda_pkey, un índex únic B-tree sobre id, creat per la PRIMARY KEY.
2. Té dues claus foranes, comanda_id (→ comandes, ON DELETE CASCADE) i producte_id (→ productes, ON DELETE RESTRICT), i cap de les dues no està indexada. És la taula amb més volum de l'esquema i la que apareix pràcticament a tots els JOIN del curs: és la primera candidata a índex de tota BotigaVerda.
3. Per aplicar el CASCADE, el motor ha de trobar totes les files amb comanda_id = 5. Sense índex sobre comanda_id, l'única manera és un recorregut seqüencial complet de linies_comanda: llegir les 47 files, quedar-se amb les 3 i esborrar-les. Amb 20 milions de línies, aquest mateix DELETE d'una fila a comandes obligaria a llegir 20 milions de files —i altre tant a devolucions, que també referencia comandes amb CASCADE—. La consulta lenta no seria la que has escrit, sinó la comprovació d'integritat que dispara: per això aquest cas és tan difícil de diagnosticar sense EXPLAIN (08-05).
Solució 3
1. Dos arguments:
- Cada índex encareix totes les escriptures de la taula. Amb sis índexs, un
INSERTacomandespassa d'actualitzar una estructura (la PK) a actualitzar-ne set. Un pic de comandes per Nadal es converteix en un problema d'escriptura que abans no existia. - Diversos d'aquests índexs no s'utilitzarien mai. Un índex només serveix si el planificador el tria, i per a això el filtre ha de ser selectiu.
estatté 5 valors per a 20 comandes imetode_pagamenten té 4: filtrar per un d'ells retorna una fracció enorme de la taula, i llegir-la sencera seqüencialment és més barat que anar a l'índex i tornar a la taula fila a fila.
2. El repartiment:
| Columna | Candidata? | Raó |
|---|---|---|
client_id |
✅ Sí | FK sense índex, i "les comandes d'aquest client" és la consulta més freqüent de l'aplicació |
data_comanda |
✅ Sí | Tots els informes filtren per rangs de data, i els rangs són el punt fort del B-tree |
empleat_id |
⚠️ Potser | FK sense índex, però 10 de 20 comandes tenen NULL i només hi apareixen 3 empleats: poc selectiva. Un índex parcial seria millor idea (08-02) |
estat |
❌ No a la lleugera | Baixa cardinalitat. Com a molt, un índex parcial sobre els estats minoritaris: les 6 comandes no lliurades |
metode_pagament |
❌ No | Quatre valors repartits; mai no és el filtre principal d'una consulta |
despeses_enviament |
❌ No | Ningú no busca comandes "per import de ports". És una columna que es mostra, no per la qual es filtra |
Fixa't en el criteri que hi apunta: no s'indexa una columna perquè existeixi, sinó perquè hi ha consultes reals que filtren, uneixen o ordenen per ella i retornen poques files. Aquest criteri és el fil de les tres lliçons següents.
Conclusió
Ja tens el model mental complet:
- Un índex és una còpia ordenada d'una columna més un punter a la fila, com l'índex alfabètic d'un llibre, tret que n'hi pot haver molts, cal mantenir-los vius i els utilitza el planificador, no tu.
- Sense índex només hi ha
Seq Scan, el cost del qual creix linealment. Un B-tree creix logarítmicament: amb un fanout d'unes 250 entrades per pàgina, 15 milions de files caben en 3 nivells, i els nivells alts viuen a la memòria cau. - L'índex desa la clau i el
ctid, així que normalment cal un segon accés a la taula: això separa l'Index Scande l'Index Only Scan, i d'aquí neix el concepte d'índex cobridor. - Un B-tree serveix per a igualtats, rangs,
IN, prefixosLIKE 'abc%',ORDER BY,MINiMAX; no serveix per aLIKE '%abc', per a funcions o càlculs sobre la columna filtrada ni per a<>. La columna ha d'aparèixer nua a la condició. - BotigaVerda ja té onze índexs que ningú no va crear —nou de les PK i dos dels
UNIQUE—, i li falten els de les seves onze claus foranes, perquè PostgreSQL no les indexa automàticament (MySQL/InnoDB sí). Això penalitza els JOIN i, molt en particular, els esborrats en cascada. - Un índex es paga en espai i en escriptures més lentes, així que és una aposta que cal guanyar.
A la lliçó 08-02, Creació i gestió d'índexs, passes a l'acció: la sintaxi completa de CREATE INDEX, la variant CONCURRENTLY que no bloqueja la taula, els índexs compostos i la regla del prefix per l'esquerra que decideix en quin ordre posar les columnes, els índexs parcials que només indexen les files que t'interessen, els índexs sobre expressions que tanquen el problema de LOWER(email), i com llistar-los, mesurar-ne la mida i detectar els que no fa servir ningú. Començant, és clar, per les onze claus foranes que acabes de descobrir sense cobrir.
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
