Skip to content
sandadocs

SQL reference

The SQL dialect, schemas and naming, the columns sanda adds, what sanda sql allows and refuses, limits, and worked example queries.

Your warehouse speaks PostgreSQL, so everything here is ordinary PostgreSQL. This page is the lookup for the parts that are sanda's: which schemas exist and what they are called, the columns sanda adds, exactly what each place you can type SQL accepts and refuses, the limits, and examples that run against a sanda warehouse as it is laid out.

For a walk-through of the shell itself, see sanda sql.

Dialect

The dialect is PostgreSQL. Common table expressions, window functions, filter (where ...), distinct on, lateral joins, arrays, JSON operators and the built-in date and text functions all work as in PostgreSQL, subject to what the role you run as may do.

Identifiers fold to lower case unless you double-quote them. A landed column with capital letters has to be quoted, for example "CustomerId". Names sanda creates are all lower case.

Where SQL runs

There are five places to run or store SQL, and they are not the same. What differs is who may use them, which login they run as, and how strictly the text is checked.

Where Who Runs as What it accepts
sanda sql Owners and admins The builder role One statement of any kind the role may run. Read-only unless you switch to read-write.
A view or materialized view definition in Modelling, and its preview Owners and admins The builder role One select under the strict rules below.
A procedure body in Modelling Owners and admins The builder role SQL or plpgsql, not scanned. The role's permissions are the boundary.
cherry and connected assistants, when they write SQL with run_sql Anyone using cherry, and credentials with the sql:run scope A login that can read only the tables the semantic fluid publishes in mapping One read-only select, under the strict rules below and a few more. They reach for it only when a question cannot be composed from the fluid's metrics and dimensions, and they must give a reason.
An assistant over MCP using execute_sql A credential with the sql:write scope The builder role The same as sanda sql, with a reason recorded for every statement.

Reports, the semantic workbench and the OData feed do not run SQL that you write. sanda composes their queries from the definitions on the fluid.

Schemas and naming

Schemas

Schema What it holds You can
raw Landing tables: every stream a connection syncs, and every CSV upload. Read, in sanda sql and in view definitions.
derived Your views, materialized views and procedures, and tables your own SQL creates. Read and, in read-write mode, write.
mapping Views the semantic fluid publishes for cherry and reports, plus any tables built there (for example the sanda sample dataset copied into a warehouse). Read the tables. The published views are not readable by the builder role.
search sanda search's indexes, one table per service. Not from the shell.
meta sanda's own bookkeeping. Not from the shell.

The managed sync also keeps a working area in the same database. It is not listed in the Explorer and you cannot read it.

Names

What Name Example
A table landed by a connection raw.<connection>__<stream> raw.xero__invoices
A table from a CSV upload raw.csv__<table> raw.csv__sales
Your view, materialized view or procedure derived.<name> derived.monthly_revenue
A table published on the fluid mapping.<name> mapping.invoices

<connection> is the connection's name in lower case, with every run of characters other than a to z and 0 to 9 replaced by one underscore, leading and trailing underscores removed, and cut to 40 characters. If nothing is left, sanda uses the connector's type. Two underscores separate it from the stream.

<table> for a CSV is the name you give the upload, in lower case, without a .csv, .tsv or .txt ending, with the same replacement rule, cut to 40 characters, and prefixed t_ if it starts with a digit. The CSV's columns become lower snake case with no leading or trailing underscore, at most 58 characters, with c_ added in front of a name that starts with a digit, _2, _3 added to repeats, and column_<n> for an empty heading. Types are worked out from the whole file: bigint for whole numbers, numeric for decimals, boolean for true and false, date for 2026-09-30, timestamptz for ISO timestamps, and text for anything else. Empty cells are nulls.

<name> in derived is lower case letters, digits and underscores, starts with a letter, is at most 63 characters, does not start with pg_, and cannot match a table in raw or a view in mapping.

