Components d'un SGBD
Visió general de l'arquitectura
Un Sistema Gestor de Bases de Dades (SGBD) modern és un programari extremament complex que integra desenes de subsistemes especialitzats. Malgrat la diversitat de motors (PostgreSQL, MySQL, SQL Server,...), tots comparteixen una arquitectura conceptual similar organitzada en capes.
flowchart TD
subgraph Clients["Capa de clients"]
APP["Aplicació web/mòbil"]
CLI["Client CLI\n(psql, mysql, sqlcmd)"]
GUI["Eina gràfica\n(DBeaver, pgAdmin)"]
end
subgraph Interface["Capa d'interfície"]
CONN["Gestor de connexions\n(pool, autenticació)"]
PROT["Protocol de xarxa\n(TCP/IP)"]
end
subgraph Processing["Capa de processament"]
PARSE["Parser SQL\n(anàlisi sintàctica)"]
SEM["Analitzador semàntic\n(resolució de noms)"]
OPT["Optimitzador de consultes\n(plans d'execució)"]
EXEC["Executor"]
end
subgraph Storage["Capa d'emmagatzematge"]
BUFMGR["Gestor de buffer\n(cache de pàgines)"]
TXMGR["Gestor de transaccions\n(MVCC / bloquejos)"]
STOREMGR["Motor d'emmagatzematge\n(pàgines, registres)"]
WAL["WAL / Redo Log"]
CATALOG["Catàleg del sistema\n(metadades)"]
end
subgraph Physical["Capa física"]
DATA["Fitxers de dades"]
LOG["Fitxers de log"]
IDX["Fitxers d'índex"]
end
Clients --> Interface --> Processing --> Storage --> Physical
style APP fill:#2563EB,stroke:#1d4ed8,color:#fff
style CLI fill:#2563EB,stroke:#1d4ed8,color:#fff
style GUI fill:#2563EB,stroke:#1d4ed8,color:#fff
style CONN fill:#2563EB,stroke:#1d4ed8,color:#fff
style PROT fill:#2563EB,stroke:#1d4ed8,color:#fff
style PARSE fill:#2563EB,stroke:#1d4ed8,color:#fff
style SEM fill:#2563EB,stroke:#1d4ed8,color:#fff
style OPT fill:#2563EB,stroke:#1d4ed8,color:#fff
style EXEC fill:#2563EB,stroke:#1d4ed8,color:#fff
style BUFMGR fill:#16A34A,stroke:#15803d,color:#fff
style TXMGR fill:#16A34A,stroke:#15803d,color:#fff
style STOREMGR fill:#16A34A,stroke:#15803d,color:#fff
style WAL fill:#16A34A,stroke:#15803d,color:#fff
style CATALOG fill:#7C3AED,stroke:#6d28d9,color:#fff
style DATA fill:#1e293b,stroke:#475569,color:#fff
style LOG fill:#1e293b,stroke:#475569,color:#fff
style IDX fill:#1e293b,stroke:#475569,color:#fff
style Clients fill:#1e3a8a,stroke:#3b82f6,color:#fff
style Interface fill:#1e3a8a,stroke:#3b82f6,color:#fff
style Processing fill:#1e3a8a,stroke:#3b82f6,color:#fff
style Storage fill:#14532d,stroke:#22c55e,color:#fff
style Physical fill:#0f172a,stroke:#475569,color:#fff
Cicle de vida d'una consulta SQL
Cada vegada que executem un SELECT, la consulta recorre totes les capes de l'arquitectura. El diagrama següent mostra el camí complet per a SELECT * FROM clients WHERE email = 'joan@example.com':
flowchart TD
CLI["Client\n(app / psql)"]
subgraph Interface["Capa d'interficie"]
CONN["Gestor de connexions\nAutentica i assigna worker"]
end
subgraph Processing["Capa de processament"]
PARSE["Parser SQL\nAnalisi sintactica"]
SEM["Analitzador semantic\nResol noms i privilegis"]
OPT["Optimitzador\nTria el pla mes eficient"]
EXEC["Executor\nSeq Scan / Index Scan / Join..."]
end
subgraph Storage["Capa d'emmagatzematge"]
BUF["Buffer Pool (RAM)\nCache de pagines"]
DISK["Disc\nFitxers de dades"]
end
CLI -->|"1. Envia consulta SQL"| CONN
CONN -->|"2. Consulta validada"| PARSE
PARSE -->|"3. Arbre sintactic"| SEM
SEM -->|"4. Arbre semantic"| OPT
OPT -->|"5. Pla d'execucio"| EXEC
EXEC -->|"6. Demana pagines"| BUF
BUF -->|"HIT: retorna des de RAM"| EXEC
BUF -->|"MISS: llegeix del disc"| DISK
DISK -->|"Pagina carregada"| BUF
EXEC -->|"7. Files resultat"| CLI
style CLI fill:#4f46e5,color:#fff,stroke:#3730a3
style CONN fill:#0891b2,color:#fff,stroke:#0e7490
style PARSE fill:#059669,color:#fff,stroke:#047857
style SEM fill:#059669,color:#fff,stroke:#047857
style OPT fill:#d97706,color:#fff,stroke:#b45309
style EXEC fill:#d97706,color:#fff,stroke:#b45309
style BUF fill:#7c3aed,color:#fff,stroke:#6d28d9
style DISK fill:#374151,color:#fff,stroke:#1f2937
On passa la major part del temps?
En un sistema ben configurat, el Buffer Pool absorbeix la gran majoria de les peticions (hit ratio > 95%). Quan el rendiment és dolent, l'EXPLAIN ANALYZE mostra quants "Buffer Hits" vs "Buffer Misses" hi ha hagut, i si el problema és la mida del buffer pool o la manca d'índexs.
Parser i analitzador semàntic
La primera fase del processament d'una consulta SQL és l'anàlisi sintàctica (parsing): el SGBD comprova que la sentència SQL està ben formada d'acord amb la gramàtica del llenguatge. Si hi ha un error de sintaxi (paraula reservada mal escrita, parèntesi no tancat), s'interromp el processament i es retorna un error al client.
A continuació, l'analitzador semàntic verifica que la consulta té sentit en el context de l'esquema actual: comprova que les taules i columnes referenciades existeixen, que els tipus de dades són compatibles i que l'usuari té els privilegis necessaris per executar l'operació.
Exemple d'error sintàctic:
Exemple d'error semàntic:
Optimitzador de consultes
L'optimitzador és un dels components més sofisticats d'un SGBD. La seva tasca és trobar el pla d'execució més eficient per a una consulta donada. Per a una consulta SQL, poden existir desenes o fins i tot centenars de plans d'execució equivalents (que produiran el mateix resultat), però amb costos molt diferents.
L'optimitzador basa les seves decisions en:
- Estadístiques del catàleg: Quantes files té cada taula, quina és la distribució dels valors en cada columna, quins índexs existeixen.
- Estimació de costos: El cost de cada operació (escaneig seqüencial, escaneig d'índex, hash join, nested loop join, sort, etc.) en termes de I/O de disc i CPU.
- Regles heurístiques: Per exemple, aplicar els filtres WHERE el més aviat possible per reduir el nombre de files a processar.
flowchart LR
SQL["Consulta SQL"] --> PARSE["Parser"]
PARSE --> TREE["Arbre sintàctic"]
TREE --> OPT["Optimitzador"]
STATS["Estadístiques\ndel catàleg"] --> OPT
OPT --> PLAN["Pla d'execució\n(òptim)"]
PLAN --> EXEC["Executor"]
EXEC --> RESULT["Resultat"]
Per veure el pla d'execució d'una consulta podeu usar EXPLAIN:
L'optimitzador no és perfecte
L'optimitzador pren decisions basades en estadístiques. Si les estadístiques estan desactualitzades (per exemple, si una taula ha crescut molt recentment), pot triar un pla subòptim. Per això és important executar ANALYZE (PostgreSQL), ANALYZE TABLE (MySQL) o EXEC DBMS_STATS.GATHER_TABLE_STATS('esquema','taula') (Oracle) periòdicament.
Executor
L'executor (o motor d'execució) és el component que porta a terme el pla d'execució triat per l'optimitzador. Opera sobre un conjunt d'operadors físics:
- Seq Scan: Llegeix totes les files d'una taula seqüencialment. Adequat quan s'espera llegir un gran percentatge de les files.
- índex Scan: Usa un índex B-tree per localitzar les files de forma eficient. Adequat quan s'espera llegir un petit percentatge de les files.
- Hash Join: Per a joins entre dues taules, construeix una taula hash amb la taula més petita i l'usa per trobar les coincidències amb la taula gran.
- Nested Loop Join: Per a cada fila de la taula exterior, busca les files coincidents a la taula interior (eficient si la taula interior té un índex).
- Sort: Ordena les files, necessari per a ORDER BY o per a certs algoritmes de join.
- Aggregate: Calcula funcions d'agregació (COUNT, SUM, AVG, etc.).
Gestor de buffer (Buffer Pool / Buffer Cache)
El gestor de buffer manté a la memòria RAM les pàgines de disc que s'han llegit recentment o que s'escriuen freqüentment. L'objectiu és minimitzar el nombre d'accessos al disc, que són ordres de magnitud més lents que els accessos a la RAM.
flowchart LR
EXEC["Executor"] --> BUF["Buffer Pool\n(RAM)"]
BUF -->|"pàgina en cache: hit"| EXEC
BUF -->|"pàgina no en cache: miss"| DISK["Disc\n(fitxers de dades)"]
DISK -->|"llegir pàgina"| BUF
Conceptes clau:
- Buffer hit: La pàgina sol·licitada ja està a la cache → molt ràpid.
- Buffer miss: La pàgina no està a la cache i s'ha de llegir del disc → lent.
- Hit ratio: Percentatge de peticions que es resolen amb un buffer hit. Un bon hit ratio seria >95%.
- Dirty page: Una pàgina que ha estat modificada a la cache però encara no s'ha escrit al disc.
- Algoritme LRU: Quan la cache està plena i cal carregar una nova pàgina, s'expulsa la pàgina Least Recently Used (la que fa més temps que no s'ha accedit).
La mida del buffer pool és un dels paràmetres de configuració més importants d'un SGBD:
- PostgreSQL:
shared_buffers(recomanat: 25% de la RAM del servidor) - MySQL/InnoDB:
innodb_buffer_pool_size(recomanat: 70-80% de la RAM dedicada) - SQL Server:
max server memory
Pàgines de dades i registres
Quan un SGBD emmagatzema dades al disc, no ho fa registre a registre ni byte a byte. La unitat mínima de lectura i escriptura al disc és la pàgina de dades (també anomenada bloc). Tota la comunicació entre el disc i la memòria RAM es fa en unitats de pàgines senceres.
Per què pàgines?
Llegir un sol byte del disc té un cost pràcticament igual que llegir 8 KB, perquè el temps dominant és el posicionament del capçal (en discs mecànics) o la latència d'accés (en SSD). Per això, els SGBD llegeixen i escriuen blocs de mida fixa: les pàgines.
Mida de pàgina per SGBD
| SGBD | Mida de pàgina per defecte | Configurable? |
|---|---|---|
| PostgreSQL | 8 KB | Només en compilació (--with-blocksize) |
| MySQL / InnoDB | 16 KB | Sí (innodb_page_size: 4K, 8K, 16K, 32K, 64K) |
| SQL Server | 8 KB | No (fix) |
| Oracle | 8 KB | Sí (DB_BLOCK_SIZE: 2K, 4K, 8K, 16K, 32K) |
| SQLite | 4 KB | Sí (PRAGMA page_size: 512 a 65536 bytes) |
Estructura interna d'una pàgina
Cada pàgina de dades té una estructura interna ben definida. Prenent com a referència PostgreSQL (8 KB = 8.192 bytes), una pàgina conté:
Pointer 2 → (offset, len, flags) ↓ Tuple 2
Pointer 3 → (offset, len, flags) ↓ Tuple 3
▼ creixen cap avall
Tuple 3: capçalera (23 B) + dades
Tuple 2: capçalera (23 B) + dades
Tuple 1: capçalera (23 B) + dades
pd_lower) i on comencen les tuples (pd_upper).
| Camp | Mida | Funció |
|---|---|---|
| pd_lsn | 8 bytes | Log Sequence Number (WAL) |
| pd_checksum | 2 bytes | Verificació d'integritat |
| pd_flags | 2 bytes | Flags de la pàgina |
| pd_lower | 2 bytes | Final dels item pointers ▼ |
| pd_upper | 2 bytes | Inici de les tuples ▲ |
| pd_special | 2 bytes | Inici de l'espai especial |
Components principals:
- Capçalera (PageHeader): 24 bytes amb metadades de la pàgina: LSN (Log Sequence Number per al WAL), checksum, flags, i els punters
lower(final dels item pointers) iupper(inici de les tuples). - Item Pointers (Line Pointers): Un array de punters de 4 bytes cadascun. Cada punter indica l'offset i longitud d'una tupla dins la pàgina. Creixen de dalt cap avall.
- Espai lliure (Free Space): La zona entre els item pointers i les tuples. A mesura que s'insereixen registres, aquest espai es redueix.
- Tuples (registres): Les dades reals dels registres. Cada tupla té la seva pròpia capçalera (23 bytes a PostgreSQL) seguida dels valors dels camps. Creixen de baix cap amunt.
- Espai especial: Reservat per a índexs (B-tree, GiST...); en pàgines de dades normals (heap) ocupa 0 bytes.
Per què creixen en direccions oposades?
Els item pointers creixen de dalt cap avall i les tuples de baix cap amunt. Quan les dues zones es troben, la pàgina està plena. Això permet afegir tant punters com tuples sense necessitat de moure dades existents.
Estructura d'una tupla (registre)
Cada tupla dins d'una pàgina conté, a més de les dades de l'usuari, una capçalera amb informació de control:
| Exemple de taula | Dades útils | Overhead | % Overhead |
|---|---|---|---|
| clients (5 columnes) | 79 bytes | 27 bytes | 25% |
| relació M:N (2 int) | 8 bytes | 27 bytes | 77%! |
| log amb text llarg | 500 bytes | 27 bytes | 5% |
El pes de les capçaleres
Cada tupla ocupa com a mínim 23 bytes de capçalera + 4 bytes d'item pointer = 27 bytes d'overhead. Per a una taula amb files molt petites (p.ex. una taula de relació molts-a-molts amb dos enters de 4 bytes), l'overhead pot suposar el 75% de l'espai! Això és important a l'hora de planificar l'emmagatzematge.
Identificador de tupla: TID (ctid)
Cada fila en una taula PostgreSQL té un TID (Tuple Identifier) format per dues parts:
- Número de pàgina (dins del fitxer de la taula)
- Número d'item (posició dins de la pàgina)
(pàgina, item).L'identificador apunta directament a quin bloc físic s'ha d'anar a buscar l'item.
Item 2 (0,2): Joan Mas, Barcelona
...
Item 45 (0,45): Pere Roca, Blanes
Item 2 (1,2): Marc Vila, Figueres
...
ctid a qualsevol taula per veure la distribució real i el bloc des d'on s'estan llegint els registres.
| Consulta SQL | Resultat |
|---|---|
SELECT ctid, nom, ciutat |
(0,1) | Maria | Girona (0,2) | Joan | Barcelona (0,3) | Anna | Tarragona (0,4) | Pere | Blanes (0,5) | Laura | Blanes |
Visualització i càlcul de capacitat
La següent representació explora l'anatomia pràctica d'una pàgina de dades prenent la taula clients real com a cas d'estudi.
clients. La tupla conté els elements bàsics previament definits: id, nom, cognom, ciutat, email més la capçalera de registre obligatòria de PostgreSQL.
| Concepte | Mida calculada | Explicació matemàtica |
|---|---|---|
| Espai total útil | 8.168 B | 8.192 bytes de pàgina - 24 bytes de capçalera = 8.168 útils |
| Pes per tupla | ~106 B / fila | ~102 (dades + capçalera tupla) + 4 (item pointer associat) |
| Capacitat per pàgina | ~77 files | 8.168 bytes / 106 bytes per fila ≈ 77 files màxim per pàgina |
| Acumulat (15.420 f) | 200 pàgines | 15.420 files totals / 77 files/pàgina = 200 pàgines (1,6 MB) |
ctid=(0,3) sempre carrega o demana la pàgina 0 sencera alhora, cap registre es carrega de forma solitaria!
Com es distribueixen les taules en pàgines
8 KB
8 KB
8 KB
8 KB
200 pàgines × 8 KB = 1,6 MB
Seq Scan vs Index Scan: impacte en pàgines
Ara que entenem les pàgines, podem veure per què els índexos són tan importants:
WHERE ciutat = 'Blanes'. La majoria de pàgines no contenen files de Blanes, però s'han de llegir igualment.
idx_clients_ciutat, l'executor consulta l'índex per saber exactament quines pàgines contenen files amb ciutat = 'Blanes' i salta la resta.
| Mètode | Pàgines llegides | I/O de disc | Temps estimat |
|---|---|---|---|
| Seq Scan | 200 (100%) | 1,6 MB | ~50 ms |
| Index Scan | ~12 (6%) | 96 KB | ~3 ms |
La clau del rendiment
Un Seq Scan llegeix totes les pàgines de la taula, una per una. Un Index Scan usa l'índex B-tree per anar directament a les pàgines que contenen les files desitjades. Ara entenem per què l'optimitzador de la secció anterior triava l'Index Scan: 17 vegades menys I/O de disc.
Consulta pràctica: inspeccionar pàgines reals
PostgreSQL ofereix l'extensió pageinspect per examinar l'estructura interna de les pàgines:
AC0372/01/07 — Miniactivitat
RA1 · CA1.5
Connecteu-vos a un contenidor PostgreSQL, creeu una taula senzilla amb almenys 4 columnes i inseriu 200 registres. Després:
- Consulteu el
ctidde les primeres i últimes files. Quantes pàgines ha ocupat la taula? - Calculeu manualment quantes tuples per pàgina haurien de cabre (usant la mida de les columnes) i compareu-ho amb el resultat real.
- Extra: Instal·leu
pageinspecti examineu la capçalera de la primera pàgina. Quins valors tenenloweriupper?
Gestor de transaccions
El gestor de transaccions garanteix les propietats ACID de les transaccions. Usa dues tecnologies principals:
MVCC (Multi-Version Concurrency Control)
PostgreSQL i MySQL/InnoDB usen MVCC: en lloc de bloquejar les files quan s'actualitzen, es creen noves versions de cada fila. Una transacció de lectura veu un "snapshot" consistent de les dades en el moment en què va iniciar, independent de les modificacions que estan fent altres transaccions concurrents.
Avantatge: Les lectures no bloquegen les escriptures i viceversa, millorant molt la concurrència.
2PL (Two-Phase Locking)
Alguns motors (o parts d'ells) usen bloquejos (locks): per llegir o modificar un recurs, una transacció ha d'adquirir un bloqueig sobre ell. Altres transaccions que intentin accedir al mateix recurs han d'esperar.
Tipus de bloquejos:
- Shared lock (S): Per a lectures. Múltiples transaccions poden tenir un shared lock simultàniament.
- Exclusive lock (X): Per a escriptures. Només una transacció pot tenir un exclusive lock i no es pot tenir si ja hi ha un shared lock.
Deadlock
Un deadlock es produeix quan dues transaccions es bloquegen mutuament: la transacció A espera que la B alliberi un recurs, i la B espera que l'A n'alliberi un altre. Els SGBD detecten i resolen els deadlocks automàticament fent un ROLLBACK d'una de les transaccions, però la seva aparició freqüent indica un problema de disseny en les transaccions de l'aplicació.
WAL (Write-Ahead Log)
El WAL (Write-Ahead Log), també conegut com a redo log (MySQL/InnoDB) o transaction log (SQL Server), és un fitxer especial on el SGBD anota tots els canvis abans d'aplicar-los als fitxers de dades.
sequenceDiagram
participant TX as Transaccio
participant WAL as WAL (disc)
participant BUF as Buffer Pool (RAM)
participant DATA as Fitxer de dades (disc)
TX->>WAL: 1. Escriu el canvi al WAL (COMMIT)
WAL-->>TX: 2. Confirma que està escrit (durable)
TX-->>TX: 3. Retorna OK al client
BUF->>DATA: 4. Aplica el canvi al fitxer de dades (més tard)
Gràcies al WAL, si el servidor falla després del pas 3 però abans del pas 4, en reiniciar el SGBD pot llegir el WAL i reaplicar els canvis pendents. Això garanteix la durabilitat de les transaccions confirmades.
El WAL també s'usa per a la replicació: les rèpliques llegeixen el WAL del servidor primari per mantenir-se sincronitzades.
Catàleg del sistema
El catàleg del sistema (o diccionari de dades) és un conjunt de taules internes del SGBD que emmagatzemen les metadades de tots els objectes de la base de dades: taules, columnes, tipus de dades, índexs, vistes, usuaris, privilegis, estadístiques, etc.
-- Consultar taules del cataleg
SELECT tablename, tableowner, hasindexes
FROM pg_tables
WHERE schemaname = 'public';
-- Consultar columnes d'una taula
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'clients';
-- Estadístiques de l'optimitzador
SELECT relname, n_live_tup, last_analyze
FROM pg_stat_user_tables;
-- Consultar taules de l'usuari actual
SELECT table_name, num_rows, last_analyzed
FROM user_tables;
-- Consultar columnes d'una taula
SELECT column_name, data_type, nullable
FROM user_tab_columns
WHERE table_name = 'CLIENTS';
-- Estadístiques de l'optimitzador
SELECT table_name, num_rows, blocks, last_analyzed
FROM user_tab_statistics
WHERE table_name = 'CLIENTS';
AC0372/01/06 — Miniactivitat
RA1 · CA1.5
Connecteu-vos a un contenidor PostgreSQL i executeu les consultes del catàleg de l'exemple. Quantes taules internes del catàleg (pg_catalog) podeu identificar? Cerqueu a la documentació oficial per a què serveix la taula pg_stat_user_tables i quina informació us pot ser útil com a DBA.
Simulació: del SQL al resultat
La simulació següent mostra el recorregut complet d'una consulta SQL a través dels components interns d'un SGBD. Utilitza els botons per avançar pas a pas o prem Auto per veure-ho automàticament.
FROM clients
WHERE ciutat = 'Blanes'
ORDER BY cognom;
├─ TARGET_LIST → nom, cognom
├─ FROM_CLAUSE → clients
├─ WHERE_CLAUSE
│ └─ OP_EQUAL
│ ├─ COLUMN_REF → ciutat
│ └─ STRING_CONST → 'Blanes'
└─ SORT_CLAUSE → cognom ASC
| Objectiu detectat | Resolució (Catàleg del sistema) |
|---|---|
ID clients | Existeix com a Taula base (pg_class) ✓ |
Col nom, cognom, ciutat | Són columnes VARCHAR de la taula clients ✓ |
Operador = 'Blanes' | Tipus correctes (VARCHAR compatible amb string genèric) ✓ |
| Privilegis | L'usuari execució té garantits permisos de SELECT ✓ |
| Nom del Pla i Condició | Cost I/O Estimat | Estat |
|---|---|---|
| Seq Scan: Llegeix les 15.420 files directament senceres demanant a sistema arxius sencers | ~850.0 | Descartat |
Index Scan (escollit): Sap que hi ha idx_clients_ciutat; accés filtrat i precís a memòria | ~48.0 | MÉS EFICIENT |
> EXPLAIN SELECT nom, ...;Sort (cost=72.45..74.57 rows=847 width=100)
Sort Key: cognom
-> Index Scan using idx_clients_ciutat on clients ...
Index Cond: (ciutat = 'Blanes')