Les consultes de la lliçó anterior funcionen. Aquesta lliçó va de l'altra cosa: de si d'aquí a un any, quan ja no recordis per què vas escriure aquell LEFT JOIN, algú —tu inclòs— les podrà tocar sense por. Perquè el SQL té una peculiaritat que el fa especialment traïdor: una consulta mal escrita no falla, retorna números. Un programa mal escrit peta i te n'assabentes; una consulta amb un JOIN de més retorna una xifra plausible que acaba en una diapositiva de direcció.
Aquí no hi ha sintaxi nova. Hi ha criteri: com s'anomenen les coses, com es formaten, què es decideix en dissenyar l'esquema, quines garanties es posen a la base i quines a l'aplicació, i quin procés envolta tot això. Gairebé tot són convencions, i en una convenció l'important no és quina tries, sinó que l'equip sencer triï la mateixa. La lliçó acaba amb el catàleg d'antipatrons i amb una llista de comprovació per revisar una consulta abans de donar-la per bona.
Contingut
- Nomenclatura
- Format i llegibilitat
- Disseny de l'esquema
- Fiabilitat: on viuen les garanties
- Procés: versions, revisió, proves i còpies
- Antipatrons
- Checklist de revisió d'una consulta
- Errors habituals i consells
- Exercicis
- Conclusió
- Nomenclatura
Singular o plural, i per què tant és
La discussió més vella de l'ofici: client o clients? Hi ha arguments honestos per a totes dues:
Plural (clients) |
Singular (client) |
|
|---|---|---|
| Raonament | Una taula és una col·lecció de files | Una fila és un client; la taula és el tipus |
| Es llegeix bé a | SELECT * FROM clients |
JOIN client ON client.id = ... |
| Ho fan servir | Rails, Django (per omissió), aquest curs | Hibernate/JPA per costum, molts DBA |
Cap de les dues no és millor. El que sí que és objectivament dolent és barrejar-les: un esquema amb clients, comanda i linia_comandes obliga a mirar el diccionari abans de cada consulta. BotigaVerda fa servir plural a totes les taules, sense excepció, i aquest és tot el mèrit que té la decisió.
Les regles que sí que són objectives
snake_caseen minúscules, sempre. PostgreSQL passa a minúscules qualsevol identificador que no vagi entre cometes dobles: si crees"DataComanda", l'hauràs d'escriure entre cometes per sempre, i al primer oblit sortiràcolumn "datacomanda" does not exist. Posar cometes dobles en unCREATE TABLEés una condemna.- Només ASCII als identificadors. Per això les columnes són
descripcioipuntuacio, i la taulalinies_comanda, sense accents ni punt volat (01-06). Les dades porten accents iç; els noms d'objecte, no. - Res de paraules reservades.
user,order,group,table,select,check,end... Una columna anomenadaorderobliga a entrecometar-la a cada consulta. Si el negoci diu "comanda", la taula es diucomandes; si diu "usuari",usuaris. - Noms que diguin alguna cosa.
dades,taula1,temp2,info,camp3,xno signifiquen res d'aquí a sis mesos. I sense abreviatures pròpies:data_comanda, nodat_com. - Sense prefix de tipus.
str_nom,tbl_clients,int_stocksón notació hongaresa: soroll que a més menteix tan bon punt algú canvia el tipus.
Claus, restriccions i índexs
La convenció de BotigaVerda, que és la més estesa i la que aquest curs ha fet servir en onze mòduls:
| Objecte | Convenció | Exemple del curs |
|---|---|---|
| Taula | Plural, snake_case |
linies_comanda |
| Clau primària | id, subrogada |
productes.id |
| Clau forana | <taula_singular>_id |
comandes.client_id, linies_comanda.producte_id |
| FK reflexiva | Nom del paper, no de la taula | empleats.cap_id, clients.referit_per_id |
| Booleà | Adjectiu afirmatiu, sense negar | actiu (mai no_actiu) |
| Data | data_<què> |
data_comanda, data_registre, data_alta |
Restricció CHECK |
chk_<taula>_<columna> |
chk_ressenyes_puntuacio |
| Clau forana (restricció) | fk_<taula>_<taula_referida> |
fk_comandes_clients |
UNIQUE |
uq_<taula>_<columnes> |
uq_clients_email |
| Índex | idx_<taula>_<columnes> |
idx_linies_comanda_comanda_id |
| Vista / materialitzada | v_ / mv_ |
v_detall_vendes, mv_vendes_mensuals |
| Funció / procediment / disparador | fn_ / sp_ / trg_ |
fn_total_comanda, sp_confirmar_comanda |
Dues notes. La primera: la FK reflexiva es nomena pel paper. empleats.empleat_id no diu res; cap_id ho diu tot. La segona: un booleà en negatiu és un parany —WHERE NOT no_actiu és il·legible i no_actiu = FALSE és pitjor—; anomena sempre la condició certa.
Anomena les restriccions a mà. Si no ho fas, PostgreSQL genera
productes_preu_checkocomandes_client_id_fkey, que són llegibles però no controlats per tu: canvien si canvia el nom de la columna, i apareixen tal qual al missatge d'error que veurà l'usuari. AmbCONSTRAINT chk_productes_preu_positiu CHECK (preu >= 0), l'error diu quina regla s'ha violat i l'aplicació el pot mapar a un missatge decent.
- Format i llegibilitat
Una consulta s'escriu una vegada i es llegeix vint. Compara:
-- ⚠️ INCORRECTA (no pel resultat, sinó pel que costa llegir-la i modificar-la)
select c.nom,sum(l.quantitat*l.preu_unitari*(1-l.descompte)) from categories c,productes p,
linies_comanda l where c.id=p.categoria_id and p.id=l.producte_id group by c.nom order by 2 desc;
-- ✅ CORRECTA
SELECT cat.nom AS categoria,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS facturacio
FROM linies_comanda AS lc
JOIN productes AS p ON p.id = lc.producte_id
JOIN categories AS cat ON cat.id = p.categoria_id
GROUP BY cat.nom
ORDER BY facturacio DESC;| categoria | facturacio |
|---|---|
| Alimentació | 256.27 |
| Begudes | 195.28 |
| Cosmètica natural | 156.32 |
| Llar sostenible | 88.58 |
| Higiene personal | 31.50 |
Cinc categories: Complements no hi apareix perquè el seu únic producte està descatalogat i no es va vendre mai, i un INNER JOIN no s'inventa files. Les dues consultes retornen el mateix; només una es pot modificar sense rellegir-la sencera. Les regles que aplica la segona:
- Una clàusula per línia, amb les paraules clau alineades a l'esquerra. Els ulls troben el
WHEREsense buscar-lo. JOINexplícit, mai la coma.FROM a, b WHERE a.id = b.a_idés sintaxi del 1989: barreja la unió amb el filtre i, si oblides la condició, produeix un producte cartesià silenciós (03-06). AmbJOIN ... ON, la unió i el filtre estan separats.- Àlies significatius.
lc,p,cats'entenen;a,b,cobliguen a pujar a mirar. Els del curs estan fixats des del mòdul 3 i no canvien mai. ASexplícit als àlies de columna. És opcional a PostgreSQL, i ometre'l fa que una coma oblidada converteixipreu, costenpreu AS cost, un error que cap eina no detecta.- Paraules clau en majúscules, identificadors en minúscules. No és cosmètica: separa d'un cop d'ull el llenguatge de les teves dades.
- Condició de
JOINen ordre constant:taula_nova.columna = taula_ja_coneguda.columna. Llegit en cadena, explica el recorregut.
Comentaris: el perquè, no el què
-- ⚠️ Inútil: repeteix el que el codi ja diu
-- Suma l'import de les línies
SELECT SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)) FROM linies_comanda AS lc;
-- ✅ Útil: explica una decisió que no es dedueix del codi
-- Fem servir preu_unitari i no productes.preu: és el preu HISTÒRIC de la venda.
-- Les comandes 1 i 2 són anteriors a la pujada de tarifes de l'abril del 2025.
SELECT SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)) FROM linies_comanda AS lc;I per al que sí que és de l'esquema, PostgreSQL té el lloc correcte: COMMENT ON, que queda dins de la base de dades i el veu qualsevol amb \d+.
COMMENT ON COLUMN linies_comanda.descompte IS 'Fracció entre 0 i 1 (0.10 = 10 %), no percentatge';
COMMENT ON TABLE ressenyes IS 'Sense accents ni punt volat per portabilitat d''identificadors; la dada sí que en porta';Formatadors automàtics
Discutir la indentació en una revisió de codi és temps llençat. Delega-ho:
| Eina | Què és | Nota |
|---|---|---|
pgFormatter (pg_format) |
Formatador per a PostgreSQL, en Perl | Molt configurable; hi ha complement per als editors habituals |
| sqlfluff | Linter i formatador, en Python | A més detecta males pràctiques; entén plantilles de dbt/Jinja |
| Formatador de l'IDE | DBeaver, DataGrip, pgAdmin | Còmode, però cadascun formata diferent |
La recomanació: fixa un estil en un fitxer de configuració dins del repositori (.sqlfluff, .pg_format) i executa'l al ganxo de pre-commit o a la integració contínua. Així l'estil deixa de ser una opinió i passa a ser una comprovació automàtica.
- Disseny de l'esquema
Normalitzar per defecte, desnormalitzar amb motiu
La regla, reprenent 01-05 i 05-06: comença normalitzat. La normalització elimina la redundància, i la redundància és el que permet que dues còpies de la mateixa dada acabin dient coses diferents. Desnormalitza només quan tinguis una mesura que ho justifiqui i un mecanisme que mantingui la còpia sincronitzada.
BotigaVerda té els dos casos, i convé distingir-los:
| Cas | Què és | Veredicte |
|---|---|---|
linies_comanda.preu_unitari |
Sembla còpia de productes.preu |
No és desnormalització: és una dada històrica diferent. El preu de venda del març no és el d'avui |
comandes.total (mòdul 10) |
Suma precalculada de les línies | Sí que ho és: cal mantenir-la amb un disparador, i si el disparador falla la fila menteix |
La pregunta que resol el dubte: el valor pot canviar pel seu compte després de desar-se? Si no —el preu al qual es va vendre—, no és redundància, és història, i s'ha de desar. Si sí —el total de la comanda, la valoració mitjana—, és un agregat i té cost de manteniment.
Tipus: el més restrictiu que serveixi
Un tipus és la restricció més barata que hi ha, perquè no costa res comprovar-la.
| En lloc de | Fes servir | Perquè |
|---|---|---|
TEXT per a tot |
VARCHAR(n), INTEGER, DATE, NUMERIC |
Un TEXT accepta "tres" en una quantitat |
FLOAT/REAL per a diners |
NUMERIC(10,2) |
Coma flotant = errors d'arrodoniment (01-04) |
VARCHAR per a dates |
DATE / TIMESTAMPTZ |
'31/02/2025' cap en un VARCHAR |
INTEGER 0/1 per a banderes |
BOOLEAN |
actiu = 2 no hauria d'existir |
VARCHAR(255) per costum |
La longitud real del domini | 255 és una herència de MySQL, no una mesura |
NOT NULL per defecte, NULL com a decisió
Reprenent 04-03: un NULL és una font de complexitat que es propaga per tota l'aplicació —agregats que l'ignoren, comparacions que no són certes ni falses, NOT IN que retorna zero files—. Per això la postura per omissió ha de ser NOT NULL, i cada columna nul·lable ha de ser una decisió que sàpigues defensar.
A BotigaVerda hi ha tres columnes nul·les i totes tres tenen un significat explícit: comandes.empleat_id = comanda web sense comercial; clients.referit_per_id = va arribar pel seu compte; empleats.cap_id = direcció general. Cap no significa "encara no ho sabem" ni "el formulari venia buit", i aquí hi ha la diferència: si un NULL pot significar dues coses diferents, el disseny està malament.
Claus subrogades enfront de naturals, i l'EAV
Clau subrogada (id) |
Clau natural (email, NIF) |
|
|---|---|---|
| Estabilitat | Total: no la canvia res | El client canvia de correu |
| Mida de les FK | Petita (4-8 bytes) | La del text, repetida a cada taula filla |
| Llegibilitat d'una FK | Cap: cal unir per saber qui és | Es llegeix directament |
| Unicitat de negoci | No la garanteix: cal un UNIQUE a part |
La garanteix per definició |
El criteri: id subrogat com a PK, i la clau natural protegida amb UNIQUE. És exactament el que fa clients (PK id, UNIQUE(email)), i així tens les dues coses: estabilitat i unicitat de negoci.
I l'antipatró que cal reconèixer per fugir-ne: l'EAV (Entity-Attribute-Value), una taula atributs(entitat_id, nom_atribut, valor) que promet flexibilitat infinita. El preu: tot és text, no hi ha tipus ni restriccions, consultar tres atributs són tres autounions, i ningú no sap quins atributs existeixen. Si els camps varien de debò, la resposta moderna és JSONB (10-06); si no varien, són columnes.
CHECK o taula de catàleg en lloc de text lliure
comandes.estat podria ser un VARCHAR sense més, i al cap de sis mesos tindries lliurat, Lliurat, LLIURAT i lliruat. Hi ha dues maneres d'impedir-ho:
CHECK (estat IN (...)) |
Taula de catàleg + FK | |
|---|---|---|
| Afegir un valor | ALTER TABLE (migració, 05-06) |
Un INSERT |
| Desar atributs del valor | No pot | Sí: ordre, color, si és final |
| Traduir a altres idiomes | No | Sí |
| Quan fer-ho servir | Llista tancada i curta que gairebé mai no canvia | Llista que creix o té atributs propis |
BotigaVerda fa servir CHECK per a estat i metode_pagament perquè són cinc i quatre valors que formen part del model de negoci. Un catàleg de països o de categories, en canvi, és una taula.
- Fiabilitat: on viuen les garanties
La regla que separa una base sana d'una de malalta: la integritat viu a la base de dades, no només a l'aplicació.
És temptador pensar "ja valido jo al formulari". No n'hi ha prou, per quatre motius que es compleixen sempre: arribarà una segona aplicació (el panell d'administració, un guió de migració, una integració); algú executarà un UPDATE a mà a psql una nit; hi haurà un bug a la validació de l'aplicació; i hi haurà concurrència, i una comprovació "llegeixo i després escric" feta a l'aplicació té una condició de cursa que només una restricció UNIQUE tanca de debò (09-04).
| Garantia | Lloc correcte | A BotigaVerda |
|---|---|---|
| "Aquest client existeix" | FK | comandes.client_id REFERENCES clients(id) |
| "No hi ha dos correus iguals" | UNIQUE |
clients.email |
| "La puntuació va d'1 a 5" | CHECK |
chk_ressenyes_puntuacio |
| "El camp és obligatori" | NOT NULL |
comandes.data_comanda |
| "No es pot demanar més estoc del que hi ha" | Transacció + bloqueig, o disparador | sp_confirmar_comanda (10-04) |
| "El missatge d'error ha de ser bonic" | Aplicació | Traduir l'error de la restricció |
L'aplicació també valida: per donar missatges útils i no fer viatges inútils al servidor. Però valida a més, no en lloc de.
Dos detalls més. DEFAULT assenyats: stock DEFAULT 0, actiu DEFAULT TRUE, descompte DEFAULT 0 eviten que un INSERT incomplet fiqui nuls on no deu. I TIMESTAMPTZ amb UTC per a tot instant: TIMESTAMP sense zona desa un número sense significat, i en una botiga que ven a Espanya, Portugal i França això es paga el dia del canvi d'hora. Desa en UTC, converteix en mostrar. Les dates de calendari pures —data_comanda, data_registre— sí que són DATE, perquè el 4 de març és el 4 de març a tot arreu.
- Procés: versions, revisió, proves i còpies
L'esquema és codi. Tot el de la lliçó 05-06 es resumeix en una frase: si l'esquema de producció no es pot reconstruir des del repositori, no tens control de versions.
- Migracions numerades, versionades i immutables. Cada canvi és un fitxer (
V007__add_index_comandes_data.sql), va a Git amb el codi que el necessita, i no s'edita una vegada aplicat: es corregeix amb una migració nova. Eines: Flyway, Liquibase, Alembic, o les migracions de l'ORM (11-05). - Cada migració amb la seva tornada enrere, o almenys amb un pla escrit de què fer si falla. I els canvis trencadors, amb el patró expand/contract de 05-06: afegir, desplegar, migrar dades, i només llavors treure.
- Revisió de codi també per al SQL. Una consulta d'informe mereix la mateixa revisió que una funció: és igual de fàcil equivocar-se i molt més difícil adonar-se'n. Un
JOINque duplica files produeix números, no excepcions. - Un entorn de proves amb dades realistes. Realistes en volum i forma —un pla sobre 20 files no diu res del que passarà amb 20 milions (08-05)— però ficticis o anonimitzats.
⚠️ No copiïs dades personals de producció a desenvolupament. És la pràctica més estesa i una de les més perilloses: multiplica les còpies de dades reals en portàtils, entorns sense xifrar i bolcats que ningú no esborra. Genera dades sintètiques, o pseudonimitza abans de copiar (substituir noms i correus, desplaçar dates, arrodonir imports) — amb l'advertiment que la pseudonimització mal feta és reversible. Abans de moure dades personals entre entorns, consulta-ho amb el responsable de protecció de dades o amb l'assessoria jurídica de la teva organització. És el mateix avís de 05-04, i aquí també s'aplica.
- Còpies de seguretat provades. Una còpia que no s'ha restaurat mai no és una còpia: és un fitxer del qual suposes coses. Programa una restauració de prova periòdica en un entorn a part i mesura quant triga, perquè aquest número és el teu temps real de recuperació. Comprova també que la còpia inclou el que et penses (rols, extensions, seqüències) i que la retenció cobreix el temps que trigues a detectar un problema: si l'esborrat es descobreix als deu dies i en guardes set, no hi ha còpia.
- Monitoratge de consultes lentes.
pg_stat_statementsordenat per temps total —no per temps mitjà, que amaga l'N+1 (08-04)—,log_min_duration_statementper registrar el que passi d'un llindar, i una revisió periòdica del rànquing. Sense això no t'assabentes que alguna cosa va malament: t'ho explica un usuari enfadat.
- Antipatrons
| Antipatró | Per què fa mal | Què fer |
|---|---|---|
SELECT * en producció |
Porta columnes que ningú no fa servir, impedeix l'Index Only Scan, i es trenca quan algú afegeix una columna |
Enumerar columnes. SELECT * només per explorar a psql (08-04) |
| La mateixa regla de negoci en cinc llocs | El dia que canvia l'IVA cal trobar els cinc | Una vista o una funció que sigui l'única definició (10-01, 10-04) |
DELETE/UPDATE sense WHERE en producció |
Esborra la taula sencera i ja està | BEGIN primer, SELECT la mateixa condició, comprovar el recompte i després COMMIT (05-04). I \set AUTOCOMMIT off a psql |
| Construir SQL concatenant cadenes | Injecció SQL, sense més | Consultes parametritzades → 11-03 |
| Consultes dins d'un bucle | L'N+1: 21 o 501 consultes per pintar una pantalla | Un JOIN, o càrrega anticipada de l'ORM → 11-05 |
| Índexs "per si de cas" | Cada índex frena totes les escriptures i ocupa disc; els que no es fan servir només costen | Crear amb una consulta concreta al davant; revisar pg_stat_user_indexes (08-02) |
| "Ho arreglo directament en producció" | El canvi no és al repositori: al desplegament següent desapareix, o al revés, la migració xoca | Corregir en una migració i desplegar-la. Sense excepcions |
| Lògica de negoci en un disparador sorpresa | Un INSERT fa coses que no són al codi i ningú no les troba |
Disparadors per a integritat i auditoria; la lògica visible, al codi (10-05) |
Un NULL que significa diverses coses |
"No ho sé", "no aplica" i "zero" no són el mateix | Separar en columnes o valors explícits (04-03) |
| Arrodonir al final d'una cadena de mitjanes | Mitjana de mitjanes, errors acumulats | Arrodonir només en presentar (11-04) |
- Checklist de revisió d'una consulta
Abans de donar una consulta per bona, en aquest ordre:
Correcció
- Els
JOINmultipliquen files? Comprova el recompte abans i després de cadascun: sicomandespassa de 20 a 47, estàs sumant línies, no comandes. - Hi ha
LEFT JOINon el negoci admet absència? Les 10 comandes web sense comercial desapareixen amb unINNER JOIN. - Què passa amb els
NULL? AlWHERE, alNOT IN, a les agregacions, a les concatenacions. - El resultat quadra amb una xifra coneguda? Si el total d'un desglossament no dona 727,95 €, el desglossament està malament.
- L'
ORDER BYés determinista? Sense desempat, dues execucions poden retornar ordres diferents.
Rendiment
- Les condicions són sargables (columna nua)? Existeixen els índexs que necessita (08-01)?
- Retorna només les columnes i les files que es faran servir? Té
LIMITsi va a una pantalla? - L'has executada amb
EXPLAIN ANALYZEsobre un volum realista (08-05)?
Mantenibilitat
- Es llegeix? Àlies significatius, una clàusula per línia,
ASexplícit? - Els comentaris expliquen per què, no què?
- És al repositori, amb la pregunta de negoci que respon escrita al costat?
Seguretat
- Tots els valors de l'usuari van com a paràmetres (11-03)?
- Retorna dades personals que qui l'executa no hauria de veure?
Errors habituals i consells
- Entrecometar identificadors al
CREATE TABLE."DataComanda"t'obliga a escriure-ho amb cometes per sempre.snake_caseen minúscules i s'ha acabat. - Barrejar singular i plural, o dues convencions de FK. El cost no és estètic: és haver de consultar el diccionari a cada consulta.
- Deixar que PostgreSQL anomeni les restriccions. El nom generat acaba al missatge d'error que veu l'usuari i canvia si canvia la columna. Anomena-les:
chk_,fk_,uq_,idx_. - Desar diners en
FLOATo dates enVARCHAR. És la decisió que més car es paga i la més difícil de revertir amb dades a dins. - Posar tota la validació a l'aplicació. Arribarà una segona aplicació, un guió nocturn i una condició de cursa. La integritat va a la base.
- Fer servir
TIMESTAMPsense zona per a instants. Desa en UTC ambTIMESTAMPTZi converteix en mostrar. - Editar una migració ja aplicada. Els entorns queden desincronitzats en silenci. Es corregeix amb una migració nova.
- Consell: la primera consulta de qualsevol informe és
SELECT COUNT(*). Si el recompte canvia en afegir unJOIN, atura't i esbrina per què abans de continuar. - Consell: automatitza l'estil amb
sqlfluffopg_formata la integració contínua. Les revisions de codi haurien de discutir la lògica, no la indentació. - Consell: escriu la convenció en un fitxer del repositori. Mitja pàgina n'hi ha prou, i converteix "així ho fem" en una cosa que un company nou pot llegir.
Exercicis
Exercici 1
Aquest esquema és real en el sentit que s'assembla molt al que et trobaràs. Enumera tots els problemes de nomenclatura, tipus i disseny, i reescriu-lo.
CREATE TABLE "Comandes_Client" (
"ID" VARCHAR(50) PRIMARY KEY,
"user" VARCHAR(255),
order_date VARCHAR(20),
total FLOAT,
"Estat" VARCHAR(255),
no_actiu INTEGER,
dades TEXT
);Exercici 2
El teu equip discuteix on posar la regla "una comanda cancel·lada no pot rebre línies noves". Hi ha tres propostes: (a) validar-ho al formulari web; (b) un CHECK a linies_comanda; (c) un disparador BEFORE INSERT. (1) Quina funciona i quina no, i per què? (2) Quina triaries i què faries a més? (3) Canvia la resposta si el sistema té també un panell d'administració i un procés nocturn d'importació?
Exercici 3
Reps aquesta consulta en una revisió de codi, amb la nota "dona el nombre de comandes i la facturació per comercial". Aplica-hi el checklist de l'apartat 7 i digues què està malament.
select e.nom, count(*), sum(l.quantitat*l.preu_unitari)
from empleats e, comandes c, linies_comanda l
where e.id=c.empleat_id and c.id=l.comanda_id
group by e.nom;Solucions
Solució 1
Els problemes, un a un: identificadors entre cometes amb majúscules ("Comandes_Client", "ID", "Estat"), que obliguen a escriure cometes per sempre; "user" és paraula reservada; barreja d'idiomes (order_date al costat d'Estat); ID VARCHAR(50) com a PK quan hauria de ser un enter subrogat; order_date VARCHAR en lloc de DATE; total FLOAT per a diners; VARCHAR(255) per costum; no_actiu en negatiu i com a INTEGER en comptes de BOOLEAN; dades TEXT que no diu què conté; cap FK cap a clients; cap NOT NULL, cap CHECK i cap restricció amb nom.
CREATE TABLE comandes_client (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
client_id INTEGER NOT NULL
CONSTRAINT fk_comandes_client_clients REFERENCES clients(id) ON DELETE RESTRICT,
data_comanda DATE NOT NULL,
total NUMERIC(10,2) NOT NULL DEFAULT 0
CONSTRAINT chk_comandes_client_total CHECK (total >= 0),
estat VARCHAR(20) NOT NULL
CONSTRAINT chk_comandes_client_estat
CHECK (estat IN ('pendent','pagat','enviat','lliurat','cancellat')),
actiu BOOLEAN NOT NULL DEFAULT TRUE,
observacions TEXT
);I una pregunta que cal fer-se abans d'escriure res: total és un agregat precalculat. Si es pot sumar de les línies, potser no hauria d'existir; i si existeix per rendiment, cal el mecanisme que el mantingui (10-05) i la decisió escrita de per què.
Solució 2
1. La (b) no funciona: un CHECK només pot mirar la fila que s'està inserint, i l'estat de la comanda és en una altra taula. PostgreSQL no permet subconsultes en un CHECK precisament perquè no podria garantir que continuï sent cert quan canviï l'altra taula. La (a) funciona però no protegeix: cobreix un camí d'entrada dels diversos que hi ha. La (c) sí que funciona: un disparador BEFORE INSERT sobre linies_comanda pot consultar comandes.estat i llançar RAISE EXCEPTION (10-05).
2. El disparador, i a més la validació al formulari, per donar un missatge decent sense anar al servidor. La base garanteix; la interfície explica. I cal pensar el cas simètric —què passa si la comanda es cancel·la després de tenir línies?—, que aquell disparador no cobreix.
3. No canvia la resposta, la reforça. Amb tres vies d'entrada, la validació al formulari en protegeix una de tres, i precisament el procés nocturn és el que inserirà milers de files sense que ningú les miri. Aquest és l'argument sencer de l'apartat 4.
Solució 3
Quatre problemes de correcció i uns quants de forma. (1) El COUNT(*) està malament: en unir amb linies_comanda, cada comanda hi apareix tantes vegades com línies té, així que no compta comandes sinó línies. Ha de ser COUNT(DISTINCT c.id). (2) Falta el descompte: l'import és quantitat * preu_unitari * (1 - descompte), i sense ell la facturació surt inflada. (3) INNER JOIN amb empleats deixa fora les 10 comandes web: si l'informe vol "per comercial", cal decidir explícitament si aquelles comandes s'exclouen o hi apareixen com a "Web" amb un LEFT JOIN i COALESCE. (4) GROUP BY e.nom agrupa per nom de pila: dos comercials que es diguin igual es fondrien en una fila. Cal agrupar per e.id. En la forma: sintaxi de comes en lloc de JOIN, sense AS ni àlies de columna, sense majúscules i sense ORDER BY.
SELECT e.id, e.nom || ' ' || e.cognoms AS comercial,
COUNT(DISTINCT co.id) AS comandes,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS facturacio
FROM empleats AS e
JOIN comandes AS co ON co.empleat_id = e.id
JOIN linies_comanda AS lc ON lc.comanda_id = co.id
GROUP BY e.id, e.nom, e.cognoms
ORDER BY facturacio DESC;| id | comercial | comandes | facturacio |
|---|---|---|---|
| 5 | Laia Puig Sanchis | 4 | 191.63 |
| 4 | Óscar Peris Blasco | 4 | 132.90 |
| 6 | Marc Estévez Roig | 2 | 54.30 |
Tres comercials, 10 comandes i 378,83 € — el canal telefònic. Els altres 349,12 € són les 10 comandes web sense comercial, i que no hi apareguin és ara una decisió, no un descuit.
Conclusió
Aquesta lliçó no afegia sintaxi: afegia criteri.
- Nomenclatura:
snake_caseen minúscules i només ASCII; singular o plural tant és, barrejar-los no;idper a la PK i<taula>_idper a la FK, amb el nom del paper a les reflexives; booleans en afirmatiu; res de paraules reservades ni dedades/taula1; i restriccions i índexs anomenats a mà ambchk_,fk_,uq_,idx_. - Format: una clàusula per línia,
JOIN ... ONexplícit en lloc de la coma, àlies significatius,ASexplícit, paraules clau en majúscules, comentaris que expliquen el perquè,COMMENT ONper al que és de l'esquema, i un formatador automàtic configurat al repositori. - Disseny: normalitzar per defecte i desnormalitzar amb una mesura i un mecanisme al darrere; el tipus més restrictiu que serveixi;
NOT NULLper defecte i cadaNULLamb un significat únic;idsubrogat mésUNIQUEsobre la clau natural; res d'EAV; iCHECKper a llistes tancades, taula de catàleg per a les que creixen. - Fiabilitat: la integritat viu a la base de dades, perquè arribarà una segona aplicació, un
UPDATEa mà, un bug i una condició de cursa.DEFAULTassenyats iTIMESTAMPTZen UTC. - Procés: migracions versionades i immutables, revisió de codi també per al SQL, entorn de proves amb dades fictícies o anonimitzades —mai còpies de producció amb dades personals—, còpies de seguretat provades (una còpia sense restaurar no és una còpia) i monitoratge de consultes lentes per temps total.
- I el checklist de tretze punts, que comença per la pregunta que més errors evita: aquest
JOINmultiplica files?
Dos dels antipatrons de la taula s'han quedat amb una promesa: la construcció de SQL per concatenació i el "qui pot veure què" que ha anat apareixent des del mòdul 5. A la lliçó següent, Seguretat: injecció SQL, permisos i rols, es tanquen tots dos: què és exactament una injecció SQL i per què passa; la defensa que funciona de debò —les consultes parametritzades— escrita en quatre llenguatges; el que no és una defensa; el cas especial dels identificadors dinàmics; i després el model de permisos de PostgreSQL complet, amb GRANT, REVOKE, rols, ALTER DEFAULT PRIVILEGES, seguretat a nivell de fila i un disseny de rols concret per a BotigaVerda.
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
