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

  1. On s'encalla l'esquema relacional de BiblioRed
  2. Què significa "NoSQL" i què no significa
  3. Característica 1: esquema flexible (schema-on-read)
  4. Característica 2: orientació a l'agregat
  5. Característica 3: escalat horitzontal enfront de vertical
  6. Característica 4: distribució
  7. Com s'escala de debò (I): particionat o sharding
  8. Com s'escala de debò (II): replicació
  9. Què es paga a canvi
  10. Primeres passes amb MongoDB: instal·lació i mongosh
  11. La jerarquia de MongoDB comparada amb la relacional
  12. BSON, JSON i el camp _id
  13. Primeres operacions: insertOne, insertMany, find
  14. Quan NO fer servir NoSQL
  15. Errors habituals i consells
  16. Exercicis
  17. Conclusió

  1. 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:

  1. Una taula ampla amb totes les columnes. materials tindria isbn, issn, director, narrador, durada, pagines, codec… i a cada fila la majoria estarien a NULL. 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.
  2. 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 un UNION de quatre taules, i afegir un cinquè tipus de material —còmics, previstos per al 2027— significa una taula nova i tocar totes les consultes.
  3. Entitat-atribut-valor (EAV). Una taula atributs (material_id, clau, valor) amb valor en 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/any

Disset 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.

  1. 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è 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?".

  1. 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 TABLE de 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 TABLE al 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

  1. 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.

  1. 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 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.

  1. 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:

  1. Afegir un node és una operació rutinària, no una migració.
  2. La caiguda d'un node no és una caiguda del servei, si hi ha rèpliques.
  3. 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.

  1. 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_id 1–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.

  1. 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:

  1. Totes les escriptures van al primari. N'hi ha un de sol, i per això no hi ha conflictes d'escriptura.
  2. 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.
  3. 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.
  4. 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

  1. 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:

  1. Duplicar la dada dins del document (l'habitual).
  2. Fer dues consultes des de l'aplicació i combinar-les en memòria.
  3. Fer servir $lookup, l'operació de MongoDB semblant a un LEFT 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.

  1. Primeres passes amb MongoDB: instal·lació i mongosh

Anem 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}}"
mongo-biblioRed	Up 12 seconds
# Obrir la shell dins del contenidor
docker exec -it mongo-biblioRed mongosh

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)
mongosh

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:

mongosh "mongodb+srv://cluster0.exemple.mongodb.net/" --username alumne

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.

db.version()
7.0.11

  1. 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.

use bibliored
switched to db bibliored
// Encara no existeix físicament: no hi ha dades
show dbs
admin   40.00 KiB
config  12.00 KiB
local   72.00 KiB

bibliored no hi apareix. És normal: apareixerà després de la primera inserció.

  1. BSON, JSON i el camp _id

BSON

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—.

const id = new ObjectId()
id
id.getTimestamp()
ObjectId('66ab13a45c9e1b2f3d4a6c81')
ISODate('2026-08-02T09:14:12.000Z')

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".

  1. Primeres operacions: insertOne, insertMany, find

Creem 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")
})
{
  acknowledged: true,
  insertedId: ObjectId('66ab14b25c9e1b2f3d4a6c82')
}

Llegeix-ho amb calma, perquè hi ha tres coses noves respecte d'un INSERT de SQL:

  1. No hem creat res abans. Ni la base bibliored, ni la col·lecció ressenyes. Existeixen des d'aquesta línia.
  2. soci és un document imbricat. En SQL això serien dues columnes o una taula a part; aquí és un objecte dins de l'objecte.
  3. 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.

// Totes les ressenyes — equival a SELECT * FROM ressenyes
db.ressenyes.find()
[
  { _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, ... }
]
// Filtre per igualtat — WHERE llibre_id = 331
db.ressenyes.find({ llibre_id: 331 })
[
  { _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.

// Comprovació final
db.ressenyes.countDocuments()
show collections
4
ressenyes

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.

  1. 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:

  1. 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.
  2. Necessites transaccions sobre diverses entitats com a norma, no com a excepció. Registrar un préstec toca prestecs i exemplars alhora i ha de ser tot o res. És un cas relacional de llibre.
  3. 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 if al codi.
  4. 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.
  5. 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.
  6. 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.
  7. "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()
5

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:

  1. Totes les ressenyes escrites pel soci 14.
  2. Totes les ressenyes marcades com a spoiler.
  3. 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")
  }
])
{
  acknowledged: true,
  insertedIds: { '0': 'MAT-0801', '1': 'MAT-0802' }
}

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

// 1. Ressenyes del soci 14
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, ... }
]
-- Equivalent aproximat
SELECT * FROM ressenyes WHERE soci_id = 14;
// 2. Ressenyes marcades com a spoiler
db.ressenyes.find({ spoiler: true })
[
  { _id: ObjectId('...c85'), soci: { soci_id: 14, nom: 'Marta Alsina' }, spoiler: true, ... }
]
SELECT * FROM ressenyes WHERE spoiler = TRUE;
// 3. Ressenyes de la sucursal 3 sobre el llibre 412
db.ressenyes.find({ sucursal_id: 3, llibre_id: 412 })
[
  { _id: ObjectId('...c84'), soci: { soci_id: 16, nom: 'Núria Bastos' }, puntuacio: 5, ... }
]
SELECT * FROM ressenyes WHERE sucursal_id = 3 AND 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 JOIN i 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 JOIN del 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'_id obligatori, únic, immutable i generat al client com a ObjectId. Amb insertOne, insertMany i find ja 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.

© Copyright 2026. Tots els drets reservats