Saltar a contenido

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) o x::type, y leer el error cuando no se puede
  • Quitar espacios sobrantes con TRIM y reparar separadores decimales y de millares con REPLACE
  • Convertir fechas de texto escritas en dos formatos en una única columna DATE real con TO_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) o x::type) convierte un valor o se detiene con un error. Cambia el resultado de la consulta, nunca la tabla.
  • TRIM corta los espacios de ambos extremos; REPLACE quita 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 con CASE.
  • Un error ruidoso te detiene. Un error silencioso ('04/11/2026'::date dando 11 de abril) es peor: nada te avisa.
  • Verifica siempre una conversión con una fila de comprobación: recuento, MIN, MAX, número de NULL.
  • 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?
  • CAST cambia 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
  • CAST funciona 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 NULL a 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 numeric ocupan menos espacio en disco
  • Porque double precision no puede guardar decimales
  • Porque numeric guarda los decimales con exactitud, y el dinero tiene que cuadrar al céntimo
  • Porque double precision se 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::date es más seguro porque se adapta a cada fila
  • Con data::date, 31/12/2025 da un error pero 04/11/2025 se 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.