# 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.