How-toOracle NetSuiteSuiteQL / SuiteAnalytics

How to write and run SuiteQL queries in NetSuite

Question
how to write SuiteQL queries in NetSuite

Also searched as

  • netsuite suiteql query examples
  • how to run suiteql in netsuite
  • netsuite suiteql N/query module
  • netsuite suiteql vs saved search

Short answer

SuiteQL is NetSuite's read-only SQL dialect over the underlying record tables, run either interactively from Analytics > SuiteQL Query Tool (or the older /app/suiteanalytics query page), through REST at /services/rest/query/v1/suiteql, or programmatically via the N/query module in SuiteScript 2.x. It supports standard SELECT, JOIN, WHERE, GROUP BY and window functions against table names that mostly match record type IDs (transaction, transactionline, item, customer).

Applies to: NetSuite 2020.2 and later (SuiteQL Query Tool bundle and REST endpoint); N/query available since SuiteScript 2.1. Requires the SuiteAnalytics Workbook feature and appropriate role permissions.

Run a SuiteQL query

  1. 1Enable SuiteAnalytics Workbook (Setup > Company > Enable Features > Analytics) if the query tool is not visible.
  2. 2Go to Analytics > SuiteQL Query Tool (or search for the SuiteQL Query Tool bundle 665142 if not installed) to query interactively.
  3. 3Write a standard SELECT against a NetSuite table, e.g. SELECT id, tranid, trandate FROM transaction WHERE type = 'SalesOrd' AND trandate >= TO_DATE('2026-01-01','YYYY-MM-DD').
  4. 4Join to related tables with the actual join column, not the saved search dot-notation, e.g. JOIN transactionline ON transactionline.transaction = transaction.id.
  5. 5For scripted use, call query.runSuiteQL({ query: sql, params: [] }) from N/query and iterate .asMappedResults() or .results.
  6. 6For REST/external tools, POST the SQL text as JSON to /services/rest/query/v1/suiteql with header Prefer: transient (or paginate via the Content-Type: application/vnd.oracle.resource+json header) using OAuth 1.0 TBA or OAuth 2.0.
  7. 7Paginate large result sets with LIMIT and OFFSET or the REST endpoint's built-in offset parameter - SuiteQL does not auto-page in the query tool UI beyond the default limit.
  8. 8Watch governance: N/query counts against SuiteScript usage units, and the query tool UI caps result rows (commonly 4,000-5,000) even if more rows match.

SuiteQL table and column names differ from saved search field IDs

Saved search field IDs (like {tranid} or {entity}) do not map one-to-one onto SuiteQL column names. Most transaction body fields live on the transaction table, line-level fields on transactionline, and the two are joined via transaction.id = transactionline.transaction. Custom fields keep their script ID as the column name (custbody_x, custcol_x, custentity_x, custrecord_x) but only appear if selected explicitly - SELECT * is not supported the way it is in plain SQL for most tables.

BUILTIN.DF works in SuiteQL the same way it does in saved search formulas, converting an internal ID/list value into its display text. For record type names, use BUILTIN.DF(transaction.type) rather than trying to join to a system list table.

SELECT
  t.tranid,
  t.trandate,
  BUILTIN.DF(t.entity) AS customer,
  tl.item,
  tl.quantity,
  tl.netamount
FROM transaction t
JOIN transactionline tl ON tl.transaction = t.id
WHERE t.type = 'SalesOrd'
  AND t.trandate >= TO_DATE('2026-01-01','YYYY-MM-DD')
ORDER BY t.trandate DESC

Running SuiteQL from a SuiteScript with N/query

N/query.runSuiteQL() takes an object with a query string and an optional params array for bind variables (use ? placeholders instead of concatenating values into the SQL string, both for performance and to avoid injection-style bugs). The result exposes .asMappedResults() for an array of plain objects keyed by column alias, which is usually easier to work with than the raw .results array of Result objects.

SuiteQL through N/query is subject to the same governance limits as any other SuiteScript API call and to the platform's SQL execution time limits, so very large joins should be paginated with LIMIT/OFFSET inside a scheduled script rather than pulled in one call from a user event or RESTlet.

var query.runSuiteQL is called like:
var sql = 'SELECT id, tranid FROM transaction WHERE type = ? AND trandate >= ?';
var results = query.runSuiteQL({ query: sql, params: ['SalesOrd', '1/1/2026'] }).asMappedResults();

REST query endpoint for external BI tools

The /services/rest/query/v1/suiteql endpoint accepts a POST body of { "q": "SELECT ..." } and returns JSON, making it the standard integration point for external tools that are not using SuiteAnalytics Connect ODBC. Authentication is OAuth 1.0 Token-Based Authentication or OAuth 2.0 client credentials/authorization code, same as RESTlets. Set the Prefer header to transient to avoid NetSuite creating a saved query record for every call.

Pagination on the REST endpoint uses offset and limit in the URL or request; the response includes hasMore and totalResults so a client can loop until hasMore is false. This is the pattern most integration middleware (Boomi, Celigo, custom scripts) uses to pull incremental data out of NetSuite via SuiteQL rather than SOAP SuiteTalk searches.

When to use SuiteQL over a saved search

