Writing analytical queries
Analytics accepts exactly one PostgreSQL-flavored SELECT query. Common table expressions, joins, filters, grouping, aggregate functions, ordering, and SQL limits are available through DataFusion.
Use only table and column names returned by the Analytics catalog. The query engine is DataFusion rather than a direct PostgreSQL session, so specialized PostgreSQL functions or extensions may not be supported even when the underlying data lives in PostgreSQL.
Start with a bounded detail query
Section titled “Start with a bounded detail query”This is useful for checking names and data types before writing an aggregate:
SELECT id, number, customer_id, issue_date, totalFROM invoiceORDER BY issue_date DESCLIMIT 100The SQL LIMIT is part of the query. The server’s separate result cap still applies afterward; use both when the report itself should have a deliberate maximum.
Filtering
Section titled “Filtering”Use ordinary SQL predicates:
SELECT number, issue_date, totalFROM invoiceWHERE issue_date >= DATE '2026-01-01' AND issue_date < DATE '2027-01-01' AND status = 'issued'ORDER BY issue_dateFor time intervals, a closed lower boundary and open upper boundary usually avoids overlap between adjacent reports.
Null requires IS NULL or IS NOT NULL:
SELECT COUNT(*) AS invoice_without_customerFROM invoiceWHERE customer_id IS NULLDeleted rows need no predicate because the analytical table has already removed them.
Grouping and totals
Section titled “Grouping and totals”Group by every selected value that is not aggregated:
SELECT status, COUNT(*) AS invoice_count, SUM(total) AS total_amount, AVG(total) AS average_amountFROM invoiceGROUP BY statusORDER BY total_amount DESCMost aggregates ignore null inputs. COUNT(*) counts rows, while COUNT(total) counts only rows where total is not null.
Use COALESCE when the report needs a replacement for a missing aggregate or dimension:
SELECT COALESCE(region, 'Unassigned') AS region, SUM(total) AS total_amountFROM invoiceGROUP BY COALESCE(region, 'Unassigned')Joining tables
Section titled “Joining tables”Join a link column to the target table’s id:
SELECT customer.name AS customer_name, COUNT(*) AS invoice_count, SUM(invoice.total) AS invoiced_amountFROM invoiceJOIN customer ON customer.id = invoice.customer_idGROUP BY customer.nameORDER BY invoiced_amount DESCLIMIT 100When joining a header to repeated lines, aggregate the line values rather than repeating a header total once per line:
SELECT invoice.number, SUM(invoice_line.line_total) AS calculated_totalFROM invoiceJOIN invoice_line ON invoice_line.invoice_id = invoice.idGROUP BY invoice.id, invoice.numberORDER BY invoice.numberCommon table expressions
Section titled “Common table expressions”A common table expression can make a multi-stage report easier to verify:
WITH customer_totals AS ( SELECT customer_id, SUM(total) AS invoiced_amount FROM invoice GROUP BY customer_id)SELECT customer.name, customer_totals.invoiced_amountFROM customer_totalsJOIN customer ON customer.id = customer_totals.customer_idORDER BY customer_totals.invoiced_amount DESCThis remains one query and is accepted by the read-only query guard.
Dates, money, and exact arithmetic
Section titled “Dates, money, and exact arithmetic”Use the catalog’s field type, currency, and rounding metadata to format results correctly. A money column’s currency belongs to that column; Analytics does not silently convert different currencies before summing them.
Do not combine money values of different currencies into one meaningful total unless the query explicitly applies an appropriate conversion model. For accounting conversions and authoritative balances, use the accounting workflows rather than reconstructing them casually in a report.
Decimal and money calculations retain their analytical numeric type inside the query. At the external result boundary, exact decimals are represented as strings so clients do not lose precision by converting them to floating point.
Naming result columns
Section titled “Naming result columns”Give calculated columns clear aliases:
SELECT issue_date, SUM(net_total) AS net_sales, SUM(tax_total) AS tax, SUM(gross_total) AS gross_salesFROM invoiceGROUP BY issue_dateORDER BY issue_dateThese aliases become the result column names. They are independent of table-definition aliases and exist only for that query.
Quote an identifier with double quotes when it conflicts with an SQL keyword or when exact case must be preserved. String values use single quotes.
Unsupported query shapes
Section titled “Unsupported query shapes”Analytics is intentionally read-only:
- only one statement is accepted;
- inserts, updates, deletes, DDL,
COPY, and other write operations are rejected; SELECT INTOis unsupported and must not be used;- multiple semicolon-separated queries are rejected;
- an empty query is rejected.
The SQL text itself may not exceed 64 KiB.
Continue with Results and limits to understand streaming, truncation, timeouts, types, and resource bounds.