This tutorial shows you how to run the same expression on different execution engines. You’ll learn when to choose each backend, see how Xorq moves data between them using Apache Arrow, and compare backend performance to find the best fit for your workload.
After completing this tutorial, you’ll know how to pick the right backend and understand the performance trade-offs.
Important
This tutorial requires DuckDB support. Install with pip install "xorq[duckdb]" or pip install "xorq[examples]" for all tutorial dependencies.
How to follow along
Each code example includes complete setup, so you can run any section independently. For best learning, run them in sequence.
Run the code using:
Python interactive shell: Open a terminal, run python, then copy and paste each code block
Jupyter notebook: Run each code block in a separate cell
Python script: Create switch_backends.py and replace the file content with each code block for testing
Each section demonstrates a different concept. You can run sections independently, or run them all in sequence to see the complete workflow.
Why switch backends?
Each backend has specific capabilities. DuckDB supports temporal joins like AsOf joins and handles analytical queries on larger datasets. Pandas works well for small datasets and interactive prototyping. The embedded backend (DataFusion) supports custom UDFs and works without external dependencies.
Xorq lets you write your expression once and run it anywhere. Same code, different engines.
Tip
Xorq uses Apache Arrow to move data between backends without serialization overhead. This makes backend switching fast and memory-efficient.
To see this in practice, you’ll run the same expression on the iris dataset across three backends: embedded, DuckDB, and Pandas.
Run on the embedded backend
You’ll start with Xorq’s default embedded backend. This uses DataFusion, an in-memory query engine optimized for Arrow operations.
Backend: <xorq.backends.duckdb.Backend object at 0x000002A89D527CA0>
species avg_width
0 Versicolor 2.890000
1 Virginica 3.036585
Notice how the expression code is identical. Only the backend connection changed. The results are the same across backends.
Note
This DuckDB connection is in-memory. To use a persistent database file, pass database="my_db.duckdb" to connect().
Switch to Pandas
Now you’ll run the same expression on Pandas. Pandas is great for small datasets and interactive analysis, making it perfect for prototyping and working with data that fits in memory.
Backend: <xorq.backends.pandas.Backend object at 0x000002A89D527CA0>
species avg_width
0 Versicolor 2.890000
1 Virginica 3.036585
So far, you’ve loaded data separately into each backend. In practice, you might start analysis in one backend and need to switch mid-workflow. For example, you might load data in the embedded backend, then move it to DuckDB for an AsOf join that the embedded backend doesn’t support. That’s where data transfer comes in.
Move data between backends
Sometimes you need to move data from one backend to another. Xorq makes this easy with .into_backend(). This section shows a new example using the iris dataset to demonstrate data transfer.
First, see what you can do on the embedded backend:
What happened? The trade at time 12 gets the price from time 10 (most recent before 12). The trade at time 28 gets the price from time 20 (most recent before 28). This is an “as-of” temporal join, a DuckDB feature not available in the embedded backend.
.into_backend() transfers data between backends using Apache Arrow, which minimizes serialization overhead.
Tip
Move data to a different backend when you need specific features (like DuckDB’s AsOf joins for temporal data) or better performance for your query type.
Compare backend performance
You’ll time the same query on different backends to see performance characteristics. This example uses a slightly different filter condition (> 5 instead of > 6) to include more rows for a more meaningful performance comparison.
For small datasets like iris, performance differences are minimal. Performance characteristics vary with dataset size and query complexity.
What you learned
You’ve seen how to run the same expression on different backends and move data between them. In the examples above, you:
Ran identical expressions on embedded, DuckDB, and Pandas backends
Moved data from memtables to DuckDB using .into_backend() for AsOf joins
Compared performance across backends
The key takeaway: switch backends when you need features that only specific backends provide (like DuckDB’s AsOf joins) or when you want to compare performance. Moving data between backends has minimal overhead because Xorq uses Apache Arrow for efficient data transfer.
Warning
Not all backends support every operation. For example, some complex window functions might work in DuckDB but not in Pandas. If you hit an unsupported operation error, check the backend documentation or switch to a backend that supports the operation.
Now that you understand when to use each backend, here’s a complete workflow that ties everything together.
Complete example
This demonstrates Xorq’s multi-engine capability. Load data once, then run the same expression on different backends without rewriting code. This lets you compare results across engines or use backend-specific features.
import xorq.api as xo# Connect to all backendsembedded = xo.connect()duckdb = xo.duckdb.connect()pandas = xo.pandas.connect()# Load data once in embedded backenddata = xo.examples.iris.fetch(backend=embedded)# Build expressionexpr = ( data .filter(xo._.sepal_length >6) .group_by("species") .agg(avg_width=xo._.sepal_width.mean()))# Execute on embedded backendresult1 = expr.execute()print("Embedded result:")print(result1)# Move data to DuckDB and execute theredata_in_duck = data.into_backend(duckdb)expr_duck = ( data_in_duck .filter(xo._.sepal_length >6) .group_by("species") .agg(avg_width=xo._.sepal_width.mean()))result2 = expr_duck.execute()print("\nDuckDB result:")print(result2)# Move data to Pandas and execute theredata_in_pandas = data.into_backend(pandas)expr_pandas = ( data_in_pandas .filter(xo._.sepal_length >6) .group_by("species") .agg(avg_width=xo._.sepal_width.mean()))result3 = expr_pandas.execute()print("\nPandas result:")print(result3)
All three backends produce the same results. The difference is where the computation happens and which engine performs it.
Next steps
Now you know how to switch backends. Continue learning: