NetSuite SuiteQL Capabilities

Type: ERP Integration System: Oracle NetSuite (2026.1) Confidence: 0.92 Sources: 8 Verified: 2026-03-01 Freshness: 2026-03-01

TL;DR

System Profile

SuiteQL is Oracle NetSuite's SQL-based query language, introduced in 2020.1. It queries the SuiteAnalytics data source — the same data exposed in SuiteAnalytics Workbook — with role-based access restrictions enforced automatically. SuiteQL supports both SQL-92 and Oracle SQL syntax (but not mixed in a single query) and is available across all NetSuite editions that have SuiteCloud enabled. SuiteQL is read-only — it queries records but cannot insert, update, or delete them. [src1, src5]

PropertyValue
VendorOracle
SystemOracle NetSuite 2026.1
API SurfaceSuiteQL (SQL-92 / Oracle SQL)
Current API Version2026.1 (Cloud Latest)
Editions CoveredAll editions with SuiteCloud enabled
DeploymentCloud
API DocsSuiteQL Documentation
StatusGA (since 2020.1)

API Surfaces & Capabilities

SuiteQL is accessible through three primary interfaces, each with different record limits and pagination behavior. [src1, src2, src5, src6]

API SurfaceProtocolBest ForMax Records/RequestPaginationGovernanceReal-time?
N/query (runSuiteQL)SuiteScript 2.xQuick in-script queries, <5K rows5,000None (single result set)10 unitsYes
N/query (runSuiteQLPaged)SuiteScript 2.xLarge result sets, iterative processing1,000/pagePage-based (5-1,000 per page)10 unitsYes
SuiteTalk RESTHTTPS/JSON POSTExternal integrations, middleware3,000/pagelimit/offset in URL paramsN/A (REST rate limits)Yes
SuiteAnalytics ConnectJDBC/ODBCBI tools, bulk analytics, unlimited rowsUnlimitedClient-managedN/ANo (near real-time)

Rate Limits & Quotas

Per-Request Limits

Limit TypeValueApplies ToNotes
Max records per runSuiteQL()5,000N/query moduleUse runSuiteQLPaged() for larger sets [src6]
Max records per REST page3,000SuiteTalk REST APIPaginate with limit/offset [src8]
Max page size (paged)1,000runSuiteQLPaged()Min 5, default 50 [src5]
Min page size (paged)5runSuiteQLPaged()[src5]
CLOB sort limit250 charactersAll interfacesSorting on CLOB fields only evaluates first 250 chars [src4]

Rolling / Daily Limits

Limit TypeValueWindowNotes
Total REST query results100,000 rowsPer queryHard ceiling — use SuiteAnalytics Connect for larger sets [src2]
SuiteScript governance10 units per queryPer executionSame for runSuiteQL and runSuiteQLPaged [src6]
REST API concurrencyAccount-levelPer accountShared with all REST endpoints — NetSuite fair-use policy
SuiteAnalytics ConnectUnlimited rowsN/ARequires feature enabled, uses NetSuite2.com data source [src2]

Transaction / Governor Limits

NetSuite SuiteScript governance applies to SuiteQL when executed via the N/query module. Each script type has a total governance budget. [src6]

Limit TypePer-Transaction ValueNotes
runSuiteQL() cost10 unitsPer call, regardless of result size [src6]
runSuiteQLPaged() cost10 unitsPer call, pages iterate within same budget [src5]
Scheduled Script budget10,000 unitsCan run ~1,000 SuiteQL queries per execution
Map/Reduce budget10,000 units per phaseUse for very large data processing jobs
User Event Script budget1,000 unitsLimits SuiteQL calls in before/afterSubmit
Client Script budget1,000 unitsLimits SuiteQL calls in browser context

Authentication

FlowUse WhenToken LifetimeRefresh?Notes
OAuth 2.0 Authorization CodeREST API with user context, new integrationsAccess: configurable, Refresh: until revokedYesNow the recommended flow for REST Web Services [src2]
Token-Based Auth (TBA)REST API server-to-server, legacyUntil revokedNo expiryOAuth 1.0 signature — still fully supported [src2]
SuiteScript contextIn-script queries (N/query)Script execution durationN/ARuns as the user/role executing the script [src5]
SuiteAnalytics ConnectJDBC/ODBC BI toolsSession-basedReconnectSeparate credentials [src1]

