Descripción General y Arquitectura de DataFusion
Apache Arrow DataFusion es un query engine, no una base de datos. Proporciona:
- Un parser y planner de SQL
- Un optimizador de queries basado en costo (con 60+ reglas de optimización)
- Un motor de ejecución paralelo usando Arrow RecordBatches
- Una API de extensión para customizar todo
Lo que NO te da: gestión de almacenamiento, transacciones, replicación, gestión de usuarios. Es una librería, no un servidor.
use datafusion::prelude::*;
#[tokio::main]
async fn main() -> datafusion::error::Result<()> {
let ctx = SessionContext::new();
// Registrar un archivo Parquet como tabla
ctx.register_parquet(
"events",
"s3://mybucket/events/*.parquet",
ParquetReadOptions::default(),
).await?;
// SQL simplemente funciona
let df = ctx.sql("
SELECT date_trunc('hour', timestamp) as hour,
count(*) as event_count,
approx_percentile_cont(latency_ms, 0.99) as p99
FROM events
WHERE timestamp > '2026-02-01'
GROUP BY 1
ORDER BY 1
").await?;
df.show().await?;
Ok(())
}
Eso es una query completamente paralela con predicate pushdown a row groups de Parquet, ejecución columnar sobre arrays Arrow, y computación de percentil aproximado. En unas 15 líneas.
Custom Table Providers: Tus Datos, Tus Reglas
Acá es donde DataFusion resulta particularmente relevante. El trait TableProvider te permite envolver cualquier fuente de datos:
#[async_trait]
impl TableProvider for MetricsTableProvider {
fn as_any(&self) -> &dyn Any { self }
fn schema(&self) -> SchemaRef {
self.schema.clone()
}
fn table_type(&self) -> TableType {
TableType::Base
}
async fn scan(
&self,
state: &SessionState,
projection: Option<&Vec<usize>>,
filters: &[Expr],
limit: Option<usize>,
) -> Result<Arc<dyn ExecutionPlan>> {
// Extraer rango temporal de los filtros para pushdown
let time_range = extract_time_range(filters);
let projected_schema = project_schema(&self.schema, projection);
Ok(Arc::new(MetricsScanExec {
schema: projected_schema,
client: self.metrics_client.clone(),
time_range,
limit,
}))
}
fn supports_filters_pushdown(
&self,
filters: &[&Expr],
) -> Result<Vec<TableProviderFilterPushDown>> {
filters.iter().map(|f| {
if is_time_filter(f) {
Ok(TableProviderFilterPushDown::Exact)
} else {
Ok(TableProviderFilterPushDown::Unsupported)
}
}).collect()
}
}
El método supports_filters_pushdown le dice al optimizador qué predicados tu provider puede manejar nativamente. Devolver Exact significa que el optimizador no va a aplicar ese filtro de nuevo después del scan, tu provider lo manejó completamente. Esto elimina filtrado redundante y puede cortar queries de métricas de ~800ms a ~200ms.
Funciones Definidas por Usuario
DataFusion soporta UDFs escalares, de agregación, y de ventana:
// UDF escalar: distancia haversine custom
fn haversine_distance(args: &[ArrayRef]) -> Result<ArrayRef> {
let lat1 = args[0].as_any().downcast_ref::<Float64Array>().unwrap();
let lon1 = args[1].as_any().downcast_ref::<Float64Array>().unwrap();
let lat2 = args[2].as_any().downcast_ref::<Float64Array>().unwrap();
let lon2 = args[3].as_any().downcast_ref::<Float64Array>().unwrap();
let result: Float64Array = lat1.iter().zip(lon1.iter())
.zip(lat2.iter().zip(lon2.iter()))
.map(|((la1, lo1), (la2, lo2))| {
match (la1, lo1, la2, lo2) {
(Some(la1), Some(lo1), Some(la2), Some(lo2)) => {
Some(haversine(la1, lo1, la2, lo2))
}
_ => None,
}
})
.collect();
Ok(Arc::new(result))
}
ctx.register_udf(create_udf(
"haversine",
vec![DataType::Float64; 4],
Arc::new(DataType::Float64),
Volatility::Immutable,
Arc::new(haversine_distance),
));
// Ahora usalo en SQL
let df = ctx.sql("
SELECT city_name,
haversine(lat, lon, -34.6037, -58.3816) as distancia_a_bsas
FROM cities
ORDER BY distancia_a_bsas
LIMIT 10
").await?;
Resultados de Benchmark
En una máquina de 32 cores, consultando 50 GB de datos Parquet (1.2 mil millones de filas):
Query: SELECT date_trunc('hour', ts), count(*), avg(value)
FROM events WHERE ts > '2026-02-01' GROUP BY 1
DuckDB (in-process): 1,8s
DataFusion (default): 2,4s
DataFusion (tuneado): 1,1s
Spark (local[32]): 14,2s
El DataFusion tuneado fue en realidad más rápido que DuckDB en este workload específico porque el reader Parquet custom solapaba I/O con computación, mientras DuckDB bloqueaba en lecturas, DataFusion estaba procesando el batch anterior.
Para analytics sobre datos heterogéneos sin un servidor de base de datos administrado, DataFusion es una de las opciones más sólidas disponibles. El ecosistema Rust alrededor de Arrow está madurando rápido, y la comunidad es muy activa. Las APIs de extensión son poderosas pero no triviales, conocimiento sólido de Rust es un prerrequisito.