SQLFrame: Migrate PySpark to DuckDB Without Code Changes
Introduction
In the data engineering world, PySpark has been the de facto standard for large-scale data processing. However, when you only need to handle medium-sized datasets (a few GB to tens of GB), or want to quickly prototype and validate analysis locally, spinning up a Spark cluster feels like overkill.
SQLFrame was built precisely to solve this problem — it lets you seamlessly migrate PySpark applications to DuckDB without modifying any code. This means all your existing PySpark DataFrame operation code can be switched to run on DuckDB, delivering faster execution speeds and lower resource consumption.
This approach was featured prominently at DuckCon #7 in June 2026 by Nicolas Renkamp from Merck KGaA, generating significant interest among attendees.

What is SQLFrame?
SQLFrame is a Python library that implements a compatibility layer for the PySpark DataFrame API. Its core principle is straightforward:
- API Compatibility: Provides a DataFrame API identical to PySpark’s
- Backend Swap: Uses DuckDB as the execution engine underneath
- Zero Modifications: Only import statements and connection configs change — business logic remains untouched
# Original PySpark code
from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
df = spark.read.parquet("data/")
# Migrated SQLFrame code
from sqlframe.duckdb import DuckDBSession
session = DuckDBSession()
df = session.read.parquet("data/")
As you can see, the only change is replacing SparkSession with DuckDBSession. All other code stays exactly the same.
Installation and Setup
Environment Preparation
pip install sqlframe-duckdb duckdb
Or add to your project dependencies:
[project]
dependencies = [
"sqlframe-duckdb>=1.0.0",
"duckdb>=1.0.0",
]
Basic Usage
from sqlframe.duckdb import DuckDBSession
import duckdb
# Create a DuckDB Session
session = DuckDBSession()
# Read data
df = session.read.csv(
"sales_data.csv",
header=True,
inferSchema=True
)
# Display results
df.show(5)
Hands-On Example: E-Commerce Sales Analysis
Let’s walk through a complete e-commerce sales analysis scenario using SQLFrame, from data loading to advanced analytics.
Step 1: Load Data
from sqlframe.duckdb import DuckDBSession
from sqlframe.duckdb import connection
session = DuckDBSession()
# Connect to DuckDB (supports both in-memory and file-based databases)
conn = connection.DuckDBConnection()
session._conn = conn
# Simulate loading multiple CSV files
orders = session.read.csv(
"data/orders/*.csv",
header=True,
inferSchema=True
)
customers = session.read.csv(
"data/customers.csv",
header=True,
inferSchema=True
)
products = session.read.csv(
"data/products.csv",
header=True,
inferSchema=True
)
Step 2: Data Cleaning
# Remove null values
orders_clean = orders.dropna()
# Convert date formats
from pyspark.sql.functions import col, to_date, year, month
orders_clean = orders_clean.withColumn(
"order_date",
to_date(col("order_date"), "yyyy-MM-dd")
)
# Add computed columns
orders_clean = orders_clean.withColumn(
"year",
year(col("order_date"))
).withColumn(
"month",
month(col("order_date"))
)
Step 3: Multi-Table Join Analysis
# Join orders, customers, and products
joined_df = (
orders_clean
.join(customers, "customer_id", "inner")
.join(products, "product_id", "inner")
)
# Monthly sales aggregation
monthly_sales = (
joined_df
.groupBy("year", "month")
.agg(
{"quantity": "sum", "amount": "sum"}
)
.orderBy("year", "month")
)
monthly_sales.show()
Sample output:
+----+-----+--------+-------+
|year|month|sum_qty |sum_amt|
+----+-----+--------+-------+
|2024| 1 | 15234| 892341|
|2024| 2 | 14892| 876523|
|2024| 3 | 16789| 945672|
|2024| 4 | 15678| 923456|
+----+-----+--------+-------+
Step 4: Advanced Analytics
from pyspark.sql.window import Window
from pyspark.sql.functions import rank, sum as spark_sum
# Calculate cumulative spending rank per customer
window_spec = Window.partitionBy("year").orderBy(
spark_sum("amount").over(
Window.partitionBy("customer_id")
).desc()
)
customer_ranking = joined_df.withColumn(
"rank",
rank().over(window_spec)
).filter(col("rank") <= 10)
customer_ranking.select(
"customer_id", "name", "year", "total_amount"
).show()
Comparison with Traditional Approaches
| Feature | PySpark Standalone | SQLFrame + DuckDB | Spark on YARN/K8s |
|---|---|---|---|
| Installation Complexity | High (cluster config needed) | Low (pip install) | Very High |
| Startup Time | Minutes | Milliseconds | Minutes |
| Memory Footprint | GB~TB scale | MB~GB scale | GB~TB scale |
| Suitable Data Scale | >100GB | <50GB | >100GB |
| Local Development Experience | Poor | Excellent | Poor |
| Runs Locally | No | Yes | No |
| Distributed Scaling | Native | Limited | Native |
Performance Benchmark
In a typical e-commerce analysis scenario (~5 million order rows, 3-table join aggregation):
import time
from sqlframe.duckdb import DuckDBSession
session = DuckDBSession()
# Read data
start = time.time()
orders = session.read.csv("data/orders/*.csv", header=True)
print(f"Read time: {time.time() - start:.2f}s")
# Complex query
start = time.time()
result = (
orders
.filter(col("amount") > 100)
.groupBy("category")
.agg({"amount": "avg", "quantity": "sum"})
.orderBy(col("avg_amount").desc())
)
result.show()
print(f"Query time: {time.time() - start:.2f}s")
Typical output:
Read time: 0.85s
Query time: 0.32s
Compared to equivalent Spark Standalone mode (startup + execution ~45 seconds), SQLFrame + DuckDB is over 100x faster in development iteration scenarios.
Caveats and Limitations
1. Data Scale Limits
DuckDB is a single-process analytical engine suitable for local machines. Performance degrades significantly when data exceeds available memory. Recommendations:
- < 10GB: Runs perfectly, no concerns
- 10-50GB: Ensure sufficient memory (64GB+ recommended)
- > 50GB: Consider Spark or other distributed solutions
2. API Compatibility
While SQLFrame covers the most commonly used PySpark APIs, note the following:
- Custom UDFs may need adaptation
- Some advanced Spark functions (e.g., MLlib) are unavailable
- Streaming functionality is not supported
3. Transaction Support
DuckDB supports ACID transactions, but when writing to CSV/Parquet, pay attention to atomicity issues. Use DuckDB’s native file format or transaction-supporting storage formats for critical writes.
Monetization Advice
For Enterprise Data Teams
If you’re considering reducing Spark infrastructure costs, SQLFrame + DuckDB can help you:
- Reduce Cloud Spend: Migrate 80% of small-to-medium ETL tasks from Spark to local DuckDB, saving thousands of dollars annually on cloud resources
- Accelerate Development Cycles: Data engineers can prototype and test locally without waiting for cluster scheduling
- Lower Operational Burden: No need to maintain Spark clusters, reducing DevOps overhead
For Individual Developers and Data Scientists
- Build Data Products: Leverage DuckDB’s embedded nature to embed analytical capabilities into web apps, CLI tools, or desktop applications
- Rapid Prototyping: When pitching new projects or validating ideas, skip complex Spark environments and start analyzing within minutes
- Educational Use: As a teaching tool, students can learn big data processing concepts on their own laptops
For Consulting Firms
- Faster Delivery: When advising clients on data migration and modernization, SQLFrame enables PoCs in days rather than weeks
- Hybrid Architecture Design: Help clients design “Spark + DuckDB” hybrid architectures — Spark for massive-scale data, DuckDB for daily analytics and reporting
Conclusion
SQLFrame offers an elegant fallback path for PySpark users — when you don’t need distributed processing power, DuckDB delivers faster development cycles and superior local performance. For most medium-scale data analysis scenarios, it’s a compelling alternative worth considering.
As demonstrated at DuckCon #7, many enterprises are already using this pattern in production, achieving multi-fold improvements in development efficiency.
Original source: https://duckdb.org/2026/06/24/duckcon7/sessions/sqlframe-migration