Limpieza: tipos y formatos¶
Ejercicio cleaning-types-formats-annex.pdf · PDF
Pregunta rápida: ¿qué es mayor, 10 o 9? Ni has tenido que pensarlo. Ahora hazle la misma pregunta a una base de datos, con un pequeño cambio:
SELECT '10' < '9' AS as_text, 10 < 9 AS as_number;
| as_text | as_number |
|---|---|
| true | false |
Con comillas, 10 es menor que 9. Sin comillas, no. Las mismas cifras, respuestas opuestas. Las comillas convierten un número en texto, y el texto se compara como las palabras de un diccionario: 1 va antes que 9, y punto. Esta es exactamente la razón por la que games no podía ordenar sus propias fechas de lanzamiento. En esta página lo arreglas.
Qué aprenderás
- Explicar qué sale mal cuando un número o una fecha se guarda como texto: ordenar, comparar, aritmética
- Convertir un valor a otro tipo con
CAST(x AS type)ox::type, y leer el error cuando no se puede - Quitar espacios sobrantes con
TRIMy reparar separadores decimales y de millares conREPLACE - Convertir fechas de texto escritas en dos formatos en una única columna
DATEreal conTO_DATE - Distinguir un error ruidoso de un error silencioso, y decir cuál es peor
- Evitar la trampa: "la consulta se ejecutó sin error" no significa que la conversión sea correcta
El horno: del mineral a los lingotes¶
En la auditoría de calidad de los datos abriste el cofre y solo miraste. Ahora enciendes el horno. En un juego de crafteo, el mineral parece metal, pero no puedes craftear con él hasta fundirlo en un lingote. '19.99' parece un número. Todavía no lo es.
Un tipo de dato es la clase de valor que contiene una columna. Decide qué operaciones tienen sentido: puedes sumar dos números, puedes restar dos fechas, solo puedes pegar dos textos.
| Tipo en PostgreSQL | Contiene | Ejemplo | Puedes |
|---|---|---|---|
text |
Cualquier carácter | 'Nov 16, 2004' |
Buscar dentro, pegar, ordenar alfabéticamente |
integer, bigint |
Números enteros | 42 |
Sumar, hacer la media, ordenar por tamaño |
numeric(8,2) |
Decimales exactos | 19.99 |
Hacer aritmética de dinero sin sorpresas de redondeo |
date |
Un día del calendario | 2004-11-16 |
Ordenar por tiempo, extraer el año, contar días entre dos fechas |
Normalizar una columna significa darle un único tipo y un único formato, de modo que todos los valores se escriban de la misma manera.
Mantenemos la analogía del horno durante toda la página: el texto es el mineral, los valores con tipo son los lingotes.
Casting: encender el horno¶
El casting es convertir un valor de un tipo a otro. O el valor sale con el tipo nuevo, o la consulta se detiene con un error.
PostgreSQL tiene dos formas de escribirlo. Hacen exactamente lo mismo:
SELECT '19.99'::numeric + 1, CAST('42' AS integer) * 2;
| Primera columna | Segunda columna |
|---|---|
| 20.99 | 84 |
| Forma | Se lee como | Nota |
|---|---|---|
CAST(x AS type) |
"funde x en este tipo" |
SQL estándar, funciona en todas las bases de datos |
x::type |
Lo mismo, más corto | Atajo de PostgreSQL |
Cuando el horno no puede tragarse un valor, lo dice:
SELECT '1,234.50'::numeric;
ERROR: invalid input syntax for type numeric: "1,234.50"
Esa coma es el problema. Quédate con este error: volveremos a si es una buena o una mala noticia.
Cuidado: CAST no cambia la tabla
Un cast cambia el resultado de esta consulta. La tabla games sigue tan sucia como estaba. Por eso guardarás tu consulta de limpieza al final de la página: la consulta es la reparación, y la tabla limpia se construirá a partir de ella más adelante.
Recortar: espacios que no ves¶
Recortar (trimming) es cortar los espacios de ambos extremos de un texto.
Un espacio es un carácter como cualquier otro. Para ti, ' Valve ' y 'Valve' son la misma distribuidora. Para una base de datos son dos distintas:
SELECT ' Valve ' = 'Valve' AS as_typed,
TRIM(' Valve ') = 'Valve' AS trimmed;
| as_typed | trimmed |
|---|---|
| false | true |
Un solo espacio perdido en un archivo y tu GROUP BY publisher muestra la misma empresa dos veces, con los juegos repartidos entre las dos filas. TRIM(x) quita los espacios del principio y del final. No toca los del medio, así que 'Half-Life 2' conserva su espacio.
En games, TRIM(game) cambia 0 filas: los nombres ya están ordenados. Lo dejamos igualmente en la consulta de limpieza. No cuesta nada y te protege el día que alguien suba un archivo menos ordenado.
Separadores: decoración para humanos¶
Un separador decimal marca dónde termina la parte entera de un número. Un separador de millares solo agrupa las cifras para que las personas puedan leerlas.
Aquí está el truco: los países no se ponen de acuerdo sobre qué símbolo hace cada trabajo.
| Escrito como | Dónde | Separador de millares | Separador decimal |
|---|---|---|---|
1,234.50 |
Países de habla inglesa | coma | punto |
1.234,50 |
España y la mayor parte de Europa | punto | coma |
1234.50 |
Lo que acepta el horno | ninguno | punto |
Las dos primeras líneas son el mismo número: mil doscientos treinta y cuatro y medio. El horno solo acepta la tercera forma. Así que primero quitas la decoración, con REPLACE(text, old, new), que cambia cada old por new:
SELECT REPLACE('1,234.50', ',', '')::numeric; -- 1234.50
SELECT REPLACE(REPLACE('1.234,50', '.', ''), ',', '.')::numeric; -- 1234.50
La versión española necesita dos pasos, y el orden importa: primero borra los puntos (millares), luego convierte la coma en punto (decimales). Hazlo al revés y borrarás el punto decimal que acabas de crear.
Cuidado: ¿es un decimal o un millar?
1.234 es "uno coma dos tres cuatro" en Londres y "mil doscientos treinta y cuatro" en Palma. El valor por sí solo no te lo dice. Tienes que saber de dónde viene el archivo antes de elegir qué REPLACE escribir.
Fechas: un valor, muchas grafías¶
Las fechas son las peores, porque cada país y cada sitio web las escribe de forma distinta. TO_DATE(text, pattern) lee una fecha escrita como dice el patrón:
| Pieza del patrón | Significa | Coincide con |
|---|---|---|
Mon |
Mes en tres letras | Nov |
DD |
Día del mes | 16 |
MM |
Mes como número | 11 |
YYYY |
Año con cuatro cifras | 2004 |
La mayoría de las fechas de games tienen el aspecto Nov 16, 2004, así que el patrón es 'Mon DD, YYYY'. Prueba primero la forma perezosa, sobre toda 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"
Las dos líneas se rompen con el mismo tipo de valor: una fecha sin día. La auditoría ya nos avisó de que 42 filas tenían otra forma. Antes de convertir, cuenta las formas con exactitud.
Cuenta las formas¶
Dos herramientas nuevas. CASE es una bifurcación en el camino: cada fila toma la primera rama cuya condición sea verdadera (tiene su propia lección en Categorización). Y ~ pregunta "¿este texto encaja con este patrón?".
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 necesitas escribir estos patrones de memoria, solo leerlos:
| Pieza | Significa |
|---|---|
^ ... $ |
Desde el principio mismo del texto hasta el final |
[A-Z][a-z]{2} |
Una letra mayúscula y dos minúsculas: Nov |
[0-9]{1,2} |
Una o dos cifras: el día |
[0-9]{4} |
Exactamente cuatro cifras: el año |
La única fila de other es 8 Oct, 2014, el juego Hazard Ops: primero el día, después el mes, al revés que todas las demás.
Construye el conversor¶
La misma bifurcación, pero ahora cada camino funde su propia forma. No hay ELSE: lo que no encaja en ningún camino se convierte 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í hay tres cosas nuevas:
| Pieza | Qué hace |
|---|---|
WITH step1 AS (...) |
Le da un nombre a una consulta, para que el SELECT de abajo use su resultado como si fuera una tabla |
price::numeric(8,2) |
Un decimal exacto con dos decimales: el tipo correcto para dinero |
WHERE game IS NOT NULL |
Descarta las 3 filas sin nombre, como decidió el inventario |
El último SELECT es una fila de comprobación: cuántas filas, la fecha más antigua, la más reciente, cuántas no tienen fecha. Dice 26,685 juegos, de 2004 a 2018, y exactamente uno sin fecha. Ese es Hazard Ops.
¿Por qué numeric para el dinero?
La columna price original tiene el tipo double precision, que guarda los decimales de forma aproximada. Súmalos todos y los diminutos errores se notan: SUM(price) da 211089.76999994315. Con SUM(price::numeric(8,2)) obtienes 211089.77. El dinero se cuenta en céntimos exactos, así que recibe un tipo exacto.
Úsalo¶
Con una fecha real, una pregunta que era imposible en la tabla original ocupa una línea. Mantén el bloque WITH y cambia solo el SELECT final:
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 "Juegos lanzados por año en la tabla de clase"
x-axis [2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018]
y-axis "Juegos" 0 --> 9000
bar [53, 58, 112, 143, 183, 334, 340, 364, 426, 556, 1352, 2544, 4503, 6871, 8845]
53 juegos en 2004, 8,845 en 2018. Esa curva llevaba todo el tiempo escondida dentro de una columna de texto.
Errores ruidosos y errores silenciosos¶
Volvamos al horno rechazando '1,234.50'. ¿Buena o mala noticia?
Un error ruidoso detiene la consulta y te dice qué valor no pudo convertir. Un error silencioso te da un valor equivocado y ningún aviso.
Buena noticia. Un error ruidoso es el tipo fácil: lo ves, lo arreglas. Ahora conoce al otro tipo. Toma el 4 de noviembre de 2026, escrito a la española:
SELECT '04/11/2026'::date; -- 2026-04-11
SELECT TO_DATE('04/11/2026', 'DD/MM/YYYY'); -- 2026-11-04
La primera línea devolvió el 11 de abril. Sin error. PostgreSQL tuvo que adivinar, y adivinó mes primero, a la americana. A la segunda línea se le dijo el formato, así que acertó.
flowchart TB
T["<b>Valor de texto</b><br>'04/11/2026'"] --> G{"¿Le dijiste<br>el formato?"}
G -- "No: '04/11/2026'::date" --> S["<b>Error silencioso</b><br>2026-04-11, sin aviso"]
G -- "Sí: TO_DATE(..., 'DD/MM/YYYY')" --> OK["<b>Correcto</b><br>2026-11-04"]
S --> CH["Acaba en un gráfico.<br>Alguien decide con él."]
| Error ruidoso | Error silencioso | |
|---|---|---|
| Qué ocurre | La consulta se detiene | La consulta se ejecuta |
| Qué obtienes | Un mensaje que nombra el valor erróneo | Un valor equivocado que parece correcto |
| Ejemplo | '1,234.50'::numeric |
'04/11/2026'::date da 11 de abril |
| Cómo detectarlo | No puedes pasarlo por alto | Solo comprobando: MIN, MAX, recuento de NULL |
La regla: cuando conviertes, le dices el formato al horno. Nunca dejas que adivine. Y comprueba siempre el resultado con una fila de comprobación, aunque nada se haya quejado.
Cuidado: una fecha NULL no es una conversión fallida
En una fila, Hazard Ops, elegimos NULL a propósito. Sabemos que es una fila y lo apuntamos. Un NULL conocido y contado es honesto; una fecha inventada no lo es.
La misma honestidad se aplica a las 41 fechas con solo mes. TO_DATE('Oct 2010', 'Mon YYYY') devuelve 2010-10-01: el día 1 del mes. Nadie lanzó 41 juegos un día 1. Elegimos ese día nosotros, y el informe final debe decirlo.
Pruébalo: release_date se convierte en una fecha real¶
Abre SQL → SQL Lab, base de datos Dades classe, esquema public. Ejecuta una sentencia cada vez: selecciónala y pulsa Run.
Paso 1 — Prueba la forma perezosa. Ejecuta las dos líneas que fallan (release_date::date y TO_DATE con un solo patrón). Deberías obtener dos mensajes de error, ambos nombrando un valor como "Jun 2009".
Paso 2 — Cuenta las formas. Ejecuta la consulta con CASE. Deberías ver full 26,646, month only 41 y other 1. Copia los patrones de esta página en lugar de reescribirlos: un espacio que falte cuesta cinco minutos.
Paso 3 — Construye el conversor. Ejecuta la consulta WITH step1. La fila de comprobación debe decir 26,685 filas, la más antigua 2004-01-11, la más reciente 2018-12-31, no_date 1. Si no_date es 42 o 26,685, algún patrón está mal escrito.
Paso 4 — Úsalo y guárdalo. Cambia el SELECT final por la consulta de juegos por año. Después pulsa Save, junto a Run, y llama a la consulta games_step1. Aparece en SQL → Saved Queries. La próxima lección empieza desde ella.
La consulta falla cerca de ')' o cerca de 'SELECT'
Después de un bloque WITH no debe haber punto y coma antes del SELECT final: el bloque y el SELECT son una única sentencia. Un error cerca de ) suele significar un ; perdido. Un error cerca de SELECT suele significar que falta el ) de cierre del bloque.
Ideas clave¶
- Un tipo de dato decide qué puedes hacer con una columna. Un número o una fecha guardado como texto se ordena como un diccionario:
'10' < '9'es verdadero. - El casting (
CAST(x AS type)ox::type) convierte un valor o se detiene con un error. Cambia el resultado de la consulta, nunca la tabla. TRIMcorta los espacios de ambos extremos;REPLACEquita los separadores de millares y corrige el separador decimal antes de un cast.TO_DATE(text, pattern)lee una fecha en el formato que declaras. Formas distintas necesitan patrones distintos, elegidos conCASE.- Un error ruidoso te detiene. Un error silencioso (
'04/11/2026'::datedando 11 de abril) es peor: nada te avisa. - Verifica siempre una conversión con una fila de comprobación: recuento,
MIN,MAX, número deNULL. - Cada decisión de limpieza (una fecha
NULL, 41 fechas puestas en día 1) es una decisión que hay que apuntar.
Comprobación rápida¶
1. ¿Por qué SELECT '10' < '9' devuelve verdadero?
- Porque PostgreSQL tiene un error con los números de dos cifras
- Porque los dos valores son texto, y el texto se compara carácter a carácter
- Porque las comillas convierten los números en fechas
- Porque
<significa "más largo que" para el texto
Entre comillas los valores son texto, y el texto se ordena como un diccionario: el carácter 1 va antes que 9, así que '10' es "menor". Sin comillas son números y 10 < 9 es falso. Decide el tipo, no el aspecto.
2. ¿Cuál es la diferencia entre CAST('42' AS integer) y '42'::integer?
-
CASTcambia la tabla y::solo cambia el resultado -
::es más rápido pero menos exacto - Ninguna: son dos formas de escribir la misma conversión
-
CASTfunciona con texto y::solo con números
Las dos son casting. CAST(x AS type) es SQL estándar y x::type es un atajo de PostgreSQL. Ninguna cambia la tabla: un cast solo cambia el resultado de la consulta en la que está escrito.
3. Una columna preu contiene texto como 1.250,75. ¿Qué expresión lo convierte en número?
-
preu::numeric -
REPLACE(preu, ',', '')::numeric -
REPLACE(REPLACE(preu, '.', ''), ',', '.')::numeric -
TRIM(preu)::numeric
Está escrito a la española: el punto es el separador de millares y la coma es el separador decimal. Primero borra los puntos, luego convierte la coma en punto: 1250.75. Borrar solo la coma daría 1.25075, un número completamente distinto, sin ningún error.
4. SELECT '04/11/2026'::date devuelve 2026-04-11. ¿Qué tipo de problema es este?
- Un error ruidoso: la consulta se detuvo
- Un error silencioso: un valor equivocado y ningún aviso
- Ningún problema: el 4 de noviembre y el 11 de abril son la misma fecha
- Un valor ausente
PostgreSQL tuvo que adivinar el formato y leyó el mes primero. La consulta se ejecutó, así que nada te avisó de que la fecha era errónea. Eso es un error silencioso, el tipo peligroso. TO_DATE('04/11/2026', 'DD/MM/YYYY') declara el formato y devuelve el 4 de noviembre.
5. Tras la conversión, la fila de comprobación dice no_date = 1. ¿Qué significa?
- La conversión falló y hay que rehacerla
- Se borró un juego de la tabla
- Una fecha no encaja en ningún patrón y se dejó como
NULLa propósito - Un juego se lanzó el día 1 del mes
El CASE no tiene ELSE, así que un valor que no encaja en ningún patrón se convierte en NULL. Es la única fila escrita como 8 Oct, 2014. Es un NULL conocido y contado: una decisión apuntada, no un accidente.
6. ¿Por qué price se convierte a numeric(8,2) en lugar de dejarlo como double precision?
- Porque las columnas
numericocupan menos espacio en disco - Porque
double precisionno puede guardar decimales - Porque
numericguarda los decimales con exactitud, y el dinero tiene que cuadrar al céntimo - Porque
double precisionse ordena alfabéticamente
double precision guarda los decimales de forma aproximada: sumar todos los precios da 211089.76999994315. Con numeric(8,2) la misma suma es 211089.77. Para el dinero quieres el tipo exacto.
7. Un CSV de un ayuntamiento español tiene una columna data con texto como 31/12/2025 y 04/11/2025. Elige las dos afirmaciones verdaderas.
-
TO_DATE(data, 'DD/MM/YYYY')convierte correctamente los dos valores -
data::datees más seguro porque se adapta a cada fila - Con
data::date,31/12/2025da un error pero04/11/2025se convierte en silencio en el 11 de abril - Los valores no se pueden convertir porque contienen barras
Declara el formato y los dos valores salen bien. Deja que la base de datos adivine y leerá el mes primero: el mes 31 no existe, así que esa fila falla con ruido, mientras que 04/11 pasa en silencio como una fecha equivocada. La fila ruidosa es la afortunada: te avisa de todas las demás.
Las fechas y los precios ya son valores reales. Pero owners sigue siendo un rango de texto, y 74 nombres de juegos aparecen más de una vez. A continuación: Limpieza: unidades y duplicados, donde dos cosas que parecen idénticas resultan no serlo.