Authentication Gotchas

Constraints

Integration Pattern Decision Tree

START — User needs to query NetSuite data
├── Where is the query executed?
│   ├── Inside NetSuite (SuiteScript)
│   │   ├── Result set < 5,000 rows?
│   │   │   ├── YES → query.runSuiteQL() — simplest, 10 governance units
│   │   │   └── NO → query.runSuiteQLPaged() — 1,000 rows/page, iterate
│   │   └── Need complex SQL (JOINs, subqueries, UNION)?
│   │       ├── YES → SuiteQL (this card)
│   │       └── NO, simple field lookups → search.lookupFields (5 units, faster)
│   ├── External system (middleware, app)
│   │   ├── Result set < 100,000 rows?
│   │   │   ├── YES → SuiteTalk REST /query/v1/suiteql — POST, paginate
│   │   │   └── NO → SuiteAnalytics Connect (JDBC/ODBC) — no row ceiling
│   │   └── Need real-time results?
│   │       ├── YES → REST API (sub-second for simple queries)
│   │       └── NO → SuiteAnalytics Connect (better for bulk)
│   └── BI/Reporting tool
│       └── SuiteAnalytics Connect → JDBC/ODBC driver, unlimited rows
├── What SQL features are needed?
│   ├── JOINs across multiple record types → SuiteQL
│   ├── UNION queries → SuiteQL only (not in saved searches)
│   ├── Subqueries → SuiteQL only
│   ├── GROUP BY + HAVING → SuiteQL
│   └── Simple filters + sort → Saved search may suffice
└── Need to write data?
    ├── YES → Use SuiteTalk REST/SOAP or N/record module
    └── NO → SuiteQL is the right choice

Quick Reference

SuiteQL Supported SQL Syntax

SQL FeatureSupported?Syntax ExampleNotes
SELECTYesSELECT id, companyname FROM customerField names use internal IDs [src3]
WHEREYesWHERE isperson = 'T'Supports =, <>, >, <, LIKE, IN, BETWEEN [src3]
JOIN (INNER)YesINNER JOIN transactionLine ON ...Full ANSI JOIN syntax [src3]
JOIN (LEFT/RIGHT)YesLEFT JOIN customer ON ...Outer joins supported [src7]
GROUP BYYesGROUP BY emailStandard SQL aggregation [src3]
HAVINGYesHAVING COUNT(*) > 2Filter on aggregates [src3]
ORDER BYYesORDER BY createdate DESCCLOB fields: first 250 chars only [src4]
UNIONYesSELECT ... UNION SELECT ...Not available in saved searches [src3]
SubqueriesYesWHERE id IN (SELECT ...)In SELECT, FROM, and WHERE [src3]
TOP NYesSELECT TOP 10 ...Alternative to LIMIT [src3]
DISTINCTYesSELECT DISTINCT emailDeduplicates results [src3]
CASEYesCASE WHEN ... THEN ... ENDConditional logic [src7]
AggregatesYesCOUNT(*), SUM(amount)COUNT, SUM, AVG, MIN, MAX [src3]
COALESCEYesCOALESCE(email, 'none')Null handling [src3]
EXISTSYesWHERE EXISTS(SELECT 1 FROM ...)Existence checks [src3]
Parameter bindingYesWHERE field = ? with params arrayPrevents SQL injection [src5]
INSERT/UPDATE/DELETENoN/ASuiteQL is read-only [src1]
Mixed SQL-92 + OracleNoN/APick one syntax per query [src4]

Step-by-Step Integration Guide

1. Execute a SuiteQL query via SuiteScript (N/query module)

Use query.runSuiteQL() for result sets under 5,000 rows. This is the simplest approach for in-script data access. [src5, src6]

