# Query data in the Viam app

Write SQL and MQL queries against captured data in the Viam app or programmatically.
> Source: https://docs.viam.com/data/query-data/


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