Always write the schema, as in raw.csv__sales.

Columns sanda adds

Some columns in your tables are not from your source. Their names start with an underscore, and sanda treats every one as bookkeeping.

Column Where What it holds
_becca_loaded_at Every raw.csv__* table timestamptz, defaulting to the moment the row was loaded.
Underscore-prefixed sync columns Every table a connection lands Loading details such as a record identifier, the time the row was extracted and a metadata record. Their names start with an underscore and one of two reserved prefixes.

A CSV heading never keeps a leading underscore, so a heading called _becca_loaded_at lands as becca_loaded_at and cannot collide.

Because they are bookkeeping, these columns are left out wherever a person or cherry would see business data:

  • the Explorer counts them but does not list or preview them;
  • the views the fluid publishes in mapping do not name them, so cherry and reports cannot read them;
  • the fluid's map and the context cherry is given do not include them.

They are real columns, so select * in sanda sql returns them, and you can use them. The prefixes are reserved and are matched as prefixes, so a column of your own called _notes is untouched.

What sanda sql allows and refuses

The builder role

Every statement in sanda sql runs as the warehouse's builder role. The role, not a list of banned words, is what limits a statement.

The role can The role cannot
Read raw.*, derived.* and the tables in mapping.* Read a view the semantic fluid publishes in mapping
Read the database catalogue: information_schema and pg_catalog Read search.* or meta.*, or the managed sync's working area
Create, change and drop objects in derived (read-write mode) Write anywhere else, or reach another database or role

Anything outside those permissions fails with a permission error that says what the role may do.

Statements

  • One statement per press. sanda reads the text the way PostgreSQL does. A semicolon inside a string, a double-quoted name, dollar-quoted text or a comment is text, and a semicolon between two statements is a boundary. A single trailing semicolon, with comments after it, is fine.
  • Comments and dollar quoting are allowed. -- and /* ... */ comments (including nested block comments), E'...' strings and $tag$...$tag$ quoting all work in sanda sql. They are refused in view definitions.
  • At most 20,000 characters.
  • Read-only or read-write. In read-only mode the statement runs in a read-only transaction and PostgreSQL refuses anything that would change data, including a writable with and a function with side effects. In read-write mode it runs as written and commits at once.
  • Statements that start with select, with, values or table are read through a cursor, so the row limit is enforced at the source. Anything else runs directly. If it returns rows, as explain does, they are shown under the same cap. Otherwise the result is its command tag and the number of rows it touched.

Rows, values and secrets

  • At most 500 rows come back. The rows menu chooses 50, 200 or 500.
  • A value is cut at 300 characters, with an ellipsis. Dates arrive as ISO strings and JSON as JSON text.
  • A result column whose name looks like a secret is returned with empty values, and the result lists it as withheld. The test reads the column's name in the result as words, split at underscores, hyphens, digits and changes of case (so userPassword2 is user, password and 2), and ignores case. These words count wherever they appear, even run together with others: password, passwd, passphrase, passcode, passkey, token, credential, apikey, privatekey, secretkey, socialsecurity, taxfile and cardnumber. secret counts as a word or at the end of one, so clientsecret is withheld and secretary is not. These count only as a whole word: pass, ssn, tfn, iban, swift, cvv, pan and routing. Pairs of words count too: api_key, private_key, secret_key, social_security, tax_file and card_number, however they are separated. So pass_hash and auth_token are withheld, and passenger_count and compass_heading are not. Giving a column a different name in your select shows it, so use that only where the match is a false one.
  • The Explorer's preview applies the same test to column names.

Time

The shell uses your warehouse's statement ceiling, which is set under Limits and is at most 60 seconds on Basic, 60 seconds on Standard and 5 minutes on Enterprise. It never waits longer than 55 seconds, so a longer ceiling is lowered to 55 seconds. A statement that runs past it is stopped.

Rules for view definitions and assistant queries

