Els esquemes canvien. Sempre. Cal desar el telèfon dels clients, el VARCHAR(60) es queda curt, algú s'ha adonat que aquella columna estava mal anomenada, màrqueting vol una columna total a comandes per no recalcular-la a cada informe, i una regla de negoci que fins ara vivia al codi hauria de ser en un CHECK. La base de dades que vas crear a 05-01 i vas omplir a 05-02 no és un monument: és un organisme en producció que mutarà desenes de vegades.
ALTER TABLE és la instrucció que ho permet. La seva sintaxi s'aprèn en vint minuts. El que separa un professional d'un accident no és la sintaxi: és saber quines operacions bloquegen la taula i quines no, entendre que a PostgreSQL una migració es pot desfer amb ROLLBACK i a MySQL no, conèixer el patró que permet canviar un esquema sense aturar el servei, i acceptar la regla que regeix tota la indústria: cap canvi d'estructura s'escriu a mà en una consola de producció.
Aquesta lliçó cobreix les dues meitats, i tanca el mòdul.
⚠️ Avís de seguretat
ALTER TABLEés una operació destructiva i irreversible en els seus efectes sobre les dades. UnDROP COLUMNelimina una columna sencera i tot el seu contingut; unALTER COLUMN TYPEpot truncar valors; unADD CONSTRAINTpot rebutjar files existents.
- Executa tots els exemples sobre la teva base de dades de pràctiques (
botigaverda), mai en producció.- Fes una còpia de seguretat prèvia:
pg_dump -U curs_sql -d botigaverda -f copia.sql.- Treballa dins de
BEGIN…ROLLBACK: a PostgreSQL el DDL és transaccional i ho pots desfer.- En un sistema real, tot canvi d'esquema ha d'anar revisat pel responsable de la base de dades, provat abans en un entorn equivalent al de producció i aplicat com a migració versionada, mai a mà.
Contingut
ALTER TABLE: el mapa d'operacionsADD COLUMN, amb i senseDEFAULTDROP COLUMNRENAME COLUMNiRENAME TOALTER COLUMN TYPEi la clàusulaUSINGSET/DROP NOT NULLiSET/DROP DEFAULTADD/DROP CONSTRAINT, i la tècnicaNOT VALID+VALIDATE- Què bloqueja la taula i què no
- DDL transaccional: PostgreSQL davant de MySQL
- El patró expand/contract
- Migracions versionades
- Què fer i què no fer en producció
- Errors habituals i consells
- Exercicis
- Conclusió del mòdul
ALTER TABLE: el mapa d'operacions
ALTER TABLE: el mapa d'operacionsLes accions més habituals:
| Acció | Què fa |
|---|---|
ADD COLUMN col TIPUS [restriccions] |
Afegeix una columna |
DROP COLUMN col [CASCADE] |
Elimina una columna i les seves dades |
RENAME COLUMN vella TO nova |
Reanomena una columna |
RENAME TO nova_taula |
Reanomena la taula |
ALTER COLUMN col TYPE tipus_nou [USING expr] |
Canvia el tipus |
ALTER COLUMN col SET NOT NULL / DROP NOT NULL |
Afegeix o treu obligatorietat |
ALTER COLUMN col SET DEFAULT expr / DROP DEFAULT |
Afegeix o treu valor per omissió |
ADD CONSTRAINT nom ... |
Afegeix CHECK, UNIQUE, PRIMARY KEY o FOREIGN KEY |
DROP CONSTRAINT nom |
Elimina una restricció |
VALIDATE CONSTRAINT nom |
Valida una restricció declarada NOT VALID |
Un detall de sintaxi que estalvia molt de temps: diverses accions caben en una sola sentència, separades per comes. I això no és només elegància — és que la taula es bloqueja una vegada en lloc de N:
ALTER TABLE clients
ADD COLUMN telefon VARCHAR(20),
ADD COLUMN newsletter BOOLEAN NOT NULL DEFAULT FALSE; Table "public.clients"
Column | Type | Nullable | Default
-----------------+------------------------+----------+----------------------------------
id | integer | not null | generated by default as identity
nom | character varying(60) | not null |
cognoms | character varying(90) | not null |
email | character varying(120) | not null |
ciutat | character varying(80) | |
pais | character varying(60) | not null |
data_registre | date | not null | CURRENT_DATE
referit_per_id | integer | |
telefon | character varying(20) | |
newsletter | boolean | not null | falseAvís sobre els objectes d'aquesta lliçó.
clients.telefon,clients.newslettericomandes.totalsón exemples puntuals: no formen part de l'esquema canònic de BotigaVerda i no apareixeran als mòduls següents. Recarregabotigaverda.sqlen acabar.
ADD COLUMN, amb i sense DEFAULT
ADD COLUMN, amb i sense DEFAULT(Recarrega botigaverda.sql abans de continuar: els exemples d'aquest apartat tornen a afegir les dues columnes de l'anterior, aquesta vegada una a una.)
Sense DEFAULT
La columna neix nul·lable i totes les files existents queden amb NULL. És instantani independentment de la mida de la taula: PostgreSQL només anota la columna nova al seu catàleg, sense tocar ni un sol byte de dades.
| id | nom | cognoms | telefon |
|---|---|---|---|
| 1 | Lucía | Martínez Soler | (null) |
| 2 | Carlos | Ferrer Ibáñez | (null) |
| 3 | Marta | Sanchis Gil | (null) |
Amb DEFAULT
| sense_newsletter | total |
|---|---|
| 15 | 15 |
Les quinze files tenen FALSE. I aquí hi ha una de les millores més importants de PostgreSQL de l'última dècada:
Des de PostgreSQL 11, afegir una columna amb
DEFAULTconstant ja no reescriu la taula. Abans, aquesta operació recorria i reescrivia cada fila per gravar-hi el valor: en una taula de cent milions de files eren hores de bloqueig exclusiu — el cas d'estudi clàssic de "com tombar producció amb una línia d'SQL".Ara PostgreSQL desa el valor per omissió al catàleg i el retorna "al vol" per a les files antigues, escrivint-lo físicament només quan aquelles files s'actualitzen per un altre motiu. L'operació passa d'hores a mil·lisegons.
L'excepció, que continua sent cara:
-- ⚠️ Això SÍ que reescriu la taula sencera: el DEFAULT és volàtil
ALTER TABLE clients
ADD COLUMN token UUID NOT NULL DEFAULT gen_random_uuid();Si el DEFAULT no és constant —una funció aleatòria, un NOW() que ha de diferir per fila— cada fila necessita el seu propi valor i no hi ha cap drecera possible. La regla:
DEFAULT |
Reescriu la taula? |
|---|---|
| Cap | No |
Constant (0, FALSE, 'pendent') |
No (PostgreSQL 11+) |
Funció estable avaluada una vegada (CURRENT_DATE) |
No: el valor es fixa en executar l'ALTER |
Funció volàtil (gen_random_uuid(), random()) |
Sí, fila a fila |
Nota de dialecte: MySQL 8 amb InnoDB permet
ADD COLUMNambALGORITHM=INSTANTen molts casos (columna al final de la taula, sense canvi de format de fila); si no, reconstrueix. Oracle té una optimització equivalent des d'11g. SQLite afegeix columnes al final de manera barata però no permet afegir-les amb unDEFAULTno constant. SQL Server distingeix entre afegir una columna nul·lable (instantani) i unaNOT NULLambDEFAULT(instantani des del 2012 Enterprise, reescriptura a la resta d'edicions).
DROP COLUMN
DROP COLUMNTambé és instantani: PostgreSQL no esborra les dades, marca la columna com a eliminada al catàleg i deixa de mostrar-la. L'espai es recupera quan cada fila es reescriu (per un UPDATE o per un VACUUM FULL).
Això té dues implicacions que convé conèixer:
- La dada continua físicament en disc fins que es reescrigui la fila. Si la columna contenia informació sensible, un
DROP COLUMNno és un esborrat segur. - L'operació és irreversible des d'SQL un cop confirmada. No hi ha
UNDROP.
Si alguna cosa depèn de la columna (una restricció, una vista, un índex), DROP COLUMN falla:
ERROR: cannot drop column preu of table productes because other objects depend on it DETAIL: constraint productes_preu_check on table productes depends on column preu of table productes HINT: Use DROP ... CASCADE to drop the dependent objects too.
Amb CASCADE s'endú per davant el que en depengui:
I amb això acabes de destruir la columna més important del catàleg i la seva restricció. CASCADE a ALTER TABLE mereix el mateix respecte que a DELETE.
RENAME COLUMN i RENAME TO
RENAME COLUMN i RENAME TOTotes dues són purament de catàleg: instantànies, sense tocar dades, i reversibles amb un altre RENAME.
I totes dues són, en un sistema amb aplicacions connectades, de les operacions més perilloses que existeixen. El motiu no és tècnic: és que en l'instant en què confirmes el canvi, tot el codi que esmenta el nom antic deixa de funcionar:
Un ALTER TABLE de dos segons ha trencat l'aplicació sencera. I no hi ha finestra de transició: o el codi fa servir el nom vell, o fa servir el nou.
La regla: un
RENAMEen producció mai no es fa de cop. Es fa amb el patró expand/contract de l'apartat 10, o no es fa. I si el nom és lleig però funciona, moltes vegades la resposta correcta és deixar-lo lleig.
Desfem els dos canvis abans de continuar:
ALTER COLUMN TYPE i la clàusula USING
ALTER COLUMN TYPE i la clàusula USINGAmpliar la precisió d'un NUMERIC funciona directament, perquè tots els valors existents caben al tipus nou. El mateix passa en passar de VARCHAR(60) a VARCHAR(120) o a TEXT: PostgreSQL reconeix aquells casos com a compatibles a nivell binari i no reescriu res.
Reduir, en canvi, pot fallar:
I això és una bona notícia: PostgreSQL comprova totes les files abans d'aplicar el canvi, i si una sola no hi cap, avorta sense tocar res. Mai no trunca en silenci.
Nota de dialecte: MySQL, segons el seu mode SQL, sí que pot truncar en silenci en reduir un
VARCHAR. Ambsql_modeaSTRICT_TRANS_TABLES(el valor per omissió des de MySQL 5.7) dóna error; amb el mode relaxat, retalla el text i emet un avís que gairebé ningú no llegeix. És una de les diferències de comportament més perilloses entre motors.
La clàusula USING
Quan la conversió no és automàtica, PostgreSQL t'ho diu i t'ofereix la sortida:
-- Exemple puntual: taula auxiliar amb dades "brutes" d'una importació
CREATE TABLE comandes_importades (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
referencia VARCHAR(20) NOT NULL,
data_text VARCHAR(10) NOT NULL,
import_text VARCHAR(15) NOT NULL
);
INSERT INTO comandes_importades (referencia, data_text, import_text) VALUES
('BV-2026-001', '2026-03-01', '47.05'),
('BV-2026-002', '2026-03-02', '26.70'),
('BV-2026-003', '2026-03-03', '34.48');Un intent directe de convertir el text a data:
ERROR: column "data_text" cannot be cast automatically to type date HINT: You might need to specify "USING data_text::date".
USING rep una expressió que calcula el valor nou a partir de l'antic, fila a fila:
ALTER TABLE comandes_importades
ALTER COLUMN data_text TYPE DATE USING data_text::DATE,
ALTER COLUMN import_text TYPE NUMERIC(10,2) USING import_text::NUMERIC(10,2);| id | referencia | data_text | import_text |
|---|---|---|---|
| 1 | BV-2026-001 | 2026-03-01 | 47.05 |
| 2 | BV-2026-002 | 2026-03-02 | 26.70 |
| 3 | BV-2026-003 | 2026-03-03 | 34.48 |
USING admet qualsevol expressió, no només conversions. Per exemple, per passar linies_comanda.descompte de fracció (0.10) a percentatge enter (10):
-- Exemple hipotètic: NO ho apliquis a BotigaVerda, trencaries
-- tots els càlculs dels mòduls 2 a 4
ALTER TABLE linies_comanda
ALTER COLUMN descompte TYPE SMALLINT USING (descompte * 100)::SMALLINT;Tres avisos sobre ALTER COLUMN TYPE:
| Avís | Detall |
|---|---|
| Reescriu la taula sencera llevat dels casos binari-compatibles | Amb bloqueig exclusiu durant tot el procés |
| Reconstrueix els índexs que la incloguin | Cost addicional proporcional |
| Pot invalidar restriccions i vistes dependents | PostgreSQL les recrea si pot, i si no, falla |
SET / DROP NOT NULL i SET / DROP DEFAULT
SET / DROP NOT NULL i SET / DROP DEFAULTFalla si hi ha files amb NULL… llevat que a BotigaVerda tots els clients tinguin ciutat, que és el cas. Provem-ho amb una que sí que té nuls:
Les deu comandes web ho impedeixen, i correctament: aquell NULL significa alguna cosa (04-03).
L'operació inversa sempre funciona, perquè relaxar una restricció no pot invalidar mai dades existents:
Els valors per omissió funcionen igual, i són pur catàleg:
ALTER TABLE productes ALTER COLUMN stock SET DEFAULT 10;
ALTER TABLE productes ALTER COLUMN stock SET DEFAULT 0; -- ho deixem com estava
ALTER TABLE clients ALTER COLUMN data_registre DROP DEFAULT;
ALTER TABLE clients ALTER COLUMN data_registre SET DEFAULT CURRENT_DATE;Canviar el DEFAULT no afecta les files existents. Només canvia el que s'omplirà a les insercions futures. És un error clàssic esperar el contrari.
ADD / DROP CONSTRAINT, i la tècnica NOT VALID + VALIDATE
ADD / DROP CONSTRAINT, i la tècnica NOT VALID + VALIDATEAquí es cobra la insistència de 05-01 a anomenar les restriccions: per treure'n una cal anomenar-la, i no pots anomenar el que no saps com es diu.
Funciona perquè cap dels 20 productes no té un cost superior al seu preu. Si n'hi hagués, PostgreSQL rebutjaria l'ALTER TABLE sencer:
Treure-la:
I les altres tres famílies:
ALTER TABLE ressenyes ADD CONSTRAINT uq_ressenyes_producte_client UNIQUE (producte_id, client_id);
-- Sobre la taula auxiliar d'importació de l'apartat 5
ALTER TABLE comandes_importades
ADD CONSTRAINT uq_comandes_importades_ref UNIQUE (referencia);
ALTER TABLE comandes_importades
ADD CONSTRAINT chk_comandes_importades_import CHECK (import_text >= 0);El problema: validar bloqueja
Afegir un CHECK o una FOREIGN KEY obliga PostgreSQL a comprovar totes les files existents, i ho fa amb un bloqueig exclusiu. En una taula de deu milions de files això pot ser un minut llarg durant el qual ningú no pot llegir ni escriure.
La solució en dos temps:
-- Fase 1: afegir sense validar. Instantani.
ALTER TABLE productes
ADD CONSTRAINT chk_productes_marge CHECK (cost IS NULL OR cost <= preu) NOT VALID;-- Fase 2: validar. Pot trigar, però NO bloqueja lectures ni escriptures.
ALTER TABLE productes VALIDATE CONSTRAINT chk_productes_marge;Què fa exactament NOT VALID:
Amb NOT VALID |
Després de VALIDATE CONSTRAINT |
|
|---|---|---|
| Files noves o modificades | Es comproven des del primer instant | Es comproven |
| Files existents | No es comproven | Es comproven una vegada |
| Bloqueig | ACCESS EXCLUSIVE, però instantani |
SHARE UPDATE EXCLUSIVE: no bloqueja lectures ni escriptures |
| El planificador la pot aprofitar | No | Sí |
És a dir: NOT VALID et dóna la protecció cap al futur immediatament, i deixa la comprovació de l'històric per a un moment tranquil. És la tècnica estàndard per afegir restriccions a taules grans en producció.
I comprovar quines restriccions estan sense validar:
SELECT conname AS restriccio, convalidated AS validada
FROM pg_constraint
WHERE conrelid = 'productes'::regclass
ORDER BY conname;| restriccio | validada |
|---|---|
| chk_productes_marge | true |
| productes_categoria_id_fkey | true |
| productes_cost_check | true |
| productes_pkey | true |
| productes_preu_check | true |
| productes_proveidor_id_fkey | true |
| productes_stock_check | true |
Desfem l'exemple:
- Què bloqueja la taula i què no
Aquest és l'apartat que separa una migració innòcua d'una caiguda de producció.
PostgreSQL protegeix cada operació amb un nivell de bloqueig. El més agressiu és ACCESS EXCLUSIVE: mentre es manté, cap altra sessió no pot ni tan sols llegir la taula. Totes les consultes es queden esperant.
| Operació | Nivell de bloqueig | Reescriu? | Escaneja? | Durada |
|---|---|---|---|---|
ADD COLUMN sense DEFAULT |
ACCESS EXCLUSIVE |
No | No | Instantània |
ADD COLUMN amb DEFAULT constant |
ACCESS EXCLUSIVE |
No (PG 11+) | No | Instantània |
ADD COLUMN amb DEFAULT volàtil |
ACCESS EXCLUSIVE |
Sí | Sí | Proporcional a la mida |
DROP COLUMN |
ACCESS EXCLUSIVE |
No | No | Instantània |
RENAME COLUMN / RENAME TO |
ACCESS EXCLUSIVE |
No | No | Instantània |
SET DEFAULT / DROP DEFAULT |
ACCESS EXCLUSIVE |
No | No | Instantània |
DROP NOT NULL |
ACCESS EXCLUSIVE |
No | No | Instantània |
SET NOT NULL |
ACCESS EXCLUSIVE |
No | Sí | Proporcional |
ALTER COLUMN TYPE (binari-compatible) |
ACCESS EXCLUSIVE |
No | No | Instantània |
ALTER COLUMN TYPE (resta) |
ACCESS EXCLUSIVE |
Sí | Sí | Proporcional |
ADD CONSTRAINT CHECK |
ACCESS EXCLUSIVE |
No | Sí | Proporcional |
ADD CONSTRAINT CHECK ... NOT VALID |
ACCESS EXCLUSIVE |
No | No | Instantània |
ADD FOREIGN KEY |
ACCESS EXCLUSIVE a totes dues taules |
No | Sí | Proporcional |
ADD FOREIGN KEY ... NOT VALID |
SHARE ROW EXCLUSIVE |
No | No | Instantània |
VALIDATE CONSTRAINT |
SHARE UPDATE EXCLUSIVE |
No | Sí | No bloqueja lectures ni escriptures |
ADD UNIQUE / ADD PRIMARY KEY |
ACCESS EXCLUSIVE |
No | Sí (construeix índex) | Proporcional |
DROP CONSTRAINT |
ACCESS EXCLUSIVE |
No | No | Instantània |
CREATE INDEX |
SHARE |
No | Sí | Bloqueja escriptures |
CREATE INDEX CONCURRENTLY |
SHARE UPDATE EXCLUSIVE |
No | Sí (dues passades) | No bloqueja escriptures (mòdul 8) |
El detall que mata: la cua de bloqueigs
I ara el veritablement important, que gairebé ningú no explica:
Una operació "instantània" no és inofensiva si no aconsegueix el bloqueig.
Un ALTER TABLE ADD COLUMN triga un mil·lisegon… un cop ha obtingut l'ACCESS EXCLUSIVE. Si en aquell moment hi ha una consulta llarga llegint la taula, l'ALTER es posa a esperar. I mentre espera, PostgreSQL encua darrere seu totes les consultes noves, perquè els bloqueigs es concedeixen per ordre d'arribada.
sequenceDiagram
participant R as Informe (5 min)
participant A as ALTER TABLE
participant N as 200 consultes noves
R->>R: SELECT llarg · bloqueig ACCESS SHARE
A->>A: demana ACCESS EXCLUSIVE → ESPERA
N->>N: demanen ACCESS SHARE → esperen DARRERE de l'ALTER
Note over R,N: 💥 La taula queda inaccessible 5 minuts<br/>per un ALTER d'1 ms
Un ALTER TABLE d'un mil·lisegon acaba de deixar la taula inaccessible cinc minuts. La protecció estàndard:
Si en tres segons no aconsegueix el bloqueig, avorta amb canceling statement due to lock timeout en lloc de bloquejar la base. Es reintenta més tard. Posar lock_timeout a tota migració és una de les pràctiques més rendibles que existeixen.
- DDL transaccional: PostgreSQL davant de MySQL
Aquesta és una diferència crítica entre motors, i canvia completament com s'escriu una migració.
A PostgreSQL, el DDL és transaccional:
BEGIN;
ALTER TABLE clients ADD COLUMN telefon VARCHAR(20);
ALTER TABLE clients ADD COLUMN newsletter BOOLEAN NOT NULL DEFAULT FALSE;
SELECT id, nom, telefon, newsletter FROM clients ORDER BY id LIMIT 2;| id | nom | telefon | newsletter |
|---|---|---|---|
| 1 | Lucía | (null) | false |
| 2 | Carlos | (null) | false |
Com si no hagués passat mai. Els dos ALTER TABLE s'han desfet.
La conseqüència pràctica és enorme: a PostgreSQL pots embolcallar una migració de quinze passos en BEGIN … COMMIT i tenir la garantia que o s'aplica sencera o no s'aplica res. Si el pas 12 falla, l'esquema queda exactament com estava.
| PostgreSQL | MySQL / MariaDB | SQL Server | Oracle | SQLite | |
|---|---|---|---|---|---|
| DDL transaccional | Sí | No | Sí | No | Sí |
ROLLBACK d'un ALTER TABLE |
Funciona | Impossible | Funciona | Impossible | Funciona |
BEGIN abans d'un DDL |
Es respecta | Confirma implícitament la transacció oberta | Es respecta | Confirma implícitament | Es respecta |
| Migració a mitges possible | No | Sí | No | Sí | No |
A MySQL i Oracle, cada sentència DDL confirma implícitament la transacció en curs. Si una migració de quinze passos falla al dotze, els onze primers ja estan aplicats i no hi ha manera de desfer-los. Per això en aquells motors tota migració necessita el seu script de reversió escrit a mà, i per això les eines de l'apartat 11 insisteixen tant en el concepte de down migration.
Si treballes amb MySQL, interioritza això: no existeix la xarxa de seguretat. Cada pas de la migració ha de ser reversible per si sol, i l'ordre importa moltíssim més.
- El patró expand/contract
Com es canvia un esquema sense aturar el servei.
El problema de fons: la base de dades i l'aplicació es despleguen per separat, i durant una estona conviuen la versió antiga i la nova del codi. Qualsevol canvi que trenqui una de les dues provoca errors. Un RENAME COLUMN, com has vist, trenca la versió antiga en l'instant en què es confirma.
Expand/contract (també anomenat parallel change) resol això descomponent el canvi en cinc fases, cadascuna compatible cap endavant i cap enrere:
flowchart TD
A["1 · EXPAND<br/>Afegir el nou<br/>sense tocar el vell"] --> B["2 · DOBLE ESCRIPTURA<br/>L'aplicació escriu<br/>als dos llocs"]
B --> C["3 · BACKFILL<br/>Omplir el nou<br/>amb les dades històriques"]
C --> D["4 · CANVIAR LA LECTURA<br/>L'aplicació llegeix del nou<br/>i es verifica que coincideix"]
D --> E["5 · CONTRACT<br/>Deixar d'escriure al vell<br/>i eliminar-lo"]
style A fill:#e8f5e9
style E fill:#ffebee
La clau és que en cap moment no existeix un estat en què l'aplicació pugui fallar: entre la fase 1 i la 5, totes dues versions del codi funcionen.
El cas: afegir comandes.total
BotigaVerda calcula el total de cada comanda sumant-ne les línies i afegint-hi els ports. És correcte (01-05 ho justificava: res d'agregats precalculats), però els informes del mòdul 4 repeteixen aquella expressió una vegada i una altra, i amb volum alt el cost es nota. Direcció demana una columna total desnormalitzada.
Fase 1 — Expand: afegir la columna, nul·lable.
Instantani, nul·lable, sense DEFAULT. El codi antic continua funcionant: no sap que la columna existeix i no la necessita.
Fase 2 — Doble escriptura. Es desplega una versió de l'aplicació que, a més de crear la comanda i les seves línies, omple total. Les comandes noves tenen valor; les antigues continuen a NULL. Cap canvi d'esquema en aquesta fase — és pur desplegament de codi.
Fase 3 — Backfill: omplir cap enrere.
Amb les eines d'aquest mòdul, sense subconsultes, en dos passos:
CREATE TEMP TABLE totals_comanda AS
SELECT lc.comanda_id,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS import_productes
FROM linies_comanda AS lc
GROUP BY lc.comanda_id;UPDATE comandes AS co
SET total = t.import_productes + co.despeses_enviament
FROM totals_comanda AS t
WHERE t.comanda_id = co.id
AND co.total IS NULL;Fixa't en l'AND co.total IS NULL: fa l'UPDATE idempotent (05-03) i, sobretot, garanteix que el backfill no trepitja els valors que la fase 2 ja està escrivint en temps real. És el detall que converteix un backfill perillós en un de segur.
En una taula real el backfill es faria per lots —WHERE total IS NULL AND id BETWEEN 1 AND 10000, repetit— per no mantenir una transacció gegant bloquejant files.
Fase 4 — Canviar la lectura, i verificar.
Abans que els informes comencin a fer servir la columna nova, cal comprovar que coincideix amb el càlcul en viu:
SELECT co.id,
co.data_comanda,
co.estat,
co.total AS total_columna,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2)
+ co.despeses_enviament AS total_calculat,
co.total - (ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2)
+ co.despeses_enviament) AS diferencia
FROM comandes AS co
JOIN linies_comanda AS lc ON lc.comanda_id = co.id
GROUP BY co.id, co.data_comanda, co.estat, co.total, co.despeses_enviament
ORDER BY co.id
LIMIT 8;| id | data_comanda | estat | total_columna | total_calculat | diferencia |
|---|---|---|---|---|---|
| 1 | 2025-03-04 | lliurat | 47.05 | 47.05 | 0.00 |
| 2 | 2025-03-12 | lliurat | 26.70 | 26.70 | 0.00 |
| 3 | 2025-04-02 | lliurat | 34.48 | 34.48 | 0.00 |
| 4 | 2025-04-19 | lliurat | 36.70 | 36.70 | 0.00 |
| 5 | 2025-05-07 | lliurat | 32.10 | 32.10 | 0.00 |
| 6 | 2025-05-23 | cancellat | 31.70 | 31.70 | 0.00 |
| 7 | 2025-06-11 | lliurat | 37.10 | 37.10 | 0.00 |
| 8 | 2025-06-28 | lliurat | 74.78 | 74.78 | 0.00 |
(8 primeres de 20 files.)
I la comprovació global, que és la que de debò importa:
SELECT COUNT(*) AS comandes,
SUM(total) AS suma_columna,
COUNT(*) FILTER (WHERE total IS NULL) AS sense_omplir
FROM comandes;| comandes | suma_columna | sense_omplir |
|---|---|---|
| 20 | 846.20 | 0 |
846,20 €: exactament la xifra canònica del mòdul 4 (727,95 € de producte + 118,25 € de ports). Vint comandes, cap sense omplir. El backfill és correcte.
En aquesta fase, el professional és deixar la comparació funcionant uns dies amb una alerta: si algun cop total deixa de coincidir amb la suma de les línies, és que la doble escriptura té un forat.
Fase 5 — Contract: consolidar i netejar.
Ara que totes les files tenen valor, la columna pot ser obligatòria. I es retira del codi el càlcul antic.
Queda un problema obert, i convé dir-ho amb claredat: comandes.total és un agregat precalculat i cal mantenir-lo sincronitzat. Si demà algú modifica una línia de comanda amb un UPDATE directe, total queda obsolet i ningú no se n'assabenta. Les tres solucions —un trigger que el recalculi, una vista materialitzada, o una columna generada si la fórmula visqués a la mateixa fila— són del mòdul 10. És exactament l'advertiment de 01-05, secció 8: desnormalitza amb mesures a la mà, i documenta com el mantindràs al dia.
La variant: partir nom i cognoms
El mateix patró, aplicat a un canvi de forma. Suposa que BotigaVerda hagués nascut amb una única columna nom_complet i volguessis separar-la:
| Fase | Acció |
|---|---|
| 1 · Expand | ALTER TABLE clients ADD COLUMN nom VARCHAR(60), ADD COLUMN cognoms VARCHAR(90); |
| 2 · Doble escriptura | L'aplicació omple les tres columnes a cada alta i a cada modificació |
| 3 · Backfill | UPDATE que parteix nom_complet pel primer espai (funcions de cadena: mòdul 6) |
| 4 · Canviar la lectura | Els formularis i llistats passen a fer servir nom i cognoms; es compara amb nom_complet |
| 5 · Contract | SET NOT NULL a les dues noves i DROP COLUMN nom_complet |
Fixa't en el que mai no es fa: un RENAME COLUMN de cop, ni un DROP COLUMN abans que ningú no la llegeixi. Cada fase és reversible i cap no trenca la versió anterior del codi.
- Migracions versionades
Tot l'anterior té un requisit previ que no és tècnic:
Un canvi d'esquema no és una ordre: és un fitxer versionat al repositori.
Què és una migració versionada
Un fitxer numerat, amb nom descriptiu, que conté un canvi d'esquema i (idealment) la seva reversió:
db/migrations/ ├── V001__esquema_inicial.sql ├── V002__afegir_telefon_clients.sql ├── V003__afegir_total_comandes.sql ├── V004__backfill_total_comandes.sql └── V005__total_comandes_not_null.sql
Una eina de migració manté a la mateixa base de dades una taula de control amb les migracions ja aplicades, i en arrencar aplica només les que falten, en ordre.
Per què això és innegociable:
| Sense migracions versionades | Amb migracions versionades |
|---|---|
| Ningú no sap quin esquema té cada entorn | L'esquema és reproduïble des de zero |
| Els canvis no passen per revisió de codi | Cada canvi és una pull request revisable |
| No hi ha història: qui va afegir aquella columna i per què? | git log respon |
| Desenvolupament, proves i producció divergeixen | Els tres apliquen la mateixa seqüència |
| Un desplegament pot oblidar el canvi de base | El desplegament l'aplica automàticament |
| Reproduir un error és impossible | N'hi ha prou amb clonar el repositori |
Eines habituals
| Eina | Ecosistema | Format | Reversió |
|---|---|---|---|
| Flyway | Java, JVM, CLI | SQL pur o Java | U (Teams) o manual |
| Liquibase | Java, multiplataforma | XML, YAML, JSON o SQL | Automàtica en molts casos |
| Alembic | Python (SQLAlchemy) | Python (upgrade/downgrade) |
Explícita, molt usada |
| Active Record Migrations | Ruby on Rails | Ruby (change o up/down) |
Automàtica quan es pot inferir |
| Django migrations | Python (Django) | Python autogenerat des dels models | Automàtica majoritàriament |
| Laravel migrations | PHP | PHP (up/down) |
Explícita |
| Sqitch | Agnòstica, CLI | SQL pur (deploy/revert/verify) | Explícita, amb verificació |
| golang-migrate / dbmate | Go, agnòstiques | SQL pur (.up.sql / .down.sql) |
Explícita |
Totes resolen el mateix problema i difereixen sobretot en si el canvi s'escriu en SQL o en el llenguatge de l'aplicació. Escriure'l en SQL pur (Flyway, Sqitch, golang-migrate) dóna control total sobre lock_timeout, NOT VALID i CONCURRENTLY; escriure'l en el llenguatge de l'aplicació (Alembic, Rails, Django) dóna portabilitat entre motors i reversió automàtica, al preu de perdre el control fi que necessiten les taules grans.
Les cinc regles d'or
- Una migració = un canvi. Si el fitxer fa cinc coses i la tercera falla en producció, no sabràs en quin estat ha quedat (i a MySQL, a més, no ho podràs desfer).
- Sempre amb script de reversió. Encara que el teu motor tingui DDL transaccional, escriure el down t'obliga a pensar si el canvi és reversible. Moltes vegades descobriràs que no ho és —un
DROP COLUMNno es desfà— i això és informació valuosíssima abans d'aplicar-lo. - Provada en un entorn igual al de producció. No al teu portàtil amb 20 files: en una còpia amb volum realista. Un
ALTER COLUMN TYPEque triga 40 ms amb 20 files triga 40 minuts amb 40 milions. - Amb còpia de seguretat prèvia i verificada. I verificada significa restaurada alguna vegada, no "el cron diu que funciona".
- Mai a mà a la consola de producció. Ni "només aquesta vegada", ni "és un canvi petit", ni "és urgent". El canvi que no és al repositori no existeix, i el pròxim entorn que es creï no el tindrà.
I un corol·lari que surt de la fase 2 de l'expand/contract: les migracions que trenquen la compatibilitat cap enrere s'han de partir en diverses. V003 afegeix la columna, V004 l'omple, V005 la fa obligatòria — cadascuna desplegable per separat, i entre elles es desplega el codi que les necessita.
- Què fer i què no fer en producció
Què NO fer
| ❌ | Per què |
|---|---|
ALTER TABLE a mà a la consola de producció |
No queda registre, no passa per revisió i el pròxim entorn no ho tindrà |
DROP COLUMN d'una columna que "sembla que no es fa servir" |
Si t'equivoques, la dada no torna. Primer deixa de llegir-la durant setmanes, després esborra-la |
RENAME COLUMN de cop |
Trenca tot el codi que l'anomena en l'instant del COMMIT |
ALTER COLUMN TYPE en una taula gran en horari de treball |
Reescriu la taula amb bloqueig exclusiu |
ADD CONSTRAINT sense NOT VALID en una taula gran |
Escaneja la taula sencera amb bloqueig exclusiu |
Migrar sense lock_timeout |
Un ALTER d'1 ms pot encuar tota la càrrega darrere seu |
| Un fitxer de migració amb quinze canvis | Si falla el vuitè, bona sort |
| Aplicar la migració i desplegar el codi alhora | Durant el desplegament conviuen totes dues versions. Expand/contract existeix per això |
Què SÍ fer
| ✅ | Per què |
|---|---|
| Fitxer versionat, revisat en pull request | Història, revisió i reproductibilitat |
| Còpia de seguretat verificada abans | L'única xarxa de seguretat real |
| Provar-la en una còpia amb volum realista | Els temps no escalen linealment a la intuïció |
SET LOCAL lock_timeout a cada migració |
Converteix una caiguda en un reintent |
NOT VALID + VALIDATE CONSTRAINT en taules grans |
Protecció immediata sense bloqueig llarg |
CREATE INDEX CONCURRENTLY (mòdul 8) |
Construeix l'índex sense bloquejar escriptures |
| Backfill per lots i idempotent | Transaccions curtes, reexecutable sense mal |
| Finestra de manteniment per al que reescriu | Si alguna cosa ha de trigar, que trigui quan no molesta |
| Expand/contract per a tot canvi incompatible | Zero temps d'aturada |
| Revisió del responsable de la base de dades | Algú que conegui el volum real, la càrrega i les dependències |
La regla que resumeix les dues taules: en producció, la pregunta no és "funciona aquest
ALTER TABLE?", sinó "què passa mentre s'executa, i què passa si falla a mitges?". Si no saps respondre les dues, la migració no està a punt.
Errors habituals i consells
- Escriure
ALTER TABLEa mà en producció. L'error arrel del qual surten gairebé tots els altres. - Creure que
ADD COLUMNambDEFAULTsempre reescriu la taula. Des de PostgreSQL 11 no ho fa, llevat que elDEFAULTsigui volàtil. - Creure que una operació "instantània" és inofensiva. Necessita l'
ACCESS EXCLUSIVE, i si no l'aconsegueix encua tota la càrrega darrere.lock_timeout, sempre. RENAMEde cop. Trenca el codi antic en l'instant delCOMMIT. Expand/contract, o no ho facis.ALTER COLUMN TYPEsenseUSINGquan la conversió no és automàtica. ElHINTde PostgreSQL et diu exactament què escriure.- Reduir un
VARCHARsense comprovar les dades. PostgreSQL avorta; MySQL en mode relaxat trunca en silenci. ADD CONSTRAINTsenseNOT VALIDen taules grans. Escaneig complet amb bloqueig exclusiu.- Esperar que canviar el
DEFAULTactualitzi les files existents. No ho fa: només afecta les insercions futures. - Fer servir
DROP COLUMNcom a esborrat segur de dades sensibles. La dada continua en disc fins que la fila es reescrigui. - Suposar que el DDL és transaccional a tot arreu. A PostgreSQL, SQL Server i SQLite sí; a MySQL i Oracle, no: una migració a mitges es queda a mitges.
- Backfill en una sola transacció gegant. Bloqueja files, infla el WAL i si falla cal començar de zero. Per lots.
- Backfill que trepitja la doble escriptura. Afegeix
WHERE columna IS NULLper tocar només el que falta. - Migracions sense script de reversió. Escriure'l és la manera més barata de descobrir que el canvi no és reversible.
- Consell:
\d taulaabans i després de cadaALTER TABLE. Comprovar el que has fet costa dos segons. - Consell: embolcalla les migracions en
BEGIN…COMMITsi el teu motor ho permet. A PostgreSQL, quinze canvis passen a ser atòmics. - Consell: mesura el temps en una còpia amb volum real abans de tocar producció. És la diferència entre una finestra de manteniment de cinc minuts i una de cinc hores.
Exercicis
Treballa sobre la base acabada de recarregar, dins de BEGIN … ROLLBACK.
Exercici 1
BotigaVerda vol registrar el canal pel qual entra cada comanda (web o telefon), una dada que fins ara es deduïa indirectament de si empleat_id era NULL.
- Afegeix a
comandesuna columnacanal VARCHAR(10), obligatòria, amb valor per omissió'web'i protegida amb unCHECKamb nom que només admeti'web'i'telefon'. - Omple cap enrere: les comandes amb comercial assignat són
'telefon'; la resta,'web'. - Comprova el resultat agrupant per canal.
- Respon: per què el pas 1 no requereix reescriure la taula? I què hauria passat si haguessis posat el
CHECKabans d'omplir les dades?
Exercici 2
Un company et passa aquest fitxer de migració perquè el revisis abans d'aplicar-lo a producció, on productes té 4 milions de files i linies_comanda 90 milions:
-- ⚠️ V017__millores_cataleg.sql
ALTER TABLE productes RENAME COLUMN nom TO denominacio;
ALTER TABLE productes ADD COLUMN sku VARCHAR(20) NOT NULL DEFAULT gen_random_uuid()::text;
ALTER TABLE productes ADD CONSTRAINT uq_productes_sku UNIQUE (sku);
ALTER TABLE productes ALTER COLUMN preu TYPE NUMERIC(14,4);
ALTER TABLE linies_comanda ADD CONSTRAINT chk_lc_import CHECK (quantitat * preu_unitari >= 0);
ALTER TABLE productes DROP COLUMN cost;- Identifica tots els problemes, indicant per a cadascun si és de bloqueig, de compatibilitat, de reversibilitat o de procés.
- Estima quines de les sis operacions són instantànies i quines no.
- Reescriu la migració com hauria d'estar, partint-la en els fitxers que calgui.
Exercici 3
Aplica el patró expand/contract complet per afegir a clients una columna total_gastat NUMERIC(10,2) que acumuli el que cada client ha comprat (producte + ports).
- Escriu les cinc fases, indicant a cadascuna què és canvi d'esquema, què és canvi de codi i què és canvi de dades.
- Executa les fases 1, 3 i 5 sobre BotigaVerda (la 2 i la 4 són d'aplicació).
- Verifica que la suma de
total_gastatde tots els clients coincideix amb el total de comandes del curs. - Discuteix: ha de ser la columna
NOT NULL? Quin valor tenen els tres clients sense comandes, i què implica això?
Solucions
Solució 1
BEGIN;
-- 1) Afegir la columna amb el seu DEFAULT i el seu CHECK, en una sola sentència
ALTER TABLE comandes
ADD COLUMN canal VARCHAR(10) NOT NULL DEFAULT 'web',
ADD CONSTRAINT chk_comandes_canal CHECK (canal IN ('web', 'telefon'));-- 2) Backfill: els que tenen comercial van entrar per telèfon
UPDATE comandes
SET canal = 'telefon'
WHERE empleat_id IS NOT NULL
AND canal <> 'telefon';-- 3) Comprovació
SELECT canal,
COUNT(*) AS comandes,
COUNT(empleat_id) AS amb_comercial,
COUNT(*) - COUNT(empleat_id) AS sense_comercial,
SUM(despeses_enviament) AS ports
FROM comandes
GROUP BY canal
ORDER BY comandes DESC, canal;| canal | comandes | amb_comercial | sense_comercial | ports |
|---|---|---|---|---|
| telefon | 10 | 10 | 0 | 72.15 |
| web | 10 | 0 | 10 | 46.10 |
Deu i deu, exactament el repartiment que 01-06 descrivia: la meitat del canal és web. I les tres formes de COUNT de 04-04 confirmen la coherència: al canal telefon les 10 comandes tenen comercial, a web cap. Els ports sumen 72,15 € + 46,10 € = 118,25 €, la xifra canònica del mòdul 4 — i de passada revelen una cosa que no s'havia mirat mai: el canal telefònic paga bastants més ports, perquè concentra les comandes a Portugal i França.
4. Les dues preguntes.
Per què no reescriu la taula: el DEFAULT 'web' és una constant, i des de PostgreSQL 11 això es desa al catàleg i es retorna al vol per a les files antigues. Si el valor per omissió hagués estat alguna cosa volàtil, sí que hauria reescrit les 20 files (irrellevant aquí, decisiu amb 20 milions).
Què hauria passat amb el CHECK abans de les dades: en aquest cas concret, res dolent, perquè el DEFAULT 'web' deixa totes les files amb un valor vàlid. Però si haguéssim afegit la columna nul·lable i sense DEFAULT i després el CHECK, tampoc no hauria fallat: les 20 files tindrien NULL, i un CHECK que s'avalua a UNKNOWN s'accepta (05-01, apartat 3.5). El CHECK hauria passat sense protestar i amb la columna buida. És la trampa clàssica: l'ordre importa, però el CHECK no sempre t'avisa que has fet les coses a l'inrevés. El NOT NULL sí que ho hauria fet.
En una taula gran l'ordre correcte i segur seria: afegir la columna nul·lable → backfill per lots → ADD CONSTRAINT ... NOT VALID → VALIDATE CONSTRAINT → SET NOT NULL.
Solució 2
1 i 2. Els problemes, operació per operació:
| # | Operació | Instantània | Problemes |
|---|---|---|---|
| 1 | RENAME COLUMN nom TO denominacio |
Sí | Compatibilitat: trenca tot el codi que diu nom en l'instant del COMMIT. Necessita expand/contract |
| 2 | ADD COLUMN sku ... DEFAULT gen_random_uuid()::text |
No | Bloqueig: DEFAULT volàtil → reescriu 4 milions de files amb ACCESS EXCLUSIVE. A més, un UUID no és un SKU: és un identificador sense significat per a una columna que hauria de tenir format de negoci |
| 3 | ADD CONSTRAINT uq_productes_sku UNIQUE |
No | Bloqueig: construeix un índex únic sobre 4 milions de files amb bloqueig exclusiu. Hauria de ser CREATE UNIQUE INDEX CONCURRENTLY + ADD CONSTRAINT ... USING INDEX |
| 4 | ALTER COLUMN preu TYPE NUMERIC(14,4) |
No | Bloqueig: canviar l'escala d'un NUMERIC no és binari-compatible → reescriu la taula i reconstrueix índexs. I de passada canvia la semàntica dels diners del sistema (01-04) |
| 5 | ADD CONSTRAINT chk_lc_import sense NOT VALID |
No | Bloqueig: escaneig complet de 90 milions de files amb bloqueig exclusiu. I la restricció és inútil: quantitat > 0 i preu_unitari >= 0 ja ho garanteixen |
| 6 | DROP COLUMN cost |
Sí | Reversibilitat: destrueix la dada de la qual surten tots els marges del negoci, sense marxa enrere |
I dos problemes de procés que afecten el fitxer sencer:
- Sis canvis heterogenis en una migració. Viola la primera regla d'or: si falla el quart, quedes a mitges (a PostgreSQL el
ROLLBACKet salva; a MySQL, no). - Sense
lock_timeout. Qualsevol d'aquestes operacions pot encuar tota la càrrega darrere seu.
Estimació de temps amb aquells volums: les operacions 2, 3 i 4 es compten en minuts o hores cadascuna; la 5, en diversos minuts. La migració completa deixaria el catàleg inaccessible durant tot aquell temps.
3. La versió correcta, partida en fitxers:
-- V017__afegir_sku_productes.sql
-- Fase EXPAND. Instantània: columna nul·lable, sense DEFAULT.
BEGIN;
SET LOCAL lock_timeout = '3s';
ALTER TABLE productes ADD COLUMN sku VARCHAR(20);
COMMIT;-- V018__backfill_sku_productes.sql
-- Per lots, idempotent. S'executa fora d'hora punta.
-- (El generador real de SKU l'aporta l'aplicació; aquí, un exemple)
UPDATE productes
SET sku = 'BV-' || LPAD(id::text, 6, '0')
WHERE sku IS NULL
AND id BETWEEN :des_de AND :fins_a;-- V019__sku_unic_i_obligatori.sql
-- L'índex es construeix SENSE bloquejar escriptures (mòdul 8).
CREATE UNIQUE INDEX CONCURRENTLY uq_productes_sku_idx ON productes (sku);
BEGIN;
SET LOCAL lock_timeout = '3s';
ALTER TABLE productes
ADD CONSTRAINT uq_productes_sku UNIQUE USING INDEX uq_productes_sku_idx;
ALTER TABLE productes ALTER COLUMN sku SET NOT NULL;
COMMIT;I les tres operacions restants:
| Operació | Veredicte |
|---|---|
RENAME COLUMN nom TO denominacio |
Rebutjada tal qual. Si de debò cal, expand/contract en quatre migracions i diversos desplegaments. Preguntar abans si el nom nou aporta alguna cosa |
ALTER COLUMN preu TYPE NUMERIC(14,4) |
Rebutjada. Canvia la semàntica dels diners de tot el sistema. Si calgués més precisió per a casos concrets, és una columna nova, no un canvi de tipus |
DROP COLUMN cost |
Rebutjada. Abans: comprovar que cap consulta no la fa servir, deixar de llegir-la durant setmanes, i només llavors plantejar l'esborrat en la seva pròpia migració amb còpia de seguretat verificada |
ADD CONSTRAINT chk_lc_import |
Rebutjada per innecessària. quantitat > 0 i preu_unitari >= 0 ja ho garanteixen. I si tot i així es volgués, NOT VALID + VALIDATE |
La lliçó de l'exercici: la major part d'una revisió de migracions consisteix a dir que no. De sis operacions, una està ben plantejada i cinc no s'haurien d'aplicar tal com estan.
Solució 3
1. Les cinc fases:
| Fase | Què és | Acció |
|---|---|---|
| 1 · Expand | Esquema | ALTER TABLE clients ADD COLUMN total_gastat NUMERIC(10,2); — nul·lable, instantani |
| 2 · Doble escriptura | Codi | L'aplicació suma el total de la comanda a clients.total_gastat en confirmar cada compra |
| 3 · Backfill | Dades | Omplir cap enrere amb l'històric, per lots, només on total_gastat IS NULL |
| 4 · Canviar la lectura | Codi | Els informes llegeixen la columna; es compara amb el càlcul en viu durant uns dies |
| 5 · Contract | Esquema | SET DEFAULT 0 i SET NOT NULL; es retira el càlcul antic del codi |
2. Execució de les fases 1, 3 i 5:
La temptació és fer-ho d'una sentència, unint clients, comandes i linies_comanda:
-- ⚠️ INCORRECTA: l'error de 04-04, secció 11
SELECT co.client_id,
SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte))
+ SUM(co.despeses_enviament) AS total
FROM linies_comanda AS lc
JOIN comandes AS co ON lc.comanda_id = co.id
GROUP BY co.client_id;despeses_enviament viu a comandes, i després del JOIN cada comanda apareix tantes vegades com línies tingui: els ports es multiplicarien i la suma total donaria 278,70 € en lloc de 118,25 €. És exactament l'error que el mòdul 3 va advertir tres vegades i el 4 va liquidar.
La forma correcta és agregar en dues etapes, reutilitzant la taula totals_comanda de l'apartat 10:
CREATE TEMP TABLE totals_comanda AS
SELECT lc.comanda_id,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS productes
FROM linies_comanda AS lc
GROUP BY lc.comanda_id;CREATE TEMP TABLE despesa_client AS
SELECT co.client_id,
ROUND(SUM(t.productes + co.despeses_enviament), 2) AS total
FROM comandes AS co
JOIN totals_comanda AS t ON t.comanda_id = co.id
GROUP BY co.client_id;UPDATE clients AS c
SET total_gastat = g.total
FROM despesa_client AS g
WHERE g.client_id = c.id
AND c.total_gastat IS NULL;SELECT c.id,
c.nom || ' ' || c.cognoms AS client,
c.pais,
c.total_gastat
FROM clients AS c
ORDER BY c.total_gastat DESC NULLS LAST, c.id;| id | client | pais | total_gastat |
|---|---|---|---|
| 7 | Sofia Moreira Costa | Portugal | 131.68 |
| 1 | Lucía Martínez Soler | Espanya | 112.55 |
| 9 | Camille Dubois | França | 95.87 |
| 10 | Julien Moreau | França | 79.40 |
| 4 | Javier Ortega Ruiz | Espanya | 72.83 |
| 6 | Pau Llorens Vidal | Espanya | 68.78 |
| 5 | Ana Belmonte Roca | Espanya | 64.75 |
| 2 | Carlos Ferrer Ibáñez | Espanya | 59.46 |
| 8 | Tiago Almeida Nunes | Portugal | 54.50 |
| 12 | Diego Ramos Herrera | Espanya | 36.65 |
| 11 | Elena Navarro Puig | Espanya | 35.25 |
| 3 | Marta Sanchis Gil | Espanya | 34.48 |
| 13 | Núria Bosch Ferrer | Espanya | (null) |
| 14 | Hugo Iglesias Pardo | Espanya | (null) |
| 15 | Inés Carrasco Vega | Espanya | (null) |
Sofia Moreira Costa encapçala el rànquing amb 131,68 € en dues comandes, per davant de la Lucía, que n'ha fet tres. I els tres últims, amb NULL, són els clients sense comandes de 01-06.
3. Verificació:
SELECT COUNT(*) AS clients,
COUNT(total_gastat) AS amb_compres,
COUNT(*) - COUNT(total_gastat) AS sense_compres,
SUM(total_gastat) AS total
FROM clients;| clients | amb_compres | sense_compres | total |
|---|---|---|---|
| 15 | 12 | 3 | 846.20 |
846,20 €: la xifra canònica del curs (727,95 € de producte + 118,25 € de ports). Dotze clients amb compres i tres sense cap, exactament els buits deliberats de 01-06. Les tres formes de COUNT de 04-04 tornen a fer tota la feina de verificació.
-- FASE 5 · CONTRACT
ALTER TABLE clients
ALTER COLUMN total_gastat SET DEFAULT 0;
UPDATE clients SET total_gastat = 0 WHERE total_gastat IS NULL;ALTER TABLE clients
ALTER COLUMN total_gastat SET NOT NULL,
ADD CONSTRAINT chk_clients_total_gastat CHECK (total_gastat >= 0);4. La discussió. Ha de ser NOT NULL?
NULL per a qui no ha comprat |
0 per a qui no ha comprat |
|
|---|---|---|
| Semàntica | "No ha comprat mai" es distingeix de "va comprar i ho va tornar tot" | Es confonen els dos casos |
| Consultes | AVG(total_gastat) ignora els tres, donant la mitjana de compradors |
AVG els inclou i abaixa la mitjana |
| Ordenació | Necessita NULLS LAST explícit |
Ordena sola |
| Aritmètica | total_gastat + 10 dóna NULL |
Funciona |
| Fidelitat al model | Més gran | Menor |
No hi ha resposta universal, i aquesta és la resposta correcta. Depèn de si "no ha comprat" i "ha comprat 0 €" són el mateix fet de negoci. A BotigaVerda no ho són: la Núria, l'Hugo i la Inés no han demanat mai res, i això és informació. Si es vol NOT NULL per comoditat aritmètica, cal documentar que el 0 significa dues coses i afegir una columna comandes_realitzades INTEGER NOT NULL DEFAULT 0 que sí que les distingeixi.
És exactament la discussió de 04-03, secció 11 —dissenyar amb NULL, amb sentinelles o amb NOT NULL— i la prova que una decisió d'esquema no és mai només tècnica.
I el problema obert, el mateix que amb comandes.total: aquesta columna és un agregat precalculat i cal mantenir-la sincronitzada. Cada comanda nova, cada línia modificada i cada devolució la deixen obsoleta. La solució —un trigger, o una vista materialitzada que es refresqui— és del mòdul 10. Mentre no existeixi, la columna és una bomba de rellotgeria, i per això la fase 2 del patró (la doble escriptura) no és opcional: és l'única que evita que la dada es podreixi.
Conclusió del mòdul
ALTER TABLE tanca el mòdul 5 i amb ell, la teva capacitat d'operar sobre una base de dades completa:
- Les operacions d'
ALTER TABLE:ADD COLUMN(instantània, fins i tot ambDEFAULTconstant des de PostgreSQL 11, llevat que sigui volàtil),DROP COLUMN(instantània, irreversible i no un esborrat segur),RENAME(instantània i la més perillosa, perquè trenca el codi antic a l'acte),ALTER COLUMN TYPEambUSINGper a les conversions no trivials,SET/DROP NOT NULL,SET/DROP DEFAULT—que no toca les files existents—, iADD/DROP CONSTRAINT, on es cobra la insistència de 05-01 a anomenar-les. NOT VALID+VALIDATE CONSTRAINT: protecció immediata per a les files noves i comprovació de l'històric sense bloquejar lectures ni escriptures. La tècnica estàndard en taules grans.- Què bloqueja i què no, amb la taula de nivells; i el detall que de debò tomba producció: una operació instantània que no aconsegueix l'
ACCESS EXCLUSIVEencua tota la càrrega darrere seu.SET LOCAL lock_timeoutés la protecció. - DDL transaccional: a PostgreSQL, SQL Server i SQLite un
ALTER TABLEes pot desfer ambROLLBACKi una migració de quinze passos és atòmica; a MySQL i Oracle no, i una migració que falla a mitges es queda a mitges. - Expand/contract: afegir el nou → escriure als dos → omplir cap enrere → canviar la lectura → eliminar el vell. Cinc fases en què mai no existeix un estat que trenqui l'aplicació. Demostrat amb
comandes.total, el backfill idempotent del qual (WHERE total IS NULL) va quadrar en els 846,20 € canònics del curs. - Migracions versionades: un fitxer numerat per canvi, al repositori, revisat com a codi, aplicat per una eina (Flyway, Liquibase, Alembic, Rails, Django, Laravel, Sqitch). I les cinc regles d'or: una migració = un canvi, sempre amb reversió, provada amb volum realista, amb còpia de seguretat verificada, i mai a mà en producció.
I amb això es tanca el mòdul 5. Repassa el que has guanyat en sis lliçons. Saps crear taules amb CREATE TABLE i declarar les sis restriccions que portaves cinc mòduls llegint, entenent per què CHECK accepta els nuls, per què UNIQUE n'admet diversos i per què GENERATED BY DEFAULT és el que obliga a aquell bloc de setval. Saps inserir amb INSERT, llistant sempre les columnes, en lots, i recuperant amb RETURNING l'id que necessites per penjar-hi les línies d'una comanda. Saps modificar amb UPDATE, amb un protocol de cinc passos i una transacció oberta, entenent que totes les assignacions s'avaluen sobre la fila antiga i que preu * 1.05 no és idempotent. Saps esborrar amb DELETE, distingir-lo de TRUNCATE, predir què s'endú per davant cada ON DELETE, i —el més important— decidir si cal esborrar, perquè actiu = FALSE existeix just per no fer-ho. Saps fusionar amb ON CONFLICT i amb MERGE, i per què mirar-i-després-decidir no és mai segur. I saps canviar l'esquema sense tombar el servei.
Ha canviat alguna cosa més que el repertori d'instruccions: ha canviat el nivell de responsabilitat. Als mòduls 2 a 4, un error teu retornava un número equivocat. Des d'aquest mòdul, un error teu modifica dades reals. Per això aquí has après tants hàbits com sintaxi: escriure el SELECT abans que l'UPDATE, comptar les files, obrir la transacció, comprovar el RETURNING, fer còpia abans, posar lock_timeout, versionar la migració. Cap d'aquells hàbits no apareix a la referència del llenguatge, i tots separen qui porta anys tocant bases de dades de qui en porta un mes.
Amb BotigaVerda ja ho pots fer tot: consultar-la, creuar-la, agregar-la, poblar-la, corregir-la i fer-la evolucionar. El que encara no pots és transformar i presentar el que en treus. Continues retornant nom i cognoms en dues columnes quan en vols una; continues escrivint ROUND(...) a mà sense conèixer-ne les germanes; continues sense poder extreure l'any d'una data, calcular l'antiguitat d'un client, posar un text en majúscules, convertir un tipus en un altre de manera explícita, substituir un NULL per un valor llegible, ni classificar files en categories segons una condició. Al mòdul 6, Funcions, arriben les eines que falten: funcions de cadena per compondre i netejar text, funcions numèriques per arrodonir i calcular amb criteri, funcions de data i hora per respondre per fi a "quants dies va trigar aquella comanda?", CAST i COALESCE per convertir tipus i domar els nuls que portes dos mòduls esquivant, i CASE per ficar lògica condicional dins d'una consulta. A partir d'aquí, les teves consultes deixaran de retornar dades i començaran a retornar respostes.
Curs de SQL
Mòdul 1: Introducció a SQL
- Què és SQL?
- Configurar el teu entorn SQL
- Sintaxi bàsica de SQL
- Entendre bases de dades i taules
- El model relacional: claus primàries i foranes
- La base de dades del curs: BotigaVerda
Mòdul 2: Consultes bàsiques de SQL
- Instrucció SELECT
- Àlies, expressions i columnes calculades
- Filtrar dades amb WHERE
- DISTINCT i eliminació de duplicats
- Ordenar dades amb ORDER BY
- Limitar resultats amb LIMIT
Mòdul 3: Treballar amb múltiples taules
- Operacions JOIN
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN i CROSS JOIN
- Unions de conjunts: UNION, INTERSECT i EXCEPT
Mòdul 4: Filtratge avançat de dades
- Utilitzar LIKE per a coincidència de patrons
- Operadors IN i BETWEEN
- Valors NULL i IS NULL
- Funcions d'agregació: COUNT, SUM, AVG, MIN i MAX
- Agregar dades amb GROUP BY
- Clàusula HAVING
Mòdul 5: Manipulació de dades
- Crear taules i restriccions amb CREATE TABLE
- Instrucció INSERT
- Instrucció UPDATE
- Instrucció DELETE
- Instrucció UPSERT (MERGE)
- Modificar l'esquema: ALTER TABLE i migracions segures
Mòdul 6: Funcions avançades de SQL
- Funcions de cadena
- Funcions numèriques
- Funcions de data i hora
- Conversió de tipus i tractament dels NULL: CAST i COALESCE
- Expressions condicionals
Mòdul 7: Subconsultes i consultes imbricades
- Introducció a les subconsultes
- Subconsultes correlacionades
- EXISTS i NOT EXISTS
- Utilitzar subconsultes en les clàusules SELECT, FROM i WHERE
- Subconsultes o JOIN: quin triar
Mòdul 8: Índexs i optimització del rendiment
- Entendre els índexs
- Creació i gestió d'índexs
- Tipus d'índex i quan no indexar
- Tècniques d'optimització de consultes
- Anàlisi del rendiment de les consultes
Mòdul 9: Transaccions i concurrència
- Introducció a les transaccions
- Propietats ACID
- Instruccions de control de transaccions
- Nivells d'aïllament i anomalies de concurrència
- Gestió de la concurrència: bloquejos i interbloquejos
Mòdul 10: Temes avançats
- Vistes
- Expressions de taula comunes (CTE)
- Funcions de finestra
- Procediments emmagatzemats
- Disparadors (triggers)
- JSON i dades semiestructurades
Mòdul 11: SQL a la pràctica
- Casos d'ús al món real
- Bones pràctiques
- Seguretat: injecció SQL, permisos i rols
- SQL per a l'anàlisi de dades
- SQL en el desenvolupament web
