Skip to main content

xarray-sql

Query Xarray with SQL

PyPI Version ci lint ci-build ci-rust PyPI Downloads PyPI Downloads

pip install xarray-sql

What is this?

This is an experiment to provide a SQL interface for array datasets. Succinctly, we "pivot" Xarray Datasets to treat them like tables so we can run SQL queries against them — on the query engine of your choice. xarray-sql translates data, not queries: it registers a lazy Dataset as a table on DataFusion (built in), DuckDB, or Polars, and turns any engine's Arrow result back into a labeled Dataset. Dialects, geometry functions, and optimizers stay with the engine.

Quickstart

Open a Dataset, register it as a table with from_dataset, compute a climatology in SQL, then write the result back to Xarray and plot it:

Note: this example also needs pooch and a netCDF backend (for the tutorial download) and matplotlib (for the plot): pip install pooch netCDF4 matplotlib.

import xarray as xr
import xarray_sql as xql

# 4x-daily surface air temperature on a lat/lon grid, 2013-2014.
ds = xr.tutorial.open_dataset('air_temperature')

ctx = xql.XarrayContext()
ctx.from_dataset('air', ds, chunks=dict(time=100))

# A climatology — the mean annual cycle — computed in SQL: average air
# temperature for each month of the year, over all grid cells and years.
clim = ctx.sql('''
  SELECT
    CAST(date_part('month', "time") AS INTEGER) AS month,
    AVG("air") AS air
  FROM "air"
  GROUP BY CAST(date_part('month', "time") AS INTEGER)
  ORDER BY month
''')

# Round-trip the result back to Xarray. `month` is a derived column, so name
# it as the dimension.
clim_ds = clim.to_dataset(dims=["month"])

# Plot the annual cycle as a time series.
clim_ds["air"].plot()  # in a script, call matplotlib.pyplot.show() to display

That's the round trip — Xarray in, SQL in the middle, Xarray (and a plot) back out.

The same Dataset registers on other engines with one call — DuckDB gets a native lazy table with predicate pushdown, Polars scans the same object:

import duckdb

con = duckdb.connect()
xql.register(con, 'air', ds, chunks=dict(time=100))
rel = con.sql('SELECT time, AVG("air") AS air FROM air GROUP BY time ORDER BY time')
xql.to_dataset(rel, template=ds)   # any engine's Arrow result round-trips

table_names (below) works the same way on every engine, so a query written against era5.surface is not tied to the engine it was written for.

See Engines for the support matrix, DuckDB/Polars details, and the lazy chunked round-trip.

A bigger example: ARCO-ERA5

The same interface scales to cloud-native datasets with hundreds of variables, like ARCO-ERA5.

Note: reading from gs:// requires gcsfs (pip install gcsfs).

import xarray as xr
import xarray_sql as xql


# Open ARCO-ERA5 — a weather dataset with 273 variables since 1940. 
# Turning off dask means we don't have to wait to construct a task graph.
ds = xr.open_zarr(
  'gs://gcp-public-data-arco-era5/ar/full_37-1h-0p25deg-chunk-1.zarr-v3',
  chunks=None,  # Turn dask off
  storage_options={'token': 'anon'}  # Anonymous read from the public GCS bucket — no auth required.
)

ctx = xql.XarrayContext()
# Make sure to pass `chunks`!
ctx.from_dataset('era5', ds, chunks=dict(time=6), table_names={
    ('time', 'latitude', 'longitude'): 'surface',
    ('time', 'level', 'latitude', 'longitude'): 'atmosphere',
})
# Registration takes ~10s on my machine.

# Heads up: ARCO-ERA5 has 262 surface + 11 atmospheric variables. The library
# pushes column projection down to Zarr, so SELECT only fetches what you ask
# for — but `SELECT * FROM era5.surface` would try to pull every variable
# across the year (terabytes from GCS). 
#  ---> Always SELECT specific columns. <---

# Average 2m-temperature over NYC on the morning of 2020-01-01. The library
# pushes WHERE clauses on dimension columns down to partition pruning.
ctx.sql('''
  SELECT AVG("2m_temperature") - 273.15 AS avg_c
  FROM era5.surface
  WHERE time BETWEEN TIMESTAMP '2020-01-01'
                 AND TIMESTAMP '2020-01-01 05:00:00'
    AND latitude  BETWEEN 39 AND 40
    AND longitude BETWEEN 286 AND 287  -- ERA5 uses 0-360 longitudes
''').to_pandas()
#       avg_c
# 0  8.640069

