CSV, JSON, Avro, Parquet: cómo elegir formato
El formato en que guardas un dataset no es un detalle de implementación: define cuánto pagas por almacenamiento, cuánto tarda cada consulta y qué tan fácil es evolucionar el esquema mañana.
Casi nadie discute el formato de fichero en la fase de diseño de un pipeline. Se elige CSV porque "así llega el dato", o JSON porque "es lo que devuelve la API", y el tema se cierra. El problema es que esa decisión, tomada sin pensar, se propaga: determina cuánto espacio ocupa el dato en el lago, cuánto tiempo tarda un job de Spark en escanear una tabla, cuánto cuesta cada consulta en un motor que cobra por bytes leídos, y qué tan doloroso es agregar una columna nueva sin romper los consumidores existentes. El formato de fichero no es un detalle de implementación, es una decisión de arquitectura con impacto directo en costo y rendimiento, y como toda decisión de arquitectura, conviene tomarla a propósito.
Row-oriented vs. columnar: la decisión que lo explica casi todo
La pregunta de fondo detrás de cualquier formato de almacenamiento analítico es una sola: ¿cómo se organizan físicamente los bytes en el disco? Hay dos respuestas clásicas.
En un formato orientado a fila (row-wise), cada registro se escribe completo antes de pasar al siguiente. Todos los valores de la fila 1 quedan contiguos, luego todos los de la fila 2, y así sucesivamente. Esto es excelente cuando la carga de trabajo es transaccional (OLTP): necesitas leer o escribir un registro entero —un pedido, un usuario, una transacción— y quieres hacerlo en una sola operación de I/O.
En un formato columnar, en cambio, los valores de una misma columna se agrupan juntos en el disco: todos los "customer_id" contiguos, luego todos los "email", luego todos los "create_date". Esto favorece cargas analíticas (OLAP), donde una consulta típica agrega o filtra sobre pocas columnas pero recorre millones de filas. Si solo necesitas el promedio de una columna numérica sobre una tabla de cien columnas, un motor columnar lee únicamente esa columna del disco; un motor orientado a fila tiene que traer registros completos y descartar el 99% de los bytes leídos.
El costo real de una consulta analítica casi nunca es la CPU: es cuántos bytes tuviste que leer del disco para responderla.
La organización columnar también habilita técnicas de compresión que no funcionan igual de bien en formatos por fila, como run-length encoding o diccionarios de valores repetidos, porque una columna suele tener mucha menos variabilidad de valores que una fila completa. Existe además un tercer enfoque híbrido (a veces llamado PAX), usado por formatos como Parquet y ORC, que agrupa filas en bloques y dentro de cada bloque organiza los datos por columna: así se obtiene compresión y poda columnar sin perder del todo la localidad de fila.
CSV y JSON: siguen siendo la opción correcta en muchos casos
Es tentador tratar a CSV y JSON como formatos "legacy" que hay que reemplazar en cuanto se pueda, pero eso ignora para qué fueron diseñados. Son formatos textuales, legibles por humanos, editables con cualquier editor y sin dependencia de una librería específica para inspeccionarlos. Esa portabilidad tiene valor real.
CSV sigue siendo la lengua franca del intercambio de datos tabulares simples: exportaciones desde sistemas legacy, cargas manuales, integraciones con herramientas de negocio que no hablan otra cosa. Su limitación estructural es que no lleva tipos ni metadatos: todo es texto plano, así que quien lo consume tiene que inferir o castear tipos, lidiar con encodings inconsistentes y confiar en que el delimitador no aparezca dentro de un valor.
JSON gana sentido en cuanto los datos dejan de ser planos. Es semi-estructurado: soporta objetos anidados, arrays, y registros de una misma "entidad" con campos distintos entre sí, sin que eso rompa el fichero. Es el formato natural para payloads de APIs REST, logs de aplicación, eventos de tracking o documentos con estructura variable. El costo es el opuesto al de CSV: la flexibilidad hace que no haya garantía de esquema, cada parser tiene que validar estructura en tiempo de lectura, y el peso en disco es mayor porque los nombres de campo se repiten en cada registro.
La regla práctica: si el dato es pequeño, se comparte entre humanos y sistemas heterogéneos, o su estructura cambia de un registro a otro, CSV o JSON siguen siendo razonables. El problema aparece cuando se usan como formato de almacenamiento de fondo para datasets grandes que se consultan repetidamente con fines analíticos.
Avro: cuando el esquema evoluciona y el dato viaja fila a fila
Avro es un formato binario orientado a fila, pero con una diferencia crucial frente a CSV: el esquema forma parte del contrato. Cada fichero Avro embebe (o referencia mediante un registro de esquemas) una definición formal en JSON de los campos, tipos y valores por defecto, lo que permite que productor y consumidor evolucionen de forma independiente sin que un cambio rompa al otro. Se puede añadir un campo opcional, dar de baja uno con un valor por defecto, o renombrar con alias, y los lectores antiguos y nuevos siguen siendo compatibles.
Esa capacidad de evolución de esquema, combinada con su naturaleza row-wise, lo hace la opción natural para streaming y sistemas de mensajería: escribir un registro completo es una operación local y barata, no hay que tocar múltiples columnas dispersas en el disco. Es el formato por defecto en ecosistemas como Kafka, donde los eventos se escriben uno a uno y a alta frecuencia, y donde el esquema del payload puede cambiar con el tiempo sin coordinar un despliegue simultáneo de todos los productores y consumidores.
La contrapartida es previsible: al ser row-wise, las consultas analíticas que solo necesitan un subconjunto de columnas no se benefician de poda columnar, y la compresión es menos agresiva que en un formato columnar porque los valores de una misma columna no quedan contiguos.
Parquet: el formato por defecto para analítica a escala
Parquet es columnar (en la práctica, híbrido tipo PAX) y es hoy el estándar de facto para almacenar datos en un lago o lakehouse que se van a consultar con motores como Spark, Trino, Presto o Athena. Tres propiedades explican por qué domina el espacio analítico.
Primero, la poda columnar: si una consulta solo toca 3 de 50 columnas, el motor lee físicamente solo los bloques correspondientes a esas 3, y esto reduce directamente el I/O y, en motores serverless que cobran por bytes escaneados, el costo de la consulta. Segundo, la compresión: valores homogéneos agrupados por columna comprimen mucho mejor que filas heterogéneas, lo que se traduce en menor huella en almacenamiento. Tercero, splittability: un fichero Parquet se organiza en row groups independientes con sus propios metadatos y estadísticas (mínimos, máximos, conteo de nulos), lo que permite a un motor distribuido paralelizar la lectura entre varios workers y, además, saltarse row groups enteros cuando sabe de antemano que no contienen los valores buscados.
El costo de esta eficiencia en lectura es que la escritura es más pesada: no se puede simplemente añadir una fila al final del fichero como en CSV, hay que reescribir bloques enteros, y por eso Parquet no es la opción adecuada para cargas transaccionales o de escritura fila a fila de alta frecuencia. Su soporte de evolución de esquema también es más limitado que el de Avro: añadir columnas suele funcionar, pero cambios de tipo o reordenamientos requieren más cuidado.
Un patrón habitual en pipelines de ingesta: recibir el dato en Avro o JSON (bueno para escritura y evolución de esquema), y convertirlo a Parquet en la capa de curación antes de exponerlo a analítica (bueno para lectura y compresión). No hace falta elegir un solo formato para todo el pipeline.
| Formato | Compresión | Splittability | Soporte de esquema | Caso de uso típico |
|---|---|---|---|---|
| CSV | Baja (sin compresión nativa) | Alta (texto plano, se puede cortar en cualquier línea) | Ninguno (sin tipos, cabecera opcional) | Exportaciones, intercambio simple entre sistemas heterogéneos |
| JSON | Baja-media | Baja a media (depende de si es NDJSON o array) | Flexible, sin esquema fijo (autodescriptivo) | Payloads de API, logs, eventos con estructura variable |
| Avro | Media-alta | Alta (bloques con sync markers) | Fuerte, con evolución de esquema (fila) | Streaming, mensajería (Kafka), escritura fila a fila |
| Parquet | Alta (RLE, dictionary encoding) | Alta (row groups independientes) | Fuerte, evolución más limitada (columnar) | Analítica OLAP, lagos de datos, motores de consulta distribuidos |
Cuándo usarlo en la práctica
- Si el consumidor final es humano, o el dato se comparte entre herramientas dispares que no tienen librerías específicas para leer binarios: usa CSV. Acepta el costo de tener que castear tipos y validar el delimitador.
- Si el dato tiene estructura anidada o variable, y el consumidor es un servicio o API que ya trabaja en JSON de forma nativa: usa JSON o su variante NDJSON (un objeto por línea) si necesitas splittability para procesarlo en paralelo.
- Si estás construyendo un pipeline de streaming (Kafka, Kinesis) o cualquier sistema donde el esquema del payload cambiará con el tiempo y necesitas que productores y consumidores no se rompan entre sí: usa Avro, apoyado en un schema registry.
- Si el destino final del dato es analítica —consultas agregadas, dashboards, motores tipo Spark, Trino o Athena sobre un lago de datos— convierte a Parquet en la capa de curación, aunque el dato haya entrado en otro formato. La ganancia en costo de consulta y tiempo de escaneo casi siempre justifica el paso de conversión.
- Si el dataset requiere upserts, deletes o control de versiones sobre archivos Parquet u ORC —no solo el formato de fichero sino también cómo se gestiona la tabla como tal— evalúa un formato de tabla abierta como Delta Lake, Apache Iceberg o Apache Hudi por encima de Parquet, ya que añaden transacciones ACID, time travel y evolución de esquema y particiones sin que tengas que reescribir el pipeline entero.
- Evita el error más común: dejar datos de alto volumen y consulta frecuente en JSON o CSV solo porque "así llegaron". Ese es exactamente el escenario donde el costo de no convertir a un formato columnar se paga, con intereses, en cada consulta.