Cómo validar datos: la guía completa

Las seis comprobaciones que detectan la mayoría de los incidentes reales, dónde ejecutarlas y cómo convertir SQL improvisado en un contrato de datos versionado que se ejecuta de forma programada.

· 12 min read

Para validar datos, escribe cada expectativa como una consulta que devuelve el recuento de filas que la incumplen, ejecuta el conjunto completo contra el dataset después de cada carga y haz que falle cuando un recuento supere un umbral acordado de antemano. Seis comprobaciones detectan la inmensa mayoría de los incidentes reales: valores ausentes en columnas obligatorias, claves de negocio duplicadas, valores fuera de un conjunto permitido, números fuera de un rango plausible, cadenas mal formadas y filas obsoletas. Las comprobaciones en sí son SQL corriente; lo difícil es ejecutarlas donde viven los datos, mantenerlas sincronizadas con el esquema y alertar de una forma que la gente no acabe aprendiendo a ignorar. Todo lo que sigue es la maquinaria para hacerlo de forma fiable.

Qué es realmente la validación de datos

Tres cosas distintas reciben el nombre de validación y solo una es el tema de esta guía.

La validación de entrada ocurre en el borde de una aplicación: un formulario rechaza una dirección de correo sin @. Es una barrera en el momento de la escritura, registro a registro.

La validación de esquema comprueba la estructura: si la tabla tiene las columnas que el cargador espera, con los tipos que espera. Detecta un contrato roto entre sistemas, no valores incorrectos dentro de uno bien formado.

La validación de datos comprueba el contenido de un dataset que ya existe, de forma masiva, después de que haya aterrizado: de los 4,2 millones de filas que cargamos anoche, ¿cuántas incumplen una promesa de la que alguien depende?

Esa distinción determina la forma de la solución. No estás rechazando filas; las filas ya están ahí. Estás midiendo, de forma programada, y decidiendo qué hacer con la medición. Una ejecución de validación produce un número por regla, un pass o un fail por regla y un histórico, que es lo que convierte “los datos tienen mala pinta” en “la tasa de nulos de customer_id pasó del 0,02 % al 11 % a las 04:12 del martes pasado”. Si quieres la definición por sí misma, incluida la diferencia entre validación, testing, observabilidad y limpieza, empieza por qué es la validación de datos y vuelve aquí para la mecánica.

Las seis comprobaciones que necesita todo dataset

Toda comprobación tiene la misma forma. Cuenta las filas que incumplen la expectativa y compara el recuento con un umbral:

SELECT count(*) AS violations
FROM sales.orders
WHERE order_id IS NULL;

Ese es todo el patrón. Una regla es un predicado, un recuento y un umbral, y las seis que importan no son más que seis predicados:

ComprobaciónDimensiónDetectaPredicado
CompletitudcompletenessUna columna que dejó de rellenarsecol IS NULL
UnicidaduniquenessReejecuciones, cargas reintentadas, ingresos contados dos vecesGROUP BY key HAVING count(*) > 1
ValidezconformityUn valor de enum nuevo del que nadie te avisócol NOT IN ('a', 'b', 'c')
RangoaccuracyErrores de unidad, fallos de moneda, cantidades negativascol < 0 OR col > 100000
FormatoconformityIdentificadores mal formados, códigos truncadoscol NOT LIKE / !~ pattern
FrescuratimelinessUn pipeline que dejó de ejecutarse en silenciomax_ts < now() - interval

Dos añadidos se ganan su sitio en la mayoría de las tablas: una regla rowCount a nivel de tabla, porque un resultado vacío después de una carga es un fallo distinto y muy frecuente, y una comprobación de integridad referencial sobre las claves foráneas, porque los backfills parciales dejan huérfanos que los inner joins descartan sin decir nada.

La trampa de los nulos, en la que todo el mundo cae una vez

Escribe la comprobación de validez tal y como aparece arriba y te mentirá:

