La idea
Los datos sucios rara vez son basura aleatoria. Son lo mismo del mundo real escrito de varias formas: "New York", "NEW YORK" y "New York " (con un espacio al final) son una sola ciudad para ti y tres valores distintos para la base de datos. SQL compara el texto exactamente, byte por byte, así que felizmente hará tres grupos, tres clientes o tres claves de join a partir de una sola.
Limpiar es decidir cuál es la forma canónica y convertir cada variante a ella antes de agrupar, unir o contar. La segunda mitad es decidir qué hacer con las filas que no deberían contar en absoluto (duplicados, valores imposibles) y con los valores que faltan.
Figura 1
Cuatro escrituras, una ciudad
| city | customers |
|---|---|
| New York | 175 |
| NEW YORK | 3 |
| New York | 2 |
| new york | 1 |
| city_key | customers |
|---|---|
| new york | 181 |
GROUP BY citytrata el texto exactamente como está guardado. Las mayúsculas y un espacio al final forman cuatro grupos para una sola ciudad.TRIMquita los espacios sobrantes yLOWERiguala las mayúsculas. Ambas se aplican a cada fila antes de agrupar.- Agrupar por la expresión limpia pliega las cuatro filas en una: 175 + 3 + 2 + 1 = 181 clientes. En toda la tabla, 129 escrituras distintas se vuelven 61 ciudades reales.
Por qué existe
Un informe construido sobre datos sucios está mal de una forma que parece bien. Un gráfico de ciudades con cuatro New York igual dibuja barras. Un conteo de clientes que incluye a veinte personas dos veces igual imprime un número. Nada falla, así que nadie se entera hasta que alguien compara contra un total en el que confía.
Arreglarlo en la tabla de origen suele ser imposible: no eres dueño del CRM, el archivo del proveedor llega otra vez como texto la semana que viene, y no deberías sobrescribir datos crudos que quizá necesites después. Por eso los analistas limpian en la consulta: un CTE o una vista que convierte las filas crudas en una versión confiable, y todos los informes leen de ahí. Un solo lugar donde corregir, y un registro de lo que se hizo.
Cómo funciona por dentro
Construye una clave limpia
Una clave es el valor por el que agrupas o emparejas. Créala con funciones que quiten las diferencias
irrelevantes: TRIM para espacios sobrantes, LOWER o UPPER para las mayúsculas, REPLACE para la
puntuación. Aplica la misma expresión en todos los lugares donde se use el valor. Agrupa por la clave
limpia y conserva las escrituras originales al lado si quieres mostrarlas.
Fíjate en lo que no estás haciendo: no estás decidiendo que "NYC" significa New York. Eso necesita una tabla de equivalencias, no una función, y es una decisión de negocio.
Elige qué duplicado gana
Las personas duplicadas son filas distintas que deberían ser una. Elimínalas con ROW_NUMBER(), no con
DISTINCT: DISTINCT solo descarta filas idénticas en todas las columnas, y estas difieren en el id,
la fecha de registro o las mayúsculas del email.
Figura 2
Dejar una fila por persona con ROW_NUMBER
| customer_id | first_name | last_name | signup_date | |
|---|---|---|---|---|
| 355 | Douglas | Diaz | ddiaz@brightlinedesign.com | 2023-07-10 |
| 2010 | Douglas | Diaz | ddiaz@brightlinedesign.com | 2023-10-11 |
| 1698 | Brandon | Jones | bjones76@gmail.com | 2025-10-12 |
| 2014 | Brandon | Jones | BJONES76@GMAIL.COM | 2025-12-31 |
| customer_id | email_key | rn |
|---|---|---|
| 355 | ddiaz@brightlinedesign.com | 1 |
| 2010 | ddiaz@brightlinedesign.com | 2 |
| 1698 | bjones76@gmail.com | 1 |
| 2014 | bjones76@gmail.com | 2 |
- Dos personas existen dos veces cada una. Douglas tiene el mismo email en ambas filas; la segunda fila de Brandon es la misma dirección en mayúsculas.
- Particiona por el email limpio, para que
BJONES76@GMAIL.COMybjones76@gmail.comcaigan en un mismo grupo. Ordena cada grupo porsignup_date. - Numera las filas dentro de cada grupo. El registro más antiguo es el 1, el duplicado posterior es el 2.
- Conserva
rn = 1. Cada persona aparece exactamente una vez, y tú decidiste qué registro gana en lugar de que la base de datos elija al azar.
El ORDER BY dentro de la ventana es la regla de negocio: el registro más antiguo, la actualización más
reciente, la fila más completa. Los clientes 2001 a 2018 de Harbor & Pine son duplicados así, algunos
con el mismo email exacto y otros que solo difieren en mayúsculas o espacios. Recuerda pasar los
pedidos al id que sobrevive si vas a contar por cliente.
Respeta el NULL
NULL significa "desconocido", y SQL arrastra lo desconocido. 5 + NULL es NULL. NULL = NULL no es
verdadero. WHERE referral_source <> 'search' descarta las filas NULL, porque NULL <> 'search' es
desconocido, no verdadero. Los agregados saltan los NULL, así que AVG de una columna con vacíos
promedia solo las filas llenas.
Figura 3
NULL cambia lo que cuentas
| customer_id | first_name | referral_source |
|---|---|---|
| 2 | Douglas | NULL |
| 3 | Henry | NULL |
| 4 | Brian | |
| 5 | Kyle | NULL |
| 6 | Olivia | NULL |
| 7 | Abigail | search |
| 8 | Miguel | search |
| count_star | count_referral | count_coalesced |
|---|---|---|
| 7 | 3 | 7 |
- Siete clientes. Cuatro no tienen
referral_source. La columna es NULL, que significa "desconocido", no una cadena vacía. COUNT(*)cuenta filas, así que dice 7.COUNT(referral_source)salta los NULL y dice 3. Las dos son correctas, para preguntas distintas.COALESCE(referral_source, 'unknown')le pone etiqueta a los vacíos, así que unGROUP BYmuestra un grupo "unknown" visible en lugar de descartarlos en silencio.
COALESCE(a, b) devuelve el primer valor que no es NULL, así que puedes sustituir una etiqueta o un
valor por defecto. Úsalo para mostrar ('unknown'), no para inventar números: reemplazar un costo que
falta por 0 hace que el margen se vea mejor de lo que es.
NULLIF(a, b) hace lo contrario: devuelve NULL cuando a es igual a b. Es la protección estándar
contra la división entre cero, porque x / NULLIF(y, 0) da NULL en lugar de un error cuando y es 0.
Convierte el texto a propósito
Los archivos pegados de proveedores y transportistas llegan como texto: $1,299.00, 2023-02-06,
06/02/2023. CAST hace fallar toda la consulta por un solo valor malo; TRY_CAST (y try_strptime
para fechas) devuelve NULL, así que puedes contar e inspeccionar los fallos. Limpia primero el texto
(quita el $ y la coma) y analiza cada formato de fecha en su propia rama.
Marca, no borres
Algunas filas parecen incorrectas pero pueden ser reales: líneas de pedido con cantidad 0 o -1, o un
precio diez veces la mediana. Si las borras en silencio, los totales bajan y nadie sabe por qué. Agrega
una columna de marca (is_outlier, is_valid_quantity), reporta sobre las filas limpias y guarda un
conteo de lo que se excluyó. Una regla declarada, como "fuera de 1.5 veces el rango intercuartílico",
es algo que un compañero puede discutir. Un borrado silencioso, no.
El orden importa al limpiar
Limpia en capas, cada una en su propio CTE: primero el texto (espacios, mayúsculas, símbolos), luego los
tipos (conversión y análisis), luego los duplicados, luego las marcas. Una capa solo puede confiar en la
anterior. Deduplicar por email antes de pasarlo a minúsculas no detecta BJONES76@GMAIL.COM, y convertir
un precio antes de quitarle el $ convierte un valor bueno en un fallo. Cuando un número se vea raro,
ejecuta las capas una por una y mira cuál lo cambió.
Errores comunes
"DISTINCT elimina los clientes duplicados"
Elimina las filas que coinciden en todas las columnas seleccionadas. Dos registros de una misma persona
difieren al menos en el id, así que para DISTINCT no son duplicados. Si seleccionas solo las columnas
que definen a una persona, las mayúsculas igual importan, y por eso primero limpias la clave.
"NULL es lo mismo que cero o una cadena vacía"
NULL es desconocido. Cero es un número, y '' es un texto de longitud cero. Se comportan distinto en
conteos, promedios y comparaciones. Antes de reemplazar los NULL, pregunta qué significa el vacío:
"no se registró la fuente de referencia" no es una fuente llamada vacío.
"Limpiar significa arreglar la tabla"
Limpiar en SQL significa escribir una consulta o vista que devuelva la versión corregida. La tabla cruda queda intacta, así que puedes volver a ejecutar la limpieza cuando cambien las reglas y demostrar qué cambiaste.
Ponte a prueba
Una pregunta. Al responderla, la lección queda marcada en el mapa; nunca bloquea un nivel.
No del todo. COUNT(*) cuenta filas. COUNT(columna) cuenta solo las filas donde esa columna no es NULL.Prueba otra respuesta.
No del todo. Ese es el número que salta. Para contar los NULL usarías COUNT(*) - COUNT(referral_source) o WHERE referral_source IS NULL.Prueba otra respuesta.
Correcto. 2,018 - 666 = 1,352. COUNT(columna) ignora los NULL, por eso un conteo puede ser menor que el número de filas.
No del todo. Los agregados saltan los NULL; no se vuelven NULL. Solo un cálculo normal como a + NULL devuelve NULL.Prueba otra respuesta.
Lección hecha: ya está marcada en el mapa.