# Run SQL
`nexus.sql()` runs one Spark SQL statement on Nexus and waits for the answer. In the app, the same queries run in . For long scripts or heavy processing, see [Spark sessions](/docs/developers/spark) and [Jobs](/docs/developers/jobs).
## Before you start [#before-you-start]
- The SDK is installed and you can connect ([SDK quickstart](/docs/developers)).
- Your key's **Access** allows what the statement does. A read-only key can only read.
- The workspace has credits.
## Run a query [#run-a-query]
```python title="top_customers.py"
from klarety import Nexus, RefusedError, ServiceError
with Nexus.connect() as nexus:
try:
result = nexus.sql(
"SELECT customer, SUM(amount) AS total "
"FROM orders GROUP BY customer ORDER BY total DESC",
max_rows=10,
)
except RefusedError as refused:
raise SystemExit(f"Query refused: {refused}")
except ServiceError as failed:
raise SystemExit(f"Nexus could not answer, try again: {failed}")
for row in result.rows:
print(row["customer"], row["total"])
print(f"Read {result.bytes_scanned} bytes")
```
Tables without a schema name are read from `default`. Name another schema as `schema.table`.
### What comes back [#what-comes-back]
`sql()` returns a `ResultSet`:
| Field | Meaning |
| --- | --- |
| `rows` | The rows, each a dictionary keyed by column name. `len(result)` is the number of rows. |
| `bytes_scanned` | How much data the statement read. |
| `truncated` | `True` when the statement produced more rows than came back. |
| `result_id` | Set when the rest of the result was kept aside for you to read page by page. |
| `pages` | How many pages were kept aside. |
| `result_truncated` | `True` when even the kept result passed your plan's size, so only its first part is kept. |
Two value types need care:
- A whole number larger than 253 comes back as text, so no JSON reader rounds it. Convert it with `int()`.
- A `timestamptz` value comes back in UTC with no offset written. Add `Z` before a strict parser reads it.
## Pass values with parameters [#pass-values-with-parameters]
Never join user input into SQL. Use `params`: each `:name` is filled as a **string literal**, never read as SQL.
```python
result = nexus.sql(
"SELECT * FROM orders WHERE customer = :customer AND amount > CAST(:minimum AS DOUBLE)",
params={"customer": "Acme", "minimum": "100"},
)
```
Every value is a string; `CAST` numbers and dates in the SQL, as above. A parameter name starts with a letter or underscore and has up to 64 letters, digits or underscores; a statement takes at most 256 parameters.
## Control how many rows come back [#control-how-many-rows-come-back]
`max_rows` caps the rows returned: 1 to 10,000, 1,000 by default, never past your plan's page of rows. If there are more, `truncated` is `True`.
## Read a large result [#read-a-large-result]
When a result is bigger than one answer, Nexus keeps the rest for a day and sets `result_id`. `nexus.result_rows(result)` reads every row page by page, without holding it all in memory:
```python
result = nexus.sql("SELECT * FROM events WHERE day = '2026-11-01'")
if result.truncated:
count = 0
for row in nexus.result_rows(result):
count += 1
print(f"{count} rows in total")
```
If `result_id` is `None`, `result_rows()` returns the rows you have, so it is always safe to call.
## Estimate before you run [#estimate-before-you-run]
`nexus.estimate()` reports what a statement would read, without running or charging for it:
```python
estimate = nexus.estimate("SELECT * FROM events")
print(estimate["bytes_scanned"], estimate["allowed_bytes"], estimate["within_budget"])
```
If `within_budget` is `False`, it would be refused with `OverBudgetError`. Add a `WHERE` on a column the table is organised by, select fewer columns, or aggregate first.
## Start Nexus ahead of time [#start-nexus-ahead-of-time]
After a quiet period, the first statement can wait while Nexus starts. Before a known burst (for example, at app start), call `nexus.warm()`; it returns at once and starts Nexus in the background. `nexus.spark_status()` tells you whether it is off, starting, ready or busy.
## Retries and timeouts [#retries-and-timeouts]
You do not need to write retry loops around `sql()`:
- Every statement carries an SDK-chosen ID. If the connection drops, the SDK resends it with the same ID, and Nexus runs it once.
- When every slot your plan allows is busy, or Nexus is starting, the SDK waits and tries again for up to five minutes.
- A statement that has not finished in that time raises `AnswerNotReadyError`. Run it again: nothing ran twice.
## What can go wrong [#what-can-go-wrong]
| Exception | Cause | What to do |
| --- | --- | --- |
| `RefusedError` | The statement is not allowed for this key, or the SQL is invalid. The message says which. | Fix the statement or use a key with more access. |
| `OverBudgetError` | The statement would read more than your plan allows. | Narrow it, then check with `estimate()`. |
| `RateLimitedError` | Too many calls per minute. | The SDK already waited; slow your loop down. |
| `OverBudgetError` (busy) | Every slot is taken and the queue is full. | Send it again shortly. |
| `InsufficientCreditsError` | The workspace is out of credits. | Add credits. |
| `AnswerNotReadyError` | No answer within the waiting time. | Ask again. |
## Next steps [#next-steps]
- [Tables and schemas](/docs/developers/tables) to create and change the tables you query.
- [Jobs and batch jobs](/docs/developers/jobs) to change rows with SQL.
- [Errors](/docs/developers/errors) for the full list of exceptions.