Tanquem el mòdul 2 amb biblioredb en molt bona forma: set taules, claus foranes que impedeixen els orfes, informes de gestió que quadren. Aquest és el nucli del sistema i ho continuarà sent. Però tan bon punt la direcció de BiblioRed va aprovar el nou portal per als socis, van aparèixer quatre necessitats que l'esquema relacional no encaixa amb comoditat: els lectors volen publicar ressenyes, el catàleg vol mostrar portades, sinopsis i etiquetes de materials que ja no són només llibres, l'equip vol registrar cada consulta que es fa al cercador, i màrqueting vol recomanar títols a partir del que han llegit lectors semblants.
Ja a la lliçó 01-02 vam anticipar la decisió: aquesta part del sistema aniria a MongoDB. En aquesta lliçó justifiquem el perquè de debò. Veurem què significa realment "NoSQL" —i què no significa—, quines quatre característiques comparteixen totes les bases d'aquesta família, com s'escala en horitzontal mitjançant particionat i replicació, quin preu es paga per això, i farem les primeres passes pràctiques amb MongoDB creant la col·lecció ressenyes de BiblioRed.
En acabar sabràs per què existeix aquest moviment, no només com s'hi escriu una consulta. I —igual d'important— sabràs dir quan no convé fer-lo servir.
Contingut
- On s'encalla l'esquema relacional de BiblioRed
- Què significa "NoSQL" i què no significa
- Característica 1: esquema flexible (schema-on-read)
- Característica 2: orientació a l'agregat
- Característica 3: escalat horitzontal enfront de vertical
- Característica 4: distribució
- Com s'escala de debò (I): particionat o sharding
- Com s'escala de debò (II): replicació
- Què es paga a canvi
- Primeres passes amb MongoDB: instal·lació i
mongosh - La jerarquia de MongoDB comparada amb la relacional
- BSON, JSON i el camp
_id - Primeres operacions:
insertOne,insertMany,find - Quan NO fer servir NoSQL
- Errors habituals i consells
- Exercicis
- Conclusió
- On s'encalla l'esquema relacional de BiblioRed
Abans de parlar de tecnologia, mirem el problema real. Són tres situacions concretes.
Situació A: el catàleg enriquit
BiblioRed ja no presta només llibres. Al catàleg hi ha llibres, DVD, revistes i audiollibres, i cada tipus de material té metadades pròpies:
| Tipus | Metadades específiques |
|---|---|
| Llibre | ISBN, editorial, nombre de pàgines, enquadernació |
| DVD | director, durada, format d'imatge, subtítols, qualificació per edat |
| Revista | ISSN, número, volum, periodicitat |
| Audiollibre | narrador, durada, còdec, mida del fitxer |
Amb el model relacional hi ha tres sortides clàssiques, i cap no és agradable:
- Una taula ampla amb totes les columnes.
materialstindriaisbn,issn,director,narrador,durada,pagines,codec… i a cada fila la majoria estarien aNULL. En una fila de revista, el 70 % de les columnes sobren. L'esquema deixa de descriure la realitat i passa a ser una unió de realitats incompatibles. - Una taula per tipus.
llibres,dvds,revistes,audiollibres. Net sobre el paper, però qualsevol consulta del cercador ("tot el que sigui de Jules Verne") necessita unUNIONde quatre taules, i afegir un cinquè tipus de material —còmics, previstos per al 2027— significa una taula nova i tocar totes les consultes. - Entitat-atribut-valor (EAV). Una taula
atributs (material_id, clau, valor)ambvaloren text. Flexible, sí, però es perd el tipatge, es perden les restriccions i una fitxa completa exigeix pivotar vint files. És la solució que més vegades s'ha lamentat en la història del disseny de bases de dades.
Situació B: el servei de ressenyes canvia cada mes
La primera versió de les ressenyes era: text i una puntuació d'1 a 5. Durant el primer trimestre el producte va demanar, en aquest ordre:
- afegir etiquetes lliures que posa el lector (
"novella historica","per regalar"); - permetre votar si una ressenya ha estat útil, guardant qui va votar;
- permetre respostes d'un bibliotecari a una ressenya;
- afegir spoiler: sí/no i amagar el text per defecte si ho és.
Al món relacional, cadascun d'aquests canvis és un ALTER TABLE o una taula nova, amb la seva migració, la seva finestra de desplegament i la seva coordinació amb l'equip d'aplicació. Quatre canvis d'esquema en tres mesos sobre una taula que encara no té un format estable.
Situació C: el registre d'activitat
Cada vegada que un soci cerca al catàleg, obre una fitxa o filtra per autor, el portal vol registrar l'esdeveniment. Les estimacions de l'equip:
12.000 socis × ~1,5 sessions/setmana × ~18 esdeveniments/sessió ≈ 324.000 esdeveniments/setmana
≈ 17 milions/anyDisset milions de files l'any, escrites de manera contínua, que gairebé mai no es llegeixen fila a fila (es llegeixen agregades: "cerques més freqüents del mes"), que no necessiten claus foranes i que al cap de dos anys perden gairebé tot el seu valor. Ficar-les a la mateixa base transaccional que els préstecs significa fer créixer els índexs, allargar les còpies de seguretat i competir per la memòria cau d'una base que ha de respondre ràpid al taulell.
Cap de les tres situacions no és una fallada del model relacional: és una manca d'encaix. El model relacional brilla amb dades homogènies, molt relacionades entre si i amb regles d'integritat estrictes —exactament el que són els préstecs—. Aquestes tres situacions són una altra cosa.
- Què significa "NoSQL" i què no significa
El nom és, sincerament, dolent. Va néixer el 2009 com l'etiqueta d'una trobada tècnica a San Francisco i s'hi va quedar. La lectura que s'ha acabat imposant és "not only SQL": no en lloc de SQL, sinó a més de SQL.
Què sí que significa a la pràctica:
- Un conjunt de bases de dades que no fan servir el model relacional de taules, files i columnes com a estructura principal.
- Sistemes dissenyats des del primer dia per distribuir-se en diverses màquines.
- Models de dades alternatius: documents, parells clau-valor, famílies de columnes, grafs.
Què no significa, i convé desmuntar-ho ja:
| Mite | Realitat |
|---|---|
| "NoSQL vol dir que no hi ha llenguatge de consulta" | MongoDB té el seu llenguatge de consulta i el seu aggregation framework; Cassandra fa servir CQL, que s'assembla moltíssim a SQL; Neo4j fa servir Cypher. Algunes fins i tot accepten SQL directament. |
| "NoSQL vol dir que no hi ha esquema" | Vol dir que l'esquema no l'imposa el servidor, no que no existeixi. L'esquema existeix sempre: és al codi de l'aplicació. Ho veurem en detall a l'apartat 3. |
| "NoSQL substitueix les relacionals" | A la immensa majoria d'arquitectures reals hi conviuen. És exactament el que passarà a BiblioRed. |
| "NoSQL és més modern, per tant millor" | Són eines amb encaixos diferents. Una base documental per gestionar préstecs amb recàrrecs seria una mala decisió, i ho serà el 2030 igual que avui. |
| "NoSQL no té transaccions" | Va ser cert en molts productes durant anys. MongoDB té transaccions multidocument des del 2018. Hi tornarem a la lliçó 03-04. |
Una manera útil de veure-ho: les bases NoSQL renuncien deliberadament a algunes garanties del model relacional a canvi d'alguna cosa concreta —escalat, flexibilitat o eficiència en un tipus de consulta—. La pregunta que t'has de fer sempre no és "és moderna?", sinó "a què renuncia aquest producte i em surt a compte?".
- Característica 1: esquema flexible (schema-on-read)
És la diferència que més es nota el primer dia.
- Schema-on-write (relacional): l'esquema es defineix abans d'escriure. El servidor rebutja qualsevol dada que no encaixi. És el
CREATE TABLEde la lliçó 02-02 i les restriccions de la 02-06. - Schema-on-read (documental): s'escriu el que sigui i la interpretació passa en llegir. L'aplicació és qui sap quins camps espera.
Mira-t'ho amb el catàleg de BiblioRed. Dos documents de la mateixa col·lecció:
{
"_id": "MAT-0331",
"tipus": "llibre",
"titol": "El mapa del temps",
"isbn": "9788401339097",
"editorial": "Edicions Vallmar",
"pagines": 612
}{
"_id": "MAT-0742",
"tipus": "dvd",
"titol": "Cartografies del Mediterrani",
"director": "Aina Ferriol",
"durada_min": 94,
"subtitols": ["es", "ca", "en"]
}Conviuen a la col·lecció cataleg sense cap NULL, sense UNION, sense taula nova. Quan el 2027 arribin els còmics, s'insereixen documents amb "tipus": "comic" i camps dibuixant i numero_tom. Sense ALTER TABLE, sense migració, sense finestra de desplegament.
Ara la lletra petita, que importa molt:
L'esquema no ha desaparegut. S'ha mogut de lloc: del
CREATE TABLEal codi de l'aplicació. I allà no ho comprova ningú automàticament.
Si un desenvolupador escriu "durada" en uns documents i "durada_min" en d'altres, MongoDB accepta tots dos encantat i l'error apareix mesos després, en un informe que retorna la meitat de les dades. Per això a la lliçó 03-03 veurem la validació d'esquema ($jsonSchema), que permet recuperar part d'aquesta xarxa de seguretat de manera voluntària.
| Schema-on-write | Schema-on-read | |
|---|---|---|
| Qui valida | El servidor de base de dades | L'aplicació |
| Quan falla | En l'escriptura, immediatament | En la lectura, potser mesos després |
| Cost de canviar | ALTER TABLE + migració |
Escriure el camp nou |
| Documents heterogenis | Difícil (nuls o EAV) | Natural |
| Garantia de coherència | Alta i automàtica | La que hi posi l'equip |
- Característica 2: orientació a l'agregat
Aquest concepte és el que de debò explica NoSQL, i se subestima perquè sona abstracte. Anem-hi a poc a poc.
Un agregat és un conjunt de dades que l'aplicació tracta com una unitat: es llegeix junt, s'escriu junt i, normalment, s'esborra junt.
A BiblioRed, la fitxa d'una ressenya és un agregat: quan el portal mostra una ressenya, mostra alhora el seu text, la seva puntuació, les seves etiquetes, qui la va escriure i quants vots útils té. Mai no mostra "les etiquetes" soles.
En el model relacional, aquest agregat està escampat en diverses taules perquè la normalització ho exigeix (ho estudiarem formalment al mòdul 5). Reconstruir-lo exigeix JOIN:
SELECT r.text, r.puntuacio, s.nom, e.etiqueta
FROM ressenyes r
INNER JOIN socis s ON s.soci_id = r.soci_id
LEFT JOIN ressenyes_etiquetes e ON e.ressenya_id = r.ressenya_id
WHERE r.llibre_id = 331;Tres taules, un JOIN i una explosió de files (una fila per etiqueta) que l'aplicació ha de tornar a plegar en memòria. En el model documental, l'agregat és el document:
{
"_id": "RES-1001",
"llibre_id": 331,
"titol_llibre": "El mapa del temps",
"soci": { "soci_id": 14, "nom": "Marta Alsina" },
"puntuacio": 5,
"text": "Una novel·la que juga amb el temps sense marejar el lector.",
"etiquetes": ["novella historica", "ciencia ficcio", "recomanada"],
"vots_utils": 7,
"data": "2026-03-14"
}Una sola lectura, un sol objecte, zero JOIN. Aquest és el nucli de la proposta.
I aquí arriba la conseqüència que gairebé ningú no explica al principi: si la dada es llegeix junta, també es pot guardar junta al disc. I si és junta al disc, es pot moure sencera a una altra màquina. L'agregat és la unitat natural de distribució, i per això l'orientació a l'agregat i l'escalat horitzontal són la mateixa idea vista des de dos angles.
També és la frontera natural de l'atomicitat: a MongoDB, l'escriptura d'un document complet és atòmica sense necessitat de transacció. Tot el que càpiga dins de l'agregat s'actualitza d'una peça. Tot el que quedi fora, no.
- Característica 3: escalat horitzontal enfront de vertical
Quan un sistema es queda curt hi ha dos camins.
- Escalat vertical (scale up): màquina més gran. Més CPU, més RAM, discos més ràpids. És el primer que es fa i funciona sorprenentment bé durant molt de temps.
- Escalat horitzontal (scale out): més màquines, cadascuna amb una part de la feina.
| Vertical | Horitzontal | |
|---|---|---|
| Com es creix | Se substitueix el servidor | S'afegeixen servidors |
| Sostre | Existeix i és dur: la màquina més gran del catàleg | Pràcticament il·limitat |
| Cost | Creix més que linealment (el doble de CPU costa bastant més del doble) | Aproximadament lineal |
| Complexitat | Baixa: l'aplicació no se n'assabenta | Alta: cal repartir i coordinar dades |
| Punt únic de fallada | Sí | No, si hi ha replicació |
| Aturada per créixer | Normalment sí | No |
El model relacional clàssic s'escala en vertical amb naturalitat, i en horitzontal amb dificultat: un JOIN entre dues taules que viuen en màquines diferents exigeix moure dades per la xarxa, i una transacció que toca diverses màquines exigeix protocols de compromís en dues fases, que són lents i fràgils. Les bases NoSQL eliminen per disseny les dues operacions problemàtiques —el JOIN del costat del servidor i la transacció distribuïda generalitzada— i a canvi es reparteixen sense fricció.
Un advertiment contra l'exageració de les presentacions comercials: BiblioRed té 12.000 socis i 17 milions d'esdeveniments l'any. Això cap de sobres en una sola màquina PostgreSQL ben configurada. La raó per la qual BiblioRed farà servir MongoDB no és el volum, és l'heterogeneïtat del catàleg i la velocitat de canvi del servei de ressenyes. Ser honest amb el motiu real forma part de l'ofici.
- Característica 4: distribució
La quarta característica és conseqüència de la tercera: aquests sistemes es van dissenyar partint de la base que viuran en diverses màquines, no com una extensió afegida després.
Això implica tres coses que es donen per descomptades a NoSQL i que en una relacional clàssica són projectes:
- Afegir un node és una operació rutinària, no una migració.
- La caiguda d'un node no és una caiguda del servei, si hi ha rèpliques.
- Les dades es col·loquen automàticament: el sistema decideix a quin node va cada dada i el reequilibra sol.
Els dos mecanismes que ho fan possible són el particionat i la replicació. Són diferents, resolen problemes diferents i es fan servir alhora. Anem-hi.
- Com s'escala de debò (I): particionat o sharding
Particionar (o fragmentar) és repartir el conjunt de dades entre diversos nodes, de manera que cada node guardi només una part. Objectiu: repartir volum i repartir càrrega d'escriptura.
La peça clau és la clau de partició (shard key): el camp el valor del qual decideix a quin fragment viu cada document.
flowchart TD
APP["Aplicació<br/>del portal BiblioRed"] --> R["Router / mongos<br/>consulta el mapa de fragments"]
R --> S1["Fragment A<br/>soci_id 1 – 4000<br/>~5,7 M esdeveniments"]
R --> S2["Fragment B<br/>soci_id 4001 – 8000<br/>~5,6 M esdeveniments"]
R --> S3["Fragment C<br/>soci_id 8001 – 12000<br/>~5,9 M esdeveniments"]
CFG["Servidors de configuració<br/>rangs → fragment"] -.-> R
Com funciona una operació:
- Escriptura d'un esdeveniment del soci 15 → el router calcula que 15 cau al fragment A → escriu només allà. Els altres dos fragments ni se n'assabenten, i per això les escriptures es reparteixen.
- Lectura filtrada per
soci_id: 15→ el router va directe al fragment A. És una consulta dirigida i és ràpida. - Lectura sense filtrar per la clau (per exemple, "esdeveniments de l'últim dia") → el router ha de preguntar als tres fragments i unir els resultats. És una consulta dispersa, i és cara.
D'aquí surt la regla pràctica més important del particionat:
Triar la clau de partició és triar quines consultes seran ràpides. Una mala clau converteix totes les consultes en disperses i fa el sistema més lent que una sola màquina.
Què caracteritza una bona clau de partició:
| Criteri | Per què importa | Exemple dolent a BiblioRed |
|---|---|---|
| Cardinalitat alta | Molts valors diferents = molts fragments possibles | tipus_material (només 4 valors: màxim 4 fragments) |
| Distribució uniforme | Evita que un fragment rebi gairebé tot | sucursal_id si la sucursal Centre concentra el 60 % de l'activitat |
| Sense monotonia creixent | Una clau sempre creixent envia totes les escriptures noves a l'últim fragment (hotspot) | data_esdeveniment en brut |
| Present a les consultes freqüents | Si no, totes les lectures són disperses | _id aleatori quan sempre es filtra per soci |
Dues estratègies de repartiment:
- Per rang: el fragment A guarda
soci_id1–4000, el B 4001–8000. Avantatge: les consultes per rang van a pocs fragments. Risc: desequilibri si els valors no es reparteixen bé. - Per hash: s'aplica una funció hash a la clau i el resultat decideix el fragment. Avantatge: repartiment molt uniforme. Inconvenient: les consultes per rang es tornen disperses, perquè valors contigus acaben en fragments diferents.
I una nota de realisme: BiblioRed no particionarà res. Els seus volums caben folgadament en un sol node. El particionat s'estudia per entendre la proposta arquitectònica de NoSQL i per reconèixer quan farà falta, no perquè s'hagi d'activar el primer dia. Activar-lo sense necessitat afegeix complexitat operativa a canvi de res.
- Com s'escala de debò (II): replicació
Replicar és mantenir còpies completes del mateix conjunt de dades en diversos nodes. Objectiu: sobreviure a fallades i, secundàriament, repartir càrrega de lectura.
El model dominant és primari-secundaris. A MongoDB s'anomena conjunt de rèpliques (replica set):
flowchart TD
APP["Aplicació"] -->|escriptures| P["PRIMARI<br/>accepta lectures i escriptures"]
APP -.->|lectures opcionals| S1
APP -.->|lectures opcionals| S2
P -->|replica l'oplog| S1["SECUNDARI 1<br/>còpia completa"]
P -->|replica l'oplog| S2["SECUNDARI 2<br/>còpia completa"]
S1 <-->|batec cada 2 s| S2
Regles del joc:
- Totes les escriptures van al primari. N'hi ha un de sol, i per això no hi ha conflictes d'escriptura.
- El primari registra cada canvi en un registre d'operacions (l'oplog), i els secundaris l'apliquen en el mateix ordre. Van uns mil·lisegons per darrere.
- Els nodes s'envien batecs cada pocs segons. Si el primari deixa de respondre, els secundaris trien un nou primari per votació i el servei continua. Aquest procés s'anomena failover i sol trigar entre 5 i 15 segons.
- Perquè hi hagi majoria en la votació convé un nombre senar de nodes. Tres és la configuració mínima sensata.
Què hi guanya BiblioRed: si el servidor que allotja les ressenyes s'apaga a les tres de la matinada, el portal deixa de funcionar uns segons i continua. Amb una sola màquina, deixa de funcionar fins que algú hi arribi.
I aquí apareix un matís que convé sembrar ja, encara que el desenvoluparem a la lliçó 03-04: si l'aplicació llegeix d'un secundari, pot llegir una dada lleugerament endarrerida. Una ressenya que Marta Alsina acaba de publicar podria no aparèixer-li a Ivan Pereda durant uns mil·lisegons. Aquest fenomen s'anomena consistència eventual, i és la contrapartida del repartiment. A 03-04 l'estudiarem juntament amb el teorema CAP i amb el contrast entre ACID i BASE.
| Particionat (sharding) | Replicació | |
|---|---|---|
| Què guarda cada node | Una part de les dades | Totes les dades |
| Problema que resol | Volum i càrrega d'escriptura | Disponibilitat i càrrega de lectura |
| Si cau un node | Es perd accés a aquella part | No passa res: hi ha còpies |
| Decisió clau | La clau de partició | Quants nodes i des d'on es llegeix |
| Es fan servir junts? | Sí: en producció, cada fragment és alhora un conjunt de rèpliques |
- Què es paga a canvi
Cap d'aquests avantatges no és gratis. Aquestes són les quatre factures, i cal veure-les abans de signar.
9.1 No hi ha JOIN del costat del servidor
Al mòdul 2 vam creuar set taules amb una consulta. En una base documental, si la informació que necessites és en dues col·leccions, tens tres opcions i totes tres tenen cost:
- Duplicar la dada dins del document (l'habitual).
- Fer dues consultes des de l'aplicació i combinar-les en memòria.
- Fer servir
$lookup, l'operació de MongoDB semblant a unLEFT JOIN, que existeix però és lenta i no està pensada per fer-se servir a tot arreu (a la lliçó 03-03 la veurem com a antipatró quan se n'abusa).
9.2 No hi ha integritat referencial declarativa
Tot el mòdul 2 es va tancar explicant que la base de dades és l'última línia de defensa. A MongoDB aquesta línia no existeix: pots guardar una ressenya amb llibre_id: 9999 sense que ningú no protesti, encara que aquell llibre no existeixi. La responsabilitat passa íntegra a l'aplicació.
9.3 Duplicació deliberada de dades
Fixa't en el document de ressenya de l'apartat 4: guarda "titol_llibre": "El mapa del temps" i "nom": "Marta Alsina". Això és informació que també és a llibres i a socis. Està duplicada expressament, per poder pintar la ressenya sense anar a buscar res més.
I aquesta duplicació té conseqüències: si la Marta es casa i canvia de cognom, hi ha 34 ressenyes seves amb el cognom antic. Cal actualitzar-les totes? De vegades sí (nom per mostrar) i de vegades no (el nom en el moment de la ressenya és una dada històrica legítima). És una decisió de disseny conscient, i la tractarem a fons a 03-03.
9.4 La coherència es trasllada a l'aplicació
Resumit en una taula, perquè quedi clar qui fa què:
| Responsabilitat | Relacional | Documental |
|---|---|---|
| Que els tipus siguin correctes | Servidor (CREATE TABLE) |
Aplicació |
| Que no faltin camps obligatoris | Servidor (NOT NULL) |
Aplicació |
| Que no hi hagi duplicats | Servidor (UNIQUE) |
Servidor (índex únic) o aplicació |
| Que les referències existeixin | Servidor (FOREIGN KEY) |
Aplicació |
| Que la dada duplicada estigui al dia | No aplica (no es duplica) | Aplicació |
| Que diverses escriptures siguin tot o res | Servidor (transacció) | Dins del document: servidor. Entre documents: transacció explícita |
La conclusió operativa: NoSQL no elimina feina, la mou de la base de dades al codi. Si l'equip és disciplinat i les revisions de codi són serioses, el tracte surt bé. Si no, surt molt malament.
- Primeres passes amb MongoDB: instal·lació i
mongosh
mongoshAnem a la pràctica. Necessites un servidor MongoDB i el seu client de línia d'ordres, mongosh.
Opció A: contenidor Docker (recomanada per aprendre)
És la via més neta: no embruta el sistema i s'esborra sencera quan acabis.
# Descarrega la imatge i arrenca un servidor al port 27017
docker run -d --name mongo-biblioRed -p 27017:27017 mongo:7
# Comprovar que està en marxa
docker ps --filter name=mongo-biblioRed --format "{{.Names}}\t{{.Status}}"Opció B: instal·lació local
# Debian / Ubuntu (després d'afegir el repositori oficial de MongoDB)
sudo apt install -y mongodb-org
sudo systemctl start mongod
sudo systemctl status mongod --no-pager | head -3● mongod.service - MongoDB Database Server
Loaded: loaded (/lib/systemd/system/mongod.service; enabled)
Active: active (running)Opció C: MongoDB Atlas
És el servei gestionat al núvol del mateix fabricant i té una capa gratuïta suficient per al curs. Et donen una cadena de connexió i t'hi connectes amb:
Comprovar que la shell funciona
En entrar veuràs una cosa així:
Current Mongosh Log ID: 66ab0f1c2d4e5f6a7b8c9d0e
Connecting to: mongodb://127.0.0.1:27017/
Using MongoDB: 7.0.11
Using Mongosh: 2.2.6
test>Aquest test> és l'indicador: ets a la base de dades test. I aquí arriba el detall més agradable de mongosh: és un intèrpret de JavaScript complet. Pots declarar variables, fer servir bucles i cridar funcions. No és un llenguatge a part com SQL: són crides a mètodes d'objectes.
- La jerarquia de MongoDB comparada amb la relacional
La correspondència mental que necessites és aquesta:
| PostgreSQL | MongoDB | Comentari |
|---|---|---|
| Servidor / clúster | Servidor / deployment | Un procés escoltant en un port |
| Base de dades | Base de dades | Mateix concepte |
| Taula | Col·lecció | Conjunt de documents, sense esquema imposat |
| Fila | Document | Estructura tipus JSON, pot imbricar |
| Columna | Camp | Pot faltar en uns documents i ser-hi en d'altres |
| Clau primària | Camp _id |
Obligatori i únic, generat si no el poses |
| Índex | Índex | Mateix concepte i mateixa finalitat (mòdul 6) |
JOIN |
$lookup |
Existeix, però no és el camí habitual |
| Esquema (DDL) | (res equivalent obligatori) | Validació opcional amb $jsonSchema (03-03) |
flowchart LR
subgraph REL["PostgreSQL — biblioredb"]
T1["taula socis"] --> F1["fila: soci_id 14, Marta Alsina"]
T2["taula prestecs"] --> F2["fila: prestec_id 902"]
end
subgraph DOC["MongoDB — bibliored"]
C1["col·lecció ressenyes"] --> D1["document:<br/>{_id, soci:{...}, etiquetes:[...]}"]
C2["col·lecció cataleg"] --> D2["document:<br/>{_id, tipus, metadades:{...}}"]
end
Un detall pràctic que sorprèn qui ve de SQL: les bases i les col·leccions es creen soles. No hi ha CREATE DATABASE ni CREATE TABLE. N'hi ha prou de seleccionar una base i inserir; MongoDB la materialitza en el primer document escrit.
bibliored no hi apareix. És normal: apareixerà després de la primera inserció.
- BSON, JSON i el camp
_id
_idBSON
Els documents s'escriuen amb aspecte de JSON, però MongoDB els guarda internament en BSON (Binary JSON). Les diferències importen:
| JSON | BSON | |
|---|---|---|
| Format | Text | Binari |
| Tipus numèrics | Un sol tipus number |
int32, int64, double, decimal128 |
| Dates | No existeixen (es fan servir cadenes) | Tipus Date natiu |
| Dades binàries | No (cal codificar en base64) | Tipus BinData |
| Recorregut | Cal analitzar tot el text | Porta longituds: pot saltar camps |
| Mida | Més compacte en text pla | Una mica més gran, però molt més ràpid de recórrer |
Conseqüència pràctica: fes servir els tipus natius. Una data guardada com a cadena "2026-03-14" no es pot comparar per rang ni agrupar per mes de manera fiable; guardada com a ISODate sí.
// Malament: la data és una cadena
{ data: "2026-03-14" }
// Bé: la data és un tipus Date de BSON
{ data: ISODate("2026-03-14T10:25:00Z") }Compte també amb el límit de 16 MB per document. Sembla enorme —són unes 8.000 pàgines de text— però és el límit que fa inviable, per exemple, ficar dins del document d'un llibre popular els seus 40.000 esdeveniments de consulta. Tornarem a aquest límit a 03-03, perquè és el criteri que governa la decisió d'incrustar o referenciar.
El camp _id
Tot document té un camp _id que fa de clau primària:
- És obligatori: si no el poses, MongoDB el genera.
- És únic dins de la col·lecció, amb un índex creat automàticament que no es pot eliminar.
- És immutable: no es pot modificar després.
- Pot ser de qualsevol tipus:
ObjectId, cadena, número, fins i tot un document.
Per defecte és un ObjectId, un identificador de 12 bytes que es genera al client (no al servidor) i que conté la marca de temps de creació, un identificador de procés i un comptador. Això el fa únic sense coordinació entre màquines, que és justament el que necessita un sistema distribuït —a diferència del SERIAL de PostgreSQL, que exigeix un comptador central—.
Quan tinguis un identificador natural amb significat propi, fes-lo servir com a _id: t'estalvies un índex. Al catàleg de BiblioRed farem servir codis com ara "MAT-0331".
- Primeres operacions:
insertOne, insertMany, find
insertOne, insertMany, findCreem per fi la col·lecció ressenyes de BiblioRed. Totes les dades són fictícies.
Inserir un document
use bibliored
db.ressenyes.insertOne({
llibre_id: 331,
isbn: "9788401339097",
titol_llibre: "El mapa del temps",
soci: { soci_id: 14, nom: "Marta Alsina" },
sucursal_id: 1,
puntuacio: 5,
text: "Una novel·la que juga amb el temps sense marejar el lector. Molt recomanable.",
etiquetes: ["novella historica", "ciencia ficcio"],
vots_utils: 7,
spoiler: false,
data: ISODate("2026-03-14T10:25:00Z")
})Llegeix-ho amb calma, perquè hi ha tres coses noves respecte d'un INSERT de SQL:
- No hem creat res abans. Ni la base
bibliored, ni la col·leccióressenyes. Existeixen des d'aquesta línia. sociés un document imbricat. En SQL això serien dues columnes o una taula a part; aquí és un objecte dins de l'objecte.etiquetesés un array. El model relacional no admet valors múltiples en una cel·la —la primera forma normal ho prohibeix, i ho veurem a 05-02—. El model documental sí, i aquesta és una de les seves diferències més profundes.
Inserir diversos documents
db.ressenyes.insertMany([
{
llibre_id: 331,
isbn: "9788401339097",
titol_llibre: "El mapa del temps",
soci: { soci_id: 15, nom: "Ivan Pereda" },
sucursal_id: 2,
puntuacio: 3,
text: "Comença molt bé, però la part final se'm va fer llarga.",
etiquetes: ["novella historica"],
vots_utils: 2,
spoiler: false,
data: ISODate("2026-03-22T18:40:00Z")
},
{
llibre_id: 412,
isbn: "9788401337208",
titol_llibre: "Els pilars de la Terra",
soci: { soci_id: 16, nom: "Núria Bastos" },
sucursal_id: 3,
puntuacio: 5,
text: "Mil pàgines que passen volant. La construcció de la catedral enganxa.",
etiquetes: ["novella historica", "per regalar", "classic modern"],
vots_utils: 12,
spoiler: false,
resposta_bibliotecari: {
nom: "Equip Sucursal Sud",
text: "Si t'ha agradat, tenim disponible la continuació a la sucursal Sud.",
data: ISODate("2026-03-25T09:10:00Z")
},
data: ISODate("2026-03-24T12:05:00Z")
},
{
llibre_id: 412,
isbn: "9788401337208",
titol_llibre: "Els pilars de la Terra",
soci: { soci_id: 14, nom: "Marta Alsina" },
sucursal_id: 1,
puntuacio: 4,
text: "Molt entretinguda, tot i que alguns personatges són massa plans.",
etiquetes: ["novella historica"],
vots_utils: 4,
spoiler: true,
data: ISODate("2026-04-02T20:15:00Z")
}
]){
acknowledged: true,
insertedIds: {
'0': ObjectId('66ab15c15c9e1b2f3d4a6c83'),
'1': ObjectId('66ab15c15c9e1b2f3d4a6c84'),
'2': ObjectId('66ab15c15c9e1b2f3d4a6c85')
}
}Observa que el tercer document té un camp, resposta_bibliotecari, que els altres no tenen. Ningú no ha protestat. Això és schema-on-read en acció: quan el producte va demanar les respostes del bibliotecari, no hi va haver ALTER TABLE; simplement van començar a escriure's documents amb aquell camp.
Llegir documents
find és l'equivalent de SELECT. Rep un document de filtre: cada camp és una condició i es combinen amb I lògic.
[
{ _id: ObjectId('...c82'), llibre_id: 331, titol_llibre: 'El mapa del temps', puntuacio: 5, ... },
{ _id: ObjectId('...c83'), llibre_id: 331, titol_llibre: 'El mapa del temps', puntuacio: 3, ... },
{ _id: ObjectId('...c84'), llibre_id: 412, titol_llibre: 'Els pilars de la Terra', puntuacio: 5, ... },
{ _id: ObjectId('...c85'), llibre_id: 412, titol_llibre: 'Els pilars de la Terra', puntuacio: 4, ... }
][
{ _id: ObjectId('...c82'), soci: { soci_id: 14, nom: 'Marta Alsina' }, puntuacio: 5, ... },
{ _id: ObjectId('...c83'), soci: { soci_id: 15, nom: 'Ivan Pereda' }, puntuacio: 3, ... }
]// Filtre sobre un camp imbricat: es fa servir la notació de punt entre cometes
db.ressenyes.find({ "soci.soci_id": 14 })[
{ _id: ObjectId('...c82'), titol_llibre: 'El mapa del temps', puntuacio: 5, ... },
{ _id: ObjectId('...c85'), titol_llibre: 'Els pilars de la Terra', puntuacio: 4, ... }
]// Filtre sobre un array: coincideix si QUALSEVOL element hi encaixa
db.ressenyes.find({ etiquetes: "per regalar" })[
{ _id: ObjectId('...c84'), titol_llibre: 'Els pilars de la Terra', soci: { soci_id: 16, nom: 'Núria Bastos' }, ... }
]Aquesta última consulta mereix un moment d'atenció. En SQL, per consultar etiquetes necessitaries una taula ressenyes_etiquetes, un JOIN i un DISTINCT. Aquí és un filtre d'igualtat sobre un array, i MongoDB entén automàticament que cal mirar a dins. És un exemple perfecte de què guanya l'orientació a l'agregat.
Els operadors de consulta complets ($gt, $in, $regex, projecció, ordenació, actualitzacions amb $set i $push, i l'aggregation pipeline) els veurem a la lliçó següent, 03-02, juntament amb les altres tres famílies NoSQL.
- Quan NO fer servir NoSQL
Aquesta secció és la més honesta de la lliçó i probablement la més útil en la teva carrera. Aquests són els senyals que la resposta correcta és una base relacional:
- Les dades són molt relacionades i les consultes són impredictibles. Si demà algú pot demanar "préstecs de socis de la sucursal Nord, de llibres en català publicats després del 2015, l'autor dels quals tingui un altre llibre reservat", vols SQL. Aquest és exactament el terreny on el model relacional no té rival.
- Necessites transaccions sobre diverses entitats com a norma, no com a excepció. Registrar un préstec toca
prestecsiexemplarsalhora i ha de ser tot o res. És un cas relacional de llibre. - La integritat és un requisit, no una preferència. Diners, recàrrecs, historials legals, dades regulades. Si un orfe és inacceptable, vols que ho impedeixi el servidor, no un
ifal codi. - El volum cap en una màquina. Que és gairebé sempre. Una sola instància de PostgreSQL amb maquinari corrent gestiona sense despentinar-se centenars de gigabytes i milers de transaccions per segon. Si aquesta és la teva escala, l'escalat horitzontal només t'aporta complexitat.
- L'equip no té experiència operant sistemes distribuïts. Un clúster mal operat és menys fiable que una màquina ben operada. La tecnologia no compensa la manca de rodatge.
- Els informes i l'anàlisi són l'ús principal. Les eines de BI, els quadres de comandament i els analistes parlen SQL. Portar les dades a un model documental per després haver-les de treure és feina en contra.
- "Perquè és el que es porta". És el pitjor motiu possible i, estadísticament, un dels més freqüents.
El consell per defecte, que repetirem amb més arguments a la lliçó 03-04: comença per la relacional i afegeix NoSQL quan tinguis un motiu concret que puguis escriure en una frase. BiblioRed la pot escriure: "el catàleg és heterogeni per tipus de material, les ressenyes canvien de forma cada mes i l'activitat és un volum alt d'escriptures que no necessita integritat referencial". Això és un motiu. "Volem modernitzar-nos" no ho és.
Errors Habituals i Consells
Error 1: creure que NoSQL vol dir "sense esquema" i no dissenyar res.
L'esquema existeix sempre; només canvia qui el vigila. Escriu l'esquema esperat de cada col·lecció a la documentació del projecte des del primer dia, encara que el servidor no ho exigeixi. A 03-03 veuràs com fer que el servidor també el vigili amb $jsonSchema.
Error 2: migrar tota la base relacional a MongoDB "per unificar".
És la decisió que més penediments ha produït en l'última dècada. BiblioRed no mou prestecs ni socis: allà el model relacional guanya. Només es mou el que encaixa malament.
Error 3: adoptar NoSQL "per rendiment" sense haver mesurat. Molts problemes atribuïts a "que la relacional és lenta" són en realitat la manca d'un índex o una consulta mal escrita. Abans de canviar de tecnologia, mesura i optimitza el que tens (mòdul 6, lliçó 03).
Error 4: guardar dates i números com a cadenes de text.
{ data: "14/03/2026" } i { puntuacio: "5" } funcionen en inserir i arruïnen qualsevol comparació, ordenació o agregació posterior. Fes servir ISODate(...) i números de debò. És l'error més comú i el més car d'arreglar a posteriori.
Error 5: triar la clau de partició amb la primera idea que se t'acudeixi. És la decisió més difícil de rectificar de tot el sistema. Abans de triar-la, escriu les cinc consultes més freqüents de l'aplicació i comprova quines serien dirigides i quines disperses.
Consell 1: fes servir un identificador natural com a _id quan n'hi hagi.
"MAT-0331" és més llegible que un ObjectId als registres d'errors i t'estalvia un índex addicional.
Consell 2: anomena els camps amb una convenció i respecta-la.
Tria snake_case o camelCase, escriu-ho al manual de l'equip i no ho barregis. Sense CREATE TABLE que hi posi ordre, la convenció és la teva única defensa contra durada / durada_min / durationMin a la mateixa col·lecció.
Consell 3: a mongosh tens JavaScript complet.
Per generar dades de prova, un bucle n'hi ha prou:
const docs = []
for (let i = 1; i <= 5; i++) {
docs.push({ esdeveniment: "cerca", terme: "verne", soci_id: 14, ordre: i })
}
db.activitat_proves.insertMany(docs)
db.activitat_proves.countDocuments()Consell 4: aprèn a llegir el resultat acknowledged: true.
Vol dir que el servidor ha confirmat l'escriptura. Quan a 03-04 veiem writeConcern, entendràs quin nivell de confirmació hi ha al darrere d'aquell true i per què es pot ajustar.
Exercicis
Exercici 1
BiblioRed vol guardar al catàleg un audiollibre i una revista. Justifica en tres o quatre frases per què això encaixa millor en una col·lecció documental que a la taula llibres de l'esquema relacional del mòdul 2, i després escriu les dues insercions a mongosh sobre una col·lecció cataleg, fent servir un _id natural de l'estil "MAT-0801". L'audiollibre és una versió de "El mapa del temps" narrada per Àlex Roure, de 14 h 20 min, en format MP3. La revista és "Vallmar Cultural", ISSN 2604-1188, número 42, volum 7, periodicitat mensual.
Exercici 2
Per al registre d'activitat de BiblioRed (17 milions d'esdeveniments l'any, consultats gairebé sempre com a "activitat d'un soci concret" i ocasionalment com a "cerques més freqüents del mes"), avalua aquestes tres candidates a clau de partició i tria'n una, justificant la decisió amb els quatre criteris de l'apartat 7:
- (a)
data_esdeveniment - (b)
tipus_esdeveniment(valors possibles:cerca,fitxa,filtre,descarrega) - (c)
soci_id
Exercici 3
Sobre la col·lecció ressenyes que has creat a l'apartat 13, escriu les consultes find que responguin a aquestes tres preguntes, i digues a més quin seria l'equivalent SQL aproximat de cadascuna:
- Totes les ressenyes escrites pel soci 14.
- Totes les ressenyes marcades com a spoiler.
- Totes les ressenyes de la sucursal 3 sobre el llibre 412.
Solucions
Solució 1
Justificació. Un audiollibre i una revista comparteixen amb el llibre només un grapat de camps (titol, idioma, any) i difereixen en tota la resta: narrador, durada i format_audio enfront d'issn, numero, volum i periodicitat. A la taula llibres caldria afegir set columnes que estarien a NULL a la immensa majoria de files, o crear dues taules noves que obligarien a un UNION a cada consulta del cercador. En una col·lecció documental, cada material porta només els camps que té sentit que porti, i afegir el tipus "còmic" el 2027 no exigirà cap canvi d'esquema ni cap migració.
use bibliored
db.cataleg.insertMany([
{
_id: "MAT-0801",
tipus: "audiollibre",
titol: "El mapa del temps",
idioma: "es",
obra_relacionada: { llibre_id: 331, isbn: "9788401339097" },
metadades: {
narrador: "Àlex Roure",
durada_min: 860,
format_audio: "MP3",
mida_mb: 742
},
etiquetes: ["novella historica", "audio"],
alta: ISODate("2026-05-11T09:00:00Z")
},
{
_id: "MAT-0802",
tipus: "revista",
titol: "Vallmar Cultural",
idioma: "ca",
metadades: {
issn: "2604-1188",
numero: 42,
volum: 7,
periodicitat: "mensual"
},
etiquetes: ["cultura local", "hemeroteca"],
alta: ISODate("2026-05-11T09:04:00Z")
}
])Fixa't que insertedIds retorna les cadenes que hi hem posat nosaltres, no ObjectId: en proporcionar un _id, MongoDB el respecta.
Solució 2
| Candidata | Cardinalitat | Distribució | Monotonia | A les consultes | Veredicte |
|---|---|---|---|---|---|
(a) data_esdeveniment |
Alta | Uniforme a la llarga | Sempre creixent | Només a la consulta mensual | Dolenta. Totes les escriptures d'avui caurien al mateix fragment: un punt calent permanent. És l'error de particionat més clàssic. |
(b) tipus_esdeveniment |
Molt baixa (4 valors) | Molt desigual (cerca seria la majoria) |
No | Gairebé mai no s'hi filtra | Dolenta. Màxim quatre fragments i un d'ells amb el 60 % de les dades. La cardinalitat baixa és descalificatòria. |
(c) soci_id |
Alta (12.000 valors) | Raonablement uniforme | No | Sí: és el filtre de la consulta freqüent | L'escollida. |
Decisió: soci_id. Compleix els quatre criteris i, sobretot, el quart: la consulta habitual ("activitat del soci 14") es converteix en una consulta dirigida a un sol fragment. La consulta mensual de cerques freqüents sí que serà dispersa, però és ocasional, s'executa fora d'hora punta i és agregada per naturalesa, així que el seu cost és assumible.
Una millora encara millor seria una clau composta { soci_id: 1, data_esdeveniment: 1 }: reparteix per soci i, dins de cada soci, manté els esdeveniments ordenats per data, cosa que accelera les consultes del tipus "activitat d'aquest soci l'últim mes".
I el recordatori realista: amb 17 milions de documents l'any, BiblioRed encara no necessita particionar. L'exercici serveix per saber quina clau triaria el dia que faci falta, no per activar-ho demà.
Solució 3
[
{ _id: ObjectId('...c82'), titol_llibre: 'El mapa del temps', puntuacio: 5, ... },
{ _id: ObjectId('...c85'), titol_llibre: 'Els pilars de la Terra', puntuacio: 4, ... }
]// 3. Ressenyes de la sucursal 3 sobre el llibre 412
db.ressenyes.find({ sucursal_id: 3, llibre_id: 412 })Detall important de la tercera: posar dos camps al document de filtre equival a AND. No hi ha un operador $and explícit en el cas normal; la conjunció és el comportament per defecte.
Conclusió
En aquesta lliçó hem creuat la frontera entre els dos mons del curs:
- L'esquema relacional de BiblioRed no falla, deixa d'encaixar en tres casos concrets: un catàleg amb metadades diferents per tipus de material, un servei de ressenyes la forma del qual canvia cada mes i un registre d'activitat de 17 milions d'esdeveniments anuals que no necessita integritat referencial.
- "NoSQL" vol dir not only SQL: bases que no fan servir el model relacional com a estructura principal i que neixen distribuïdes. No vol dir "sense llenguatge de consulta", ni "sense esquema", ni "substitut del relacional".
- Esquema flexible (schema-on-read): l'esquema no desapareix, es muda del servidor al codi de l'aplicació, amb la qual cosa es guanya agilitat i es perd una xarxa de seguretat.
- Orientació a l'agregat: la unitat de dades que es llegeix i s'escriu junta es guarda junta. D'aquí surten alhora l'absència de
JOINi la facilitat per distribuir. - Escalat horitzontal en lloc de vertical: més màquines en comptes d'una de més gran, amb sostre pràcticament il·limitat i cost aproximadament lineal, a canvi de complexitat operativa.
- Particionat: cada node guarda una part de les dades; la clau de partició ha de tenir cardinalitat alta, repartiment uniforme, absència de creixement monòton i presència a les consultes freqüents. Replicació: cada node guarda una còpia completa; el primari accepta les escriptures, els secundaris segueixen l'oplog i trien un nou primari si l'actual cau.
- El preu: sense
JOINdel costat del servidor, sense integritat referencial declarativa, amb duplicació deliberada de dades i amb la coherència traslladada a l'aplicació. NoSQL no elimina feina: la mou de la base de dades al codi. - MongoDB a la pràctica: servidor → base de dades → col·lecció → document; bases i col·leccions que es creen soles; BSON amb tipus natius (fes servir
ISODate, no cadenes) i límit de 16 MB per document; l'_idobligatori, únic, immutable i generat al client com aObjectId. AmbinsertOne,insertManyifindja hem creat i consultat la col·leccióressenyes, amb documents imbricats, arrays i camps que només tenen alguns documents. - Quan no fer-lo servir: dades molt relacionades amb consultes impredictibles, transaccions multientitat habituals, integritat com a requisit, volums que caben en una màquina, equip sense rodatge en sistemes distribuïts, càrregues analítiques… o la moda com a únic argument.
A la lliçó següent, 03-02, Tipus de Bases de Dades NoSQL, baixem al detall de les quatre famílies. Tornarem sobre el panorama que a 01-02 només vam insinuar, però aquesta vegada amb operació real: consultes i actualitzacions completes a MongoDB —inclosa la primera aggregation pipeline com a equivalent del GROUP BY de la lliçó 02-05—, memòria cau i caducitat a Redis, files amples i CQL a Cassandra, i recorreguts de diversos salts en Cypher sobre Neo4j per resoldre el "lectors com tu també van llegir" que necessita el motor de recomanacions de BiblioRed.
Fonaments de Bases de Dades
Mòdul 1: Introducció a les Bases de Dades
- Conceptes Bàsics de Bases de Dades
- Tipus de Bases de Dades
- Història i Evolució de les Bases de Dades
- Sistemes Gestors de Bases de Dades i Arquitectura
Mòdul 2: Bases de Dades Relacionals
- Model Relacional
- Llenguatge SQL
- Operacions Bàsiques en SQL
- Consultes Multitaula: JOIN i Subconsultes
- Agregació i Agrupació de Dades
- Integritat Referencial
Mòdul 3: Bases de Dades No Relacionals
- Introducció a NoSQL
- Tipus de Bases de Dades NoSQL
- Modelatge de Dades en NoSQL
- Comparació entre Bases de Dades Relacionals i No Relacionals
Mòdul 4: Disseny d'Esquemes
- Principis de Disseny d'Esquemes
- Diagrames Entitat-Relació (ER)
- Transformació de Diagrames ER a Esquemes Relacionals
- Tipus de Dades i Restriccions
Mòdul 5: Normalització
Mòdul 6: Transaccions, Rendiment i Seguretat
- Transaccions i Propietats ACID
- Concurrència i Nivells d'Aïllament
- Índexs i Optimització de Consultes
- Seguretat, Permisos i Còpies de Seguretat
Mòdul 7: Exercicis Pràctics
- Exercicis de SQL
- Exercicis de Disseny d'Esquemes
- Exercicis de Normalització
- Exercicis de Consultes Avançades i Transaccions
Mòdul 8: Casos d'Estudi
- Cas d'Estudi: Base de Dades Relacional
- Cas d'Estudi: Base de Dades No Relacional
- Cas d'Estudi: Persistència Poliglota
