Saltar al contenido principal
InicioTutorialesIngeniería de datos

DuckDB para Ingenieros de Datos: Acelera tus cadenas de datos 10 veces y más

DuckDB es un potente motor analítico que vive en tu portátil. Puedes utilizarlo para acelerar la lectura y el procesamiento de datos y reducir los tiempos de ejecución de tu pipeline de minutos a segundos. Sigue esta guía práctica para aprender a hacerlo.
Actualizado 22 sept 2024  · 27 min leer

¿Cuántas veces te ha preguntado un directivo cuánto tarda en funcionar tu pipeline y te ha dado vergüenza contestar?

No estás sola. La comodidad de Python hay que pagarla en algún sitio. Ese lugar suele ser el velocidad de ejecución. Los pipelines son rápidos de escribir pero difíciles de escalar, y a menudo requieren cada vez más tiempo a medida que crecen los requisitos del proyecto.

Pero quizá no sea tu código el cuello de botella. Quizá no haya ningún rendimiento adicional que puedas exprimir de los pandas. Tal vez cambiar a otro motor de procesamiento de datos pueda reducir el tiempo de ejecución de horas a minutos.

Ahí es donde interviene DuckDB.

En este artículo, te contaré exactamente qué es DuckDB y por qué es importante para los ingenieros de datos. Aprenderás a utilizar DuckDB mediante ejemplos prácticos y comprobarás lo mucho más rápido que es que las bibliotecas de procesamiento de datos más populares de Python. 

¿Qué es DuckDB, y es la próxima gran cosa para los ingenieros de datos?

DuckDB es un SGBD OLAP relacional, incrustado y en proceso, de código abierto.

Si lo prefieres en lenguaje llano, piensa que es una base de datos analítica columnar que funciona en memoria. Al ser una base de datos analítica, está optimizada para las declaraciones SELECT en lugar de INSERT y UPDATE. Es como SQLite, pero al revés.

DuckDB existe desde hace unos 5 años, pero la primera versión "estable" se anunció en junio de 2024, es decir, hace 3 meses en el momento de escribir esto. Pero no dejes que el factor novedad te engañe: DuckDB ha sido probado por muchos y casi siempre es una herramienta recomendada cuando la velocidad es fundamental.

Por qué elegir DuckDB para tus canalizaciones de datos

Si eres ingeniero de datos, aquí tienes un par de ventajas concretas de utilizar DuckDB en tus canalizaciones de datos:

  • Es rápido: Piensa en órdenes de magnitud más rápidas que las bibliotecas de marcos de datos por defecto de Python. En la mayoría de los casos, es incluso más rápido que las bibliotecas optimizadas para la velocidad (por ejemplo, polars).
  • Es de código abierto: Todo el código fuente está disponible en unrepositorio público de GitHub . Puedes utilizarlo, modificarlo e incluso contribuir al proyecto.
  • Retrasa el momento de pasar a la nube: La computación en nube puede resultar cara, pero el precio suele estar justificado cuando la velocidad de procesamiento es crítica para la misión. Con DuckDB, puedes analizar cientos de millones de filas en tu portátil.
  • Es fácil de coger: Es fácil empezar a utilizar DuckDB. Es probable que tengas que hacer cambios mínimos en tus procesos de datos existentes, ya que DuckDB se integra perfectamente con las bibliotecas de procesamiento de datos de Python.
  • Se integra bien con las plataformas de almacenamiento en la nube: Por ejemplo, puedes escribir una consulta SQL de DuckDB que lea datos directamente de AWS S3. No es necesario descargar primero los datos.

Por estas razones (y muchas más que vendrán), creo que DuckDB es el próximo gran salto para los ingenieros de datos.

Conviértete en Ingeniero de Datos

Desarrolla tus habilidades en Python para convertirte en un ingeniero de datos profesional.

Empieza Gratis

Cómo empezar con DuckDB

Como he dicho antes, DuckDB funciona en tu portátil. Lo primero es conseguirlo. 

Te mostraré los pasos de instalación para macOS, pero las instrucciones para Windows y Linux están bien explicadas y son fáciles de seguir.

Paso 1: Instalar DuckDB

Si tienes un Mac, la forma más sencilla de instalar DuckDB es con Homebrew. Ejecuta el siguiente comando shell:

brew install duckdb

Verás el siguiente resultado en cuestión de segundos:

Instalar DuckDB con Homebrew

Instalar DuckDB con Homebrew

