
The Business Logic: Why This Works
Most small and medium enterprises face a core dilemma: their data is scattered everywhere and can’t be analyzed together.
Orders live in PostgreSQL, logs are in S3 Parquet files, Excel reports sit on shared drives, and real-time API data sits in caches. The traditional approach requires a data engineer to build ETL pipelines, set up a data warehouse, and write scheduling scripts — costing tens of thousands of dollars and taking weeks.
What if you offered a “Data Lake as a Service”? Customers connect all their data sources to a single DuckDB instance, and a single SQL query joins everything together. You charge per query or by subscription.
The moat: DuckDB’s ATTACH + httpfs + native Parquet reading lets you achieve “data stays put, queries move” — zero ETL required. Customers don’t need to migrate data, and your operational costs approach zero.
Technical Architecture: Zero-ETL Unified Query Layer
Customer Data Sources Your Service
┌──────────────┐
│ PostgreSQL │ ──ATTACH postgres──┐
└──────────────┘ │
┌──────────────┐ │
│ S3/OSS │ ──ATTACH s3───────┼── DuckDB Engine
│ Parquet Files │ read_parquet() │ (Serverless)
└──────────────┘ │
┌──────────────┐ │
│ HTTP API │ ──ATTACH httpfs───┘
│ JSON/CSV │ read_json_auto()
└──────────────┘
No data is moved at any layer. DuckDB performs predicate pushdown directly on the data sources, reading only the columns and rows needed.
Complete Code Implementation
Step 1: Building the Unified Query Engine
import duckdb
# Serverless mode: in-memory database, destroyed after use
con = duckdb.connect(":memory:")
# Register PostgreSQL data source (customer's production DB)
con.execute("""
ATTACH 'postgresql://user:***@host:5432/orders' AS orders (TYPE POSTGRES);
""")
# Register S3/OSS Parquet data lake
con.execute("""
ATTACH 's3://my-data-lake/' AS datalake (TYPE duckdb_io_s3, REGION us-east-1);
""")
# Register HTTP data source (external API)
con.execute("""
ATTACH 'http://api.example.com/data/' AS api_data (TYPE httpfs);
""")
Step 2: Cross-Source Join Query — The Core Selling Point
SELECT
o.order_id,
o.customer_id,
o.amount,
p.category,
p.unit_cost,
o.order_date,
ROUND(o.amount - p.unit_cost * o.quantity, 2) AS profit,
weather.temperature,
weather.rainfall
FROM orders.main.orders o
JOIN datalake.products.parquet p
ON o.product_id = p.product_id
LEFT JOIN api_data.weather_data weather
ON DATE(o.order_date) = weather.date
WHERE o.order_date >= '2026-01-01'
ORDER BY profit DESC
LIMIT 100;
How this SQL executes:
- DuckDB parses the SQL and generates a physical execution plan
- The
order_datepredicate is pushed down to PostgreSQL, fetching only 2026 data datalake/products.parquetuses Parquet zone maps for partition pruning- An HTTP request is made to
api_datafor weather data - JOINs and calculations happen in memory
- Results are returned
Zero full data transfers occur during the entire process.
Step 3: Encapsulating Reusable Report Templates
def generate_profit_report(start_date, end_date, region=None):
sql = """
SELECT
p.region,
p.category,
COUNT(*) AS order_count,
SUM(o.amount) AS total_revenue,
SUM(o.amount - p.unit_cost * o.quantity) AS total_profit,
AVG(o.amount - p.unit_cost * o.quantity) AS avg_margin,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY o.amount) AS median_order_value
FROM orders.main.orders o
JOIN datalake.products.parquet p
ON o.product_id = p.product_id
WHERE o.order_date BETWEEN '{start}' AND '{end}'
""".format(start=start_date, end=end_date)
if region:
sql += f" AND p.region = '{region}'"
sql += " GROUP BY ALL ORDER BY total_profit DESC"
result = con.execute(sql).fetchdf()
return result
Step 4: Automated Scheduled Reports
import duckdb
import smtplib
from email.mime.text import MIMEText
from datetime import datetime, timedelta
def daily_report():
con = duckdb.connect(":memory:")
con.execute("ATTACH 'postgresql://user:***@host:5432/orders' AS orders (TYPE POSTGRES)")
con.execute("ATTACH 's3://my-data-lake/' AS datalake (TYPE duckdb_io_s3, REGION us-east-1)")
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
sql = f"""
SELECT
DATE(order_date) AS sale_date,
COUNT(*) AS orders,
SUM(amount) AS revenue,
SUM(amount) - SUM(unit_cost * quantity) AS profit
FROM orders.main.orders o
JOIN datalake.products.parquet p ON o.product_id = p.product_id
WHERE DATE(order_date) = '{yesterday}'
GROUP BY ALL
"""
result = con.execute(sql).fetchdf()
html = f"""
<h2>📊 Yesterday's Business Report | {yesterday}</h2>
<p>Orders: <b>{result['orders'].sum()}</b></p>
<p>Revenue: <b>${result['revenue'].sum():,.2f}</b></p>
<p>Profit: <b>${result['profit'].sum():,.2f}</b></p>
<p>Margin: <b>{result['profit'].sum()/result['revenue'].sum()*100:.1f}%</b></p>
"""
msg = MIMEText(html, 'html')
msg['Subject'] = f'Daily Report {yesterday}'
msg['From'] = 'report@yourdomain.com'
msg['To'] = 'ceo@company.com'
with smtplib.SMTP('smtp.company.com', 587) as server:
server.starttls()
server.login('report@yourdomain.com', 'password')
server.send_message(msg)
Comparison with Traditional Approaches
| Approach | Data Migration | Dev Cycle | Ops Cost | Scalability |
|---|---|---|---|---|
| Traditional ETL + DW | Full transfer needed | 2-4 weeks | High (scheduling, monitoring) | Poor |
| Flink/Kafka Real-time Pipeline | Requires system changes | 1-3 months | Very high | Moderate |
| DuckDB ATTACH Zero-ETL | None needed | 1-2 days | Minimal | Good |
The DuckDB advantage: customers’ existing data infrastructure stays untouched. You’re adding a SQL query layer on top. For customers, the risk is near zero — no sunk cost loss. For you, customer acquisition friction is minimal.
FastAPI example
from fastapi import FastAPI, HTTPException import duckdb
app = FastAPI()
@app.post("/query") def run_query(config: dict, sql: str): con = duckdb.connect(":memory:") for source in config.get(“sources”, []): con.execute(f"ATTACH …") try: result = con.execute(sql).fetchdf() return {“columns”: result.columns.tolist(), “data”: result.values.tolist()} except Exception as e: raise HTTPException(status_code=400, detail=str(e))
### Path 3: Data Products (High Value-Add)
Package query results as industry reports:
- Deep industry profit analysis report priced at ¥299
- 100 sales/month = ¥29,900
- Zero marginal cost — develop once, sell repeatedly
**Key insight**: Target industries with high profit margins and low data transparency (e.g., cross-border e-commerce, local life services).
## Key Considerations
1. **Security**: ATTACH credentials must be encrypted — never hardcode. Use environment variables or a secrets manager.
2. **Performance**: For large datasets, use `EXPLAIN ANALYZE` to verify predicate pushdown is working.
3. **Cost**: DuckDB is CPU-intensive. Choose appropriately sized instances (8+ cores offer the best price-performance ratio).
4. **Concurrency**: DuckDB natively supports multi-threading, but watch concurrent connection limits when ATTACHing remote sources. Implement connection pooling at the API layer.
5. **Timeout Control**: HTTP API data sources may respond slowly. Set reasonable timeout parameters to avoid blocking.
## Why Now Is the Right Time
DuckDB in 2026 has matured to production-grade data lake querying:
- **PostgreSQL extension**: `duckdb_attach_postgres()` connects directly to production databases
- **Native S3/OSS support**: `duckdb_io_s3` type requires no extra configuration
- **httpfs extension**: Query remote JSON/CSV/Parquet directly
- **Parquet predicate pushdown**: Automatically prunes unnecessary columns and data blocks
- **GROUP BY ALL**: Simplifies aggregation queries, reducing code by 80%
The combination of these capabilities turns "Data Lake as a Service" from a concept into **a product you can launch today**.
## 本文信息
| 项目 | 内容 |
|------|------|
| DuckDB 版本 | v1.5.x(部分功能基于 v2.0 Preview) |
| 最后验证 | 2026-09-12 |
| 测试环境 | Linux / x86_64 / 16GB RAM |
| 官方文档 | [DuckDB Documentation](https://duckdb.org/docs/) |
| GitHub | [pengzz9527/duckdb-blog](https://github.com/pengzz9527/duckdb-blog) |
如发现错误,欢迎通过 [GitHub Issue](https://github.com/pengzz9527/duckdb-blog/issues) 或邮件 contact@duckdblab.org 反馈。