# Querying data

Tilebox offers a powerful and flexible querying API to access and filter data from your datasets. When querying, you can
[filter by time](/docs/datasets/query/filter-by-time), [filter by custom fields](/docs/datasets/query/filter-by-fields), and for
[Spatio-temporal datasets](/docs/datasets/types/spatiotemporal), optionally [filter by a location in the form of a geometry](/docs/datasets/query/filter-by-location).

## Running a query

You can query data from either a specific collection of a dataset, from a selected set of collections of a dataset, or from all collections of a dataset at once.
Below is a simple example showcasing those options by querying Sentinel-2 data for April 2025 over the state of Colorado.

**Python**

```python title="Python"
from shapely import Polygon
from tilebox.datasets import Client

area = Polygon(  # area roughly covering the state of Colorado
    ((-109.05, 41.00), (-109.045, 37.0), (-102.05, 37.0), (-102.05, 41.00), (-109.05, 41.00)),
)

client = Client()
sentinel2_msi = client.dataset("open_data.copernicus.sentinel2_msi")

# query data from a specific collection
collection = sentinel2_msi.collection("S2A_S2MSI2A")
data = collection.query(
    temporal_extent=("2025-04-01", "2025-05-01"),
    spatial_extent=area,
    show_progress=True,
)

# query data from a selected set of collections
data = sentinel2_msi.query(
    collections=["S2A_S2MSI2A", "S2B_S2MSI2A", "S2C_S2MSI2A"],
    temporal_extent=("2025-04-01", "2025-05-01"),
    spatial_extent=area,
    show_progress=True,
)

# query data from all collections in the dataset
data = sentinel2_msi.query(
    # omit the collections argument to query all collections
    temporal_extent=("2025-04-01", "2025-05-01"),
    spatial_extent=area,
    show_progress=True,
)
```

**Go**

```go title="Go"
startDate := time.Date(2025, 4, 1, 0, 0, 0, 0, time.UTC) 
endDate := time.Date(2025, 5, 1, 0, 0, 0, 0, time.UTC)
timeInterval := query.NewTimeInterval(startDate, endDate)
area := orb.Polygon{  // area roughly covering the state of Colorado
    {{-109.05, 41.00}, {-109.045, 37.0},  {-102.05, 37.0}, {-102.05, 41.00}, {-109.05, 41.00}},
}

collectionA, err := client.Collections.Get(ctx, dataset.ID, "S2A_S2MSI2A")
if err != nil {
    log.Fatalf("Failed to get collection: %v", err)
}

collectionB, err := client.Collections.Get(ctx, dataset.ID, "S2B_S2MSI2A")
if err != nil {
    log.Fatalf("Failed to get collection: %v", err)
}

collectionC, err := client.Collections.Get(ctx, dataset.ID, "S2C_S2MSI2A")
if err != nil {
    log.Fatalf("Failed to get collection: %v", err)
}

// query data from a specific collection
var fromCollection []*v1.Sentinel2Msi
err = client.Datapoints.QueryInto(ctx,
    dataset.ID,
    &fromCollection,
    datasets.WithCollectionIDs(collectionA.ID),
    datasets.WithTemporalExtent(timeInterval),
    datasets.WithSpatialExtent(area),
)
if err != nil {
    log.Fatalf("Failed to query datapoints: %v", err)
}

// query data from a selected set of collections
var datapoints []*v1.Sentinel2Msi
err = client.Datapoints.QueryInto(ctx,
    dataset.ID,
    &datapoints,
    datasets.WithCollectionIDs(collectionA.ID, collectionB.ID, collectionC.ID),
    datasets.WithTemporalExtent(timeInterval),
    datasets.WithSpatialExtent(area),
)
if err != nil {
    log.Fatalf("Failed to query datapoints: %v", err)
}

// query data from all collections in the dataset
var datapointsFromAllCollections []*v1.Sentinel2Msi
err = client.Datapoints.QueryInto(ctx,
    dataset.ID,
    &datapointsFromAllCollections,
    datasets.WithTemporalExtent(timeInterval),
    datasets.WithSpatialExtent(area),
)
if err != nil {
    log.Fatalf("Failed to query datapoints: %v", err)
}
```