Una vez instalado, puedes entrar en el shell de DuckDB con el siguiente comando:

duckdb

Shell DuckDB

Shell DuckDB

A partir de aquí, ¡el mundo es tu ostra!

Puedes escribir y ejecutar consultas SQL directamente desde la consola. Por ejemplo, he descargado los datos de los 6 primeros meses de Taxis de NYC para 2024. Si has hecho lo mismo, utiliza el siguiente fragmento para mostrar el recuento de filas de un único archivo Parquet:

SELECT COUNT(*)
  FROM PARQUET_SCAN("path-to-data.parquet");

Recuento de filas de un único archivo Parquet

Recuento de filas de un único archivo Parquet

Sí, son casi 20 millones de filas de un solo archivo. He descargado 24 de ellos.

Probablemente no quieras ejecutar DuckDB desde el terminal, así que a continuación te guiaré por los pasos para conectar DuckDB con Python.

Paso 2: Conecta DuckDB a tu flujo de trabajo Python

El objetivo de esta sección es mostrarte cómo configurar y ejecutar DuckDB en el lenguaje más popular de la ingeniería de datos: Pitón.

De hecho, DataCamp ofrece una carrera completa en ingeniería de datos en Python.

Python tiene una biblioteca DuckDB específica que debes instalar primero. Ejecuta lo siguiente desde el terminal, preferiblemente en un entorno virtual:

pip install duckdb

Ahora crea un nuevo archivo Python y pega el siguiente código:

import duckdb
# Function to get the count from a single file
def get_row_count_from_file(conn: duckdb.DuckDBPyConnection, file_path: str) -> int:
    query = f"""
        SELECT COUNT(*)
        FROM PARQUET_SCAN("{file_path}")
    """
    return conn.sql(query).fetchone()[0]
if __name__ == "__main__":
    # In-memory database connection
    conn = duckdb.connect()
    # Path to a parquet file
    file_path = "fhvhv_tripdata_2024-01.parquet"
    # Get the row count
    row_count = get_row_count_from_file(conn=conn, file_path=file_path)
    print(row_count)

Resumiendo, el fragmento de código tiene una función que obtiene el recuento de filas de un único archivo, dada una conexión DuckDB válida y una ruta de archivo.

Obtendrás el siguiente resultado casi instantáneamente después de ejecutar el script:

Conexión entre DuckDB y Python

Conexión entre DuckDB y Python

Eso es. 

A continuación, te mostraré cómo hacer cosas increíbles con Python y DuckDB ¡a toda velocidad!

DuckDB en acción: Cómo acelerar los conductos de datos

Como referencia, ejecutaré el código en una parte de 2024 (enero-junio) del conjunto de datos de taxis de NYC , concretamente para los vehículos de alquiler de gran volumen. Esto es 6archivos Parquet, que ocupan aproximadamente 3GB de espacio en disco. En cuanto al hardware, estoy utilizando un MacBook Pro 16" M3 Pro con 12 núcleos de CPU y 36 GB de memoria unificada.

Tu kilometraje puede variar, pero aun así deberías acabar con cifras similares.

Lee enormes conjuntos de datos en poco tiempo

He convertido estos 6 archivos a dos formatos adicionales: CSV y JSON. Puedes ver cuánto espacio de disco ocupa cada una de ellas:

Comparación del tamaño del conjunto de datos

Comparación del tamaño del conjunto de datos

Total: 2,96 GB para Parquet, 19,31 GB para CSV y 66,04 GB para JSON.

¡La diferencia es enorme! Si no sacas nada más del artículo de hoy, al menos recuerda utilizar siempre el formato de archivo Parquet cuando se trate de archivos enormes. Te ahorrará tiempo de procesamiento y espacio en disco.

DuckDB dispone de prácticas funciones que pueden leer varios archivos del mismo formato a la vez (utilizando un patrón glob). Lo utilizaré para leer los seis y obtener el recuento de filas y comparar el tiempo de ejecución:

import duckdb
import pandas as pd
from datetime import datetime
def get_row_count_and_measure_time(file_format: str) -> str:
    # Construct a DuckDB query based on the file_format
    match file_format:
        case "csv":
            query = """
            SELECT COUNT(*)
            FROM READ_CSV("nyc-taxi-data-csv/*.csv")
            """
        case "json":
            query = """
            SELECT COUNT(*)
            FROM READ_JSON("nyc-taxi-data-json/*.json")
            """
        case "parquet":
            query = """
                SELECT COUNT(*)
                FROM READ_PARQUET("nyc-taxi-data/*.parquet")
            """
        case _:
            raise KeyError("Param file_format must be in [csv, json, parquet]")
    # Open the database connection and measure the start time
    time_start = datetime.now()
    conn = duckdb.connect()
    # Get the row count
    row_count = conn.sql(query).fetchone()[0]
    # Close the database connection and measure the finish time
    conn.close()
    time_end = datetime.now()
    return {
        "file_format": file_format,
        "duration_seconds": (time_end - time_start).seconds + ((time_end - time_start).microseconds) / 1000000,
        "row_count": row_count
    }
# Run the function
file_format_results = []
for file_format in ["csv", "json", "parquet"]:
    file_format_results.append(get_row_count_and_measure_time(file_format=file_format))
pd.DataFrame(file_format_results)

Ya están los resultados: sólo hay un claro ganador:

Comparación del tiempo de ejecución de la lectura de archivos

Comparación del tiempo de ejecución de la lectura de archivos

Gracias a DuckDB, los tres son rápidos y acaban en el mismo sitio, pero está claro que Parquet gana por un factor de 600 en comparación con CSV y por un factor de 1200 en comparación con JSON.

Por esta razón, sólo utilizaré Parquet durante el resto del artículo.

Consultar datos con SQL

DuckDB te permite agregar datos mediante SQL.

Mi objetivo para esta sección es ampliar esa idea. Para ser más preciso, te mostraré cómo calcular estadísticas resumidas a nivel mensual que incluyan información sobre el número de viajes, el tiempo de viaje, la distancia recorrida, el coste del viaje y la remuneración del conductor, todo ello a partir de 120 millones de filas de datos.

Lo mejor: ¡Sólo te llevará dos segundos!

Este es el código que utilicé dentro de un script de Python para agregar datos e imprimir los resultados:

conn = duckdb.connect()
query = """
    SELECT
        ride_year || '-' || ride_month AS ride_period,
        COUNT(*) AS num_rides,
        ROUND(SUM(trip_time) / 86400, 2) AS ride_time_in_days,
        ROUND(SUM(trip_miles), 2) AS total_miles,
        ROUND(SUM(base_passenger_fare + tolls + bcf + sales_tax + congestion_surcharge + airport_fee + tips), 2) AS total_ride_cost,
        ROUND(SUM(driver_pay), 2) AS total_rider_pay
    FROM (
        SELECT
            DATE_PART('year', pickup_datetime) AS ride_year,
            DATE_PART('month', pickup_datetime) AS ride_month,
            trip_time,
            trip_miles,
            base_passenger_fare,
            tolls,
            bcf,
            sales_tax,
            congestion_surcharge,
            airport_fee,
            tips,
            driver_pay
        FROM PARQUET_SCAN("nyc-taxi-data/*.parquet")
        WHERE 
            ride_year = 2024
        AND ride_month >= 1 
        AND ride_month <= 6
    )
    GROUP BY ride_period
    ORDER BY ride_period
"""
# Aggregate data, print it, and show the data type
results = conn.sql(query)
results.show()
print(type(results))

La salida contiene los resultados de la agregación, el tipo de variable para results, y el tiempo total de ejecución:

Agregación mensual de estadísticas resumidas

Agregación mensual de estadísticas resumidas

Deja que lo asimile: Eso son 3 GB de datos que abarcan más de 120 millones de filas, ¡todo agregado en 2 segundos en un ordenador portátil!

El único problema es que el tipo variable es algo inservible a largo plazo. Por suerte, hay una solución fácil.

Integrar con pandas y DataFrames

La ventaja de utilizar DuckDB no es sólo que es rápido, sino que también se integra bien con tu biblioteca de marcos de datos favorita: pandas.

Si tienes un conjunto de resultados temporal similar al que he mostrado anteriormente, sólo tienes que llamar al método .df() sobre él para convertirlo en un pandas DataFrame:

# Convert to Pandas DataFrame
results_df = results.df()
# Print the type and contents
print(type(results_df))
results_df

Conversión de DuckDB a pandas

Conversión de DuckDB a pandas

Del mismo modo, puedes utilizar DuckDB para realizar cálculos en DataFrames de pandas que ya se han cargado en memoria.

El truco consiste en hacer referencia al nombre de la variable después de la palabra clave FROM en una consulta SQL de DuckDB. He aquí un ejemplo:

