Contents7 sections ↓
SuiteQL for NetSuite: what it is and why it matters
SuiteQL is NetSuite's query language, based on the SQL-92 revision, and it lets you query NetSuite data with familiar SQL syntax. If you've hit the edges of saved searches — joins the interface doesn't offer, several levels of aggregation, or logic that needs a subquery — SuiteQL is the tool for it.
TL;DR: SuiteQL is NetSuite's SQL-92-based query language. It supports joins, subqueries, UNION, GROUP BY and HAVING, and it enforces the same role-based access restrictions as SuiteAnalytics Workbook. You run it through SuiteScript's N/query module, SuiteTalk REST web services, or SuiteAnalytics Connect. It reads data; it doesn't write it.
SuiteQL queries NetSuite's analytics data source. You write SELECT statements with joins, WHERE clauses, GROUP BY, HAVING and ORDER BY — the standard SQL constructs every developer knows. The difference from traditional SQL is that you're querying NetSuite's record types and fields rather than raw database tables.
SuiteQL doesn't replace saved searches. Saved searches are still better for reports that business users build and adjust, dashboard portlets, scheduled emails and alerts. But for developers building integrations, custom reports and data extraction, SuiteQL is more powerful and more readable than the equivalent saved search code.
SuiteQL syntax fundamentals
Basic SELECT
SELECT id, entityid, companyname, email
FROM customer
WHERE isinactive = 'F'
ORDER BY companynameThis returns active customers with their ID, entity ID, company name and email. The record type (customer) maps to NetSuite's Customer record. Look up table and field names in the Records Catalog (Setup > Records Catalog), which Oracle recommends for SuiteQL over the SuiteScript Records Browser.
Joins
SELECT
t.tranid,
t.trandate,
c.companyname,
t.foreigntotal
FROM transaction t
INNER JOIN customer c ON t.entity = c.id
WHERE t.type = 'SalesOrd'
AND t.trandate >= TO_DATE('2026-01-01', 'YYYY-MM-DD')
ORDER BY t.trandate DESCThis query joins transactions to customers, filtering for sales orders in 2026. SuiteQL supports inner, outer and cross joins. For performance on large queries, Oracle's guidance favors Oracle-style join syntax (tables in the FROM clause, join conditions in WHERE).
Aggregations
SELECT
c.companyname,
COUNT(t.id) AS order_count,
SUM(t.foreigntotal) AS total_revenue
FROM transaction t
INNER JOIN customer c ON t.entity = c.id
WHERE t.type = 'SalesOrd'
AND t.trandate >= TO_DATE('2025-01-01', 'YYYY-MM-DD')
GROUP BY c.companyname
HAVING SUM(t.foreigntotal) > 10000
ORDER BY total_revenue DESCGROUP BY and HAVING work as expected. This gives you customer order counts and revenue totals where total revenue exceeds $10,000.
Subqueries
SELECT id, entityid, companyname
FROM customer
WHERE id IN (
SELECT entity
FROM transaction
WHERE type = 'SalesOrd'
AND trandate >= TO_DATE('2026-01-01', 'YYYY-MM-DD')
)Subqueries are supported, and they make questions like "customers who ordered this year" easy to express. Oracle's performance guidance recommends avoiding nested SELECTs where a join does the same job, so use them when the logic needs them.
Running SuiteQL queries
In SuiteScript (N/query)
The primary programmatic interface for SuiteQL is the N/query module:
define(['N/query'], function(query) {
var results = query.runSuiteQL({
query: `SELECT TOP 100 id, companyname, email
FROM customer
WHERE isinactive = 'F'`
});
var mappedResults = results.asMappedResults();
// Returns array of objects: [{id: 1, companyname: 'Acme', email: '...'}]
});The asMappedResults() method returns an array of objects with field names as keys — much cleaner than working with saved search Result objects.
query.runSuiteQL() returns at most 5,000 results. For more, use query.runSuiteQLPaged(), which returns pages of 5 to 1,000 rows and needs a unique sort order.
Via REST web services
SuiteQL queries can run through SuiteTalk REST web services, which makes them useful for external integrations that pull data from NetSuite:
POST /services/rest/query/v1/suiteql?limit=10
Content-Type: application/json
Prefer: transient
{
"q": "SELECT id, companyname FROM customer WHERE isinactive = 'F'"
}The Prefer: transient header is required. Results come back 1,000 rows per page by default, up to 100,000 results per query.
Through SuiteAnalytics Connect
SuiteAnalytics Connect gives ODBC, JDBC and ADO.NET access for BI tools and data warehouses. It's read-only, and it's the route Oracle points to for extracts beyond the REST query limits.
SuiteAnalytics Workbook doesn't run SuiteQL you type in, but it can export a saved dataset as a SuiteQL query — a handy starting point when you want the SQL behind a report someone built in the UI.
SuiteQL queries that won't break in production?
We write saved searches and SuiteQL queries for reports, integrations and exports, and pick the right one for each job.
Get SuiteQL helpSuiteQL vs saved searches
| Capability | Saved Searches | SuiteQL |
|---|---|---|
| Joins | Join fields to related records, as offered in the UI | Inner, outer and cross joins |
| Subqueries | Not supported | Supported |
| Aggregation | Summary types: group, sum, count, min, max, average | GROUP BY, HAVING, several levels |
| Built by business users | Yes (UI-based) | No (requires SQL) |
| Dashboard portlets | Yes (Custom Search portlet) | No |
| Scheduled emails and alerts | Yes | No (needs a script) |
| Role-based access | Yes | Yes |
| UNION queries | No | Yes |
Use saved searches when:
- Business users need to create or modify the report
- You need dashboard portlets or KPI scoreboards
- You want scheduled emails or alerts when records change
Use SuiteQL when:
- You need joins the saved search interface doesn't offer
- You need subqueries or UNION queries
- You're building integrations, exports or custom reports in code
- You need several levels of aggregation with HAVING clauses
Practical examples
Aging AR by customer
SELECT
c.companyname,
SUM(CASE WHEN CURRENT_DATE - t.duedate BETWEEN 0 AND 30 THEN t.foreignamountremaining ELSE 0 END) AS current_30,
SUM(CASE WHEN CURRENT_DATE - t.duedate BETWEEN 31 AND 60 THEN t.foreignamountremaining ELSE 0 END) AS days_31_60,
SUM(CASE WHEN CURRENT_DATE - t.duedate BETWEEN 61 AND 90 THEN t.foreignamountremaining ELSE 0 END) AS days_61_90,
SUM(CASE WHEN CURRENT_DATE - t.duedate > 90 THEN t.foreignamountremaining ELSE 0 END) AS over_90
FROM transaction t
INNER JOIN customer c ON t.entity = c.id
WHERE t.type = 'CustInvc'
AND t.foreignamountremaining > 0
GROUP BY c.companyname
ORDER BY c.companynameTop items by units sold
SELECT TOP 20
i.itemid,
i.displayname,
SUM(tl.quantity * -1) AS units_sold
FROM transactionline tl
INNER JOIN transaction t ON tl.transaction = t.id
INNER JOIN item i ON tl.item = i.id
WHERE t.type = 'SalesOrd'
AND t.trandate >= TO_DATE('2025-01-01', 'YYYY-MM-DD')
AND tl.mainline = 'F'
AND tl.taxline = 'F'
GROUP BY i.itemid, i.displayname
ORDER BY units_sold DESCSales order lines store quantity as a negative value, which is why Oracle's own examples multiply it by -1. Check the sign of any amount field the same way before you total it.
Customers without orders in 6 months
SELECT c.id, c.companyname, c.email
FROM customer c
WHERE c.isinactive = 'F'
AND c.id NOT IN (
SELECT DISTINCT t.entity
FROM transaction t
WHERE t.type = 'SalesOrd'
AND t.trandate >= ADD_MONTHS(CURRENT_DATE, -6)
)
ORDER BY c.companynameTips and gotchas
Use the Records Catalog. Setup > Records Catalog documents the tables and fields SuiteQL can query, including joins. Bookmark it — you'll reference it constantly.
Cap your queries during development. Use SELECT TOP 100 while you iterate, and the REST limit parameter for paged results.
Wrap dates in TO_DATE(). SuiteQL doesn't accept plain date strings in comparisons. Date functions follow Oracle conventions: TO_DATE(), ADD_MONTHS(), CURRENT_DATE, TRUNC().
No WITH clauses. Common table expressions aren't supported. Use a subquery or a join instead.
Keep IN lists under 1,000 values. An IN list accepts at most 1,000 arguments.
Transaction lines require explicit mainline filtering. Each transaction has a mainline row (summary) and detail rows (line items). Filter with mainline = 'F' for line items or mainline = 'T' for the summary row, and exclude tax lines with taxline = 'F'.
Case sensitivity varies. String comparisons in WHERE clauses may be case-sensitive depending on the field. Use UPPER() or LOWER() for safe string matching.
Getting started
Start with the Records Catalog to understand the data model. Pick a saved search you know well and rewrite it in SuiteQL — that gives you a reference point where you know what the correct output should look like.
Then move to the queries saved searches can't handle: joins the UI doesn't offer, subqueries, UNION and several levels of aggregation. That's where SuiteQL earns its place.
What clients ask before signing
Get help with your NetSuite setup
Tell us which feature or process isn't working the way you need. We'll scope a fix — usually days, not months.

Joaquin Vigna
Co-Founder & CTO
Co-founder and Chief Technology Officer at BrokenRubik with 12+ years of experience in software architecture and NetSuite development. Leads technical strategy, innovation initiatives, and ensures delivery excellence across all projects.
