# SQL Editor (/docs/product-analytics/sql)

When a question doesn't fit a trend, funnel or retention chart, write SQL. **SQL Editor** in the sidebar has everything you can query on the left, the editor on top and the result below.

```sql
SELECT properties.mode AS mode,
  uniq(person_id) AS players,
  avg(toFloat64OrNull(properties.duration)) AS avg_seconds
FROM events
WHERE event = 'match_ended' AND timestamp > now() - INTERVAL 7 DAY
GROUP BY mode
ORDER BY players DESC
```

Press <kbd>⌘</kbd> <kbd>↵</kbd> (<kbd>Ctrl</kbd> <kbd>↵</kbd> on Windows and Linux) to run it.

## The explorer [#the-explorer]

The panel on the left lists everything you can query:

- **Tables**, with row counts. Open one to see its columns and their types. `events` also lists every property as `properties.<key>` (with how many events carry it) and its event names; `persons` lists person properties, and `groups` lists each group type's properties.
- **Views**: your saved queries, which work as tables.
- **Computed properties**: person properties that saved queries write.
- **Cohorts**, with their size.

Double-click a name, press <kbd>↵</kbd>, or drag it into the editor to insert it. Hover a row for its description, and use **▶ Preview** to see its first 100 rows or most common values. Preview runs in the editor when it's empty, and in a new tab otherwise, so you don't lose your work. Right-click a row for everything else: copy its name, insert a cohort as `inCohort(person_id, 3)`, or open, rename or delete a view.

Search finds tables, columns, properties, views and cohorts. The filter button hides built-in `$` properties, system tables or empty sections. Use the arrow keys to move through the list. Drag the panel's edge to resize it, or collapse it with <kbd>‹</kbd>.

## Tables [#tables]

| Table | One row per |
| --- | --- |
| `events` | Event your game (or website, bot, backend) sent |
| `persons` | Person: a player, by UserId |
| `sessions` | Session: start and end, duration, and web columns such as entry page for website sessions |
| `groups` | Clan or other [group](/docs/people/groups) |
| `person_overrides` | Merged identity: an old person id and the person it now belongs to |
| `cohort_static_members` | Member of a static or SQL cohort (use `inCohort()` for any cohort) |

Times are in your project's timezone. `person_id` already joins up merged ids, so `uniq(person_id)` counts real players.

## Shorthands [#shorthands]

| Write | To get |
| --- | --- |
| `properties.mode` | A property, as text. Use `properties['my key']` for keys with spaces. |
| `toFloat64OrNull(properties.kills)` | A property as a number |
| `person.properties.premium` | On `events`: the property of the player who sent the event |
| `inCohort(person_id, 3)` | Whether the person is in [cohort](/docs/people/cohorts) 3 |

The editor completes tables, views, columns, property keys and cohorts as you type. After `properties.` it suggests the keys of the table you select from.

## Saved queries are tables [#saved-queries-are-tables]

Save a query and give it a name like `paying_players`. From then on it works as a table in any other query:

```sql
SELECT count() FROM paying_players
WHERE first_purchase_at > now() - INTERVAL 14 DAY
```

That's how you build your own definitions, like "active player", "first purchase" or "whale", once, without setting anything up elsewhere. Saved queries can build on each other. RoChart checks a query runs before saving it, and won't delete one that another saved query or a cohort still reads from.

## Charts and dashboards [#charts-and-dashboards]

Results show as a table, or switch to **Line**, **Bar** or **Number** and pick the columns to plot. Saving keeps the chart. **⋯ → Create alert** watches one of its numbers (see [Alerts](/docs/alerts)). **⋯ → Add to dashboard** puts it on any [dashboard](/docs/product-analytics/dashboards), including public ones: people with the link see the result, never the SQL. A dashboard's date range doesn't apply to SQL cards, because the query sets its own.

Copy or download any result as CSV from the result bar.

## Cohorts and person properties from SQL [#cohorts-and-person-properties-from-sql]

A saved query with a `person_id` column can feed the rest of RoChart. Open it and choose **⋯ → Use with people**:

- **Create cohort** makes a [cohort](/docs/people/cohorts#build-a-cohort-from-sql) of everyone the query returns.
- **Set person properties** writes every other column onto those people. With the query below, each player gets `purchases` and `lifetime_robux`, which you can then use to target a [feature flag](/docs/feature-flags), segment an experiment or break down a chart.

```sql
SELECT person_id,
  count() AS purchases,
  sum(toFloat64OrNull(properties.$robux)) AS lifetime_robux
FROM events
WHERE event = '$purchase'
GROUP BY person_id
```

Both recalculate every hour and whenever you change the query. People who drop out of the result lose the cohort membership or the properties. RoChart manages these property names, so don't also set them from your game. Turning **Set person properties** off removes them.

## Ask AI [#ask-ai]

**Ask AI** turns a question into SQL that runs against your real events and properties, and explains what it returns. When a query fails, **Fix with AI** rewrites it using the error. Both need the AI assistant switched on for your workspace.

## Agents [#agents]

Agents use the same SQL. Over MCP, `sql_schema` lists the tables, saved queries, events, properties and cohorts; `sql` runs a query; and `save_query` saves one as a table, so an agent can define `active_players` once and every person and agent can use it. See the [tool reference](/docs/agents/tools#querying).

<CopyPrompt preset="connect">
  Hand this to your agent to connect it, then ask it questions in plain language. It writes and saves the SQL.
</CopyPrompt>

## Limits [#limits]

One read-only `SELECT` per run, up to 20 seconds and 10,000 rows. Aggregate in SQL or add a `LIMIT` for bigger results.