# Pandas DataFrame
pandas_df = pd.read_parquet("nyc-taxi-data/fhvhv_tripdata_2024-01.parquet")
# Run SQL queries through DuckDB
duckdb_res = duckdb.sql("""
    SELECT
        pickup_datetime,
        dropoff_datetime,
        trip_miles,
        trip_time,
        driver_pay
    FROM pandas_df
    WHERE trip_miles >= 300
""").df()
duckdb_res

Consulta DuckDB sobre un DataFrame Pandas existente

Consulta DuckDB sobre un DataFrame pandas existente

Para concluir, puedes evitar por completo pandas o realizar agregaciones más rápidamente en los DataFrames de pandas existentes.

A continuación, te mostraré un par de funciones avanzadas de DuckDB.

DuckDB Avanzado para Ingenieros de Datos

Hay mucho más DuckDB de lo que parece.

En esta sección, te guiaré a través de un par de funciones avanzadas de DuckDB que son esenciales para los ingenieros de datos.

Extensiones

DuckDB te permite ampliar su funcionalidad nativa mediante extensionesdel núcleo y de la comunidad. Utilizo extensiones todo el tiempo para conectarme a sistemas de almacenamiento en la nube y bases de datos y trabajar con formatos de archivo adicionales.

Puedes instalar extensiones a través de las consolas Python y DuckDB.

En el fragmento de código siguiente, te muestro cómo instalar la extensión httpfs, necesaria para conectar con AWS y leer datos de S3:

import duckdb
conn = duckdb.connect()
conn.execute("""
    INSTALL httpfs;
    LOAD httpfs;
""")

No verás ninguna salida si no encadenas el método .df() a conn.execute(). Si lo haces, verás un mensaje de "Éxito" o de "Error".

Consulta de almacenamiento en la nube

Lo más frecuente es que tu empresa te conceda acceso a los datos que tienes que incluir en tus pipelines. Suelen almacenarse en plataformas escalables en la nube, como AWS S3.

DuckDB te permite conectar S3 (y otras plataformas) directamente.

Ya te he mostrado cómo instalar la extensión (httpfs), y lo único que queda por hacer es configurarla. AWS requiere que proporciones información sobre tu región, clave de acceso y clave de acceso secreta.

Suponiendo que tengas todo esto, ejecuta el siguiente comando a través de Python:

conn.execute("""    CREATE SECRET aws_s3_secret (
        TYPE S3,
        KEY_ID '<your-access-key>',
        SECRET '<your-secret-key>',
        REGION '<your-region>'
    );
""")

Mi bucket de S3 contiene dos de los archivos del conjunto de datos de Taxis de Nueva York:

Contenido del cubo S3

Contenido del cubo S3

No es necesario descargar los archivos, ya que puedes escanearlos directamente desde S3:

import duckdb
conn = duckdb.connect()
aws_count = conn.execute("""
    SELECT COUNT(*)
    FROM PARQUET_SCAN('s3://<your-bucket-name>/*.parquet');
""").df()
aws_count

Resultados de la búsqueda en S3 de DuckDB

Resultados de la búsqueda en S3 de DuckDB

Estás leyendo bien: tardósólo 4 segundos en recorrer ~ 900 MB de datos almacenados en un bucket de S3.

Procesamiento paralelo

En cuanto a la paralelización, DuckDB la implementa por defecto basándose en grupos de filas: particiones de datos horizontales que encontrarías típicamente en Parquet. Un único grupo de filas puede tener un máximo de 122.880 filas. 

El paralelismo en DuckDB comienza cuando trabajas con más de 122.880 filas.

DuckDB lanzará automáticamente nuevos hilos cuando esto ocurra. Por defecto, el número de hilos es igual al número de núcleos de la CPU. Pero esto no te impide jugar manualmente con el número de hilos.

En esta sección, te mostraré cómo hacerlo y el impacto exacto que tiene un número diferente de hilos en una carga de trabajo idéntica.

Para ver el número actual de hilos, ejecuta lo siguiente:

SELECT current_setting('threads') AS threads;

Número actual de hilos utilizados por DuckDB

El número actual de hilos utilizados por DuckDB

Puedes obtener los mismos resultados mediante Python.

Querrás ejecutar el comando SET threads = N para cambiar el número de hilos. 

En el siguiente fragmento de código, te mostraré cómo implementar una función personalizada de Python que ejecute una consulta DuckDB en un número determinado de hilos, la convierta en un DataFrame de pandas y devuelva el tiempo de ejecución (entre otras cosas). El código que hay debajo de la función la ejecuta en un rango de [1, 12] hilos:

def thread_test(n_threads: int) -> dict:
    # Open the database connection and measure the start time
    time_start = datetime.now()
    conn = duckdb.connect()
    # Set the number of threads
    conn.execute(f"SET threads = {n_threads};")
    query = """
        SELECT
            ride_year || '-' || ride_month AS ride_period,
            COUNT(*) AS num_rides,
            ROUND(SUM(trip_time) / 86400, 2) AS ride_time_in_days,
            ROUND(SUM(trip_miles), 2) AS total_miles,
            ROUND(SUM(base_passenger_fare + tolls + bcf + sales_tax + congestion_surcharge + airport_fee + tips), 2) AS total_ride_cost,
            ROUND(SUM(driver_pay), 2) AS total_rider_pay
        FROM (
            SELECT
                DATE_PART('year', pickup_datetime) AS ride_year,
                DATE_PART('month', pickup_datetime) AS ride_month,
                trip_time,
                trip_miles,
                base_passenger_fare,
                tolls,
                bcf,
                sales_tax,
                congestion_surcharge,
                airport_fee,
                tips,
                driver_pay
            FROM PARQUET_SCAN("nyc-taxi-data/*.parquet")
            WHERE 
                ride_year = 2024
            AND ride_month >= 1 
            AND ride_month <= 6
        )
        GROUP BY ride_period
        ORDER BY ride_period
    """
    # Convert to DataFrame
    res = conn.sql(query).df()
    # Close the database connection and measure the finish time
    conn.close()
    time_end = datetime.now()
    return {
        "num_threads": n_threads,
        "num_rows": len(res),
        "duration_seconds": (time_end - time_start).seconds + ((time_end - time_start).microseconds) / 1000000
    }
thread_results = []
for n_threads in range(1, 13):
    thread_results.append(thread_test(n_threads=n_threads))
pd.DataFrame(thread_results)

Deberías ver un resultado similar al mío:

Tiempo de ejecución de DuckDB para diferente número de hilos

Tiempo de ejecución de DuckDB para diferente número de hilos

En general, cuantos más hilos lances a un cálculo, más rápido terminará. Puede haber un punto en el que lasobrecarga de al crear un nuevo hilo provoque un aumento del tiempo de ejecución total, pero no lo he encontrado en este rango.

Comparación de prestaciones: DuckDB frente a los enfoques tradicionales

En esta sección, te mostraré cómo escribir una canalización de datos desde cero. Será relativamente sencillo: leer datos del disco, realizar agregaciones y escribir los resultados. Mi objetivo es mostrar cuánto rendimiento puedes ganarcambiando de pandas a DuckDB. No sólo eso, sino que además tu código tendrá un aspecto más limpio.

No se trata de una introducción exhaustiva a los procesos ETL/ELT, sino de una visión general de alto nivel.

Objetivos de la canalización de datos

La canalización de datos que voy a mostrarte consigue lo siguiente:

  • Extracto: Lee varios archivos Parquet del disco (unos 120 millones de filas).
  • Transforma: Calcula estadísticas resumidas mensuales, como los ingresos de la empresa de taxis, el margen de ingresos (diferencia entre el coste del trayecto y el pago del conductor) y los ingresos medios por trayecto. También verás más estadísticas comunes de las que hablé antes en el artículo.
  • Load: Guarda las estadísticas mensuales localmente en un archivo CSV.

Después de ejecutar el código, deberías obtener exactamente los mismos resultados de agregación para ambas implementaciones de canalización (sin tener en cuenta algunas diferencias de redondeo):

Resultados de la tubería para DuckDB y Pandas

Resultados de la tubería para DuckDB y pandas

En primer lugar, vamos a repasar la implementación de la tubería en pandas.

Código: Python y pandas

No importa lo que haya intentado, no he podido procesar los 6 archivos Parquet a la vez. Los mensajes de advertencia del sistema como éste se mostraban en cuestión de segundos:

Error de memoria del sistema

Error de memoria del sistema

Parece que 36 GB de memoria no son suficientes para procesar 120 millones de filas a la vez. Como resultado, el script Python fue eliminado:

Matado script Python por memoria insuficiente

Matado script Python por memoria insuficiente

Para mitigarlo, tuve que procesar los archivos Parquet secuencialmente. Éste es el código que he utilizado para la canalización de datos:

import os
import pandas as pd
def calculate_monthly_stats_per_file(file_path: str) -> pd.DataFrame:
    # Read a single Parquet file
    df = pd.read_parquet(file_path)
    # Extract ride year and month
    df["ride_year"] = df["pickup_datetime"].dt.year
    df["ride_month"] = df["pickup_datetime"].dt.month
    # Remove data points that don"t fit in the time period
    df = df[(df["ride_year"] == 2024) & (df["ride_month"] >= 1) & (df["ride_month"] <= 6)]
    # Combine ride year and month
    df["ride_period"] = df["ride_year"].astype(str) + "-" + df["ride_month"].astype(str)
    # Calculate total ride cost
    df["total_ride_cost"] = (
        df["base_passenger_fare"] + df["tolls"] + df["bcf"] +
        df["sales_tax"] + df["congestion_surcharge"] + df["airport_fee"] + df["tips"]
    )
    # Aggregations
    summary = df.groupby("ride_period").agg(
        num_rides=("pickup_datetime", "count"),
        ride_time_in_days=("trip_time", lambda x: round(x.sum() / 86400, 2)),
        total_miles=("trip_miles", "sum"),
        total_ride_cost=("total_ride_cost", "sum"),
        total_rider_pay=("driver_pay", "sum")
    ).reset_index()
    # Additional attributes
    summary["total_miles_in_mil"] = summary["total_miles"] / 1000000
    summary["company_revenue"] = round(summary["total_ride_cost"] - summary["total_rider_pay"], 2)
    summary["company_margin"] = round((1 - (summary["total_rider_pay"] / summary["total_ride_cost"])) * 100, 2).astype(str) + "%"
    summary["avg_company_revenue_per_ride"] = round(summary["company_revenue"] / summary["num_rides"], 2)
    # Remove columns that aren't needed anymore
    summary.drop(["total_miles"], axis=1, inplace=True)
    return summary
def calculate_monthly_stats(file_dir: str) -> pd.DataFrame:
    # Read data from multiple Parquet files
    files = [os.path.join(file_dir, f) for f in os.listdir(file_dir) if f.endswith(".parquet")]
    
    df = pd.DataFrame()
    for file in files:
        print(file)
        file_stats = calculate_monthly_stats_per_file(file_path=file)
        # Check if df is empty
        if df.empty:
            df = file_stats
        else:
            # Concat row-wise
            df = pd.concat([df, file_stats], axis=0)
    # Sort the dataset
    df = df.sort_values(by="ride_period")
    # Change column order
    cols = ["ride_period", "num_rides", "ride_time_in_days", "total_miles_in_mil", "total_ride_cost",
            "total_rider_pay", "company_revenue", "company_margin", "avg_company_revenue_per_ride"]
    return df[cols]
if __name__ == "__main__":
    data_dir = "nyc-taxi-data"
    output_dir = "pipeline_results"
    output_file_name = "results_pandas.csv"
    # Run the pipeline
    monthly_stats = calculate_monthly_stats(file_dir=data_dir)
    # Save to CSV
    monthly_stats.to_csv(f"{output_dir}/{output_file_name}", index=False)

Es de esperar que la implementación de la tubería en DuckDB termine sin problemas de memoria.

Código: DuckDB

Ya conoces la mayor parte del código de DuckDB.

Lo único nuevo es la parte superior SELECT, ya que calcula un par de estadísticas adicionales. Dejé todo lo demás sin cambios:

import duckdb
import pandas as pd
def calculate_monthly_stats(file_dir: str) -> pd.DataFrame:
    query = f"""
        SELECT
            ride_period,
            num_rides,
            ride_time_in_days,
            total_miles / 1000000 AS total_miles_in_mil,
            total_ride_cost,
            total_rider_pay,
            ROUND(total_ride_cost - total_rider_pay, 2) AS company_revenue,
            ROUND((1 - total_rider_pay / total_ride_cost) * 100, 2) || '%' AS company_margin,
            ROUND((total_ride_cost - total_rider_pay) / num_rides, 2) AS avg_company_revenue_per_ride
        FROM (
            SELECT
                ride_year || '-' || ride_month AS ride_period,
                COUNT(*) AS num_rides,
                ROUND(SUM(trip_time) / 86400, 2) AS ride_time_in_days,
                ROUND(SUM(trip_miles), 2) AS total_miles,
                ROUND(SUM(base_passenger_fare + tolls + bcf + sales_tax + congestion_surcharge + airport_fee + tips), 2) AS total_ride_cost,
                ROUND(SUM(driver_pay), 2) AS total_rider_pay
            FROM (
                SELECT
                    DATE_PART('year', pickup_datetime) AS ride_year,
                    DATE_PART('month', pickup_datetime) AS ride_month,
                    trip_time,
                    trip_miles,
                    base_passenger_fare,
                    tolls,
                    bcf,
                    sales_tax,
                    congestion_surcharge,
                    airport_fee,
                    tips,
                    driver_pay
                FROM PARQUET_SCAN("{file_dir}/*.parquet")
                WHERE 
                    ride_year = 2024
                AND ride_month >= 1 
                AND ride_month <= 6
            )
            GROUP BY ride_period
            ORDER BY ride_period
        )
    """
    conn = duckdb.connect()
    df = conn.sql(query).df()
    conn.close()
    return df
