Tot el curs ha donat per fet que saps quines columnes necessites. I gairebé sempre és cert: una comanda té client, data, estat i ports, i això no canvia. Però hi ha dades que no tenen forma fixa. Un oli d'oliva té acidesa, varietat d'oliva i mètode d'extracció; una crema té ingredients, tipus de pell i volum; un raspall de bambú no té cap d'aquestes coses i sí que té tipus de cerra. Afegir una columna per atribut donaria una taula de vuitanta columnes gairebé totes nul·les, i muntar una taula de parells clau-valor —el clàssic EAV— converteix qualsevol consulta en un trencaclosques d'autounions.
Per a això PostgreSQL té JSONB: una columna que desa un document amb estructura pròpia, indexable i consultable amb SQL. Aquí es tanca la promesa de 08-03 (índexs GIN sobre JSON) i es cobreix el cas de la resposta d'una API, les preferències d'usuari o el registre d'esdeveniments. Veuràs com construir documents, com llegir-los, com modificar-los, com tornar-los a convertir en files per continuar fent servir tot el SQL del curs, i —el més important de la lliçó— què no ha d'anar mai dins d'un JSON.
Contingut
- Quan un esquema fix no basta
JSONenfront deJSONB- Construir documents
- La columna
productes.atributsde BotigaVerda - Accedir:
->,->>,#>,#>> - Buscar: contenció, existència i JSONPath
- Modificar documents
- Expandir a files: el pont de tornada al SQL
- Indexació amb GIN
- La discussió de disseny: què NO va en JSON
- Errors habituals i consells
- Exercicis
- Conclusió del mòdul
- Quan un esquema fix no basta
Quatre situacions en què el model relacional pur incomoda:
| Situació | Per què fa mal en columnes | Exemple a BotigaVerda |
|---|---|---|
| Atributs que varien per tipus d'element | Una columna per atribut, gairebé totes nul·les | Acidesa de l'oli, ingredients de la crema, certificacions ecològiques |
| Respostes d'una API externa | La seva forma la decideix un altre, i canvia sense avisar | El que retorna la passarel·la de pagament |
| Preferències i esdeveniments | Cada usuari i cada tipus d'esdeveniment porten dades diferents | Idioma, avisos; clics, errors, traces |
I les tres sortides clàssiques, amb el seu preu:
| Enfocament | Avantatge | Cost |
|---|---|---|
| Una columna per atribut | Tipatge, restriccions, índexs barats | Taula dispersa; cada atribut nou és un ALTER TABLE (05-06) |
Taula EAV (atribut_id, valor) |
Flexible, sense migracions | Tot és text; una consulta amb tres atributs són tres autounions |
Columna JSONB |
Flexible i consultable, amb índexs propis | Sense tipatge ni integritat referencial; fàcil de fer servir malament |
JSON enfront de JSONB
JSON enfront de JSONBPostgreSQL té dos tipus, i triar malament es paga.
JSON |
JSONB |
|
|---|---|---|
| Com es desa | Text literal, tal com el vas escriure | Binari descompost (arbre de claus i valors) |
| En escriure | Només valida la sintaxi: molt ràpid | Analitza i normalitza: una mica més lent |
| En llegir una clau | Reanalitza el text sencer cada vegada | Accés directe: molt més ràpid |
| Espais, format i ordre de claus | Es conserven | Es perden: es reordena internament |
| Claus duplicades | Es conserven totes | Es queda amb l'última |
Operadors @>, ?, @@ / índex GIN |
No / no | Sí / sí |
La regla pràctica: fes servir JSONB tret que necessitis conservar el text exacte. I aquest "tret que" és molt estret: bàsicament, desar la resposta literal d'un servei extern perquè cal verificar una signatura digital o reproduir-la byte a byte. Per a tota la resta, JSONB. La diferència es veu a l'instant:
SELECT '{"b": 1, "a": 2, "a": 3}'::json AS com_json,
'{"b": 1, "a": 2, "a": 3}'::jsonb AS com_jsonb;| com_json | com_jsonb |
|---|---|
| {"b": 1, "a": 2, "a": 3} | {"a": 3, "b": 1} |
El json desa el disbarat tal qual —clau a repetida inclosa—; el jsonb normalitza, ordena i es queda amb l'últim valor d'a.
- Construir documents
| Funció | Què fa | Exemple |
|---|---|---|
| Literal | Text amb ::jsonb |
'{"origen": "Espanya"}'::jsonb |
to_jsonb(x) |
Converteix qualsevol valor o fila a JSON | to_jsonb(p.*) → el producte sencer com a objecte |
jsonb_build_object(k, v, ...) |
Objecte amb claus i valors alterns | jsonb_build_object('id', p.id, 'preu', p.preu) |
jsonb_build_array(a, b, ...) |
Array a partir de valors solts | jsonb_build_array('bio', 'vega') |
jsonb_agg(expr) |
Agregat: ajunta moltes files en un array | Totes les línies d'una comanda |
jsonb_object_agg(k, v) |
Agregat: converteix files en parells clau-valor | {"Alimentació": 256.27, ...} |
El cas estrella —retornar una comanda sencera amb les seves línies imbricades en una sola fila, que és el que una API necessita— combina els tres últims:
SELECT jsonb_build_object(
'comanda_id', co.id, 'data', co.data_comanda, 'ports', co.despeses_enviament,
'client', jsonb_build_object('id', c.id, 'pais', c.pais,
'nom', c.nom || ' ' || c.cognoms),
'linies', (SELECT jsonb_agg(jsonb_build_object(
'producte', p.nom, 'quantitat', lc.quantitat,
'import', ROUND(lc.quantitat * lc.preu_unitari * (1 - lc.descompte), 2))
ORDER BY lc.id)
FROM linies_comanda AS lc
JOIN productes AS p ON p.id = lc.producte_id
WHERE lc.comanda_id = co.id)
) AS comanda
FROM comandes AS co JOIN clients AS c ON c.id = co.client_id
WHERE co.id = 1;{"data": "2025-03-04", "ports": 4.95, "comanda_id": 1,
"client": {"id": 1, "pais": "Espanya", "nom": "Lucía Martínez Soler"},
"linies": [{"import": 23.90, "quantitat": 2, "producte": "Oli d'oliva verge extra 500 ml"},
{"import": 11.70, "quantitat": 3, "producte": "Arròs integral ecològic 1 kg"},
{"import": 6.50, "quantitat": 2, "producte": "Infusió de camamilla ecològica 20 u"}]}Una fila, una columna, la comanda 1 completa amb els seus 42,10 € en tres línies. Fixa't en dues coses: jsonb_agg admet el seu propi ORDER BY dins dels parèntesis, i les claus surten desordenades respecte de com les vas escriure, perquè és jsonb. Això estalvia a l'aplicació la feina de reassemblar files planes en un objecte imbricat, i és la raó per la qual moltes API modernes retornen directament el que produeix la base de dades — l'enllaç amb 11-05.
I per a un resum compacte, jsonb_object_agg:
-- Compte: un agregat no es pot imbricar dins d'un altre, així que primer s'agrupa (10-02)
WITH per_categoria AS (
SELECT cat.nom, ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS total
FROM linies_comanda AS lc
JOIN productes AS p ON p.id = lc.producte_id
JOIN categories AS cat ON cat.id = p.categoria_id
GROUP BY cat.nom)
SELECT jsonb_object_agg(nom, total) AS facturacio FROM per_categoria;{"Begudes": 195.28, "Alimentació": 256.27, "Llar sostenible": 88.58,
"Higiene personal": 31.50, "Cosmètica natural": 156.32}Les cinc categories amb vendes i les seves xifres canòniques, en un sol valor.
- La columna
productes.atributs de BotigaVerda
productes.atributs de BotigaVerdaA partir d'aquí treballem amb una columna nova. No és canònica: és l'exemple d'aquesta lliçó i cap altre mòdul no la fa servir.
-- ⚠️ NO CANÒNICA: columna d'exemple de 10-06. Recarrega l'script de 01-06 en acabar.
ALTER TABLE productes ADD COLUMN atributs JSONB;
UPDATE productes SET atributs = '{"origen":"Espanya","acidesa":0.3,"varietat":"picual",
"extraccio":"en fred","certificacions":["ecologic-ue","sense-gluten"]}'::jsonb WHERE id = 1;
UPDATE productes SET atributs = '{"origen":"Espanya","tipus_gra":"integral","certificacions":["ecologic-ue"]}' WHERE id = 2;
UPDATE productes SET atributs = '{"origen":"França","volum_ml":50,"tipus_pell":"seca",
"ingredients":["àloe vera","oli de jojoba"],"certificacions":["cosmos-organic","vega"]}'::jsonb
WHERE id = 6;
UPDATE productes SET atributs = '{"origen":"Portugal","volum_ml":200,"tipus_pell":"normal",
"ingredients":["ametlla dolça","vitamina E"],"certificacions":["vega"]}'::jsonb WHERE id = 8;
UPDATE productes SET atributs = '{"origen":"Portugal","grau":"cerimonial","grams":30,"certificacions":["ecologic-ue","vega"]}' WHERE id = 15;
UPDATE productes SET atributs = '{"origen":"Alemanya","material":"bambú","cerres":"niló suau","certificacions":["vega"]}' WHERE id = 18;Sis productes amb atributs; els altres catorze tenen atributs a NULL, cosa normal en aquest tipus de columna i que les consultes hauran de tenir en compte.
- Accedir:
->, ->>, #>, #>>
->, ->>, #>, #>>Quatre operadors, i la diferència entre ells és la font d'errors número u amb JSON a PostgreSQL:
| Operador | Argument | Retorna | Exemple sobre el producte 1 |
|---|---|---|---|
-> |
Clau (text) o índex (enter) | jsonb |
atributs -> 'origen' → "Espanya" (amb cometes) |
->> |
Clau o índex | text |
atributs ->> 'origen' → Espanya |
#> |
Ruta: array de text | jsonb |
atributs #> '{certificacions,0}' → "ecologic-ue" |
#>> |
Ruta | text |
atributs #>> '{certificacions,0}' → ecologic-ue |
La regla mnemotècnica: la fletxa doble >> treu el valor "en cru", com a text. La simple continua retornant JSON, i això permet encadenar: atributs -> 'certificacions' ->> 0 baixa a l'array amb -> i en treu el primer element com a text amb ->>.
SELECT id, nom,
atributs -> 'origen' AS origen_jsonb,
atributs ->> 'origen' AS origen_text,
(atributs ->> 'volum_ml')::int AS volum_ml,
atributs -> 'certificacions' AS certificacions,
atributs #>> '{certificacions,0}' AS primera_cert
FROM productes
WHERE atributs IS NOT NULL
ORDER BY id;| id | nom | origen_jsonb | origen_text | volum_ml | certificacions | primera_cert |
|---|---|---|---|---|---|---|
| 1 | Oli d'oliva verge extra 500 ml | "Espanya" | Espanya | (null) | ["ecologic-ue", "sense-gluten"] | ecologic-ue |
| 2 | Arròs integral ecològic 1 kg | "Espanya" | Espanya | (null) | ["ecologic-ue"] | ecologic-ue |
| 6 | Crema facial d'àloe vera 50 ml | "França" | França | 50 | ["cosmos-organic", "vega"] | cosmos-organic |
| 8 | Oli corporal d'ametlles 200 ml | "Portugal" | Portugal | 200 | ["vega"] | vega |
| 15 | Te verd matcha cerimonial 30 g | "Portugal" | Portugal | (null) | ["ecologic-ue", "vega"] | ecologic-ue |
| 18 | Raspall de dents de bambú | "Alemanya" | Alemanya | (null) | ["vega"] | vega |
Tres coses que cal veure en aquesta taula. La primera: "Espanya" amb cometes no és Espanya; comparar atributs -> 'origen' = 'Espanya' falla perquè a l'esquerra hi ha un jsonb i a la dreta un text — cal fer servir ->>, o comparar contra '"Espanya"'::jsonb. La segona: tot el que surt de ->> és text i cal convertir-ho (::int, ::numeric) per comparar-ho com a número; si no, '200' < '50' és cert perquè compara alfabèticament. La tercera: una clau que no existeix retorna NULL, no un error — còmode, i perillós, perquè una clau mal escrita no es queixa.
- Buscar: contenció, existència i JSONPath
Els operadors anteriors extreuen; aquests filtren, i són els que aprofiten l'índex GIN.
| Operador | Es llegeix "…" | Exemple |
|---|---|---|
@> |
conté | atributs @> '{"origen":"Espanya"}' |
<@ |
està contingut a | '{"origen":"Espanya"}' <@ atributs |
? |
existeix la clau (o l'element, en un array) | atributs ? 'acidesa' |
?| / ?& |
existeix alguna / totes aquestes claus | atributs ?& ARRAY['origen','certificacions'] |
SELECT id, nom, atributs ->> 'origen' AS origen
FROM productes
WHERE atributs @> '{"certificacions": ["vega"]}'
ORDER BY id;| id | nom | origen |
|---|---|---|
| 6 | Crema facial d'àloe vera 50 ml | França |
| 8 | Oli corporal d'ametlles 200 ml | Portugal |
| 15 | Te verd matcha cerimonial 30 g | Portugal |
| 18 | Raspall de dents de bambú | Alemanya |
Quatre productes vegans. Fixa't en la potència de @>: busca dins d'un array sense desmuntar-lo i sense saber en quina posició és l'element. I SELECT id FROM productes WHERE atributs ? 'volum_ml' retorna 2 files —els productes 6 i 8, els únics amb aquella clau—, que és com es pregunta "quins productes tenen definit aquest atribut".
JSONPath
Per a condicions que @> no pot expressar —comparacions numèriques, filtres dins d'arrays, expressions— PostgreSQL 12 va afegir JSONPath, un llenguatge de rutes a l'estil XPath:
| Operador / funció | Què fa |
|---|---|
@? |
La ruta troba alguna cosa? Retorna booleà |
@@ |
L'expressió JSONPath és certa? |
jsonb_path_query(doc, ruta) |
Retorna tots els valors que hi casen, com a files |
jsonb_path_query_first / _array |
El primer / tots en un array |
SELECT id, nom FROM productes WHERE atributs @? '$.volum_ml ? (@ > 100)'; -- 1
SELECT DISTINCT jsonb_path_query(atributs, '$.certificacions[*]') #>> '{}' AS certificacio
FROM productes WHERE atributs IS NOT NULL ORDER BY 1; -- 2La primera retorna una fila, el producte 8 (oli corporal, 200 ml): el 6 en té 50 i la resta no té la clau. La segona en retorna cinc: cosmos-organic, ecologic-ue, sense-gluten, vega i —en afegir qualsevol producte nou— el que sigui que porti. Llegeix $.volum_ml ? (@ > 100) així: $ és l'arrel del document, .volum_ml baixa a aquella clau, ? (...) és un filtre i @ és el valor actual. Amb [*] es recorren tots els elements d'un array.
- Modificar documents
| Operació | Com | Exemple |
|---|---|---|
| Fusionar | || |
atributs || '{"stock_min": 10}'::jsonb — afegeix o trepitja les claus que coincideixin |
| Esborrar clau | - amb text |
atributs - 'acidesa' |
| Esborrar-ne diverses / per ruta | - amb array / #- |
atributs - ARRAY['acidesa','varietat'], atributs #- '{certificacions,1}' |
| Fixar un valor | jsonb_set(doc, ruta, valor [, crear]) |
Canvia si existeix; amb el 4t argument a true (per omissió) el crea |
| Inserir en un array | jsonb_insert(doc, ruta, valor [, després]) |
Afegeix sense trepitjar; falla si la ruta ja existeix |
UPDATE productes
SET atributs = jsonb_set(atributs, '{acidesa}', '0.25'::jsonb)
|| '{"revisat": true}'::jsonb
WHERE id = 1;
SELECT atributs ->> 'acidesa' AS acidesa, atributs ->> 'revisat' AS revisat
FROM productes WHERE id = 1;| acidesa | revisat |
|---|---|
| 0.25 | true |
I aquí el detall de rendiment que cal interioritzar: un UPDATE sobre una columna JSONB reescriu el document sencer. No existeix "actualitzar una clau"; PostgreSQL crea una versió nova de la fila completa amb el document complet (és MVCC, 09-02). Canviar un booleà en un document de 200 KB escriu 200 KB. La conseqüència pràctica: JSONB és per escriure poc i llegir molt. Si un camp s'actualitza constantment, aquell camp vol ser una columna.
Una nota fina: jsonb_set amb un NULL de SQL en qualsevol argument retorna NULL, no el document sense tocar — i com que és un UPDATE, esborra el document sencer sense avisar. Fes servir COALESCE o jsonb_set(COALESCE(atributs, '{}'::jsonb), ...) quan la columna pugui ser nul·la.
- Expandir a files: el pont de tornada al SQL
Aquest apartat és el que connecta JSON amb tota la resta del curs: convertir un document en files per poder agrupar, unir, ordenar i aplicar-hi funcions de finestra.
| Funció | Converteix | En |
|---|---|---|
jsonb_array_elements(doc) |
Un array JSON | Una fila per element (jsonb) |
jsonb_array_elements_text(doc) |
Un array JSON | Una fila per element (text) |
jsonb_each(doc) / jsonb_object_keys(doc) |
Un objecte | Una fila per parell (key, value) / per clau |
jsonb_to_record / jsonb_to_recordset |
Objecte / array d'objectes | Files amb columnes tipades |
SELECT cert.valor AS certificacio, COUNT(*) AS productes
FROM productes AS p
CROSS JOIN LATERAL jsonb_array_elements_text(p.atributs -> 'certificacions') AS cert(valor)
WHERE p.atributs ? 'certificacions'
GROUP BY cert.valor
ORDER BY productes DESC, certificacio;| certificacio | productes |
|---|---|
| vega | 4 |
| ecologic-ue | 3 |
| cosmos-organic | 1 |
| sense-gluten | 1 |
I aquí ja no hi ha res de JSON: és un GROUP BY del mòdul 4 sobre files normals, amb el LATERAL de 07-04 fent de pont. Aquest és exactament el punt: una vegada expandit, un document és una taula més, i tot el que has après en onze lliçons torna a aplicar-se. jsonb_to_recordset va un pas més enllà i produeix columnes tipades directament:
SELECT * FROM jsonb_to_recordset('[{"producte_id":1,"quantitat":2},{"producte_id":15,"quantitat":1}]'::jsonb)
AS t(producte_id INTEGER, quantitat INTEGER);| producte_id | quantitat |
|---|---|
| 1 | 2 |
| 15 | 1 |
És la forma canònica de rebre una llista de línies de comanda des d'una API i convertir-la en files inseribles amb un sol INSERT ... SELECT — molt més net que els dos arrays paral·lels de sp_confirmar_comanda a 10-04.
- Indexació amb GIN
Aquí es tanca la promesa de 08-03. Sense índex, cada consulta amb @> recorre la taula sencera i analitza cada document. Amb un índex GIN, no.
CREATE INDEX ix_productes_atributs ON productes USING GIN (atributs); -- jsonb_ops
CREATE INDEX ix_productes_atributs_pth ON productes USING GIN (atributs jsonb_path_ops); -- jsonb_path_opsjsonb_ops (per omissió) |
jsonb_path_ops |
|
|---|---|---|
| Què indexa | Cada clau i cada valor per separat | Un hash de la ruta completa fins al valor |
| Operadors que suporta | @>, <@, ?, ?|, ?&, @?, @@ |
Només @>, @? i @@ |
Mida / velocitat amb @> |
Més gran / bona | Força menor / millor |
El criteri: si només consultes per contenció (@>), jsonb_path_ops, que és més petit i més ràpid. Si necessites preguntar per existència de claus (?), no queda més remei que jsonb_ops. I recorda de 08-03 que un GIN es construeix i es manté a poc a poc: és l'índex d'"escriu poc, llegeix molt", que casualment és també el perfil d'una columna JSONB.
I la tercera via, que moltes vegades és la millor: un B-tree sobre una expressió quan la consulta va sempre per la mateixa clau.
CREATE INDEX ix_productes_origen ON productes ((atributs ->> 'origen'));
SELECT id, nom FROM productes WHERE atributs ->> 'origen' = 'Portugal';Retorna els productes 8 i 15. Un B-tree sobre (atributs ->> 'origen') és minúscul comparat amb un GIN, suporta rangs i ordenació, i és el correcte quan una clau concreta es consulta molt. Els parèntesis dobles són obligatoris: és un índex d'expressió, dels de 08-02 — i funciona perquè ->> és IMMUTABLE, la condició que 10-04 va explicar.
- La discussió de disseny: què NO va en JSON
El més important de la lliçó. JSONB és tan còmode que convida a ficar-ho tot a dins, i aquesta és una decisió que es paga durant anys.
El que NO ha d'anar en un JSON:
- El que es filtra o s'uneix constantment.
categoria_iddins d'un JSON converteix unJOINd'índex en una consulta que cal reescriure amb->>i convertir a cada ús. - El que té integritat referencial. Una clau forana no pot apuntar dins d'un document. Si deses
{"proveidor_id": 7}i esborren el proveïdor 7, ningú no t'avisa: acabes de perdre la garantia que 01-05 et donava de franc. - El que en realitat és una taula. Un array de mil comandes dins del document d'un client és una taula
comandesdisfressada, sense índexs propis, sense poder consultar-se per si sola i reescrita sencera amb cada compra. - El que té regles. No hi ha
NOT NULL, niUNIQUE, niCHECKdins d'un JSON: unpreuen JSON pot arribar a ser"car"i res no ho impedirà. I el que s'actualitza constantment, perquè cadaUPDATEreescriu el document sencer (apartat 7).
L'antipatró: fer servir la base de dades relacional com a magatzem de documents per mandra de modelar. Es reconeix per una taula amb
idi una columnadades JSONBque ho conté tot. Funciona el primer mes, i a partir del sisè cada consulta és un castell de->>i::numeric, res no té índex, res no té integritat i ningú no sap quines claus existeixen. Si saps quins camps hi ha, són columnes. Si de debò necessites un magatzem de documents, hi ha bases de dades que fan això molt millor.
I la taula que resol el dubte concret:
| Pregunta | Si la resposta és sí → |
|---|---|
| El tenen totes les files i saps què és? | Columna |
| Es filtra, s'ordena o s'uneix amb freqüència? | Columna |
Necessita NOT NULL, UNIQUE, CHECK o una FK? S'actualitza sovint per separat? |
Columna |
| És una llista d'entitats amb vida pròpia? Cal consultar-lo o agregar-lo per si sol? | Taula relacionada |
| Varia per tipus de fila i només es llegeix junt amb la resta? Ve de fora amb una forma que no controles? | JSON |
| És opcional, dispers i de baixa freqüència d'ús? | JSON |
A BotigaVerda, aplicat: preu, stock i categoria_id són columnes, sense discussió; les línies de comanda són una taula, no un array dins de comandes; i l'acidesa de l'oli o el tipus de pell d'una crema són JSON, perquè cada categoria té els seus i només es mostren a la fitxa del producte.
Nota de dialecte: MySQL 8 té un tipus
JSONbinari amb->i->>(amb la mateixa semàntica de PostgreSQL),JSON_EXTRACT,JSON_TABLEper expandir a files, i no té índexs sobre JSON: s'indexen columnes generades. SQLite porta l'extensió JSON1 compilada per omissió:json_extract(),json_each(), l'operador->>des de la 3.38, i tot desat com a text. SQL Server emmagatzema JSON enNVARCHARi el consulta ambJSON_VALUE,JSON_QUERYiOPENJSONper expandir a files, amb índexs sobre columnes calculades. Oracle té un tipusJSONnatiu des de la 21c i suporta JSONPath àmpliament. L'estàndard SQL:2016 defineix JSONPath, i per aixòjsonb_path_querys'assembla tant entre motors; la resta és dialecte pur.
Errors habituals i consells
- Confondre
->amb->>. El primer retornajsonb("Espanya", amb cometes) i el segontext(Espanya).atributs -> 'origen' = 'Espanya'no casa mai. - Comparar números sense convertir.
atributs ->> 'volum_ml' > '100'compara text:'50'és més gran que'100'. Converteix sempre:(atributs ->> 'volum_ml')::int. - Escriure malament una clau. No dona error: retorna
NULLi la fila desapareix en silenci. Comprova amb?quines claus existeixen de debò. I fer servirJSONen lloc deJSONB: sense operadors de contenció, sense índexs GIN i reanalitzant el text a cada accés.JSONBtret que necessitis el text literal exacte. - Oblidar que un
UPDATEreescriu el document sencer. En documents grans i actualitzacions freqüents, això és bloat i feina deVACUUM(09-02). jsonb_setsobre una columnaNULL. RetornaNULLi esborra el document.COALESCE(atributs, '{}'::jsonb).- Indexar amb GIN per defecte i no mesurar. Si només fas servir
@>,jsonb_path_opsocupa força menys; i si sempre consultes la mateixa clau, un B-tree sobre l'expressió guanya a tots dos. - Ficar en JSON alguna cosa que té clau forana. No hi ha integritat referencial dins d'un document, i no n'hi haurà mai. Consell: documenta les claus esperades. Un JSON sense esquema documentat és un camp de text lliure. Si el conjunt de claus és tancat, valida'l amb un
CHECK (atributs ?& ARRAY['origen'])o amb l'extensió de validació d'esquemes. - Consell: comença per columnes i mou a JSON només el que en sobri. A l'inrevés no funciona: treure d'un JSON tres anys de dades a columnes tipades és una migració cara.
- Consell:
jsonb_pretty(atributs)per llegir un document apsql. I\xper al mode expandit.
Exercicis
Exercici 1
Amb la columna atributs carregada com a l'apartat 4: (1) Llista els productes d'origen espanyol amb la seva acidesa, si en tenen. (2) Compta quants productes hi ha per origen, ordenats de més a menys. (3) Troba tots els que tenen àloe vera entre els seus ingredients, fent servir @>. (4) Afegeix la clau "revisat": false a tots els productes que tinguin atributs, sense trepitjar res del que ja hi ha.
Exercici 2
Màrqueting vol el catàleg en JSON per al web: un array amb un objecte per categoria, i dins de cadascun, el nom de la categoria i l'array dels seus productes actius amb id, nom i preu. Escriu-ho amb jsonb_agg i jsonb_build_object, i digues quants elements té l'array exterior.
Exercici 3
Decideix, amb la taula de l'apartat 10, on va cada dada i justifica-ho en una frase:
- La valoració mitjana d'un producte, que es mostra al llistat i permet ordenar.
- Les dimensions de l'embalatge (alt, ample, fons), que només coneixen alguns productes i només es fan servir en calcular els ports.
- L'historial de canvis de preu d'un producte.
- La resposta completa de la passarel·la de pagament en cobrar una comanda.
- El proveïdor d'un producte.
Solucions
Solució 1
-- 1
SELECT id, nom, (atributs ->> 'acidesa')::numeric AS acidesa
FROM productes WHERE atributs @> '{"origen":"Espanya"}' ORDER BY id;
-- 2
SELECT atributs ->> 'origen' AS origen, COUNT(*) AS productes
FROM productes WHERE atributs ? 'origen' GROUP BY 1 ORDER BY productes DESC, origen;
-- 3
SELECT id, nom FROM productes WHERE atributs @> '{"ingredients":["àloe vera"]}';
-- 4
UPDATE productes SET atributs = '{"revisat": false}'::jsonb || atributs
WHERE atributs IS NOT NULL;1 retorna dues files: l'oli (id 1) amb acidesa 0.25 —la que va deixar el jsonb_set de l'apartat 7— i l'arròs (id 2) amb NULL, perquè no té aquella clau. 2 retorna Espanya 2, Portugal 2, Alemanya 1 i França 1: quatre orígens per als sis productes amb atributs. 3 retorna una fila, la crema facial (id 6), i funciona perquè @> busca dins de l'array sense importar la posició. 4 afecta 6 files, i l'ordre del || és la clau: '{"revisat": false}' || atributs fa que guanyi atributs si la clau ja existís, mentre que atributs || '{"revisat": false}' la trepitjaria. És la diferència entre "afegeix si falta" i "força el valor".
Solució 2
SELECT jsonb_agg(jsonb_build_object(
'categoria', cat.nom,
'productes', (SELECT COALESCE(jsonb_agg(jsonb_build_object(
'id', p.id, 'nom', p.nom, 'preu', p.preu)
ORDER BY p.id), '[]'::jsonb)
FROM productes AS p
WHERE p.categoria_id = cat.id AND p.actiu)
) ORDER BY cat.id) AS cataleg
FROM categories AS cat;L'array exterior té 6 elements, un per categoria, inclosa Complements — que hi apareix amb "productes": [] perquè el seu únic producte, les Càpsules d'espirulina, està descatalogat (actiu = FALSE). Aquest COALESCE(..., '[]'::jsonb) no és cap adorn: sense ell, jsonb_agg sobre zero files retorna NULL i la clau sortiria com a null en lloc de com a array buit, cosa que trencaria qualsevol codi que recorri la llista. És exactament el problema de 04-04 amb SUM sobre el conjunt buit, ara en JSON.
Solució 3
| # | Dada | On | Per què |
|---|---|---|---|
| 1 | Valoració mitjana | Columna (o vista/materialitzada) | S'ordena i filtra per ella al llistat: dins d'un JSON no tindria cap índex útil |
| 2 | Dimensions de l'embalatge | JSON | Opcionals, disperses i només es llegeixen junt amb la resta del producte en calcular els ports |
| 3 | Historial de preus | Taula relacionada | És una llista d'entitats amb vida pròpia, que cal consultar i agregar per si sola: és l'auditoria_preus de 10-05 |
| 4 | Resposta de la passarel·la | JSON | Ve de fora amb una forma que no controles i que pot canviar sense avisar. Aquí fins i tot hi cap JSON en lloc de JSONB, si cal verificar una signatura sobre el text exacte |
| 5 | Proveïdor | Columna amb FK | Té integritat referencial: dins d'un document, ON DELETE RESTRICT no existeix |
Les cinc respostes surten d'aplicar tres preguntes: es filtra o s'ordena per això? (columna), té vida pròpia? (taula), és opcional, variable i de només lectura conjunta? (JSON).
Conclusió del mòdul
Tanques el mòdul amb l'última peça de la caixa d'eines:
JSONBenfront deJSON: binari descompost enfront de text literal.JSONBnormalitza claus, elimina duplicats, accedeix ràpid i és l'únic indexable i amb operadors de contenció. La regla:JSONBtret que necessitis el text exacte.- Construir:
to_jsonb,jsonb_build_object,jsonb_build_array, i sobretot els agregatsjsonb_aggijsonb_object_agg, que retornen la comanda 1 completa amb les seves tres línies i els seus 42,10 € en una sola fila — el cas estrella per a una API (11-05). - Accedir:
->i#>retornenjsonb;->>i#>>retornentext. Aquesta diferència és la font d'errors número u, juntament amb comparar números sense convertir-los i amb les claus inexistents que retornenNULLen silenci. - Buscar:
@>(contenció, que entra dins dels arrays),?,?|i?&(existència de claus), i JSONPath amb@?,@@ijsonb_path_queryper al que la contenció no pot expressar, com ara$.volum_ml ? (@ > 100). - Modificar:
||fusiona,-esborra,jsonb_setfixa ijsonb_insertafegeix — recordant que unUPDATEreescriu el document sencer, i que per aixòJSONBés per escriure poc i llegir molt. - Expandir a files amb
jsonb_array_elements,jsonb_eachijsonb_to_recordsetés el pont de tornada: una vegada expandit, un document és una taula més i torna a servir tot el SQL del curs — com el recompte de certificacions, ambvegaen 4 productes iecologic-ueen 3. - Indexar, tancant 08-03: GIN amb
jsonb_opssuporta tots els operadors i ocupa més;jsonb_path_opsnomés@>,@?i@@, però és més petit i més ràpid; i un B-tree sobre(atributs ->> 'clau')guanya a tots dos quan sempre consultes la mateixa clau. - I la discussió de disseny: fora del JSON tot el que es filtri o s'uneixi, tot el que tingui integritat referencial, tot el que sigui en realitat una taula, tot el que necessiti regles i tot el que s'actualitzi constantment. L'antipatró —fer servir una base relacional com a magatzem de documents per mandra de modelar— es paga amb interessos. Si saps quins camps hi ha, són columnes.
I amb això es tanca el mòdul 10. En sis lliçons has muntat la caixa d'eines completa: vistes que donen nom a una consulta i encapsulen una mètrica, amb les materialitzades per als informes cars; CTE que converteixen tres nivells de subconsultes en passos llegibles, i WITH RECURSIVE per recórrer l'organigrama sencer i la cadena de referits de tres salts; funcions de finestra que agreguen sense col·lapsar i resolen rànquings, acumulats, mitjanes mòbils i top N per grup; procediments on viu per fi la confirmació de comanda amb la seva atomicitat garantida; disparadors que apliquen sols les regles que un CHECK no abasta; i JSON per al que no cap en un esquema fix. Ja no estàs aprenent SQL: l'estàs fent servir.
El que falta no són més funcions, sinó context. Un sistema real no és una consulta ben escrita: és un conjunt de decisions sobre com s'organitza la feina, qui pot veure què, què es mesura i des d'on es crida. Al mòdul 11, Pràctica: casos d'ús reals, veuràs SQL en el seu entorn: els casos d'ús que apareixen una vegada i una altra en qualsevol projecte; les bones pràctiques d'escriptura, nomenclatura i manteniment que separen una base amb la qual es pot treballar d'una que fa por tocar; la seguretat —la injecció SQL i com s'evita de debò, els permisos i els rols que aquest mòdul ha anat remetent lliçó rere lliçó—; el SQL per a l'anàlisi de dades, on les funcions de finestra de 10-03 es converteixen en informes complets; i el SQL en el desenvolupament web, amb els ORM, el pool de connexions i el problema N+1. La caixa d'eines ja és plena; queda aprendre l'ofici.
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