/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 */
define(['N/query', 'N/log'], (query, log) => {
    const execute = (context) => {
        const results = query.runSuiteQL({
            query: `
                SELECT id, companyname, email, datecreated
                FROM customer
                WHERE isinactive = 'F'
                ORDER BY datecreated DESC
            `
        });
        const customers = results.asMappedResults();
        log.debug('Customer count', customers.length);

        // Parameterized query — prevents SQL injection
        const filtered = query.runSuiteQL({
            query: `
                SELECT id, companyname FROM customer
                WHERE subsidiary = ? AND datecreated > TO_DATE(?, 'YYYY-MM-DD')
            `,
            params: [2, '2025-01-01']
        });
        log.debug('Filtered count', filtered.asMappedResults().length);
    };
    return { execute };
});

Verify: Check script execution log → expected: Customer count: [N]

2. Handle pagination with runSuiteQLPaged()

For result sets exceeding 5,000 rows, use paged execution with page sizes between 5 and 1,000. [src5]

/**
 * @NApiVersion 2.1
 * @NScriptType MapReduceScript
 */
define(['N/query', 'N/log'], (query, log) => {
    const getInputData = (context) => {
        const pagedData = query.runSuiteQLPaged({
            query: `
                SELECT t.id, t.tranid, t.trandate, tl.item, tl.amount
                FROM transaction t
                INNER JOIN transactionLine tl ON t.id = tl.transaction
                WHERE t.type = 'SalesOrd'
                  AND t.trandate >= TO_DATE('2025-01-01', 'YYYY-MM-DD')
                ORDER BY t.trandate DESC
            `,
            pageSize: 1000
        });
        const allResults = [];
        pagedData.pageRanges.forEach((pageRange) => {
            const page = pagedData.fetch({ index: pageRange.index });
            allResults.push(...page.data.asMappedResults());
        });
        return allResults;
    };
    const map = (context) => { /* process each row */ };
    const summarize = (summary) => { /* log results */ };
    return { getInputData, map, summarize };
});

Verify: Map/Reduce deployment log → expected: Total rows: [N]

3. Query via REST Web Services (external integration)

POST to /services/rest/query/v1/suiteql with limit and offset URL parameters. The Prefer: transient header is required. [src2]

curl -X POST \
  'https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=1000&offset=0' \
  -H 'Content-Type: application/json' \
  -H 'Prefer: transient' \
  -H 'Authorization: OAuth realm="ACCOUNT_ID",oauth_consumer_key="...",oauth_token="...",...' \
  -d '{"q": "SELECT id, companyname, email FROM customer WHERE isinactive = '\''F'\'' ORDER BY id"}'

Verify: Response contains totalResults, hasMore, and items array

4. Paginate REST results beyond 1,000 records

Loop through pages using offset until hasMore is false or you hit the 100K ceiling. [src2, src8]

import requests
from requests_oauthlib import OAuth1

auth = OAuth1(client_key='KEY', client_secret='SECRET',
              resource_owner_key='TOKEN', resource_owner_secret='TOKEN_SECRET',
              realm='ACCOUNT_ID', signature_method='HMAC-SHA256')
base_url = 'https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql'
sql = "SELECT id, companyname FROM customer WHERE isinactive = 'F' ORDER BY id"

all_records, offset, limit = [], 0, 1000
while True:
    resp = requests.post(f'{base_url}?limit={limit}&offset={offset}',
                         json={'q': sql}, auth=auth,
                         headers={'Prefer': 'transient', 'Content-Type': 'application/json'})
    resp.raise_for_status()
    data = resp.json()
    all_records.extend(data['items'])
    if not data.get('hasMore', False) or offset >= 100000:
        break
    offset += limit

Verify: len(all_records) matches totalResults (if under 100K)

Code Examples

Python: Paginated SuiteQL via REST API

# Input:  OAuth 1.0 credentials, SuiteQL query string
# Output: All matching records as list of dicts (up to 100K)

import requests  # requests==2.31.0
from requests_oauthlib import OAuth1  # requests-oauthlib==1.3.1