if __name__ == "__main__":
    data_dir = "nyc-taxi-data"
    output_dir = "pipeline_results"
    output_file_name = "results_duckdb.csv"
    # Run the pipeline
    monthly_stats = calculate_monthly_stats(file_dir=data_dir)
    # Save to CSV
    monthly_stats.to_csv(f"{output_dir}/{output_file_name}", index=False)

A continuación, te mostraré las diferencias en el tiempo de ejecución.

Resultados de la comparación de prestaciones

Después de ejecutar ambos pipelines 5 veces y promediar los resultados, estos son los tiempos de ejecución que obtuve:

Tabla comparativa del tiempo de ejecución de DuckDB vs pandas

DuckDB vs. pandas runtime comparison chart

Por término medio, pandas era 24 veces más lento que DuckDB al cargar y procesar unos 120 millones de filas (~3GB), repartidas en 6 archivos Parquet.

La comparación no es del todo justa, ya que no pude procesar todos los archivos Parquet a la vez con pandas. Sin embargo, los resultados siguen siendo relevantes, ya que te encontrarás con las mismas dificultades en tu trabajo diario. 

Si una biblioteca no puede hacer lo que necesitas, prueba con otra. Y el que probablemente termine más rápido la mayoría de las veces es DuckDB.

Buenas prácticas para utilizar DuckDB en canalizaciones de datos

Antes de dejarte explorar DuckDB más a fondo por tu cuenta, quiero compartir un par de buenas prácticas genéricas y relacionadas con la ingeniería de datos:

  • Intenta utilizar siempre primero las funciones integradas de DuckDB: Puedes introducir tus funciones Python en DuckDB con funciones definidas por el usuario, lo queh lleva la flexibilidad de DuckDB a una dimensión totalmente distinta. Una de las mejores prácticas genéricas en programación es no reinventar la rueda. En otras palabras, no quieras implementar una funcionalidad desde cero si ya existe.
  • Optimiza primero los formatos de archivo: El hecho de que DuckDB ofrezca importantes ventajas de rendimiento nada más sacarlo de la caja no significa que no debas acortar tus optimizaciones. Ya has visto lo lento que es DuckDB al leer archivos CSV (hasta 600 veces) en comparación con Parquet. Este último siempre será más rápido de leer y ocupará menos espacio en disco. Todos salimos ganando. 
  • No utilices DuckDB con fines transaccionales: DuckDB es una base de datos OLAP (Procesamiento Analítico en Línea), lo que significa que está optimizada para las declaraciones SELECT. Debes evitar utilizarlo en flujos de trabajo que dependan de declaraciones frecuentes de INSERT y UPDATE. Si es así, utilizae SQLiteen su lugar.
  • Aprovecha el soporte nativo de DuckDB en Jupyter Notebooks: Los usuarios de Jupyter Lab/Notebookrs pueden ejecutar consultas DuckDB directamente, sinnecesidad de utilizar las funciones específicas de Python. Es una forma estupenda de explorar datos más rápidamente y mantener tus cuadernos ordenados.
  • Recuerda siempre cómo gestiona DuckDB la concurrencia: Puedes configurar DuckDB de forma que un proceso pueda leer y escribir en la base de datos, o de forma que varios procesos puedan leer de ella, pero ninguno pueda escribir. La primera permite a el almacenamiento en caché de datos en RAM para consultas analíticas faster. En teoría, varios procesos pueden escribir en el mismo archivo de base de datos, pero para ello tendrías que implementar la lógica de los bloqueos mutex entre procesos y abrir/cerrar la base de datos manualmente.
  • Puedes utilizar DuckDB en la nube: El proyectoe MotherDuck es un almacén de datos colaborativo que lleva y amplía la potencia de DuckDB a la nube. Puede ser una vía que merezca la pena explorar para los ingenieros de datos.

