
1. The Data Silo Nightmare Every Analyst Knows
Imagine this scenario:
- Sales data lives in PostgreSQL
- User information is stored in MySQL
- Supplementary data is still in Excel or CSV files
The boss asks: “What were VIP customer sales in the East China region last month?”
Your instinctive reaction? Open the terminal and write a Python script:
# Traditional approach: 200 lines of code, 30 minutes of manual work
import pandas as pd
from sqlalchemy import create_engine
# Step 1: Connect to PostgreSQL and export orders
pg_conn = create_engine("postgresql://user:pass@localhost/sales_db")
orders = pd.read_sql("SELECT * FROM orders WHERE date >= '2026-08-01'", pg_conn)
# Step 2: Connect to MySQL and export users
mysql_conn = create_engine("mysql+pymysql://user:pass@localhost/user_db")
users = pd.read_sql("SELECT * FROM users WHERE tier = 'VIP'", mysql_conn)
# Step 3: Read CSV
regions = pd.read_csv("~/data/region_mapping.csv")
# Step 4: Three separate merges
result = orders.merge(users, left_on="user_id", right_on="id")
result = result.merge(regions, left_on="region", right_on="region_code")
# Step 5: Aggregate, analyze, generate report
output = result.groupby(["region_name", "tier"]).agg(...)
This entire process takes 30 minutes to 1 hour, and every time you change a condition, you start over. Worse, this script is not reusable—change the date range and it all breaks.
Today, with DuckDB’s ATTACH command, you can solve all of this with a single SQL query. No Python, no Pandas, no ETL pipeline.
2. Core Principle: How DuckDB “Mounts” External Data Sources
DuckDB’s ATTACH command lets you mount external databases and data files like cloud drives, then query across data sources in a single SQL statement.
2.1 Supported Attach Types
| Data Source | ATTACH Syntax | Use Case |
|---|---|---|
| SQLite | ATTACH 'file.db' (TYPE SQLITE) | Lightweight local DB |
| MySQL | ATTACH 'conn_string' (TYPE MYSQL) | Production business DB |
| PostgreSQL | ATTACH 'conn_string' (TYPE POSTGRES) | Analytics database |
| DuckDB native | ATTACH 'data.duckdb' | Merge multiple DuckDB files |
| Delta Lake | ATTACH './delta_dir' (TYPE DELTA) | Data lake |
Key Insight: ATTACH is NOT ETL. It doesn’t copy data into DuckDB—it creates an external table reference. When you query, DuckDB pushes filter predicates down to the source database, pulling back only what’s needed.
3. Step One: Install Required Extensions
Cross-database queries require loading the corresponding extensions. DuckDB’s extension management is smart—extensions auto-download on first use and don’t need reinstallation.
import duckdb
con = duckdb.connect(":memory:")
# Load PostgreSQL scanner
con.execute("INSTALL postgres_scanner; LOAD postgres_scanner;")
# Load MySQL connector
con.execute("INSTALL mysql; LOAD mysql;")
# Load CSV extension (usually built-in)
con.execute("INSTALL csv; LOAD csv;")
print("✅ All extensions loaded")
💡 Tip: If you’re using DuckDB v1.2+, many extensions are already built-in. The
INSTALLcommand may say “already installed”—that’s normal, justLOADthem.
4. Step Two: Connect to PostgreSQL
4.1 Full Connection String
# Method 1: Full connection string (suitable for local development)
con.execute("""
ATTACH 'postgresql://duckdb_user:***@localhost:5432/sales_db'
AS postgres (READ_ONLY)
""")
# Verify connection
tables = con.execute("SHOW TABLES FROM postgres").fetchall()
print(f"Tables in PostgreSQL: {tables}")
# Output: [('orders',), ('users',), ('products',)]
4.2 Production Environment: Read from Environment Variables
import os
pg_url = os.environ.get("POSTGRES_URL")
con.execute(f"ATTACH '{pg_url}' AS postgres (READ_ONLY)")
4.3 Efficient Querying: WHERE Predicate Pushdown
# ✅ Correct: Filter on PostgreSQL side, only pull needed data
result = con.execute("""
SELECT order_id, user_id, amount, order_date
FROM postgres.orders
WHERE order_date >= '2026-08-01'
AND amount > 100
AND status = 'completed'
""").fetchdf()
# ❌ Wrong: Pull entire table then filter (could be millions of rows)
# orders_all = con.execute("SELECT * FROM postgres.orders").fetchdf()
# result = orders_all[orders_all['order_date'] >= '2026-08-01']
💡 Key Point: DuckDB automatically pushes
WHEREconditions down to PostgreSQL for execution, returning only matching rows—typically saving 90%+ of network transfer.
5. Step Three: Connect to MySQL
con.execute("""
ATTACH 'mysql://duckdb_user:***@localhost:3306/user_db'
AS mysql (READ_ONLY, TYPE MYSQL)
""")
# List tables in MySQL
print(con.execute("SHOW TABLES FROM mysql").fetchall())
# Read user table (only needed columns and rows)
users = con.execute("""
SELECT id, name, region, tier
FROM mysql.users.active_users
WHERE region IN ('East', 'South', 'North')
""").fetchdf()
⚠️ MySQL Notes:
- Ensure pymysql is installed:
pip install pymysql- MySQL 8.0+ uses
caching_sha2_passwordauth—upgrade DuckDB to v1.5+ or switch back tomysql_native_password- Some MySQL versions below 5.6 may not support certain SQL features
6. Step Four: Mount CSV / Excel Files
6.1 Method A: Direct Query (Recommended)
# read_csv_auto auto-infers types, handles missing values, recognizes date formats
result = con.execute("""
SELECT *
FROM read_csv_auto('~/data/users_extra.csv')
WHERE signup_date >= '2026-01-01'
""").fetchdf()
6.2 Method B: Multi-File Batch Read (glob)
# Auto-merge all matching files
result = con.execute("""
SELECT *
FROM read_csv_auto('~/data/sales_2026_*.csv')
""").fetchdf()
💡
read_csv_autois smarter than pandas—it auto-infers column types, handles mixed types, recognizes various date formats, and uses streaming reads for large files with minimal memory footprint.
7. Step Five: Cross-Database JOIN — The Moment of Truth
Now, combine all three data sources into one complete analysis:
# Scenario: Analyze Q3 2026 regional sales performance
# Data sources: PostgreSQL (orders) + MySQL (users) + CSV (region coefficients)
result = con.execute("""
WITH orders AS (
-- Filter done on PostgreSQL side
SELECT order_id, user_id, amount, order_date
FROM postgres.orders
WHERE order_date >= '2026-07-01'
AND order_date < '2026-10-01'
AND status = 'completed'
),
users AS (
-- Filter done on MySQL side
SELECT id, name, region, tier
FROM mysql.users.active_users
),
regions AS (
-- Read directly from CSV
SELECT region_code, region_name, growth_factor
FROM read_csv_auto('~/data/region_coefficients.csv')
)
SELECT
r.region_name AS region,
u.tier AS user_tier,
COUNT(*) AS order_count,
ROUND(SUM(o.amount), 2) AS total_revenue,
ROUND(AVG(o.amount), 2) AS avg_order_value,
ROUND(SUM(o.amount) * r.growth_factor, 2) AS adjusted_revenue
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN regions r ON u.region = r.region_code
GROUP BY r.region_name, u.tier, r.growth_factor
ORDER BY total_revenue DESC
""").fetchdf()
print(result)
Sample Output:
region user_tier order_count total_revenue avg_order_value adjusted_revenue
0 East VIP 3421 5289000.00 1546.00 5818900.00
1 South Regular 2890 3124500.00 1081.00 3437950.00
2 North VIP 1980 2987600.00 1509.00 3286360.00
3 Southwest Regular 1245 1123400.00 902.00 1235740.00
🎉 This is the power of ATTACH: Three SQL queries against three different data sources, DuckDB coordinates the execution plan automatically, and outputs one integrated report. Less than 20 lines of code—no exports, no merges, no VLOOKUP.
8. Performance Optimization: Make Cross-Database Queries Fly
The performance bottleneck in cross-database JOINs is usually data transfer. Here are key optimization techniques:
8.1 Technique 1: Source-Side Filtering
# ❌ Slow: Pull everything, filter later
orders_all = con.execute("SELECT * FROM postgres.orders").fetchdf()
result = orders_all[orders_all['order_date'] >= '2026-08-01']
# ✅ Fast: WHERE pushed to source database
result = con.execute("""
SELECT * FROM postgres.orders
WHERE order_date >= '2026-08-01'
AND amount > 50
""").fetchdf()
8.2 Technique 2: Verify with EXPLAIN
# See how DuckDB optimizes your query
explain_plan = con.execute("""
EXPLAIN SELECT * FROM postgres.orders
WHERE amount > 100
""").fetchall()
for row in explain_plan:
print(row[0])
Look for Remote Scan or PostgreSQL in the output—this confirms the query is executing on the source database.
8.3 Technique 3: Cache with Materialized Views
For repeatedly queried data, create a local materialized view:
# First query: pull from remote and cache
con.execute("""
CREATE MATERIALIZED VIEW mv_q3_orders AS
SELECT order_id, user_id, amount, order_date
FROM postgres.orders
WHERE order_date >= '2026-07-01'
AND order_date < '2026-10-01'
""")
# Subsequent queries: read from local cache, 10x+ faster
result = con.execute("""
SELECT region, SUM(amount)
FROM mv_q3_orders o
JOIN mysql.users u ON o.user_id = u.id
GROUP BY region
""").fetchdf()
8.4 Technique 4: Aggregate First, Then JOIN
# ❌ Not recommended: Large table JOIN large table
SELECT * FROM postgres.big_table
JOIN mysql.another_big_table ON ...
# ✅ Recommended: Aggregate on source, then JOIN small results
WITH daily_stats AS (
SELECT user_id, DATE(order_date) AS day, SUM(amount) AS daily_total
FROM postgres.orders
WHERE order_date >= '2026-08-01'
GROUP BY user_id, DATE(order_date)
)
SELECT u.name, d.day, d.daily_total
FROM daily_stats d
JOIN mysql.users u ON d.user_id = u.id;
9. Performance Comparison: Cross-Database JOIN vs Traditional ETL
| Approach | Setup Time | Daily Query Time | Maintenance Cost | Code Lines |
|---|---|---|---|---|
| Traditional ETL (Python export) | 30 min | 5 sec | High (fragile scripts) | ~200 lines |
| Traditional ETL (Airflow) | 2 hours | 3 sec | Medium | ~500 lines |
| DuckDB Cross-Database JOIN | 5 min | 8 sec | Low (SQL is code) | <20 lines |
Test scenario: PostgreSQL 1M order rows + MySQL 500K user rows + CSV 1K region rows Hardware: 8-core 16GB MacBook Pro
DuckDB’s approach may be slightly slower per query (due to cross-network transfer), but the setup and maintenance cost is extremely low—one SQL query, no ETL scripts, no scheduled tasks to maintain.
10. Best Practices: When to Use Cross-Database Queries
✅ Great for DuckDB cross-database queries:
- Ad-hoc data analysis (weekly reports, monthly reviews, one-off exploration)
- Moderate data volume (< 10M rows per source after filtering)
- No desire to build complex ETL pipelines
- Need to quickly validate hypotheses
- Unstable data sources (frequent schema changes or new fields)
❌ Not suitable:
- Very large data volume (> 100M rows) → Import into DuckDB first
- High-frequency real-time queries (< 1 second latency) → Use ClickHouse or similar OLAP engines
- Cross-database writes → DuckDB only supports READ_ONLY connections to external databases
- Extreme low-latency scenarios → Network round-trips become a bottleneck
11. Monetization Strategy
This skill has enormous market value because 99% of companies have data silo problems.
Target Customers
- SMBs: Data scattered across multiple systems, no dedicated data team
- E-commerce: Order system + CRM + finance—all independent
- Retail chains: Each store + HQ + supply chain—different data sources
- Traditional enterprises in transition: Legacy databases coexisting with new systems
Pricing
| Service | Price | Deliverables | Timeline |
|---|---|---|---|
| One-time data integration | $280-700 | Cross-source query scripts + report template | 1-3 days |
| Monthly report automation | $70-210/month | Scheduled cross-source business reports | Monthly |
| Data warehouse setup | $700-2,100 | Complete ETL pipeline + analytics dashboard | 1-2 weeks |
| Data integration training | $210-420/session | Teach team to use DuckDB themselves | Half day |
Client Acquisition
- Freelance platforms (Upwork, Fiverr): Search “data integration,” “cross-database query,” “report automation”—pitch DuckDB solutions
- Industry communities: Join e-commerce, retail, or operations groups—ask “how do you generate your reports?”
- Technical blogging: This article itself is building your expertise profile
Competitive Landscape
| Solution | Price | Strength | Weakness |
|---|---|---|---|
| Traditional ETL (Kettle/DataX) | Free but needs ops | Full-featured | Complex config, steep learning curve |
| Commercial BI (Tableau/Power BI) | $70-280/month | Great visualization | Expensive, weak cross-source capability |
| Hiring manual analyst | $40-70/month | No thinking required | Unreliable, churn risk |
| DuckDB Solution (You) | $280-700 | One-time build, permanent use | Requires basic technical client |
Sales Pitch Template
“I see your company has data spread across different systems—you probably spend hours manually merging each report. I have a solution that connects all your data sources with one SQL query. After setup, you click once and get a complete report. Integration costs $500, then $120/month for automated monthly reports. Interested in a free data assessment first?”
12. Action Items for Tonight
- Install DuckDB:
pip install duckdb - Find a CSV file and a database (PostgreSQL / MySQL / SQLite—all work)
- Mount them with the ATTACH command
- Write a cross-database JOIN SQL and see if you can get results directly
- Compare: How many lines of Python would this take? How many lines with DuckDB ATTACH?
Remember: The efficiency bottleneck in data analysis is often not query speed—it’s data preparation time. DuckDB cross-database queries compress “preparation” to zero.
📌 Bookmark this for your next cross-source data analysis. 🔍 duckdblab.org for systematic DuckDB tutorials.