Some datasets include assets that point to files in object storage. After selecting one datapoint, use an [asset collection and the storage client](/docs/datasets/assets-and-storage/read-and-download) to read or download those files.

To learn more about how to narrow down query results, see the following pages about filtering by time, geometry, custom
fields, or datapoint ID.

[Filter by time](/docs/datasets/query/filter-by-time)

Learn how to run queries for datapoints within a given temporal extent.

[Filter by geometry](/docs/datasets/query/filter-by-location)

Learn how to filter your query results by a certain geographical extent.

[Filter by custom fields](/docs/datasets/query/filter-by-fields)

Filter datapoints by queryable fields in the dataset schema.

[Filter by Datapoint ID](/docs/datasets/query/filter-by-id)

Find individual datapoints by their unique ID.

## Automatic pagination

Querying large datasets can result in a large number of data points. For those cases Tilebox
automatically handles pagination for you by sending paginated requests to the server.

When using the python SDK in an interactive notebook environment, you can additionally also display a
progress bar to keep track of the progress of the query by setting the `show_progress` parameter to `True`.

## Skipping data fields

Sometimes, only the ID or timestamp associated with a datapoint is required. In those cases, you can speed up
querying by skipping downloading of all dataset fields except of the `time`, the `id` and the `ingestion_time` by setting the `skip_data` parameter to `True`.

For example, when checking how many datapoints exist in a given time interval, you can use `skip_data=True` to avoid loading the data fields.
This works the same way on both collection-level and dataset-level queries.

**Python**

```python title="Python"
interval = ("2023-01-01", "2023-02-01")
data = collection.query(temporal_extent=interval, skip_data=True)
print(f"Found {data.sizes['time']} data points.")
```

**Go**

```go title="Go"
startDate := time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC)
endDate := time.Date(2023, time.February, 1, 0, 0, 0, 0, time.UTC)
interval := query.NewTimeInterval(startDate, endDate)

var datapoints []*v1.Sentinel1Sar
err = client.Datapoints.QueryInto(ctx,
  dataset.ID,
  &datapoints,
  datasets.WithCollectionIDs(collection.ID),
  datasets.WithTemporalExtent(interval),
  datasets.WithSkipData(),
)
if err != nil {
  log.Fatalf("Failed to query datapoints: %v", err)
}

log.Printf("Queried %d datapoints", len(datapoints))
log.Printf("First datapoint time: %s", datapoints[0].GetTime().AsTime())
```

Output

**Python**

```plaintext title="Python"
<xarray.Dataset> Size: 160B
Dimensions:         (time: 1)
Coordinates:
    ingestion_time  (time) datetime64[ns] 8B 2024-08-01T08:53:08.450499
    id              (time) <U36 144B '01910b3c-8552-7671-3345-b902cc0813f3'
  * time            (time) datetime64[ns] 8B 2024-08-01T00:00:01.362000
Data variables:
    *empty*
```

**Go**

```plaintext title="Go"
Queried 1 datapoints
First datapoint time: 2024-08-01 00:00:01.362 +0000 UTC
```

## Empty response

Query will not raise an error if no data points are found for the specified query, instead an empty result is returned.

In python, this is an empty `xarray.Dataset` object. In Go, an empty slice of datapoints. To check for an empty response, you can coerce the result to a boolean
or check the length of the slice.

**Python**

```python title="Python"
timestamp_with_no_data_points = "1997-02-06 10:21:00:000"
data = collection.query(temporal_extent=timestamp_with_no_data_points)
if not data:
    print("No data points found")
```

**Go**

```go title="Go"
timeWithNoDatapoints := query.NewPointInTime(time.Date(1997, time.February, 6, 10, 21, 0, 0, time.UTC))

var datapoints []*v1.Sentinel1Sar
err = client.Datapoints.QueryInto(ctx,
  dataset.ID,
  &datapoints,
  datasets.WithCollectionIDs(collection.ID),
  datasets.WithTemporalExtent(timeWithNoDatapoints),
)
if err != nil {
  log.Fatalf("Failed to query datapoints: %v", err)
}

if len(datapoints) == 0 {
  log.Println("No data points found")
}
```

Output

**Python**

```plaintext title="Python"
No data points found
```

**Go**

```plaintext title="Go"
No data points found
```