Conclusión

Para concluir, creo que deberías probar DuckDB si eres un ingeniero de datos encargado de construir y optimizar canalizaciones de datos.

No tienes nada que perder y todo que ganar. La cosa es casi garantiza ser más rápido que cualquier cosa que Python pueda ofrecer. Funciona rapidísimo en tu portátil y te permite recorrer conjuntos de datos que, de otro modo, darían lugar a errores de memoria. Incluso puede conectarse a plataformas de almacenamiento en la nube en caso de que no quieras o no te esté permitido almacenar datos localmente.

Dicho esto, DuckDB no debería ser la única optimización que pongas sobre la mesa. Siempre debes esforzarte por optimizar los datos que entran en tu sistema. Por ejemplo, abandonar CSV en favor de Parquet te ahorrará tiempo de cálculo y almacenamiento. Otra optimización que deberías tener en cuenta es implementar los principios de la arquitectura de datos moderna entus flujos de trabajo y pipelines.

DuckDB es sólo una herramienta. Pero muy potente.

Certifícate en el puesto de Ingeniero de Datos de tus sueños

Nuestros programas de certificación te ayudan a destacar y a demostrar que tus aptitudes están preparadas para el trabajo a posibles empleadores.

Timeline mobile.png

Preguntas frecuentes

¿Es gratis utilizar DuckDB?

Sí, DuckDB es un proyecto de código abierto. Puedes descargarlo, modificarlo e incluso contribuir a través de GitHub.

¿En qué se diferencia DuckDB de SQLite?

DuckDB está optimizado para cargas de trabajo analíticas mediante consultas SQL complejas (piensa en sentencias SELECT ), mientras que SQLite es más adecuado para el procesamiento transaccional (piensa en sentencias INSERT y UPDATE ).

¿Es DuckDB una base de datos NoSQL?

No, DuckDB es un sistema de gestión de bases de datos SQL OLAP (Procesamiento Analítico en Línea) en proceso.

¿Es DuckDB más rápido que los pandas?

En casi todos los escenarios, DuckDB será más rápido que pandas, a menudo por un orden de magnitud (como mínimo). También te permitirá trabajar con conjuntos de datos que provocarían errores de memoria en pandas.

¿Utiliza DuckDB un dialecto SQL especial?

No, te sentirás como en casa si tienes conocimientos básicos de SQL. DuckDB sigue de cerca las convenciones del dialecto PostgreSQL, pero incluso si estás acostumbrado a otro proveedor de bases de datos, prácticamente no hay curva de aprendizaje.

Temas

¡Aprende más sobre ingeniería de datos con estos cursos!

Course

Understanding Data Engineering

2 hr
242.1K
Discover how data engineers lay the groundwork that makes data science possible. No coding involved!
See DetailsRight Arrow
Start Course
Ver másRight Arrow
Relacionado

tutorial

21 herramientas esenciales de Python

Conozca las herramientas esenciales de Python para el desarrollo de software, raspado y desarrollo web, análisis y visualización de datos y aprendizaje automático.

Abid Ali Awan

6 min

tutorial

Guía completa para el aumento de datos

Aprende sobre técnicas, aplicaciones y herramientas de aumento de datos con un tutorial de TensorFlow y Keras.
Abid Ali Awan's photo

Abid Ali Awan

15 min

tutorial

Tutorial sobre cómo ejecutar consultas SQL en Python y R

Aprenda formas fáciles y eficaces de ejecutar consultas SQL en Python y R para el análisis de datos y la gestión de bases de datos.
Abid Ali Awan's photo

Abid Ali Awan

13 min

tutorial

Tutorial de SQLAlchemy con ejemplos

Aprende a acceder y ejecutar consultas SQL en todo tipo de bases de datos relacionales utilizando objetos Python.
Abid Ali Awan's photo

Abid Ali Awan

13 min

tutorial

Pandas Profiling (ydata-profiling) en Python: Guía para principiantes

Aprenda a utilizar la biblioteca ydata-profiling en Python para generar informes detallados de conjuntos de datos con muchas características.
Satyam Tripathi's photo

Satyam Tripathi

9 min

tutorial

Tutorial de multiprocesamiento en Python

Descubra los fundamentos del multiprocesamiento en Python y las ventajas que puede aportar a sus flujos de trabajo.
Kurtis Pykes 's photo

Kurtis Pykes

6 min

See MoreSee More