Book a Free Consultation
Back to blog
SuiteQLSuiteScriptReporting

NetSuite SuiteQL: Guide for Administrators and Developers

August 9, 2026 · Updated August 14, 2026 · 11 min read

Need help with this in your NetSuite account?

At some point, every NetSuite developer hits a Saved Search that should work but cannot. The data is in the system, the logic is right, but the Search UI cannot express what the query actually needs: a join across multiple record types, a GROUP BY aggregation that the Summary type cannot cleanly handle, or a result set that quietly stops at 4,000 records with no warning that it missed anything.

SuiteQL is the answer to all three of those problems. It is a SQL-like query language built directly into NetSuite that gives developers and technically-minded administrators access to the full data model using familiar SQL syntax. If you have been working around saved search limitations with clumsy workarounds, this guide will make those workarounds unnecessary.

Quick answer

SuiteQL is a SQL-like query language built into NetSuite for retrieving data from the NetSuite database. It is available in SuiteScript via the N/query module and via NetSuite's REST API at the suiteql endpoint. SuiteQL queries target NetSuite record types using their internal table names (Transaction, Customer, Item, Employee, etc.) rather than arbitrary database table names. The language is based on Oracle SQL syntax with NetSuite-specific extensions for display values and custom field access. SuiteQL is read-only: it retrieves data but cannot create, update, or delete records. Two execution methods are available in SuiteScript: query.runSuiteQL() for result sets under 5,000 rows, and query.runSuiteQLPaged() for larger datasets with automatic pagination. The REST API exposes SuiteQL at the /services/rest/query/v1/suiteql endpoint using a POST request with a JSON body containing a q property. Custom fields are referenced by their script IDs as column names (custbody_*, custcol_*). List fields return internal ID values by default; wrapping a column in BUILTIN.DF() returns the human-readable display value instead.

Using Google AI Search? Add SuitePacific as a Preferred Source so we show up in your AI answers.
Add as Preferred Source

How does SuiteQL differ from Saved Searches?

Saved Searches and SuiteQL retrieve NetSuite data, but they work differently and are suited to different use cases.

Characteristic Saved Search SuiteQL
Interface UI or N/search API N/query module or REST API
Syntax UI-configured filters and columns SQL-like text query
Joins Implicit, configured via related fields Explicit JOIN syntax
Result limit 4,000 (search.run()), unlimited with runPaged 5,000 per page with runSuiteQLPaged
Aggregations Available via Summary type Standard SQL GROUP BY and aggregate functions
Custom fields Available by field ID in UI Available by column name (custbody_*, custcol_*)
Readable by non-developers Yes, through the UI Requires SQL knowledge
Best for Operational dashboards, user-visible lists, portlets Data extraction, complex reporting, scripted integrations

Use a Saved Search when the output will be viewed through NetSuite's standard UI (portlets, list views, reports). Use SuiteQL when you need SQL-style aggregation, explicit control over joins, or results that will be consumed programmatically by a script or external system.

What are the key differences between SuiteQL and standard SQL?

SuiteQL is based on Oracle SQL syntax. Developers with SQL experience will find it familiar, but there are NetSuite-specific conventions to understand.

Table names are NetSuite record type names. The tables you query are not arbitrary database table names. They are the internal names that NetSuite assigns to each record type: Transaction, TransactionLine, Customer, Vendor, Item, Employee, Account, Department, Location, and so on. The SuiteQL schema reference in NetSuite's Help documentation lists all available tables and their columns.

No SELECT *. SuiteQL does not support SELECT *. You must specify each column by name. This is a deliberate design choice that encourages explicit queries and prevents accidentally returning data you did not intend to retrieve.

Internal IDs for foreign keys. Join relationships use internal NetSuite IDs, not human-readable names. When you join Transaction to TransactionLine, you join on the transaction's internal ID.

BUILTIN.DF() for display values. List fields in NetSuite store internal ID values. A custbody_status field might store the value 2, where 2 is the internal ID of the list item. To retrieve the human-readable display value instead of the internal ID, wrap the field in BUILTIN.DF(): SELECT BUILTIN.DF(custbody_status) AS status_label FROM Transaction.

Dates are stored as UTC. Date fields in SuiteQL return UTC values. If you are filtering or comparing dates, account for time zone offset when building your WHERE clause.

Custom fields use their script IDs as column names. A custom body field with the script ID custbody_po_reference is accessed in SuiteQL as custbody_po_reference. A custom column field is accessed similarly in the TransactionLine table.

How do you run SuiteQL from SuiteScript?

SuiteQL is available in SuiteScript 2.x via the N/query module. Two methods are relevant:

query.runSuiteQL() runs the query and returns all results in a single response. This is limited to 5,000 results and is appropriate for queries where you know the result set is small.

query.runSuiteQLPaged() runs the query and returns a page object you can iterate through, 5,000 rows at a time. Use this for any query that might return more than 5,000 rows.

/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 */
define(['N/query', 'N/log'], (query, log) => {
    const execute = (context) => {
        // Paged query for large result sets
        const pagedQuery = query.runSuiteQLPaged({
            query: `
                SELECT
                    t.id,
                    t.tranid,
                    t.trandate,
                    BUILTIN.DF(t.status) AS status_label,
                    t.custbody_po_reference
                FROM Transaction t
                WHERE t.type = 'PurchOrd'
                    AND t.trandate >= ?
                ORDER BY t.trandate DESC
            `,
            params: ['2026-01-01'],
            pageSize: 1000
        });

        pagedQuery.iterator().each((page) => {
            page.value.data.results.forEach((row) => {
                log.debug('row', row.values);
            });
            return true; // continue to next page
        });
    };
    return { execute };
});