# Average temperature per pressure level, globally. 
result = ctx.sql('''
  SELECT level, AVG(temperature) - 273.15 AS avg_c
  FROM era5.atmosphere
  WHERE time BETWEEN TIMESTAMP '2020-01-01'
                 AND TIMESTAMP '2020-01-01 05:00:00'
  GROUP BY level
  ORDER BY level DESC
''')
# DataFrame()
# +-------+----------------------+
# | level | avg_c                |
# +-------+----------------------+
# | 1000  | 6.6210120796502565   |
# | 975   | 5.185637919348153    |
# | 950   | 4.028428657263021    |
# | 925   | 3.0828117974912743   |
# | 900   | 2.2109172992531967   |
# | 875   | 1.395017610194202    |
# | 850   | 0.6342670572626616   |
# | 825   | -0.21037158786759846 |
# | 800   | -1.1810754318269687  |
# | 775   | -2.3064649711534457  |
# +-------+----------------------+

# `latitude`/`longitude` are inferred from the registered table's surviving
# dims; `template` is kept only to recover metadata (attrs, encoding).
ctx.sql('''
  SELECT latitude, longitude, AVG("2m_temperature") - 273.15 AS avg_c
  FROM era5.surface
  WHERE time BETWEEN TIMESTAMP '2020-01-01'
                 AND TIMESTAMP '2020-01-01 05:00:00'
  GROUP BY latitude, longitude
  ORDER BY latitude DESC, longitude
''').to_dataset(template=ds)
# <xarray.Dataset> Size: 8MB
# Dimensions:    (latitude: 721, longitude: 1440)
# Coordinates:
#   * latitude   (latitude) float32 3kB 90.0 89.75 89.5 ... -89.5 -89.75 -90.0
#   * longitude  (longitude) float32 6kB 0.0 0.25 0.5 0.75 ... 359.2 359.5 359.8
# Data variables:
#     avg_c      (latitude, longitude) float64 8MB -26.84 -26.84 ... -27.38 -27.38
# Attributes:
#     last_updated:           2026-06-20 02:33:34.265980+00:00
#     valid_time_start:       1940-01-01
#     valid_time_stop:        2025-12-31
#     valid_time_stop_era5t:  2026-06-14

(A runnable version of this example lives at perf_tests/era5_temp_profile.py.)

Why build this?

A few reasons:

  • Even though SQL is the lingua franca of data, scientific datasets are often inaccessible to non-scientists (SQL users).
  • Joining tabular data with raster data is common yet difficult. It could be easy.
  • There are many cloud-native, Xarray-openable datasets, from Google Earth Engine to the Source Cooperative. Wouldn’t it be great if these were also SQL-accessible? How can the bridge be built with minimal effort?

This is a light-weight way to prove the value of the interface.

The larger goal is to explore the hypothesis that the Pangeo ecosystem is a scientific database. Here, xarray-sql can be thought of as a missing DB front end.

How does it work?

All chunks in a Xarray Dataset are transformed into a Dask DataFrame via from_map() and to_dataframe(). For SQL support, we just use dask-sql. That's it!

2025 update: This library now implements a Dask-like from_map interface in pure DataFusion and PyArrow, but works with the same principle!

2026 update: Instead of from_map(), we create a way to translate Xarray chunks into Arrow RecordBatches. We pass a Python callback into a DataFusion TableProvider that lets the DB engine translate the underlying Dataset arrays into DataFusion partitions. The same chunks-to-batches translation is also exposed as a pyarrow.dataset.Dataset with predicate and projection pushdown, which is how DuckDB and Polars consume registered Datasets with no engine-specific code. Ultimately, the initial insight of the pivot() function -- that any ndarray can be translated into a 2D table -- underlies this performant query mechanism.

Does it work?