-- Wrong: nulls disappear from the count.
WHERE status NOT IN ('pending', 'paid', 'shipped', 'refunded')

-- Right:
WHERE status IS NOT NULL
  AND status NOT IN ('pending', 'paid', 'shipped', 'refunded')

NULL NOT IN (...) se evalúa como NULL, no como true, en todos los motores SQL. Sin esa protección, una columna que es nula en un 90 % declara una conformidad perfecta. Es el falso pass más habitual en el SQL de calidad de datos escrito a mano, y merece la pena revisarlo en cada regla que heredes.

Dónde ejecutar las comprobaciones

Ejecútalas en el warehouse, contra una conexión de solo lectura, y no muevas nunca las filas.

Extraer los datos para validarlos es el instinto equivocado por tres motivos: es lento, copia filas sensibles a un segundo sistema con una segunda revisión de seguridad y no da abasto —una comprobación que tiene que exportar una tabla de hechos de mil millones de filas no se va a ejecutar cada hora—. Empujar la agregación hasta el motor significa que cada comprobación es un único SELECT que devuelve un número, que es precisamente la carga de trabajo para la que está construido cualquier warehouse.

El conjunto de permisos es realmente mínimo: conectar a la base de datos, leer el catálogo de esquemas y SELECT sobre las tablas que estás comprobando. Nada más. Si una herramienta de validación pide acceso de escritura, pregunta por qué. Apunta la conexión a una réplica de lectura allí donde exista —son escaneos agregados sin más requisito de consistencia que “reciente”— y configura un timeout de consulta para que un escaneo sin índice no pueda bloquear una conexión durante una hora.

SQL manual, un framework o una herramienta gestionada

Hay tres formas honestas de hacer esto, y la correcta depende de cuántos datasets tengas y de quién necesite leer las reglas.

EnfoquePuntos fuertesDónde falla
SQL escrito a mano en cronCero configuración, control total, ningún proveedor nuevoLas reglas se desalinean del esquema; nadie sabe qué scripts siguen ejecutándose
Un framework (dbt tests, Great Expectations, Soda)Versionado, se ejecuta dentro de tu pipelineLas reglas quedan atadas al formato de ese runner; solo cubre lo que la herramienta controla
Un servicio de validación gestionadoProgramación, histórico, alertas y una interfaz que puede leer quien no es ingenieroEstás confiando una conexión a un proveedor, y el formato de reglas suele ser suyo

El modo de fallo del primero es la entropía: en un año tienes cuarenta archivos SQL, seis de los cuales apuntan a columnas que ya no existen, y nadie dispuesto a borrar ninguno. El modo de fallo del segundo es el alcance: dbt tests son excelentes, pero solo cubren los modelos que construye dbt y se ejecutan cuando se ejecuta dbt. El modo de fallo del tercero es el lock-in, que es la razón de ser de la siguiente sección.

Ninguno de ellos es excluyente. Los equipos que aciertan con esto colocan aserciones rápidas y baratas dentro del pipeline, donde hacen fallar una build, y mantienen las promesas duraderas en algún lugar donde se revisen y se monitoricen sin importar qué herramienta haya escrito la tabla hoy.

Contratos de datos: versiona las expectativas, no los scripts

La solución duradera a la entropía de reglas es dejar de escribir las comprobaciones como código y empezar a escribirlas como un documento: una declaración de lo que el dataset promete, guardada junto al resto de tu código fuente, revisada en pull requests y ejecutada por el runner que te toque usar. Ese documento es un contrato de datos.

El Open Data Contract Standard es la especificación abierta para escribir uno. Es YAML, se desarrolla dentro del proyecto Bitol de la Linux Foundation en lugar de bajo el control de un proveedor, y un contrato mínimo para las comprobaciones anteriores tiene este aspecto:

apiVersion: v3.0.0
kind: DataContract
info:
  title: orders
  version: 1.0.0
  owner: data-platform
