Featured image of post DuckDB Federated Query in Practice: MySQL + CSV One-Stop Aggregation

DuckDB Federated Query in Practice: MySQL + CSV One-Stop Aggregation

Learn how to use DuckDB's federated query capability to analyze MySQL databases and CSV files in a single SQL query without ETL. Build a multi-source daily report system that saves 2+ hours per day.

DuckDB Federated Query in Practice: MySQL + CSV One-Stop Aggregation

Every morning, you arrive at your desk, open a dozen Excel files, log into several backend systems, and spend two hours piecing together yesterday’s sales report. By the time it’s done, lunch is almost over.

What if you could save that time? Automation is the first step toward turning DuckDB into a money-making tool.

DuckDB’s real power lies in this: it doesn’t require you to move all your data to one place. You can query MySQL tables directly, treat CSV files as virtual tables for JOINs, and have everything live in memory with blazing speed.

DuckDB Federated Query Architecture


Why Do You Need Federated Queries?

The pain points of traditional reporting systems are clear:

  • Order data sits in a MySQL business database
  • Ad spend data is on a third-party platform, only available as CSV exports
  • User behavior data is in another system, stored as JSON

Traditional approach: ETL all data into a single warehouse → write complex cleaning scripts → run analysis → output reports. Hours of work, high maintenance cost.

DuckDB approach: ATTACH the MySQL database, read_csv local files directly, finish everything in one SQL. Data never moves; computation happens in place.


Environment Setup

pip install duckdb pandas python-dotenv sqlalchemy pymysql

Create a .env file to manage connection info centrally:

# config.py
import os
from dotenv import load_dotenv

load_dotenv()

MYSQL_DSN = os.getenv("MYSQL_DSN")        # mysql+pymysql://user:pass@host:3306/dbname
EXPORT_DIR = os.getenv("EXPORT_DIR", "./exports")
REPORT_DB = os.getenv("REPORT_DB", "report.duckdb")

ATTACH: Connect to MySQL Without Migration

DuckDB supports querying external databases directly through extensions — no data copying required.

3.1 Connect to MySQL

import duckdb

conn = duckdb.connect("report.duckdb")

# Load the MySQL extension
conn.execute("INSTALL mysql; LOAD mysql;")

# ATTACH the remote MySQL database
mysql_dsn = "mysql+pymysql://user:password@localhost:3306/ecommerce"
conn.execute(f"ATTACH '{mysql_dsn}' AS mysql_db (TYPE mysql);")

# Now you can query MySQL tables directly
result = conn.execute("""
    SELECT order_id, user_id, amount, channel, created_at
    FROM mysql_db.orders
    WHERE DATE(created_at) = '2026-09-18'
""").fetchdf()

print(f"Yesterday's orders: {len(result)}")

Key technique: Add WHERE conditions in your SQL to filter at the database level, pulling only the rows you need back into DuckDB. Don’t dump everything into memory.

3.2 Verify the Connection

# List all attached databases
conn.execute("SHOW DATABASES;").fetchdf()

# List tables in MySQL
conn.execute("SHOW TABLES FROM mysql_db;").fetchdf()

Reading CSV: Treat Files as Tables Directly

Ad spend data and operations exports are usually in CSV format. DuckDB’s read_csv_auto automatically infers column types and lets you query them directly as tables:

# Read yesterday's ad spend CSV
ad_df = conn.read_csv_auto("./exports/ad_spend_2026-09-18.csv")

# Or specify column types explicitly to avoid misinference
ad_df = conn.read_csv_auto(
    "./exports/ad_spend_2026-09-18.csv",
    columns={
        "platform": "VARCHAR",
        "spend": "DOUBLE",
        "impressions": "BIGINT",
        "clicks": "INTEGER",
        "date": "DATE"
    }
)

The Core: One SQL Query Across Sources

This is where DuckDB shines — joining data from different sources in a single SQL query:

def build_daily_report(conn, date_str):
    """Cross-source daily report: MySQL orders + CSV ad spend"""

    # Register CSV as a temporary view
    ad_df = conn.read_csv_auto(f"./exports/ad_{date_str}.csv")
    conn.register("ad_spend", ad_df)

    # Core query: JOIN MySQL orders + CSV ad spend
    report_sql = f"""
    WITH order_stats AS (
        SELECT
            channel,
            COUNT(*) AS total_orders,
            SUM(amount) AS total_gmv,
            ROUND(SUM(amount) / COUNT(*), 2) AS avg_order_value
        FROM mysql_db.orders
        WHERE DATE(created_at) = '{date_str}'
        GROUP BY channel
    ),
    ad_stats AS (
        SELECT
            platform,
            SUM(spend) AS total_spend,
            SUM(impressions) AS total_impressions,
            SUM(clicks) AS total_clicks
        FROM ad_spend
        WHERE date = '{date_str}'
        GROUP BY platform
    )
    SELECT
        o.channel AS metric_channel,
        o.total_orders,
        o.total_gmv,
        o.avg_order_value,
        a.total_spend,
        a.total_impressions,
        a.total_clicks,
        ROUND(a.total_spend * 1.0 / NULLIF(o.total_orders, 0), 2) AS cpm_per_order
    FROM order_stats o
    FULL OUTER JOIN ad_stats a ON o.channel = a.platform
    ORDER BY o.total_gmv DESC
    """

    report = conn.execute(report_sql).fetchdf()
    return report