def run_suiteql(account_id, auth, query, max_rows=100000):
    """Execute SuiteQL via REST with automatic pagination."""
    url = f'https://{account_id}.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql'
    headers = {'Prefer': 'transient', 'Content-Type': 'application/json'}
    results, offset, limit = [], 0, 1000
    while offset < max_rows:
        resp = requests.post(f'{url}?limit={limit}&offset={offset}',
                             json={'q': query}, auth=auth, headers=headers)
        resp.raise_for_status()
        data = resp.json()
        results.extend(data['items'])
        if not data.get('hasMore', False):
            break
        offset += limit
    return results

JavaScript/Node.js: SuiteQL in SuiteScript 2.1

// Input:  SuiteQL query string
// Output: Array of result objects

define(['N/query'], (query) => {
    function runSuiteQLAll(sql, params) {
        const pagedData = query.runSuiteQLPaged({
            query: sql, params: params || [], pageSize: 1000
        });
        const results = [];
        pagedData.pageRanges.forEach((range) => {
            results.push(...pagedData.fetch({ index: range.index }).data.asMappedResults());
        });
        return results;
    }
    return { runSuiteQLAll };
});

cURL: Quick SuiteQL test via REST

# Input:  NetSuite account ID, TBA credentials configured
# Output: JSON array of first 10 customers

curl -X POST \
  'https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=10' \
  -H 'Content-Type: application/json' \
  -H 'Prefer: transient' \
  -H 'Authorization: OAuth realm="ACCOUNT_ID",...' \
  -d '{"q": "SELECT TOP 10 id, companyname, email FROM customer"}'

Data Mapping

SuiteQL Record and Field Name Reference

UI LabelSuiteQL Record/FieldTypeNotes
CustomercustomerRecord typeInternal name is lowercase [src7]
Company NamecompanynameStringNo underscores in standard fields
TransactiontransactionRecord typeIncludes all transaction types
Transaction LinetransactionLineRecord typeJOIN via transactionLine.transaction = transaction.id
ItemitemRecord typeAll item types (inventory, service, etc.)
Sales Order typetype = 'SalesOrd'FilterType codes differ from UI names
Main Line flagmainline = 'T' or 'F'Boolean-like'T' = header, 'F' = line items [src7]
Custom fieldcustbody_fieldid / custcol_fieldidCustomBody-level vs column-level

Data Type Gotchas

Error Handling & Failure Points

Common Error Codes

CodeMeaningCauseResolution
INVALID_SEARCHQuery syntax errorMalformed SQL, unsupported function, or mixed syntaxValidate in SuiteAnalytics Workbook; use Oracle SQL syntax [src4]
SSS_USAGE_LIMIT_EXCEEDEDGovernance units exhaustedToo many queries in one script executionMove to Map/Reduce script type [src6]
MISSING_REQD_ARGUMENTRequired parameter missingOmitted query propertyEnsure options has query string [src6]
SSS_INVALID_TYPE_ARGInvalid parameter typeNon-string/number in paramsOnly string, number, boolean in params [src6]
INSUFFICIENT_PERMISSIONRole lacks accessMissing SuiteAnalytics permissionsEnable SuiteAnalytics Workbook on role [src1]
HTTP 400Bad request (REST)Missing Prefer: transient headerAdd required header [src2]
HTTP 401Authentication failedInvalid or expired TBA tokensRegenerate OAuth tokens [src2]

Failure Points in Production

Anti-Patterns

Wrong: Fetching all records to filter in code

// BAD — wastes governance units and memory
const all = query.runSuiteQL({ query: 'SELECT * FROM customer' });
const active = all.asMappedResults().filter(c => c.isinactive === 'F');

Correct: Filter in the query WHERE clause

// GOOD — database does the filtering, returns fewer rows
const active = query.runSuiteQL({
    query: "SELECT id, companyname, email FROM customer WHERE isinactive = 'F'"
});

Wrong: Using runSuiteQL() for large result sets

// BAD — silently truncates at 5,000 rows with no error
const results = query.runSuiteQL({
    query: 'SELECT id, tranid, amount FROM transaction ORDER BY trandate DESC'
});
// results.asMappedResults() has at most 5,000 rows — data loss!