A view or materialized view definition, its preview, and every query cherry or an assistant writes with run_sql must pass one strict check first. sanda does this as a courtesy, so you get a sentence instead of a database error, and the login's permissions still stand behind it.

Rule Detail
One statement Begins with select or with. A trailing semicolon is removed. A semicolon anywhere else is refused, even inside a string.
No comments --, /* and */ are refused anywhere in the text, even inside a string.
Ordinary quoting only Dollar-quoted text, E'...' strings and U&'...' or U&"..." are refused. Write an ordinary string and double the quote to include one.
At most 20,000 characters Longer text is refused.
Reads named tables Use schema-qualified names of raw, derived or tables in mapping. A definition that reads a view the fluid publishes is rejected after it is created, and rolled back.
Forbidden words See below.

The check reads the statement as tokens. A forbidden word inside a string literal or inside a double-quoted name is fine: where status = 'CREATED' and select "comment" both pass.

Refused as bare words:

  • statements and clauses: insert, update, delete, drop, alter, create, grant, revoke, truncate, copy, merge, call, do, vacuum, analyze, reindex, cluster, lock, listen, notify, prepare, execute, comment, refresh, set, reset, begin, commit, rollback, savepoint, security and into;
  • the catalogue: pg_catalog, information_schema, pg_class, pg_attribute, pg_namespace, pg_tables, pg_views, pg_roles, pg_user, pg_shadow, pg_authid, pg_settings, pg_database, pg_proc, pg_stat_activity and pg_stat_statements. These are refused even inside double quotes;
  • families of server, file, lock and replication functions, matched by prefix: names starting pg_sleep, pg_terminate_backend, pg_cancel_backend, set_config, dblink, pg_notify, pg_reload_conf, pg_rotate_logfile, pg_switch_wal, pg_backend_pid, pg_export_snapshot, pg_stat_file, pg_advisory_, pg_try_advisory_, pg_read_, pg_ls_, lo_, pg_logical_, pg_replication_, pg_create_, pg_drop_, txid_ and pg_current_, and the XML export functions query_to_xml, table_to_xml, cursor_to_xml, schema_to_xml and database_to_xml.

Because the word list matches whole words, a column named comment has to be written "comment", and a bare column called lo_score is refused until it is quoted.

Queries written by cherry and assistants face a few more rules. A statement is refused if any column or alias in it looks like a secret, if it uses a whole row as a value, or if it renames columns by position. Those queries read only the fluid's published tables.

Limits

Plan limits are on the Limits page. These are the limits of the SQL itself.

Limit Value
Statement in sanda sql, or in a view definition, or a procedure body 20,000 characters
Rows returned by sanda sql 500 (menu: 50, 200 or 500)
Longest value shown in a result 300 characters
Longest statement in sanda sql The warehouse's ceiling, or 55 seconds, whichever is less
Rows in a view preview, and in the Explorer's data preview 50
Columns in the Explorer's data preview 60
A view's preview Stops after 20 seconds
Build or procedure call started from the page 55 seconds
Build or procedure call in a task Up to 14 minutes for the whole run
Object names 63 characters
Columns in a drawn view 200, with at most 8 joins
Key columns on a materialized view 8
Procedure arguments 16, of the types text, integer, bigint, numeric, boolean, date, timestamptz, timestamp, interval, jsonb and uuid
Steps in a task 20
Concurrent queries in a warehouse 60 on Basic, 60 on Standard, 300 on Enterprise, at most
Warehouse statement ceiling 60 seconds on Basic, 60 seconds on Standard, 5 minutes on Enterprise, at most

Error messages

What you may read in sanda sql, and what to do.

