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.

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
| Dimension | Traditional ETL | DuckDB Federated Query |
|---|---|---|
| Data migration | Must sync MySQL to warehouse | Direct ATTACH, no migration |
| CSV handling | Import to DB or preprocess with Pandas | read_csv_auto queries directly |
| Development time | Days (ETL scripts + deployment) | Hours (one SQL query) |
| Maintenance cost | Maintain ETL pipelines | Zero ops, pure querying |
| Latency | T+1 (batch scheduling) | Real-time (on-demand queries) |
| Cost | Warehouse storage + compute | Local 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.