Correct: Use runSuiteQLPaged() for large sets

// GOOD — iterates all pages, no truncation
const paged = query.runSuiteQLPaged({
    query: 'SELECT id, tranid, amount FROM transaction ORDER BY trandate DESC',
    pageSize: 1000
});
const results = [];
paged.pageRanges.forEach(range => {
    results.push(...paged.fetch({ index: range.index }).data.asMappedResults());
});

Wrong: Using SQL-92 syntax with complex queries

-- BAD — SQL-92 JOIN syntax risks performance issues and timeouts
SELECT t.id, c.companyname
FROM transaction t, customer c
WHERE t.entity = c.id AND t.trandate > DATE '2025-01-01'

Correct: Use Oracle SQL syntax consistently

-- GOOD — Oracle syntax avoids performance issues
SELECT t.id, c.companyname
FROM transaction t
INNER JOIN customer c ON t.entity = c.id
WHERE t.trandate > TO_DATE('2025-01-01', 'YYYY-MM-DD')

Common Pitfalls

Diagnostic Commands

# Test SuiteQL connectivity via REST
curl -X POST \
  'https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=1' \
  -H 'Content-Type: application/json' -H 'Prefer: transient' \
  -H 'Authorization: OAuth ...' \
  -d '{"q": "SELECT TOP 1 id, companyname FROM customer"}'

# Verify field names for a record type
curl -X POST \
  'https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=1' \
  -H 'Content-Type: application/json' -H 'Prefer: transient' \
  -H 'Authorization: OAuth ...' \
  -d '{"q": "SELECT * FROM customer WHERE ROWNUM <= 1"}'

# Count total records (check for 100K ceiling risk)
curl -X POST \
  'https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql' \
  -H 'Content-Type: application/json' -H 'Prefer: transient' \
  -H 'Authorization: OAuth ...' \
  -d '{"q": "SELECT COUNT(*) as total FROM transaction"}'

Version History & Compatibility

Feature / VersionReleaseStatusNotes
SuiteQL introduced2020.1GAFirst availability in N/query module [src6]
REST /query/v1/suiteql2020.1GAPOST endpoint for external queries [src2]
SuiteAnalytics ConnectPre-2020GAJDBC/ODBC, unlimited rows [src1]
runSuiteQLPaged()2020.1GAPaged execution, 5-1000 page size [src5]
customScriptId parameter2021.1+GAPerformance tracking [src6]
metaDataProvider parameter2023.1+GASUITE_QL or STATIC permission checking [src6]
NetSuite.com data source EOL2025.1EndedSupport ended; migrate to NetSuite2.com data source
NetSuite.com data source removal2026.1RemovedNetSuite.com fully removed; NetSuite2.com only
OAuth 2.0 promoted as default2025.xGAOAuth 2.0 now recommended over TBA for REST
SOAP Web Services last endpoint2025.2LastNo new SOAP endpoints from 2026.1

Deprecation Policy

Oracle NetSuite follows a cloud-only release model with biannual major releases (YYYY.1 and YYYY.2). SuiteQL is the strategic query interface — Oracle has not announced any deprecation timeline. Saved searches remain supported but new features (UNION, complex subqueries) are SuiteQL-only. The NetSuite.com SuiteAnalytics data source was removed in 2026.1 — all SuiteQL queries now run against the NetSuite2.com data source, which has renamed some tables and fields (e.g., standalone Invoice table deprecated in favor of unified Transactions table). [src1, src7]

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Complex JOINs across multiple record typesSimple single-record lookups by IDsearch.lookupFields (5 governance units)
UNION queries combining different record typesNeed to write/update recordsN/record module or SuiteTalk REST API
GROUP BY + HAVING aggregationsEnd-user dashboards and portletsSaved searches with summary types
Subqueries in WHERE or FROMResult set exceeds 100K rows (REST)SuiteAnalytics Connect (JDBC/ODBC)
External middleware querying NetSuiteReal-time event notificationsUser Event scripts or SuiteFlow

Important Caveats

Related Units