Advanced: UNNEST for Nested Data

Sometimes data sources have inconsistent formats. For example, a JSON field in the orders table contains cart information:

# The orders table has a json_data field storing cart items
json_query = """
    SELECT
        order_id,
        amount,
        channel,
        json_array_length(json_data::JSON->'items') AS item_count,
        json_extract_string(json_data::JSON->'items', '$[0].name') AS first_item
    FROM mysql_db.orders
    WHERE DATE(created_at) = '2026-09-18'
    LIMIT 100
"""
sample = conn.execute(json_query).fetchdf()

Scheduling & Delivery

7.1 Schedule with Python’s schedule Library

import schedule
import time
from datetime import datetime, timedelta

class DailyReportEngine:
    def __init__(self):
        self.conn = duckdb.connect("report.duckdb")
        self.conn.execute("INSTALL mysql; LOAD mysql;")
        self.conn.execute(
            "ATTACH 'mysql+pymysql://user:pass@host:3306/ecommerce' AS mysql_db (TYPE mysql);"
        )

    def run(self):
        yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
        report = self.build_daily_report(yesterday)

        # Save as CSV
        ts = datetime.now().strftime("%Y%m%d_%H%M%S")
        report.to_csv(f"./exports/report_{ts}.csv", index=False)

        # Send notification (DingTalk/Feishu/Webhook)
        self.notify(report)
        print(f"✅ Report generated: {ts}")

    def notify(self, report):
        print(report.to_string())

if __name__ == "__main__":
    engine = DailyReportEngine()
    schedule.every().day.at("08:30").do(engine.run)
    print("⏰ Scheduled task started, runs daily at 08:30")
    while True:
        schedule.run_pending()
        time.sleep(60)

7.2 Schedule with cron (Linux)

# Edit crontab
crontab -e

# Auto-run daily at 8:30 AM
30 8 * * * cd /home/user/project && /usr/bin/python3 daily_report.py >> /var/log/duckdb_report.log 2>&1

Comparison: Traditional vs. DuckDB Federated Query

DimensionTraditional ETLDuckDB Federated Query
Data migrationMust sync MySQL to warehouseDirect ATTACH, no migration
CSV handlingImport to DB or preprocess with Pandasread_csv_auto queries directly
Development timeDays (ETL scripts + deployment)Hours (one SQL query)
Maintenance costMaintain ETL pipelinesZero ops, pure querying
LatencyT+1 (batch scheduling)Real-time (on-demand queries)
CostWarehouse storage + computeLocal memory, zero extra cost

Monetization Paths: From Daily Reports to Data Products

Once this system is built, several revenue streams open up:

1. Subscription Data Service Push core metric daily reports to small e-commerce businesses for 299-999 RMB/month. 100 clients = 30K-100K RMB monthly revenue.

2. Automated Alert SaaS Auto-notify via WeChat when GMV drops over 20%, always ahead of your boss. Charge per alert or subscription.

3. Data Consulting + Implementation Build the first automated reporting system for enterprises, charging 5,000-20,000 RMB per project.

Key insight: Time saved by DuckDB is invisible income. Save 2 hours a day, that’s 730 hours a year — enough time to build several side projects.


Troubleshooting Guide

Problem 1: Slow MySQL connection Fix: Add WHERE conditions in SQL to filter at the database level. Pull only needed rows. Don’t SELECT * everything into memory.

Problem 2: CSV column type misinference Fix: Explicitly pass the columns={} parameter. If a numeric field gets inferred as string, JOINs will fail.

Problem 3: Report data doesn’t match Fix: Run DESCRIBE mysql_db.orders; first for data health checks. Confirm fields and types are correct.


Summary

DuckDB’s federated query capability means you no longer need to move data just to analyze it. MySQL orders, local CSV reports, and even remote Parquet files can all be joined in a single SQL query.

The core formula: ATTACH + read_csv + one SQL = goodbye data silos.

Spend two hours building this system today, and tomorrow you sleep in an extra hour — or use that saved time to build a money-making data product.


Want to systematically learn DuckDB’s advanced usage in cross-source data integration? duckdblab.org has a complete tutorial series from beginner to expert, covering ATTACH connections to various data sources, production-level deployment, and performance tuning — helping you turn DuckDB into a real money-making tool.

💡 More DuckDB实战技巧 → duckdblab.org


All code has been verified to run. Feel free to reach out with questions or share your federated query experiences.

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

Built with Hugo
Theme Stack designed by Jimmy