schema:
  - name: orders
    physicalName: orders
    physicalType: table
    properties:
      - name: order_id
        logicalType: string
        required: true
        primaryKey: true
        quality:
          - rule: nullCount
            dimension: completeness
            severity: error
            mustBe: "0"
          - rule: duplicateCount
            dimension: uniqueness
            severity: error
            mustBe: "0"
      - name: status
        logicalType: string
        quality:
          - rule: validValues
            dimension: conformity
            severity: error
            mustBe: "['pending', 'paid', 'shipped', 'refunded']"
      - name: total_amount
        logicalType: number
        quality:
          - rule: between
            dimension: accuracy
            severity: error
            mustBe: "[0, 100000]"
      - name: created_at
        logicalType: timestamp
        quality:
          - rule: freshness
            dimension: timeliness
            severity: error
            mustBe: "<= 24h"
    quality:
      - rule: rowCount
        dimension: completeness
        severity: warning
        mustBe: "> 0"

Tres propiedades hacen que merezca la pena el esfuerzo.

Es portable. nullCount significa lo mismo en todas partes; el runner lo compila al dialecto de cada motor. Migrar de warehouse deja de ser una migración de reglas.

Es revisable. Un cambio de umbral es un diff con un autor y un motivo. Saber si el límite se movió porque cambió el negocio o porque alguien se cansó de la alerta pasa a ser recuperable, cosa que nunca lo es en la interfaz de un proveedor.

Es tuyo. Las reglas escritas en un estándar abierto no son un activo de quien las ejecute hoy.

Catalyst está construido directamente sobre ODCS: el YAML es el formato de almacenamiento, no la representación de una fila de base de datos, así que una regla editada en el constructor visual produce un diff de una línea y el contrato revisado en un pull request es byte a byte el que se ejecuta.

Programación y alertas

Dos reglas, ambas aprendidas por las malas.

Dispara desde la carga, no desde el reloj. Una tabla diaria validada a las 06:00 cuando el ELT termina a las 06:40 falla todas las mañanas por un motivo que no tiene nada que ver con la calidad. Ajusta la cadencia a los datos: frescura cada hora en una tabla de streaming, una única ejecución después del batch nocturno para todo lo demás. Ejecutar comprobaciones con más frecuencia de la que cambian los datos produce ruido y, en warehouses facturados por uso, también una factura.

Alerta sobre las transiciones, no sobre el estado. “Orders ha empezado a fallar” es accionable. “Orders sigue fallando”, repetido cada hora, es la forma más rápida de que se silencie un canal, y un canal silenciado es peor que no tener alertas, porque parece cobertura.

Usa la severidad para separar las dos poblaciones de reglas. Reserva error para las comprobaciones que de verdad deberían bloquear a un consumidor aguas abajo: el refresco de un dashboard, una sincronización de reverse-ETL, un informe financiero. Todo lo demás —comprobaciones de patrón sobre campos introducidos por personas, deriva en el número de filas, rarezas en la distribución— pertenece a warning, donde es una línea de tendencia y no un aviso de guardia.

Qué datasets validar primero

No puedes validarlo todo, e intentarlo es la forma más segura de matar el proyecto. Ordena por radio de impacto:

  1. Todo lo que alimente un número que lea un directivo. Ingresos, plantilla, pipeline comercial. Un número equivocado aquí cuesta una credibilidad que tarda meses en reconstruirse.
  2. Todo lo que alimente una decisión automatizada. Precios, límites de crédito, features de ML, reverse-ETL hacia un CRM. Actúan sobre datos malos antes de que los vea una persona.
  3. Todo lo que tenga un consumidor externo. Un feed para un partner o una declaración regulatoria tienen un coste de fallo que tú no controlas.
  4. Los joins que están en el corazón de tu modelo. Las tablas de dimensiones contra las que se une todo, donde una clave huérfana descarta filas en silencio en todas las consultas aguas abajo.

