# Query data in the Viam app

Explore and analyze your captured data using SQL or MQL queries. You can run queries interactively in the Viam app’s query editor, from the command line with the [Viam CLI](https://docs.viam.com/cli/manage-data/#query-data), or programmatically through the SDK. Use CLI or SDK queries for ad-hoc analysis, building dashboards, or integrating with your own tools.

Tabular data (sensor readings, motor positions, encoder ticks, and other structured key-value data) is queryable through SQL and MQL. Binary data (images, point clouds) is stored separately and accessible through the [data client API](https://docs.viam.com/reference/apis/data-client/).

#### Known issue: SQL queries need an explicit lower time bound

SQL queries against `readings` currently return no rows unless the `WHERE` clause includes an explicit lower bound on `time_received`. Every SQL example and troubleshooting step on this page (including short inline examples) includes an explicit lower bound on `time_received` for this reason. When troubleshooting, do not remove the lower-bound filter entirely: widen it by using a broad lower bound such as `AND time_received >= CAST('2000-01-01T00:00:00.000Z' AS TIMESTAMP)`. MQL queries are not affected. Tracked as APP-10891.

#### Note

Tabular data queries (`TabularDataByMQL` and `TabularDataBySQL`) share a 100 TB monthly processing limit across your organization. Queries that exceed the limit return an error. [Contact us](mailto:support@viam.com) to request an increase.

## Open the query editor

1. Go to [app.viam.com](https://app.viam.com/).
2. Click the **DATA** tab in the top navigation.
3. Click **Query** to open the query editor.
4. Select **SQL** or **MQL** mode depending on which language you want to use.

**SQL** is good for straightforward filtering, sorting, and limiting.  
**MQL** (MongoDB Query Language) uses aggregation pipelines and is more powerful for grouping, computing averages, and restructuring nested data.

By default, queries run against the `readings` collection in the `sensorData` database.  
See [Query reference](https://docs.viam.com/data/schema/#column-reference) for the full schema.

## Explore your data with basic SQL

Start with a broad query to see what data you have:

```sql
SELECT time_received, component_name, component_type, data
FROM readings
WHERE time_received >= CAST('2000-01-01T00:00:00.000Z' AS TIMESTAMP)
ORDER BY time_received DESC
LIMIT 10
```

This shows the 10 most recent readings across all components.  
Most of the interesting values are in the `data` column, which contains your actual readings as nested JSON.

To see the structure of your data, run this query for a specific component:

```sql
SELECT data FROM readings
WHERE component_name = 'my-sensor'
  AND time_received >= CAST('2000-01-01T00:00:00.000Z' AS TIMESTAMP)
LIMIT 1
```

Switch to **table view** (the table icon in the results area) to see nested fields automatically flattened into dot-notation column headers like `data.readings.temperature`. These dot-notation paths are exactly what you use in your queries to extract specific values.

For the full schema and per-component examples, see the [readings table schema](https://docs.viam.com/data/schema/#column-reference).

To narrow to a specific component:

```sql
SELECT time_received, data
FROM readings
WHERE component_name = 'my-sensor'
  AND time_received >= CAST('2000-01-01T00:00:00.000Z' AS TIMESTAMP)
ORDER BY time_received DESC
LIMIT 10
```

To filter by time range:

```sql
SELECT time_received, component_name, data
FROM readings
WHERE time_received > '2025-01-15T00:00:00Z'
  AND time_received < '2025-01-16T00:00:00Z'
ORDER BY time_received ASC
```

## Extract fields from nested JSON

The `data` column contains JSON, so you need JSON functions to extract specific values. Use dot notation to reach into the nested structure:

```sql
SELECT
  time_received,
  data.readings.temperature AS temperature,
  data.readings.humidity AS humidity
FROM readings
WHERE component_name = 'my-sensor'
  AND time_received >= CAST('2000-01-01T00:00:00.000Z' AS TIMESTAMP)
ORDER BY time_received DESC
LIMIT 20
```

For detection results from a vision service:

```sql
SELECT
  time_received,
  data.detections
FROM readings
WHERE component_name = 'my-detector'
  AND component_type = 'rdk:service:vision'
  AND time_received >= CAST('2000-01-01T00:00:00.000Z' AS TIMESTAMP)
ORDER BY time_received DESC
LIMIT 10
```

To filter by a specific machine:

```sql
SELECT time_received, component_name, data
FROM readings
WHERE robot_id = 'YOUR-MACHINE-ID'
  AND time_received >= CAST('2000-01-01T00:00:00.000Z' AS TIMESTAMP)
ORDER BY time_received DESC
LIMIT 10
```

## Write MQL aggregation pipelines

Switch to **MQL** mode in the query editor. MQL queries are JSON arrays where each element is a pipeline stage.

Get the last 10 readings from a component:

```json
[\
  { "$match": { "component_name": "my-sensor" } },\
  { "$sort": { "time_received": -1 } },\
  { "$limit": 10 },\
  {\
    "$project": {\
      "time_received": 1,\
      "data": 1,\
      "_id": 0\
    }\
  }\
]
```

Count readings per component:

```json
[\
  {\
    "$group": {\
      "_id": "$component_name",\
      "count": { "$sum": 1 }\
    }\
  },\
  { "$sort": { "count": -1 } }\
]
```

Count readings per component over a specific time window:

```json
[\
  {\
    "$match": {\
      "time_received": {\
        "$gte": { "$date": "2025-01-15T00:00:00Z" },\
        "$lt": { "$date": "2025-01-16T00:00:00Z" }\
      }\
    }\
  },\
  {\
    "$group": {\
      "_id": "$component_name",\
      "count": { "$sum": 1 }\
    }\
  },\
  { "$sort": { "count": -1 } }\
]
```

Compute average, min, and max of a sensor value:

```json
[\
  { "$match": { "component_name": "my-sensor" } },\
  {\
    "$group": {\
      "_id": null,\
      "avg_temperature": { "$avg": "$data.readings.temperature" },\
      "min_temperature": { "$min": "$data.readings.temperature" },\
      "max_temperature": { "$max": "$data.readings.temperature" },\
      "total_readings": { "$sum": 1 }\
    }\
  }\
]
```

Group readings by hour to see trends over time:

```json
[\
  { "$match": { "component_name": "my-sensor" } },\
  {\
    "$group": {\
      "_id": {\
        "$dateToString": {\
          "format": "%Y-%m-%d %H:00",\
          "date": "$time_received"\
        }\
      },\
      "avg_temperature": { "$avg": "$data.readings.temperature" },\
      "count": { "$sum": 1 }\
    }\
  },\
  { "$sort": { "_id": 1 } }\
]
```

Find all detections above a confidence threshold:

```json
[\
  { "$match": { "component_name": "my-detector" } },\
  { "$unwind": "$data.detections" },\
  { "$match": { "data.detections.confidence": { "$gte": 0.9 } } },\
  {\
    "$project": {\
      "time_received": 1,\
      "class_name": "$data.detections.class_name",\
      "confidence": "$data.detections.confidence",\
      "_id": 0\
    }\
  },\
  { "$sort": { "time_received": -1 } },\
  { "$limit": 20 }\
]
```

The `$unwind` stage is important when your data contains arrays. It flattens the array so each element becomes its own document, which you can then filter and project individually.

## Save and reuse queries

You can save MQL queries for reuse. To save a query, switch to MQL mode, click **Save query**, and give it a name. It appears under the **Saved** tab in the editor sidebar, next to a **Recent** tab of your recent queries. Saved queries are available to your whole organization. You can also run a saved query from code by passing its name to [`TabularDataByMQL`](https://docs.viam.com/data/query-data-from-code/#reuse-a-saved-query). Saving is not available for SQL queries.

## Get help writing queries with the Query Assistant

The **Query Assistant** (Beta) is a chat panel that turns a plain-language description into a query. It works in both SQL and MQL: click **Assistant**, describe the data you want, and click **Insert** to add the suggested query to the editor. It helps whenever you are not sure how to express what you want, from a simple filter to a multi-stage MQL aggregation pipeline.

## Query from code

You can run the same SQL and MQL queries from Python or Go using the data client API. See [Query data from code](https://docs.viam.com/data/query-data-from-code/) for setup instructions and examples.

## Try it

To get oriented with your own data:

1. Open the query editor and run the following to see what components have captured data:

```sql
   SELECT DISTINCT component_name FROM readings
   WHERE time_received >= CAST('2000-01-01T00:00:00.000Z' AS TIMESTAMP)
   ```

2. Pick a component and run the following to see the JSON structure of its readings:

```sql
   SELECT data FROM readings
   WHERE component_name = 'YOUR-COMPONENT'
        AND time_received >= CAST('2000-01-01T00:00:00.000Z' AS TIMESTAMP)
   LIMIT 1
   ```

3. Use the field names from step 2 to write a query that extracts a specific value with dot notation (for example, `data.readings.temperature`).

For the full schema of the readings table, see [Query reference](https://docs.viam.com/data/schema/#column-reference).

## Troubleshooting

##### Query returns empty results

- **Check the component name.** Component names are case-sensitive and must match exactly. Run the following to see all available component names:

```sql
   SELECT DISTINCT component_name FROM readings
   WHERE time_received >= CAST('2000-01-01T00:00:00.000Z' AS TIMESTAMP)
   ```

- **Check the time range.** If you narrowed the time range, widen it back to the broad lower bound `time_received >= CAST('2000-01-01T00:00:00.000Z' AS TIMESTAMP)` to confirm data exists, then tighten the range back down. Do not remove the `time_received` filter entirely: under APP-10891, SQL queries without an explicit lower bound on `time_received` return no rows.

- **Verify data has synced.** Data must sync from the machine to the cloud before it is queryable. Check the **DATA** tab to confirm entries are visible.

##### Query returns data but fields are null

- **Check the JSON path.** The nested path must match the actual structure of your data. Run the following to see the raw JSON, then build your dot-notation path to match. A common mistake is using `data.temperature` when the actual path is `data.readings.temperature`.

```sql
   SELECT data FROM readings
   WHERE time_received >= CAST('2000-01-01T00:00:00.000Z' AS TIMESTAMP)
   LIMIT 1
   ```

##### MQL pipeline returns unexpected results

- **Build incrementally.** Start with just the `$match` stage and verify it returns the documents you expect. Then add one stage at a time. This makes it easy to identify which stage is producing unexpected output.
- **Check field references.** In MQL, field references use `$` prefix (for example, `$component_name`, `$data.readings.temperature`). Missing the `$` is a common source of errors.

##### Query is slow

- **Add filters early.** Always include a `$match` stage (MQL) or `WHERE` clause (SQL) to narrow the data before doing expensive operations like grouping or sorting. Filtering by `component_name` and `time_received` is particularly effective.
- **Use LIMIT.** While developing queries, always include a `LIMIT` clause (SQL) or `$limit` stage (MQL) to avoid scanning your entire dataset.

## What’s next

- [Create a data pipeline](https://docs.viam.com/data/pipelines/create-a-pipeline/): create precomputed summaries for faster queries.
- [Hot data store](https://docs.viam.com/data/hot-data-store/): query a rolling window of recent data with lower latency.
- [Sync data to your database](https://docs.viam.com/data/sync-data-to-your-database/): export data to your own MongoDB instance.
- [Visualize data](https://docs.viam.com/data/visualize-data/): build dashboards from your captured data.
- [Query captured data from an app](https://docs.viam.com/build-apps/tasks/query-data/): run SQL and MQL queries from a custom client app using the SDK.
