---
title: explore_data
description: Run one read-only SELECT against a connection and get a small, complete answer. The bound, why it refuses instead of shortening, parameters, errors and scope.
updated: 2026-09-19
tools: [explore_data]
---

Runs one read-only `SELECT` against a connection and returns the whole small
answer: the columns, the row count, and the rows themselves.

This is the call for the questions that come **before** anything is designed.
What values does this column hold. How many customers are there. What does a row
of this table look like. How far back does the data go. Is this column ever null.

Behaviour hints for clients: read-only, not open-world.

## When your AI should use it, and when it should not

The order matters, and only your AI can apply it, because only your AI can see
its own tool list.

1. **If a semantic layer defines the metric**, that definition wins. Dashies
   keeps a dashboard fresh, versioned and shareable; it does not decide what a
   metric means, and neither does anything you learn from this call.
2. **If the session can reach your warehouse directly** (your own SQL client, or
   a server your warehouse vendor publishes), it should explore there. It is your
   system, it is not bounded by the size of an MCP result, and you can see what
   was run.
3. **If it has neither, this is the supported call.** It is a real answer rather
   than a workaround, which is the whole reason it exists as its own tool.

It is **not** the check that a statement you are about to publish will keep
working. That is
[`validate_cube_sql`](/reference/mcp-tools/validate-cube-sql), and no tool outside
Dashies can stand in for it.

## Parameters

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `sql` | string | yes | A single read-only `SELECT` or `WITH ... SELECT`. Bound it yourself: a `LIMIT`, a narrow window, or a `GROUP BY` that returns counts rather than rows. |
| `connection` | string | no | `self` (the default), a warehouse connection id, or the uploaded-file source id (which also needs `upload`). |
| `upload` | string | no | Which uploaded file to read, as an upload id from [`create_file_upload`](/reference/mcp-tools/create-file-upload). **Required when `connection` is the workspace's uploaded-file source, and refused on any other connection** - a warehouse has no uploads. There is no default and the newest upload is not assumed: a later upload would otherwise silently change what this call answers and what a published dashboard reads, so omitting it is refused with the newest ready upload named for you to pass back. |

## The bound, and why it refuses instead of shortening

| Bound | Value |
|---|---|
| Rows in one answer | 200 |
| Bytes of rows in one answer | 12288 |

The two bind independently, whichever comes first, so a handful of rows carrying
long text or JSON reaches the byte budget while a narrow answer reaches the row
bound.

:::warning{title="Past either bound you get nothing, not the first part"}
A shortened answer to "what values are in this column" reads **exactly** like a
complete one, and a dashboard built on the short version filters on a value set
that was never the whole set. So an oversized answer is refused, and the refusal
says what to change.

The consequence is worth stating positively: **every answer you do get is
complete.** There is no sample, no truncation flag, and nothing to page through.
:::

That claim is load-bearing, so it is worth saying how it is kept. The whole result
is read rather than the first page of it, and if the warehouse returns rows but
will not report how many there are in total, the answer is **refused** rather than
reported as though it were all of them. An answer that might be a fragment reads
exactly like a complete one, which is the reason the refusal is better than a
number.

There is no offset argument and no way to raise the bound, so this is not a way
to read a table out a page at a time. If what you want is a table's contents,
this is not the call.

## Returns

```text
Explored connection "self". 3 rows.
Columns:
- region: string
- orders: number

That is the whole answer, not a sample.

BEGIN_JSON
{"connection":"self","row_count":3,"columns":[{"name":"region","type":"string"},{"name":"orders","type":"number"}],"rows":[{"region":"EMEA","orders":1240},{"region":"AMER","orders":903},{"region":"APAC","orders":517}]}
END_JSON
```

The header lines and the JSON block are built from the same object, so they always agree:
the columns listed above are the columns in the block.

`row_count` is the warehouse's own total for the statement, not a count of the rows in the
block. On every answer you actually receive the two are equal, because an answer whose row
count disagrees with its own total is refused in either direction: too few rows for the total,
or more rows than the total claims. They are stated as two facts rather than one because they
come from two places.

An answer with no rows is a **result**, not a failure, and the text says so:
`Empty is a result, not a failure - the filter, the window or the table is
empty.` It usually means the filter matches nothing or the window is outside the
data, and retrying it unchanged will produce the same answer.

## Errors

The shared [connection rules](/reference/mcp-tools/conventions#connections), plus:

| Condition | Text |
|---|---|
| Missing SQL | `sql is required` |
| Over the row bound | `That returned more than 200 rows, which is more than one exploration answer holds, so nothing is being returned rather than the first part of it: ...` |
| Over the byte bound | `That answer is too wide to return, so nothing is being returned rather than part of it. Name the columns you actually want instead of selecting them all, or take fewer rows.` |
| An answer that could not be read to the end | `The answer came back in parts and could not be read to the end, so it is not being returned - a partial answer reads exactly like a small one. Retry; ...` |
| The warehouse would not say how many rows there are | `The warehouse returned rows but did not report how many there are in total, so whether these are all of them cannot be established ...` |
| More than one statement | `That is more than one statement (a ';' separates statements). ...` |
| Not a read-only SELECT | `That is not a single read-only SELECT (or WITH ... SELECT). ...` |
| A statement that writes | `That statement tries to write, and this call runs in a read-only transaction. ...` |
| A system-catalog read | `That statement reads the database's own system catalog, which this connection does not expose. ...` |
| Too slow | `That took longer than one exploration answer is allowed to take, so it was stopped. ...` |

For BigQuery, Snowflake, Redshift and Databricks an error about your own SQL (an
unknown column, a syntax error) is surfaced as the engine wrote it.

Rate limited at 120 calls per user per minute, **shared with**
[`validate_cube_sql`](/reference/mcp-tools/validate-cube-sql): the two run the
same read against the same executor, so they are one budget rather than two.

## Scope

Connection-scoped, so there is **no `workspace` argument**. It accepts any
connection belonging to a workspace you are a current member of, plus the built-in
`self` and the shared sample. A
warehouse connection requires a paid plan; `self` does not. It reads only the
tables that connection exposes, inside a read-only transaction, exactly as
[`validate_cube_sql`](/reference/mcp-tools/validate-cube-sql) does.

## Check it worked

The row count is the thing to read. If an exploratory count comes back at zero,
that is a real finding about the filter or the window rather than a failed call,
and it is worth resolving before it becomes a dashboard tile that reads zero
forever.
