SQL on pandas, Polars and Arrow

DuckDB · Practical · 12 min

Mixing SQL and DataFrames lets you use whichever is clearer for each step, and it is often much faster than pandas.

DuckDB can query a pandas DataFrame, Polars DataFrame or Arrow table that is a Python variable in scope, simply by naming it: duckdb.sql("SELECT * FROM df WHERE x > 1"). This is called a replacement scan: if a table name is not found in the database, DuckDB looks for a Python object with that name.

Results convert back just as easily:

  • .df() returns a pandas DataFrame
  • .pl() returns a Polars DataFrame
  • .arrow() returns a PyArrow table (or reader), .fetchnumpy() a dict of NumPy arrays

Arrow and Polars data is read with zero or near-zero copying, and pandas numeric columns are scanned directly. That makes DuckDB a great accelerator for heavy pandas steps: complex joins, group-bys with many keys, or window functions are often several times faster and use less memory as SQL.

To persist a DataFrame, con.execute("CREATE TABLE t AS SELECT * FROM df") or con.register("name", df) to give it an explicit name (needed when the variable is not in the calling scope, e.g. inside another function).

Watch types at the boundary: pandas object columns of strings become VARCHAR, NaN in float columns is not the same as SQL NULL, and timezone-aware timestamps map to TIMESTAMP WITH TIME ZONE.

Example

import duckdb
import pandas as pd

orders = pd.DataFrame({"user": ["a", "b", "a", "c"], "amount": [10, 5, 7, 3]})
users = pd.DataFrame({"user": ["a", "b", "c"], "country": ["AM", "FR", "AM"]})

result = duckdb.sql("""
    SELECT u.country, sum(o.amount) AS total
    FROM orders o JOIN users u USING (user)
    GROUP BY ALL ORDER BY total DESC
""").df()                       # back to pandas
print(result.to_dict("list"))
# {'country': ['AM', 'FR'], 'total': [20.0, 5.0]}

Remember

  • Name a DataFrame or Arrow table in FROM and DuckDB scans it in place
  • .df(), .pl(), .arrow() convert results back
  • Use con.register when the object is not a variable in scope

Watch out for

  • Expecting NaN in a pandas float column to behave like SQL NULL in filters and aggregates.

Check yourself

  1. How does DuckDB find `df` in `duckdb.sql('SELECT * FROM df')` when no table df exists?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: duckdb.org/docs/current/guides/python/sql_on_pandas

Builds on

Unlocks

Nothing builds on this yet.