SuiteQL is the better choice once a report needs joins saved search cannot express, window functions (RANK, SUM OVER PARTITION BY), UNION across record types, or performance at scale for scheduled/API consumption. Saved search remains better for anything end users need to build or modify themselves through the UI, since SuiteQL requires SQL literacy and either the query tool, a script, or an external BI connection to run.

Common pitfalls

  • !Assuming SELECT * works uniformly - most tables require explicit column lists, and custom fields must be named individually.
  • !Confusing saved search field IDs with SuiteQL column names; they are not interchangeable across the two systems.
  • !Not paginating large result sets, hitting the query tool's row cap or REST default limit and assuming that is the full dataset.
  • !Concatenating user input into SQL strings instead of using bind parameters with N/query.
  • !Running heavy SuiteQL joins synchronously in a user event script and hitting governance or timeout limits.
  • !Forgetting the Prefer: transient header on REST calls, leaving orphaned saved query records behind.

How an ERP-grounded AI assistant handles this

ERPray can translate a plain-language reporting question - "total netamount by item for sales orders last quarter" - directly into a SuiteQL statement against the account's real table and custom field names, including the transaction/transactionline join, and can execute it through N/query or the REST endpoint on request. For scripted use it still helps to have a developer confirm governance impact before scheduling it to run frequently.

Frequently asked questions

Is SuiteQL read-only?

Yes. SuiteQL only supports SELECT statements; inserts, updates and deletes must go through Record/SuiteScript, SOAP SuiteTalk, or the REST record API, not SuiteQL.

What is the row limit for SuiteQL results?

The SuiteQL Query Tool UI typically caps displayed rows (around 4,000-5,000 depending on version); N/query and the REST endpoint should be paginated with LIMIT/OFFSET or the offset parameter to retrieve full result sets beyond that.

Can SuiteQL query custom records?

Yes. Custom record types are queryable by their internal table name (usually customrecord_xxx) with custom fields as columns, the same as standard records.

Does SuiteQL support window functions like RANK or SUM OVER?

Yes, SuiteQL supports a substantial subset of Oracle analytic SQL including window functions, which saved search formula fields do not support - this is one of the main reasons to move a report from saved search to SuiteQL.

Related

How-to

How to use formula fields in a NetSuite saved search

In the saved search Results tab, add a column, set Field to "Formula (Text)", "Formula (Numeric)", "Formula (Date)" or "Formula (Currency)", then type an Oracle SQL expression into the Formula box using curly braces around field IDs, e.g. {trandate} or {item.custitem_weight}. Formula fields can also go on the Criteria tab so you can filter on the calculated value itself.

How-to

How to build a workflow in NetSuite with SuiteFlow

Go to Customization > Workflow > Workflows > New, pick the record type and select Server, Client, or both as the trigger context, then build states and transitions on the workflow diagram, attaching actions (Set Field Value, Send Email, Create Record, Custom Action Script) to each state or transition. Release the workflow (top right dropdown, Testing to Released) once validated, since a workflow left in Testing only fires for the workflow owner.

How-to

How to create an assembly build or work order in NetSuite

For simple manufacturing with no routing or WIP tracking, use Transactions > Manufacturing > Build Assemblies (or Enter Assembly Build), select the assembly item, quantity, and location, and NetSuite consumes the BOM components and receives the finished item in one transaction. For multi-step production needing routing, WIP accounting or partial completions, create a Work Order (Transactions > Manufacturing > Work Orders) and process it through Work Order Completion, which requires the Advanced Manufacturing feature (SuiteSuccess Manufacturing or WMS add-on) or the Manufacturing bundle.

How-to

How to run intercompany transactions in NetSuite (OneWorld)

Enable the Intercompany Framework and Automated Intercompany Management features (Setup > Company > Enable Features > Company), then create an Intercompany Sales Order or Purchase Order between two subsidiaries in the same NetSuite account; NetSuite auto-generates the matching intercompany transaction on the counterparty subsidiary and posts the elimination journal entries during period close if Advanced Intercompany Journal Entries is also enabled.

Error fix

Fix NetSuite SSS_USAGE_LIMIT_EXCEEDED Error

SSS_USAGE_LIMIT_EXCEEDED fires when a SuiteScript execution consumes all the governance units (usage points) allotted to its script type before it finishes. Fix it by checking runtime.getCurrentScript().getRemainingUsage() before expensive calls, yielding or rescheduling in Scheduled scripts, and moving heavy record-count work into Map/Reduce, which yields automatically across stages.

Error fix

Fix NetSuite INVALID_FLD_VALUE Error

INVALID_FLD_VALUE means NetSuite rejected a value you tried to set on a field because it does not match the field's expected type, list option, or reference record. Fix it by confirming the internal ID or text value actually exists on that field's source list and matches the field's value type (text versus list versus record reference) before setting it.

AI for ERP

AI for NetSuite, Beyond the Built-In Text Tools

NetSuite's built-in AI covers text generation, not grounded answers on your own data. See how a private LLM over SuiteQL adds real Q&A and controls.

Stuck on Oracle NetSuite?

Talk to engineers who work inside Oracle NetSuite every week, and who build private AI that answers these questions from your own ERP data.