Message What it means What to do
Type a statement first. The editor is empty. Type one.
That statement is longer than 20,000 characters. Over the length limit. Split the work, or move it to a procedure.
One statement at a time... The text has a second statement after a semicolon. Run them one after another.
The shell is in read-only mode, and that statement would change something. A change was attempted in read-only mode. Switch to Read-write if you mean it.
Permission denied... The builder role reads raw.*, derived.* and mapping's tables, and writes only derived.*. The statement touched something the role may not. Read from a table the role can reach, or write in derived.
Something depends on it... A drop or change would break another object. Drop or redefine the dependents first.
That ran past its time ceiling and was stopped. The statement outlasted the ceiling. Narrow it, add a filter, or materialize the result in a view.
PostgreSQL's own message, ending "(at character N)" A syntax or name error. N is the position in the text. Fix the text at that position.
This workspace's warehouse is suspended... The workspace's budget was reached. See Usage, budgets and suspension.

Examples

These examples use a CSV upload called sales, with the headings Sale date, Region, Product, Units and Revenue, and dates written 2026-09-30. sanda lands it as raw.csv__sales with the columns sale_date (a date), region, product, units and revenue. Replace the names with your own from the Explorer.

Find your way around

Every table and view the builder role can read
select table_schema, table_name, table_type
from information_schema.tables
where table_schema in ('raw', 'derived', 'mapping')
order by table_schema, table_name;
The columns of one table
select ordinal_position, column_name, data_type, is_nullable
from information_schema.columns
where table_schema = 'raw'
  and table_name = 'csv__sales'
order by ordinal_position;
The bookkeeping columns on a landed table
select column_name, data_type
from information_schema.columns
where table_schema = 'raw'
  and table_name = 'xero__invoices'
  and left(column_name, 1) = '_'
order by ordinal_position;
How big are the objects you have built
select c.relname as name,
       c.relkind,
       pg_size_pretty(pg_total_relation_size(c.oid)) as size
from pg_class c
where c.relnamespace = 'derived'::regnamespace
  and c.relkind in ('r', 'm')
order by pg_total_relation_size(c.oid) desc;

Look at the data

A quick look
select * from raw.csv__sales limit 20;
An exact row count
select count(*) as row_count from raw.csv__sales;
When a CSV table was last loaded
select max(_becca_loaded_at) as last_loaded from raw.csv__sales;
Duplicates on what should be a unique key
select sale_date, region, product, count(*) as copies
from raw.csv__sales
group by 1, 2, 3
having count(*) > 1
order by copies desc;

Answer a question

Revenue by month
select date_trunc('month', sale_date)::date as month,
       sum(revenue) as revenue
from raw.csv__sales
group by 1
order by 1;
Month-on-month change
with monthly as (
  select date_trunc('month', sale_date)::date as month,
         sum(revenue) as revenue
  from raw.csv__sales
  group by 1
)
select month,
       revenue,
       revenue - lag(revenue) over (order by month) as change,
       round(
         100.0 * (revenue - lag(revenue) over (order by month))
         / nullif(lag(revenue) over (order by month), 0),
         1
       ) as change_pct
from monthly
order by month;
Top ten products in the last 90 days
select product, sum(units) as units, sum(revenue) as revenue
from raw.csv__sales
where sale_date >= current_date - interval '90 days'
group by product
order by revenue desc
limit 10;
See the plan before you run something heavy
explain
select region, sum(revenue)
from raw.csv__sales
group by region;

Write in derived (read-write mode)

Keep a rollup as a table
create table derived.monthly_revenue as
select date_trunc('month', sale_date)::date as month,
       region,
       sum(revenue) as revenue
from raw.csv__sales
group by 1, 2;
Remove it again
drop table derived.monthly_revenue;

A table made this way has no version history and no schedule. For something sanda versions and rebuilds, define a view in Modelling. Its definition must satisfy the strict rules above: one select, no semicolon, no comments.

A definition that passes the strict rules
select date_trunc('month', s.sale_date)::date as month,
       s.region,
       sum(s.revenue) as revenue
from raw.csv__sales s
group by 1, 2

Something unclear or out of date? Tell us, and we will fix the page.