Salta el contingut

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) o x::type, i llegir l'error quan no es pot fer
  • Treure espais sobrers amb TRIM i reparar separadors decimals i de milers amb REPLACE
  • Convertir dates en text escrites en dos formats en una sola columna DATE de debò amb TO_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) o x::type) transforma un valor o s'atura amb un error. Canvia el resultat de la consulta, mai la taula.
  • TRIM talla els espais dels dos extrems; REPLACE treu 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 amb CASE.
  • Un error sorollós t'atura. Un error silenciós ('04/11/2026'::date donant l'11 d'abril) és pitjor: res no t'avisa.
  • Verifica sempre una conversió amb una fila de comprovació: recompte, MIN, MAX, nombre de NULL.
  • 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?
  • CAST canvia 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ó
  • CAST funciona 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 NULL a 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 numeric ocupen menys espai al disc
  • Perquè double precision no pot contenir decimals
  • Perquè numeric emmagatzema els decimals amb exactitud, i els diners han de quadrar al cèntim
  • Perquè double precision s'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/2025 dona un error però 04/11/2025 es 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.