Yes. The recurring worry is that the SQL interface is a toy — fine for SELECTs, but not for the operations geoscience actually runs. So we wrote a suite that takes the staples of geospatial and climate analysis — the ones we assume need an array library — and expresses each one in SQL, then checks the SQL answer against an xarray/array reference to floating-point tolerance:

  • Spectral indices (NDVI) — column arithmetic over a real Sentinel-2 scene.
  • Climatology, anomalies, zonal means — GROUP BY and self-JOIN against the 0.25° ARCO-ERA5 archive registered as a lazy table. Each query is bounded to a small window (a few days over a region) and reads only that slice — the point is that you can aim a query at a multi-decade archive and pay only for the data it asks for, not that the query scans the whole record.
  • Forecast skill — scoring the Pangu-Weather and GraphCast ML models against ERA5 (WeatherBench 2) as a JOIN on valid_time = init + lead; it reproduces the published result that GraphCast beats Pangu at every lead.
  • Raster × vector zonal stats — a range JOIN of the ERA5 grid against a table of regions.
  • Reprojection and regridding — a reproject(x, y, src_crs, dst_crs) scalar PROJ UDF, shipped as the optional geo extension (pip install xarray-sql[geo], validated against Earth Engine's own geodesy via Xee) and a sparse-weight-table JOIN (regridding real SRTM terrain).

Every case matches its array reference. The headline finding: these operations are not really "array" operations at all — they are GROUP BY, JOIN, window functions, and CASE in disguise, and a query engine runs them at scale. See benchmarks/geospatial/ and the write-up, Geospatial operations are relational operations.

Why does this work?

Underneath Xarray, Dask, and Pandas, there are NumPy arrays. These are paged in chunks and represented contiguously in memory. It is only a matter of metadata that breaks them up into ndarrays. pivot(), which uses to_dataframe(), just changes this metadata (via a ravel()/reshape()), back into a column amenable to a DataFrame. We take advantage of this lightweight metadata change to make chunked information scannable by a DB engine (DataFusion, DuckDB, Polars — anything that speaks Arrow).

What are the current limitations?

The sharp edges we know about — per engine and fundamental — are cataloged in Known issues & limitations. Currently, we're looking for early users – "tire kickers", if you will. We'd love your input to shape the direction of this project! Please, give this a try and file issues as you see fit. Check out our contributing guide, too 😉.

What would a deeper integration look like?

I have a few ideas so far. One approach involves applying operations directly on Xarray Datasets. This approach is being pursued here, as xql.

Deeper still: I was thinking we could make a virtual filesystem for parquet that would internally map to Zarr. Raster-backed virtual parquet would open up integrations to numerous tools like dask, pyarrow, duckdb, and BigQuery. More thoughts on this in #4.

2025 update: Something like this is being built across a few projects! The ones I know about are:

2026 update: A colleague and I are experimenting with native Zarr RDBMS engines. Check out:

Roadmap

  • Lazy evaluation via the pyarrow Dataset interface #93. Implemented in #100
  • Support proper parallelism via proper partition handling on the rust/datafusion side. #106
  • Support core datafusion optimizations to scan less data, like #104, ...
  • Translate a single Zarr to a collection of tables #85.
  • Distributed beyond a single node through the DataFusion integration with Ray Datasets #68 or Apache Ballista #98.
  • Demo: calculate Sea Surface Temperature from 1940 - Present in SQL #36.
  • Provide an option to integrate DataFusion directly to Zarr via Rust #4.
  • (To be formally announced eventually): The 100 Trillion Row Challenge #34.

Sponsors & Contributors

I want to give a special thanks to the following folks and institutions:

  • Pramod Gupta and the Anthromet Team at Google Research for the problem formation and design inspiration.
  • Jake Wall and AI2/Ecoscope for compute resources and key use cases.
  • Charles Stern, Stephan Hoyer, Alexander Kmoch, Wei Ji, and Qiusheng Wu for the early review and discussion of this project.
  • Tom Nichols, Kyle Barron, Tom White, and Maxime Dion for the Array Working Group and DataFusion-specific collaboration.
  • The gracious volunteer data science students at UCSD's DS3 org, who are working to make this library better.
  • Andrew Huang for the sense of taste he brings to the project and consummate code changes.
  • Aman Kumar for spending a considerable amount of his GSoC internship contributing to this project.
  • Miguel Moncada Isla for reimagining this project to be a cross SQL engine interface to and from Xarray. He brought new life to this experiment, in my opinion.

License

Copyright 2024 Alexander Merose

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

All vendored code has proper license attribution.

Metadata

Release files for xarray_sql 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for xarray_sql 0.4.0
File Size Uploaded
xarray_sql-0.4.0.tar.gz 253.4 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for xarray_sql 0.4.0
File
xarray_sql-0.4.0-cp310-abi3-win_amd64.whl CPython 3.10 abi3 Windows x86-64 Details
xarray_sql-0.4.0-cp310-abi3-manylinux_2_34_x86_64.whl CPython 3.10 abi3 Linux glibc 2.34+ x86-64 Details
xarray_sql-0.4.0-cp310-abi3-manylinux_2_28_x86_64.whl CPython 3.10 abi3 Linux glibc 2.28+ x86-64 Details
xarray_sql-0.4.0-cp310-abi3-manylinux_2_28_aarch64.whl CPython 3.10 abi3 Linux glibc 2.28+ ARM64 Details
xarray_sql-0.4.0-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details
xarray_sql-0.4.0-cp310-abi3-macosx_10_12_x86_64.whl CPython 3.10 abi3 macOS 10.12+ x86-64 Details

Total release size: 116.9 MB

Release files / xarray_sql-0.4.0.tar.gz

Download URL xarray_sql-0.4.0.tar.gz
Size 253.4 kB
Tags Source
SHA-256 checksum
How to use checksums
cb4e7d8e3a32c1f3ced64527567fb29a953e882092bfd6a3748d9288a01e3572
BLAKE2b-256 checksum
How to use checksums
2126f032c12f372886180e1716b1ab9c1890693b506f38ce1fde245cdb985968
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / xarray_sql-0.4.0-cp310-abi3-win_amd64.whl

Download URL xarray_sql-0.4.0-cp310-abi3-win_amd64.whl
Size 19.3 MB
Tags CPython 3.10 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
0d2e2bbb7f547881b7e6c2b79703dd130f21f826193d526b9da5fbebdae55878
BLAKE2b-256 checksum
How to use checksums
9a37fb6f12b08904983675bbf7d3255c174d33e68f8d05646612eb485db4c7bd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / xarray_sql-0.4.0-cp310-abi3-manylinux_2_34_x86_64.whl

Download URL xarray_sql-0.4.0-cp310-abi3-manylinux_2_34_x86_64.whl
Size 22.6 MB
Tags CPython 3.10 Linux glibc 2.34+ x86-64 abi3
SHA-256 checksum
How to use checksums
c169de711d4fb24619f338eaaa129d3221c211bc6ac410d78ec21f858fea4771
BLAKE2b-256 checksum
How to use checksums
34babd10cb666f98148b696d4279bb4b9fd0b35672475bd9cfee1fd47e451e40
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / xarray_sql-0.4.0-cp310-abi3-manylinux_2_28_x86_64.whl

Download URL xarray_sql-0.4.0-cp310-abi3-manylinux_2_28_x86_64.whl
Size 20.1 MB
Tags CPython 3.10 Linux glibc 2.28+ x86-64 abi3
SHA-256 checksum
How to use checksums
773b7bbd7917d2a35aba72e7dde4dca9ddc693b8d12ba9f74287bc90c0632329
BLAKE2b-256 checksum
How to use checksums
e352ce880fac7137331200b8c8baf02fb683307de5e878157cbb2731482e03fe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / xarray_sql-0.4.0-cp310-abi3-manylinux_2_28_aarch64.whl

Download URL xarray_sql-0.4.0-cp310-abi3-manylinux_2_28_aarch64.whl
Size 18.6 MB
Tags CPython 3.10 Linux glibc 2.28+ ARM64 abi3
SHA-256 checksum
How to use checksums
f4aeb186051ba140936e65a1523c31586a2aa28e4d4a583e9872b610c3aca5e3
BLAKE2b-256 checksum
How to use checksums
e86fd700a5977520d5a2bdba631b2ef0bb79caa2eb952e82b2a4bfc19af2ec97
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / xarray_sql-0.4.0-cp310-abi3-macosx_11_0_arm64.whl

Download URL xarray_sql-0.4.0-cp310-abi3-macosx_11_0_arm64.whl
Size 17.6 MB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
9584387cf8d764c56804d37bd830a657ee0b42f29dd1594c11878f815fa35393
BLAKE2b-256 checksum
How to use checksums
9fbe6aa655df130a66f1019b13b1a1c27dfd11220cc820fa93188704c502a4a1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / xarray_sql-0.4.0-cp310-abi3-macosx_10_12_x86_64.whl

Download URL xarray_sql-0.4.0-cp310-abi3-macosx_10_12_x86_64.whl
Size 18.4 MB
Tags CPython 3.10 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
041437c122a0f6704f6f804529cd34cac72881ec74f05503179f388fc0350316
BLAKE2b-256 checksum
How to use checksums
e0eaa612514ef761b9b70248373ca209482493d25f07780074a3540f61b93648
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

0.6.0

8 release files

0.5.0

7 release files

This release

0.4.0 This release

7 release files

0.3.3

7 release files

0.3.2

7 release files

0.3.1

7 release files

0.3.0

19 release files

0.2.3

13 release files

0.2.2

13 release files

0.2.0

13 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

0.0.2

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page