Neteja: tipus i formats¶
Exercici cleaning-types-formats-annex.pdf · PDF
Pregunta ràpida: què és més gran, 10 o 9? Ni t'ha calgut pensar-hi. Ara fes-li la mateixa pregunta a una base de dades, amb un petit canvi:
SELECT '10' < '9' AS as_text, 10 < 9 AS as_number;
| as_text | as_number |
|---|---|
| true | false |
Amb cometes, 10 és més petit que 9. Sense cometes, no. Les mateixes xifres, respostes oposades. Les cometes converteixen un nombre en text, i el text es compara com les paraules d'un diccionari: 1 va abans que 9, i punt. Aquest és exactament el motiu pel qual games no podia ordenar les seves pròpies dates de llançament. En aquesta pàgina ho arregles.
Què aprendràs
- Explicar què va malament quan un nombre o una data s'emmagatzema com a text: ordenar, comparar, aritmètica
- Convertir un valor a un altre tipus amb
CAST(x AS type)ox::type, i llegir l'error quan no es pot fer - Treure espais sobrers amb
TRIMi reparar separadors decimals i de milers ambREPLACE - Convertir dates en text escrites en dos formats en una sola columna
DATEde debò ambTO_DATE - Distingir un error sorollós d'un error silenciós, i dir quin és pitjor
- Evitar la trampa: «la consulta s'ha executat sense cap error» no vol dir que la conversió sigui correcta
El forn: del mineral als lingots¶
A l'auditoria de qualitat de les dades vas obrir el cofre i només vas mirar. Ara encens el forn. En un joc de crafting, el mineral sembla metall, però no pots fabricar res amb ell fins que es fon en un lingot. '19.99' sembla un nombre. Encara no ho és.
Un tipus de dades és la classe de valor que conté una columna. Decideix quines operacions tenen sentit: pots sumar dos nombres, pots restar dues dates, només pots enganxar dos textos.
| Tipus a PostgreSQL | Conté | Exemple | Pots |
|---|---|---|---|
text |
Qualsevol caràcter | 'Nov 16, 2004' |
Cercar-hi a dins, enganxar-lo, ordenar-lo alfabèticament |
integer, bigint |
Nombres enters | 42 |
Sumar, fer-ne la mitjana, ordenar per mida |
numeric(8,2) |
Decimals exactes | 19.99 |
Fer aritmètica de diners sense sorpreses d'arrodoniment |
date |
Un dia del calendari | 2004-11-16 |
Ordenar per temps, extreure l'any, comptar dies entre dues |
Normalitzar una columna vol dir donar-li un sol tipus i un sol format, de manera que cada valor s'escrigui de la mateixa manera.
Mantenim l'analogia del forn durant tota la pàgina: el text és el mineral, els valors amb tipus són els lingots.
Conversió: encendre el forn¶
Convertir (en anglès, casting) és passar un valor d'un tipus a un altre. El valor surt en el tipus nou, o la consulta s'atura amb un error.
PostgreSQL té dues maneres d'escriure-ho. Fan exactament el mateix:
SELECT '19.99'::numeric + 1, CAST('42' AS integer) * 2;
| Primera columna | Segona columna |
|---|---|
| 20.99 | 84 |
| Escriptura | Es llegeix com | Nota |
|---|---|---|
CAST(x AS type) |
«fon x en aquest tipus» |
SQL estàndard, funciona a totes les bases de dades |
x::type |
El mateix, més curt | Drecera de PostgreSQL |
Quan el forn no pot empassar-se un valor, ho diu:
SELECT '1,234.50'::numeric;
ERROR: invalid input syntax for type numeric: "1,234.50"
Aquesta coma és el problema. Guarda't aquest error: tornarem a veure si és una bona o una mala notícia.
Compte: CAST no canvia la taula
Una conversió canvia el resultat d'aquesta consulta. La taula games continua tan bruta com estava. Per això desaràs la teva consulta de neteja al final de la pàgina: la consulta és la reparació, i la taula neta es construirà més endavant a partir d'ella.
Retallar: espais que no veus¶
Retallar (trimming) és treure els espais dels dos extrems d'un text.
Un espai és un caràcter com qualsevol altre. Per a tu, ' Valve ' i 'Valve' són el mateix distribuïdor. Per a una base de dades, són dos de diferents:
SELECT ' Valve ' = 'Valve' AS as_typed,
TRIM(' Valve ') = 'Valve' AS trimmed;
| as_typed | trimmed |
|---|---|
| false | true |
Un sol espai sobrer en un fitxer i el teu GROUP BY publisher mostra la mateixa empresa dues vegades, amb els jocs repartits entre les dues files. TRIM(x) treu els espais del principi i del final. No toca els del mig, de manera que 'Half-Life 2' conserva el seu espai.
A games, TRIM(game) canvia 0 files: els noms ja són nets. Igualment el mantenim a la consulta de neteja. No costa res i et protegeix el dia que algú pugi un fitxer que no estigui tan endreçat.
Separadors: decoració per a humans¶
Un separador decimal marca on acaba la part sencera d'un nombre. Un separador de milers només agrupa les xifres perquè les persones les puguin llegir.
Aquí hi ha el truc: els països no es posen d'acord sobre quin símbol fa cada feina.
| Escrit com | On | Separador de milers | Separador decimal |
|---|---|---|---|
1,234.50 |
Països de parla anglesa | coma | punt |
1.234,50 |
Espanya i la major part d'Europa | punt | coma |
1234.50 |
El que accepta el forn | cap | punt |
Les dues primeres línies són el mateix nombre: mil dos-cents trenta-quatre i mig. El forn només accepta la tercera forma. Per tant, primer treus la decoració, amb REPLACE(text, old, new), que canvia cada old per new:
SELECT REPLACE('1,234.50', ',', '')::numeric; -- 1234.50
SELECT REPLACE(REPLACE('1.234,50', '.', ''), ',', '.')::numeric; -- 1234.50
L'espanyola necessita dos passos, i l'ordre importa: primer esborra els punts (milers), després converteix la coma en un punt (decimals). Fes-ho al revés i esborraràs el punt decimal que acabes de crear.
Compte: és un decimal o un miler?
1.234 és «u coma dos tres quatre» a Londres i «mil dos-cents trenta-quatre» a Palma. El valor en si no et ho pot dir. Has de saber d'on ve el fitxer abans de triar quin REPLACE escrius.
Dates: un valor, moltes grafies¶
Les dates són les pitjors, perquè cada país i cada pàgina web les escriu de manera diferent. TO_DATE(text, pattern) llegeix una data escrita com diu el patró:
| Peça del patró | Vol dir | Coincideix amb |
|---|---|---|
Mon |
Mes en tres lletres | Nov |
DD |
Dia del mes | 16 |
MM |
Mes en nombre | 11 |
YYYY |
Any amb quatre xifres | 2004 |
La majoria de les dates de games són com Nov 16, 2004, de manera que el patró és 'Mon DD, YYYY'. Prova primer la manera mandrosa, sobre tota la columna:
SELECT release_date::date FROM games;
ERROR: invalid input syntax for type date: "Jun 2009"
SELECT TO_DATE(release_date, 'Mon DD, YYYY') FROM games;
ERROR: date/time field value out of range: "Jun 2009"
Les dues línies fallen amb el mateix tipus de valor: una data sense dia. L'auditoria ja ens va avisar que 42 files tenien una altra forma. Abans de convertir, compta les formes amb exactitud.
Compta les formes¶
Dues eines noves. CASE és una bifurcació del camí: cada fila pren la primera branca la condició de la qual és certa (té la seva pròpia lliçó a Categorització). I ~ pregunta «aquest text coincideix amb aquest patró?».
SELECT CASE WHEN release_date ~ '^[A-Z][a-z]{2} [0-9]{1,2}, [0-9]{4}$' THEN 'full'
WHEN release_date ~ '^[A-Z][a-z]{2} [0-9]{4}$' THEN 'month only'
ELSE 'other' END AS shape,
COUNT(*) AS n
FROM games GROUP BY shape ORDER BY n DESC;
| shape | n |
|---|---|
| full | 26646 |
| month only | 41 |
| other | 1 |
No cal que escriguis aquests patrons de memòria, només que els sàpigues llegir:
| Peça | Vol dir |
|---|---|
^ ... $ |
Des del principi mateix del text fins al final |
[A-Z][a-z]{2} |
Una lletra majúscula i dues de minúscules: Nov |
[0-9]{1,2} |
Una o dues xifres: el dia |
[0-9]{4} |
Exactament quatre xifres: l'any |
L'única fila d'other és 8 Oct, 2014, el joc Hazard Ops: primer el dia, després el mes, al revés de tots els altres.
Construeix el convertidor¶
La mateixa bifurcació, però ara cada camí fon la seva pròpia forma. No hi ha ELSE: tot el que no encaixa en cap camí es converteix en NULL.
WITH step1 AS (
SELECT number, TRIM(game) AS game,
CASE WHEN release_date ~ '^[A-Z][a-z]{2} [0-9]{1,2}, [0-9]{4}$'
THEN TO_DATE(release_date, 'Mon DD, YYYY')
WHEN release_date ~ '^[A-Z][a-z]{2} [0-9]{4}$'
THEN TO_DATE(release_date, 'Mon YYYY')
END AS release_date,
price::numeric(8,2) AS price,
owners, developer, publisher, average_playtime, median_playtime, metascore
FROM games
WHERE game IS NOT NULL
)
SELECT COUNT(*), MIN(release_date), MAX(release_date),
COUNT(*) - COUNT(release_date) AS no_date
FROM step1;
| count | min | max | no_date |
|---|---|---|---|
| 26685 | 2004-01-11 | 2018-12-31 | 1 |
Aquí hi ha tres coses noves:
| Peça | Què fa |
|---|---|
WITH step1 AS (...) |
Dona un nom a una consulta, de manera que el SELECT de sota pugui fer servir el seu resultat com si fos una taula |
price::numeric(8,2) |
Un decimal exacte amb dos decimals: el tipus adequat per als diners |
WHERE game IS NOT NULL |
Descarta les 3 files sense nom, tal com va decidir l'inventari |
L'últim SELECT és una fila de comprovació: quantes files, la data més antiga, la data més recent, quantes no tenen data. Diu 26,685 jocs, del 2004 al 2018, i exactament un sense data. Aquest és Hazard Ops.
Per què numeric per als diners?
La columna price original té el tipus double precision, que emmagatzema els decimals de manera aproximada. Suma-ho tot i es veuen els errors minúsculs: SUM(price) dona 211089.76999994315. Amb SUM(price::numeric(8,2)) obtens 211089.77. Els diners es compten en cèntims exactes, de manera que reben un tipus exacte.
Fes-ho servir¶
Amb una data de debò, una pregunta que era impossible a la taula original es resol en una línia. Conserva el bloc WITH i canvia només l'últim SELECT:
SELECT EXTRACT(YEAR FROM release_date)::int AS year, COUNT(*) AS n
FROM step1 GROUP BY year ORDER BY year;
%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#4051b5"}}}}%%
xychart-beta
title "Jocs llançats per any a la taula de classe"
x-axis [2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018]
y-axis "Jocs" 0 --> 9000
bar [53, 58, 112, 143, 183, 334, 340, 364, 426, 556, 1352, 2544, 4503, 6871, 8845]
53 jocs el 2004, 8,845 el 2018. Aquesta corba era amagada dins d'una columna de text des del principi.
Errors sorollosos i errors silenciosos¶
Tornem al forn que rebutja '1,234.50'. Bona o mala notícia?
Un error sorollós atura la consulta i et diu quin valor no ha pogut convertir. Un error silenciós et dona un valor incorrecte i cap avís.
Bona notícia. Un error sorollós és el tipus fàcil: el veus, l'arregles. Ara coneix l'altre tipus. Pren el 4 de novembre de 2026, escrit a l'estil espanyol:
SELECT '04/11/2026'::date; -- 2026-04-11
SELECT TO_DATE('04/11/2026', 'DD/MM/YYYY'); -- 2026-11-04
La primera línia ha retornat l'11 d'abril. Cap error. PostgreSQL ha hagut d'endevinar, i ha endevinat primer el mes, a l'americana. A la segona línia se li ha dit el format, i l'ha encertat.
flowchart TB
T["<b>Valor en text</b><br>'04/11/2026'"] --> G{"Has indicat<br>el format?"}
G -- "No: '04/11/2026'::date" --> S["<b>Error silenciós</b><br>2026-04-11, sense avís"]
G -- "Sí: TO_DATE(..., 'DD/MM/YYYY')" --> OK["<b>Correcte</b><br>2026-11-04"]
S --> CH["Acaba en un gràfic.<br>Algú hi pren decisions."]
| Error sorollós | Error silenciós | |
|---|---|---|
| Què passa | La consulta s'atura | La consulta s'executa |
| Què obtens | Un missatge que anomena el valor erroni | Un valor incorrecte que sembla correcte |
| Exemple | '1,234.50'::numeric |
'04/11/2026'::date dona l'11 d'abril |
| Com detectar-lo | No te'l pots perdre | Només comprovant: MIN, MAX, recompte de NULL |
La regla: quan converteixes, li dius al forn el format. Mai no el deixes endevinar. I sempre comproves el resultat amb una fila de comprovació, encara que res no s'hagi queixat.
Compte: una data NULL no és una conversió fallida
Per a una fila, Hazard Ops, hem triat NULL a propòsit. Sabem que és una sola fila i ho hem apuntat. Un NULL conegut i comptat és honest; una data inventada no ho és.
La mateixa honestedat val per a les 41 dates que només tenen mes. TO_DATE('Oct 2010', 'Mon YYYY') retorna 2010-10-01: l'1 del mes. Ningú no va llançar 41 jocs l'1. Hem triat nosaltres aquest dia, i l'informe final ho ha de dir.
Prova-ho: release_date es converteix en una data de debò¶
Obre SQL → SQL Lab, base de dades Dades classe, esquema public. Executa una sentència cada vegada: selecciona-la i després Run.
Pas 1 — Prova la manera mandrosa. Executa les dues línies que fallen (release_date::date i TO_DATE amb un sol patró). Hauries d'obtenir dos missatges d'error, tots dos anomenant un valor com "Jun 2009".
Pas 2 — Compta les formes. Executa la consulta amb CASE. Hauries de veure full 26,646, month only 41 i other 1. Copia els patrons d'aquesta pàgina en lloc de tornar a escriure'ls: un espai que falti costa cinc minuts.
Pas 3 — Construeix el convertidor. Executa la consulta amb WITH step1. La fila de comprovació ha de dir 26,685 files, la més antiga 2004-01-11, la més recent 2018-12-31, no_date 1. Si no_date és 42 o 26,685, algun patró s'ha escrit malament.
Pas 4 — Fes-lo servir i desa'l. Canvia l'últim SELECT per la consulta de jocs per any. Després prem Save, al costat de Run, i posa-li a la consulta el nom games_step1. Apareix a SQL → Saved Queries. La lliçó següent comença des d'ella.
La consulta falla prop de ')' o prop de 'SELECT'
Després d'un bloc WITH no hi ha d'haver cap punt i coma abans de l'últim SELECT: el bloc i el SELECT són una sola sentència. Un error prop de ) normalment vol dir un ; sobrer. Un error prop de SELECT normalment vol dir que falta el ) de tancament del bloc.
Idees clau¶
- Un tipus de dades decideix què pots fer amb una columna. Un nombre o una data emmagatzemats com a text s'ordenen com un diccionari:
'10' < '9'és cert. - Convertir (
CAST(x AS type)ox::type) transforma un valor o s'atura amb un error. Canvia el resultat de la consulta, mai la taula. TRIMtalla els espais dels dos extrems;REPLACEtreu els separadors de milers i arregla el separador decimal abans d'una conversió.TO_DATE(text, pattern)llegeix una data en el format que declares. Formes diferents necessiten patrons diferents, triats ambCASE.- Un error sorollós t'atura. Un error silenciós (
'04/11/2026'::datedonant l'11 d'abril) és pitjor: res no t'avisa. - Verifica sempre una conversió amb una fila de comprovació: recompte,
MIN,MAX, nombre deNULL. - Cada decisió de neteja (una data
NULL, 41 dates posades a l'1) és una decisió que s'ha d'apuntar.
Comprovació ràpida¶
1. Per què SELECT '10' < '9' retorna cert?
- Perquè PostgreSQL té un error amb els nombres de dues xifres
- Perquè tots dos valors són text, i el text es compara caràcter a caràcter
- Perquè les cometes converteixen els nombres en dates
- Perquè
<vol dir «més llarg que» per al text
Dins de cometes els valors són text, i el text s'ordena com un diccionari: el caràcter 1 va abans que 9, de manera que '10' és «més petit». Sense cometes són nombres i 10 < 9 és fals. El tipus decideix, no l'aspecte.
2. Quina diferència hi ha entre CAST('42' AS integer) i '42'::integer?
-
CASTcanvia la taula i::només canvia el resultat -
::és més ràpid però menys exacte - Cap: són dues maneres d'escriure la mateixa conversió
-
CASTfunciona amb text i::només amb nombres
Tots dos són conversions. CAST(x AS type) és SQL estàndard i x::type és una drecera de PostgreSQL. Cap dels dos canvia la taula: una conversió només canvia el resultat de la consulta en què s'escriu.
3. Una columna preu conté text com 1.250,75. Quina expressió la converteix en un nombre?
-
preu::numeric -
REPLACE(preu, ',', '')::numeric -
REPLACE(REPLACE(preu, '.', ''), ',', '.')::numeric -
TRIM(preu)::numeric
Està escrit a l'estil espanyol: el punt és el separador de milers i la coma és el separador decimal. Primer esborra els punts, després converteix la coma en un punt: 1250.75. Esborrar només la coma donaria 1.25075, un nombre completament diferent, sense cap error.
4. SELECT '04/11/2026'::date retorna 2026-04-11. Quin tipus de problema és?
- Un error sorollós: la consulta s'ha aturat
- Un error silenciós: un valor incorrecte i cap avís
- Cap problema: el 4 de novembre i l'11 d'abril són la mateixa data
- Un valor absent
PostgreSQL ha hagut d'endevinar el format i ha llegit el mes primer. La consulta s'ha executat, de manera que res no t'ha dit que la data era incorrecta. Això és un error silenciós, el tipus perillós. TO_DATE('04/11/2026', 'DD/MM/YYYY') declara el format i retorna el 4 de novembre.
5. Després de la conversió, la fila de comprovació diu no_date = 1. Què vol dir?
- La conversió ha fallat i s'ha de tornar a fer
- S'ha esborrat un joc de la taula
- Una data no encaixa en cap patró i s'ha deixat com a
NULLa propòsit - Un joc es va llançar l'1 del mes
El CASE no té ELSE, de manera que un valor que no coincideix amb cap patró es converteix en NULL. Aquesta és l'única fila escrita 8 Oct, 2014. És un NULL conegut i comptat: una decisió apuntada, no un accident.
6. Per què price es converteix a numeric(8,2) en lloc de deixar-lo com a double precision?
- Perquè les columnes
numericocupen menys espai al disc - Perquè
double precisionno pot contenir decimals - Perquè
numericemmagatzema els decimals amb exactitud, i els diners han de quadrar al cèntim - Perquè
double precisions'ordena alfabèticament
double precision emmagatzema els decimals de manera aproximada: sumar tots els preus dona 211089.76999994315. Amb numeric(8,2) la mateixa suma és 211089.77. Per als diners vols el tipus exacte.
7. Un CSV d'un ajuntament té una columna data amb text com 31/12/2025 i 04/11/2025. Tria les dues afirmacions que són certes.
-
TO_DATE(data, 'DD/MM/YYYY')converteix correctament els dos valors -
data::dateés més segur perquè s'adapta a cada fila - Amb
data::date,31/12/2025dona un error però04/11/2025es converteix silenciosament en l'11 d'abril - Els valors no es poden convertir perquè contenen barres
Declara el format i tots dos valors surten bé. Deixa que la base de dades endevini i llegeix el mes primer: el mes 31 no existeix, de manera que aquella fila falla sorollosament, mentre que 04/11 passa silenciosament com la data equivocada. La fila sorollosa és la afortunada: t'avisa de totes les altres.
Les dates i els preus ja són valors de debò. Però owners continua sent un interval en text, i 74 noms de jocs apareixen més d'una vegada. Tot seguit: Neteja: unitats i duplicats, on dues coses que semblen idèntiques resulten no ser-ho.