Skip to content

Commit fced687

Browse files
committed
Optimizations to make registration faster (95s -> 11s).
1 parent 6511b96 commit fced687

4 files changed

Lines changed: 39 additions & 8 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ ds = xr.open_zarr(
3030

3131
ctx = xql.XarrayContext()
3232
# Make sure to pass `chunks`!
33-
ctx.from_dataset('era5', ds, chunks=dict(time=1), table_names={
33+
ctx.from_dataset('era5', ds, chunks=dict(time=6), table_names={
3434
('time', 'latitude', 'longitude'): 'surface',
3535
('time', 'level', 'latitude', 'longitude'): 'atmosphere',
3636
})

perf_tests/era5_temp_profile.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
latitude/longitude …`` down to dimension columns.
1919
2020
ARCO-ERA5's atmospheric variables are stored in native Zarr chunks of shape
21-
``(1, 37, 721, 1440)`` — about 150 MB per hour. We align Dask chunks to that
22-
shape with ``chunks=dict(time=1)`` so chunks fetch from GCS concurrently.
21+
``(1, 37, 721, 1440)`` — about 150 MB per hour. ``chunks=dict(time=6)`` groups
22+
six native chunks per DataFusion partition: large enough to keep partition
23+
count (and registration time) low, small enough that a 6-hour WHERE clause
24+
hits a single partition with no wasted I/O.
2325
2426
The Zarr is read anonymously from the public GCS bucket — no auth required.
2527
"""
@@ -57,7 +59,7 @@ def main() -> None:
5759
ctx.from_dataset(
5860
"era5",
5961
ds,
60-
chunks=dict(time=1),
62+
chunks=dict(time=6),
6163
table_names={
6264
("time", "latitude", "longitude"): "surface",
6365
("time", "level", "latitude", "longitude"): "atmosphere",

xarray_sql/df.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import itertools
2-
from collections.abc import Callable, Hashable, Iterator, Mapping
2+
from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping
33
from typing import Any
44

55
import numpy as np
@@ -402,22 +402,31 @@ def _parse_schema(ds: xr.Dataset) -> pa.Schema:
402402
PartitionBounds = dict[str, tuple[Any, Any, str]]
403403

404404

405-
def _block_metadata(coord_arrays: dict, block: Block) -> PartitionBounds:
405+
def _block_metadata(
406+
coord_arrays: dict,
407+
block: Block,
408+
dims: Iterable[Hashable] | None = None,
409+
) -> PartitionBounds:
406410
"""Compute min/max coordinate values for a single partition block.
407411
408412
Args:
409413
coord_arrays: Pre-materialised coordinate arrays keyed by dimension name
410414
string. Hoist this outside any loop to avoid repeated remote I/O
411415
for Zarr-backed datasets.
412416
block: A single block slice dict from block_slices().
417+
dims: Optional restriction to a subset of dims to compute. Used by
418+
``read_xarray_table`` to skip unchunked dims whose bounds are
419+
constant across all partitions and have been precomputed once.
420+
Defaults to all dims present in ``block``.
413421
414422
Returns:
415423
Dict mapping dimension name to (min_value, max_value, dtype_str).
416424
Dimensions with an empty slice are omitted; the Rust pruning logic
417425
treats missing dimensions conservatively (never prunes on them).
418426
"""
427+
items = ((d, block[d]) for d in dims) if dims is not None else block.items()
419428
ranges: PartitionBounds = {}
420-
for dim, slc in block.items():
429+
for dim, slc in items:
421430
coord_values = coord_arrays[str(dim)][slc]
422431
if len(coord_values) == 0:
423432
continue

xarray_sql/reader.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
_block_metadata,
2424
_parse_schema,
2525
block_slices,
26+
compute_chunks,
2627
iter_record_batches,
2728
)
2829

@@ -288,6 +289,24 @@ def make_stream(
288289

289290
return make_stream
290291

292+
# Separate dims whose chunk bounds vary across partitions from those
293+
# whose bounds are constant (one chunk spanning the whole axis). For the
294+
# latter we compute min/max once instead of re-scanning the full coord
295+
# array on every partition — dominant cost when registering hundreds of
296+
# thousands of single-time-step partitions on a 4-D dataset like ERA5.
297+
if chunks:
298+
resolved = compute_chunks(ds, chunks)
299+
elif ds.chunks:
300+
resolved = {d: tuple(c) for d, c in ds.chunks.items()}
301+
else:
302+
resolved = {}
303+
varying_dims = [d for d, tup in resolved.items() if len(tup) > 1]
304+
static_dims = [d for d in ds.dims if d not in varying_dims]
305+
static_block: Block = {d: slice(None) for d in static_dims}
306+
static_ranges = _block_metadata(
307+
coord_arrays, static_block, dims=static_dims
308+
)
309+
291310
def partition_pairs():
292311
"""Lazily yield (factory, metadata) for each partition.
293312
@@ -297,9 +316,10 @@ def partition_pairs():
297316
of O(N_partitions).
298317
"""
299318
for block in block_slices(ds, chunks):
319+
dynamic = _block_metadata(coord_arrays, block, dims=varying_dims)
300320
yield (
301321
make_partition_factory(block),
302-
_block_metadata(coord_arrays, block),
322+
{**static_ranges, **dynamic},
303323
)
304324

305325
return LazyArrowStreamTable(partition_pairs(), schema)

0 commit comments

Comments
 (0)