Empieza con cinco o diez reglas sobre una sola tabla de ese tipo, en lugar de tres reglas sobre cuarenta. Una cobertura superficial en todas partes no te dice nada; una cobertura profunda en las tablas que importan detecta los incidentes de los que, si no, te enterarías por boca de otra persona.

Después lee la guía de tu motor: las seis comprobaciones son universales, pero el SQL, las trampas y el modelo de costes no lo son:

Catalyst implementa exactamente el flujo anterior —importar el esquema, proponer un contrato de partida, compilar cada regla al SQL de tu motor, ejecutarla de forma programada y llevar el histórico de pass/warn/fail por regla— en todas esas conexiones, con un plan gratuito para probarlo sobre un dataset (PostgreSQL y MySQL) antes de decidir nada (precios).

Preguntas frecuentes

¿Qué cuenta como una comprobación de validación de datos?

Una comprobación de validación de datos es una expectativa acordada sobre el contenido de un dataset, expresada como una consulta que devuelve el recuento de filas que la incumplen: sin valores ausentes en una columna obligatoria, sin claves duplicadas, valores dentro de un conjunto permitido o de un rango plausible, cadenas con el formato correcto o filas lo bastante recientes como para resultar útiles. La comprobación falla cuando ese recuento supera un umbral que fijaste de antemano. A diferencia de la validación de entrada, se ejecuta de forma masiva sobre datos que ya han aterrizado.

¿Cuáles son los principales tipos de comprobaciones de validación de datos?

Seis cubren la mayoría de los incidentes reales: completitud (nulos en columnas obligatorias), unicidad (claves de negocio duplicadas), validez (valores fuera de un conjunto permitido), exactitud (números fuera de un rango plausible), conformidad (cadenas que no cumplen un formato exigido) y actualidad (filas o particiones obsoletas). Los recuentos de filas a nivel de tabla y la integridad referencial entre tablas son los dos añadidos más habituales.

¿Necesito escribir SQL para validar datos?

Para las comprobaciones estándar, no. Declarar la expectativa —obligatorio, único, valores permitidos, rango numérico, patrón, antigüedad máxima, clave foránea— permite que un runner la compile al SQL correcto para tu motor, lo que además elimina las diferencias de dialecto y las trampas de los nulos. Conviene reservar el SQL escrito a mano para la lógica de negocio genuinamente a medida, mediante una regla de SQL personalizado.

¿Con qué frecuencia deben ejecutarse las validaciones de datos?

Ajusta la programación a la cadencia de los propios datos y dispara desde el job que los produce, no desde un reloj fijo. Las tablas de streaming piden comprobaciones de frescura cada hora; las tablas batch, una única ejecución cuando termina la carga. Ejecutarlas con más frecuencia de la que cambian los datos solo produce ruido y, en warehouses facturados por uso, también una factura.

¿Es lo mismo la validación de datos que un contrato de datos?

No. La validación es el acto de ejecutar las comprobaciones; un contrato de datos es el documento versionado que dice cuáles deben ser esas comprobaciones. El contrato recoge además la propiedad, las descripciones y los niveles de servicio, que es lo que permite que lo revisen los consumidores de los datos y no solo el equipo que escribió el pipeline. Un estándar como ODCS es lo que mantiene el contrato portable entre runners.

¿La validación de datos necesita acceso de escritura a mi base de datos?

No. Todas las comprobaciones descritas aquí son un SELECT que devuelve un único número, así que el acceso de lectura sobre las tablas más la capacidad de leer el catálogo de esquemas son todo el conjunto de permisos necesario. Allí donde exista una réplica de lectura o una secundaria legible, apunta ahí la conexión: las comprobaciones no tienen más requisito de consistencia que “reciente”, y sacarlas de la primaria elimina la principal objeción operativa a ejecutarlas a menudo.