The params array is the mechanism for bound parameters. Always use bound parameters instead of string concatenation when including user-controlled or variable values in your query. This prevents SQL injection and avoids the manual quoting issues that come with concatenating values directly into the query string.

How do you run SuiteQL from the REST API?

NetSuite exposes a SuiteQL endpoint via its REST API:

POST https://{accountId}.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql

The request body is a JSON object with a q property containing the query string:

{
  "q": "SELECT id, tranid, trandate FROM Transaction WHERE type = 'PurchOrd' ORDER BY trandate DESC LIMIT 100 OFFSET 0"
}

The response includes the result data and pagination information. The API uses limit and offset in the query itself for pagination, rather than the paged iterator pattern in SuiteScript.

Authentication follows the standard NetSuite REST authentication pattern: OAuth 2.0 with PKCE or Token-Based Authentication (TBA). The same credentials that authorize other REST API calls authorize the SuiteQL endpoint.

What are the most common SuiteQL query patterns?

Filtering transactions by date range:

SELECT id, tranid, trandate, entity
FROM Transaction
WHERE type = 'SalesOrd'
    AND trandate >= ?
    AND trandate < ?
ORDER BY trandate DESC

Pass the start and end dates as bound parameters.

Joining Transaction and TransactionLine:

SELECT
    t.id,
    t.tranid,
    tl.linesequencenumber,
    tl.item,
    BUILTIN.DF(tl.item) AS item_name,
    tl.quantity,
    tl.rate,
    tl.amount
FROM Transaction t
INNER JOIN TransactionLine tl ON t.id = tl.transaction
WHERE t.type = 'SalesOrd'
    AND tl.mainline = 'F'
ORDER BY t.id, tl.linesequencenumber

The mainline = 'F' filter excludes the transaction header line and returns only item lines.

Aggregate query with GROUP BY:

SELECT
    BUILTIN.DF(t.subsidiary) AS subsidiary_name,
    COUNT(t.id) AS order_count,
    SUM(t.foreigntotal) AS total_amount
FROM Transaction t
WHERE t.type = 'SalesOrd'
    AND t.trandate >= ?
GROUP BY t.subsidiary
ORDER BY total_amount DESC

Querying custom record types:

Custom record types have table names based on their script ID. A custom record with the script ID customrecord_service_request is queried as:

SELECT id, name, custrecord_status, custrecord_assigned_to
FROM customrecord_service_request
WHERE custrecord_status = ?

What are the performance and governance implications of SuiteQL?

SuiteQL queries consume governance units in SuiteScript. A query.runSuiteQL call costs 10 governance units per query, the same as a record.load. A query.runSuiteQLPaged call costs 10 governance units plus additional units per page fetch.

For queries that run inside User Event scripts (which have a 1,000-unit governance limit), keep the query simple and avoid situations where the script might run it in a loop. Scheduled Scripts and Map/Reduce scripts, with their higher limits, are better suited to complex SuiteQL operations.

Avoid queries that return large volumes of data inside synchronous contexts (Suitelets serving a user-facing page, for example). Use paged queries in batch contexts where the full result set is needed.

What changed in SuiteQL's default sort order in 2026.2?

In NetSuite 2026.2, the default sort order for SuiteQL queries that include the Transaction table changed. Queries that do not specify an ORDER BY clause now sort by trandate instead of the previous default of tranDisplayName. If you have existing queries that rely on implicit sort order, they may return results in a different sequence after the 2026.2 upgrade.

The fix is to add an explicit ORDER BY clause to any query where sort order matters. Do not rely on implicit ordering. See the SuiteQL default sort change post for the full context.

Building a SuiteQL-based report or pipeline?

If your use case is more complex than a one-off query, whether that is a scheduled extraction job, a high-volume data pipeline, or a multi-subsidiary report, a conversation before you build can save you a redesign after the first failure in Production.

Talk through your use case

When is SuiteQL work more than a query?

Learning SuiteQL syntax is one thing. Building a production-grade data pipeline with it is another.

The complexity compounds quickly in a few specific situations:

High-volume extraction from a live account. If you are pulling data from a NetSuite account that is actively processing transactions, you need to account for data that changes mid-extraction, for pagination that does not skip or double-count records at page boundaries, and for error handling that does not require a manual restart from zero when something goes wrong.

Integration pipelines where NetSuite is the source of truth. When external systems depend on data extracted from NetSuite via SuiteQL, the query needs to be reliable across NetSuite version upgrades. The 2026.2 sort change above is an example: a query that worked correctly before the upgrade may now return results in a different order, which can silently break a downstream system that assumed stable ordering.

Multi-subsidiary reporting with dimension filters. Queries that need to aggregate across subsidiaries while respecting dimension restrictions (department, class, location) require careful join logic. Getting the subsidiary filter wrong either over-restricts results (missing data) or under-restricts them (data leaking across entities), neither of which surfaces obviously in the query output.

Performance-sensitive contexts. A SuiteQL query running inside a Suitelet that serves a user-facing dashboard needs to return in under two seconds. The same query returning the same data for a nightly batch job can take thirty seconds without impact. Designing for both is not the same design.


If your SuiteQL use case is more than a one-off query, if you are building an extraction pipeline, wiring it into an integration, or designing a reporting system that needs to work reliably across NetSuite upgrades, talking through the design before you build is worth an hour. Most production SuiteQL failures are predictable once you know the failure modes. See our SuiteScript development page for how we approach these builds, and reach out if you want to discuss your specific situation first.

For related reading: NetSuite SuiteQL sort change in 2026.2, NetSuite SuiteQL bound parameters, and NetSuite saved search vs SuiteAnalytics Workbook.

Have a NetSuite challenge like this?

We work with post-go-live NetSuite accounts every day. Tell us what you're working on.