---
# === IDENTITY ===
id: business/erp-integration/netsuite-suiteql-capabilities/2026
canonical_question: "What are NetSuite SuiteQL capabilities - 1000-record pagination, 100K row ceiling, query syntax?"
aliases:
  - "NetSuite SuiteQL query language reference"
  - "SuiteQL SQL syntax and pagination limits"
  - "NetSuite N/query module SuiteQL capabilities"
  - "SuiteQL vs SuiteSearch saved search comparison"
entity_type: erp_integration
domain: business > erp-integration > netsuite-suiteql-capabilities
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Oracle NetSuite"
    vendor: "Oracle"
    version: "2026.1"
    edition: "All editions (SuiteCloud enabled)"
    deployment: cloud
    api_surface: "SuiteQL (SQL-92 / Oracle SQL)"

# === VERIFICATION ===
last_verified: 2026-03-01
confidence: 0.92
version: 1.0
first_published: 2026-03-01

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: volatile
  last_breaking_change: "2020.1 — SuiteQL introduced in N/query module"
  next_review: 2026-08-28
  change_sensitivity: high

# === CONSTRAINTS ===
constraints:
  - "100,000 row ceiling via REST Web Services — queries returning more rows are truncated silently"
  - "5,000 record maximum per runSuiteQL() call in SuiteScript N/query module"
  - "3,000 record maximum per SuiteTalk REST API page"
  - "1,000 record maximum page size for runSuiteQLPaged() in SuiteScript"
  - "10 governance units consumed per runSuiteQL() / runSuiteQLPaged() call"
  - "Cannot mix SQL-92 and Oracle SQL syntax in the same query — use Oracle SQL to avoid performance issues"
  - "CLOB field sorting only evaluates first 250 characters"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User needs SuiteTalk REST/SOAP record CRUD (not analytics queries)"
    use_instead: "business/erp-integration/netsuite-suitetalk-rest-api/2026"
  - condition: "User needs NetSuite bulk CSV import/export (not SQL queries)"
    use_instead: "business/erp-integration/netsuite-csv-import/2026"
  - condition: "User needs Salesforce SOQL query capabilities"
    use_instead: "business/erp-integration/salesforce-rest-api/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: integration_pattern
    question: "What integration pattern do you need?"
    type: choice
    options:
      - "real-time sync (individual records, <1s latency)"
      - "batch/bulk (scheduled, high volume)"
      - "event-driven (webhook, CDC, platform events)"
      - "file-based (CSV/XML import/export)"
  - key: data_volume
    question: "What's your daily data volume?"
    type: choice
    options:
      - "< 1,000 records/day"
      - "1,000-100,000 records/day"
      - "> 100,000 records/day"
  - key: direction
    question: "What's the data flow direction?"
    type: choice
    options:
      - "inbound (writing to this ERP)"
      - "outbound (reading from this ERP)"
      - "bidirectional sync"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/netsuite-suiteql-capabilities/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-03-01)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "business/erp-integration/netsuite-suitetalk-rest-api/2026"
      label: "NetSuite SuiteTalk REST API — record-level CRUD operations"
  solves: []
  alternative_to:
    - id: "business/erp-integration/netsuite-csv-import/2026"
      label: "NetSuite CSV import — file-based bulk data loading"
  often_confused_with:
    - id: "business/erp-integration/netsuite-saved-search-api/2026"
      label: "NetSuite saved search (SuiteSearch) — legacy query interface"

# === SOURCES ===
sources:
  - id: src1
    title: "SuiteQL — Oracle NetSuite Help Center"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_156257770590.html
    type: official_docs
    published: 2026-02-23
    reliability: authoritative
  - id: src2
    title: "Executing SuiteQL Queries Through REST Web Services"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_157909186990.html
    type: official_docs
    published: 2026-02-23
    reliability: authoritative
  - id: src3
    title: "SuiteQL Syntax and Examples"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_156257790831.html
    type: official_docs
    published: 2026-02-23
    reliability: authoritative
  - id: src4
    title: "SuiteQL Limitations and Exceptions"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_156257796125.html
    type: official_docs
    published: 2026-02-23
    reliability: authoritative
  - id: src5
    title: "SuiteQL in the N/query Module"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_157960623712.html
    type: official_docs
    published: 2026-02-23
    reliability: authoritative
  - id: src6
    title: "query.runSuiteQL(options)"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_157960542026.html
    type: official_docs
    published: 2026-02-23
    reliability: authoritative
  - id: src7
    title: "SuiteQL: Complete Query Language Guide for NetSuite (2026)"
    author: BrokenRubik
    url: https://www.brokenrubik.com/blog/suiteql-complete-guide
    type: technical_blog
    published: 2026-01-15
    reliability: moderate_high
  - id: src8
    title: "Paginating SuiteQL Query Results"
    author: Tim Dietrich
    url: https://timdietrich.me/blog/netsuite-suiteql-pagination/
    type: technical_blog
    published: 2025-06-10
    reliability: moderate_high
---

# NetSuite SuiteQL Capabilities

## TL;DR

- **Bottom line**: SuiteQL is NetSuite's SQL-92/Oracle SQL query language available via N/query module (SuiteScript), REST Web Services, and SuiteAnalytics Connect — supports SELECT, JOIN, GROUP BY, HAVING, UNION, and subqueries. [src1]
- **Key limit**: 100,000 row ceiling via REST Web Services; 5,000 rows per runSuiteQL() call; 1,000 rows per page for paged queries. [src2, src6]
- **Watch out for**: Cannot mix SQL-92 and Oracle SQL syntax in the same query — always use Oracle SQL syntax to avoid critical performance issues and timeouts. [src4]
- **Best for**: Complex ad-hoc queries, multi-table JOINs, aggregations, and analytics that saved searches cannot handle. [src7]
- **Authentication**: Token-Based Authentication (TBA) or OAuth 2.0 for REST API; SuiteScript runs in authenticated context automatically. OAuth 2.0 is now the recommended flow. [src2]

## 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. [src1]

SuiteQL is read-only — it queries records but cannot insert, update, or delete them. For write operations, use SuiteTalk REST/SOAP APIs or SuiteScript record module. SuiteQL enforces the same role-based access restrictions used in SuiteAnalytics Workbook, meaning query results are filtered by the executing user's role permissions. [src1, src5]

| Property | Value |
|---|---|
| **Vendor** | Oracle |
| **System** | Oracle NetSuite 2026.1 |
| **API Surface** | SuiteQL (SQL-92 / Oracle SQL) |
| **Current API Version** | 2026.1 (Cloud Latest) |
| **Editions Covered** | All editions with SuiteCloud enabled |
| **Deployment** | Cloud |
| **API Docs URL** | [SuiteQL Documentation](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_156257770590.html) |
| **Status** | GA (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 Surface | Protocol | Best For | Max Records/Request | Pagination | Governance | Real-time? |
|---|---|---|---|---|---|---|
| N/query (runSuiteQL) | SuiteScript 2.x | Quick in-script queries, <5K rows | 5,000 | None (single result set) | 10 units | Yes |
| N/query (runSuiteQLPaged) | SuiteScript 2.x | Large result sets, iterative processing | 1,000/page | Page-based (5-1,000 per page) | 10 units | Yes |
| SuiteTalk REST | HTTPS/JSON POST | External integrations, middleware | 3,000/page | limit/offset in URL params | N/A (REST rate limits) | Yes |
| SuiteAnalytics Connect | JDBC/ODBC | BI tools, bulk analytics, unlimited rows | Unlimited | Client-managed | N/A | No (near real-time) |

## Rate Limits & Quotas

### Per-Request Limits

| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Max records per runSuiteQL() | 5,000 | N/query module | Use runSuiteQLPaged() for larger sets [src6] |
| Max records per REST page | 3,000 | SuiteTalk REST API | Paginate with limit/offset [src8] |
| Max page size (paged) | 1,000 | runSuiteQLPaged() | Min 5, default 50 [src5] |
| Min page size (paged) | 5 | runSuiteQLPaged() | [src5] |
| CLOB sort limit | 250 characters | All interfaces | Sorting on CLOB fields only evaluates first 250 chars [src4] |

### Rolling / Daily Limits

| Limit Type | Value | Window | Notes |
|---|---|---|---|
| Total REST query results | 100,000 rows | Per query | Hard ceiling — use SuiteAnalytics Connect for larger sets [src2] |
| SuiteScript governance | 10 units per query | Per execution | Same for runSuiteQL and runSuiteQLPaged [src6] |
| REST API concurrency | Account-level | Per account | Shared with all REST endpoints — NetSuite fair-use policy |
| SuiteAnalytics Connect | Unlimited rows | N/A | Requires 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 Type | Per-Transaction Value | Notes |
|---|---|---|
| runSuiteQL() cost | 10 units | Per call, regardless of result size [src6] |
| runSuiteQLPaged() cost | 10 units | Per call, pages iterate within same budget [src5] |
| Scheduled Script budget | 10,000 units | Can run ~1,000 SuiteQL queries per execution |
| Map/Reduce budget | 10,000 units per phase | Use for very large data processing jobs |
| User Event Script budget | 1,000 units | Limits SuiteQL calls in before/afterSubmit |
| Client Script budget | 1,000 units | Limits SuiteQL calls in browser context |

## Authentication

| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| OAuth 2.0 Authorization Code | REST API with user context, new integrations | Access: configurable, Refresh: until revoked | Yes | Now the recommended flow for REST Web Services [src2] |
| Token-Based Authentication (TBA) | REST API server-to-server, legacy integrations | Until revoked | No expiry | OAuth 1.0 signature — still fully supported [src2] |
| SuiteScript context | In-script queries (N/query) | Script execution duration | N/A | Runs as the user/role executing the script [src5] |
| SuiteAnalytics Connect | JDBC/ODBC BI tools | Session-based | Reconnect | Separate credentials from SuiteScript [src1] |

### Authentication Gotchas

- TBA tokens are role-scoped — the integration's permissions are determined by the role assigned to the token, not the user. Always create a dedicated integration role with minimal permissions. [src2]
- SuiteQL enforces SuiteAnalytics Workbook role-based access, not record-level security — a role with access to the Customer record type sees ALL customers, regardless of subsidiary restrictions on saved searches. [src1]
- REST API requests require the `Prefer: transient` header — omitting it returns a 400 error with no useful message. [src2]

## Constraints
<!-- Agents: read this section before recommending any integration approach from this unit.
     These are hard constraints that must not be violated. -->

- 100,000 row ceiling on REST Web Services — queries exceeding this return only the first 100K rows with no error or warning. Use SuiteAnalytics Connect (JDBC/ODBC) for larger datasets. [src2]
- 5,000 record maximum per runSuiteQL() call — no built-in pagination; switch to runSuiteQLPaged() for larger result sets. [src6]
- SuiteQL is read-only — cannot INSERT, UPDATE, or DELETE records. Use N/record or SuiteTalk REST for write operations. [src1]
- Cannot mix SQL-92 and Oracle SQL syntax in the same query — using SQL-92 risks critical performance issues and timeouts. [src4]
- Custom fields in Analytic APIs are not supported for listing application data. [src4]
- Square bracket notation `[ ]` is not supported in SuiteQL syntax. [src4]
- 10 governance units per query execution in SuiteScript — budget carefully in User Event scripts (1,000 unit budget). [src6]

## 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 for single records)
│   ├── External system (middleware, app)
│   │   ├── Result set < 100,000 rows?
│   │   │   ├── YES → SuiteTalk REST /query/v1/suiteql — POST, paginate with limit/offset
│   │   │   └── NO → SuiteAnalytics Connect (JDBC/ODBC) — no row ceiling
│   │   └── Need real-time results?
│   │       ├── YES → REST API (sub-second for simple queries)
│   │       └── NO → SuiteAnalytics Connect (near real-time, better for bulk)
│   └── BI/Reporting tool
│       └── SuiteAnalytics Connect → JDBC/ODBC driver, unlimited rows
├── What SQL features are needed?
│   ├── JOINs across record types → SuiteQL (saved searches limited to 2-3 joins)
│   ├── UNION queries → SuiteQL only (not supported in saved searches)
│   ├── Subqueries → SuiteQL only
│   ├── GROUP BY + HAVING → SuiteQL (saved searches have limited summary)
│   └── Simple filters + sort → Saved search may suffice
└── Need to write data?
    ├── YES → Use SuiteTalk REST/SOAP or N/record module, NOT SuiteQL
    └── NO → SuiteQL is the right choice
```

## Quick Reference

### SuiteQL Supported SQL Syntax

| SQL Feature | Supported? | Syntax Example | Notes |
|---|---|---|---|
| SELECT | Yes | `SELECT id, companyname FROM customer` | Field names use internal IDs [src3] |
| WHERE | Yes | `WHERE isperson = 'T'` | Supports =, <>, >, <, LIKE, IN, BETWEEN [src3] |
| JOIN (INNER) | Yes | `FROM transaction INNER JOIN transactionLine ON ...` | Full ANSI JOIN syntax [src3] |
| JOIN (LEFT/RIGHT) | Yes | `LEFT JOIN customer ON ...` | Outer joins supported [src7] |
| GROUP BY | Yes | `GROUP BY email` | Standard SQL aggregation [src3] |
| HAVING | Yes | `HAVING COUNT(*) > 2` | Filter on aggregates [src3] |
| ORDER BY | Yes | `ORDER BY createdate DESC` | CLOB fields: first 250 chars only [src4] |
| UNION | Yes | `SELECT ... UNION SELECT ...` | Combines result sets — not in saved searches [src3] |
| Subqueries | Yes | `WHERE id IN (SELECT ...)` | In SELECT, FROM, and WHERE clauses [src3] |
| TOP N | Yes | `SELECT TOP 10 ...` | Alternative to LIMIT for result restriction [src3] |
| DISTINCT | Yes | `SELECT DISTINCT email` | Deduplicates results [src3] |
| CASE | Yes | `CASE WHEN ... THEN ... END` | Conditional logic in SELECT [src7] |
| COUNT/SUM/AVG/MIN/MAX | Yes | `SELECT COUNT(*), SUM(amount)` | Standard aggregate functions [src3] |
| COALESCE | Yes | `COALESCE(email, 'none')` | Null handling [src3] |
| EXISTS | Yes | `WHERE EXISTS(SELECT 1 FROM ...)` | Existence checks [src3] |
| Parameter binding | Yes | `WHERE field = ?` with params array | Prevents SQL injection [src5] |
| INSERT/UPDATE/DELETE | No | N/A | SuiteQL is read-only [src1] |
| CREATE TABLE/DDL | No | N/A | No schema modification [src1] |
| Mixed SQL-92 + Oracle | No | N/A | Pick 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]

```javascript
/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 */
define(['N/query', 'N/log'], (query, log) => {
    const execute = (context) => {
        // Basic SuiteQL query — 10 governance units
        const results = query.runSuiteQL({
            query: `
                SELECT id, companyname, email, datecreated
                FROM customer
                WHERE isinactive = 'F'
                ORDER BY datecreated DESC
            `
        });

        // asMappedResults() returns [{id: 1, companyname: 'Acme', ...}, ...]
        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']
        });

        const rows = filtered.asMappedResults();
        log.debug('Filtered count', rows.length);
    };
    return { execute };
});
```

**Verify**: Check script execution log in NetSuite → Customization → Scripting → Script Deployments → 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]

```javascript
/**
 * @NApiVersion 2.1
 * @NScriptType MapReduceScript
 */
define(['N/query', 'N/log'], (query, log) => {
    const getInputData = (context) => {
        // Paged query — 10 governance units, max 1,000 per page
        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  // min 5, max 1000, default 50
        });

        log.debug('Total pages', pagedData.pageRanges.length);

        const allResults = [];
        pagedData.pageRanges.forEach((pageRange) => {
            const page = pagedData.fetch({ index: pageRange.index });
            const rows = page.data.asMappedResults();
            allResults.push(...rows);
        });

        log.debug('Total rows', allResults.length);
        return allResults;
    };

    const map = (context) => { /* process each row */ };
    const summarize = (summary) => { /* log results */ };

    return { getInputData, map, summarize };
});
```

**Verify**: Map/Reduce deployment log → expected: `Total pages: [N]`, `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]

```bash
# Input:  NetSuite account ID, TBA OAuth 1.0 credentials
# Output: JSON with items array, count, offset, totalResults, hasMore

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="...",oauth_signature_method="HMAC-SHA256",oauth_timestamp="...",oauth_nonce="...",oauth_version="1.0",oauth_signature="..."' \
  -d '{
    "q": "SELECT id, companyname, email FROM customer WHERE isinactive = '\''F'\'' ORDER BY id"
  }'
```

Response:
```json
{
  "links": [],
  "count": 1000,
  "hasMore": true,
  "offset": 0,
  "totalResults": 4523,
  "items": [
    { "links": [], "id": 1, "companyname": "Acme Corp", "email": "info@acme.com" }
  ]
}
```

**Verify**: `totalResults` should match expected record count; `hasMore: true` means more pages available

### 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]

```python
# Input:  NetSuite account ID, OAuth 1.0 credentials
# Output: All customer records up to 100K row ceiling

import requests
from requests_oauthlib import OAuth1

auth = OAuth1(
    client_key='CONSUMER_KEY',
    client_secret='CONSUMER_SECRET',
    resource_owner_key='TOKEN_ID',
    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'
query = "SELECT id, companyname, email FROM customer WHERE isinactive = 'F' ORDER BY id"

all_records = []
offset = 0
limit = 1000  # max 3,000 per page via REST

while True:
    response = requests.post(
        f'{base_url}?limit={limit}&offset={offset}',
        json={'q': query},
        auth=auth,
        headers={'Prefer': 'transient', 'Content-Type': 'application/json'}
    )
    response.raise_for_status()
    data = response.json()

    all_records.extend(data['items'])
    print(f"Fetched {len(data['items'])} records (offset={offset}, total={data['totalResults']})")

    if not data.get('hasMore', False):
        break
    offset += limit

    # Guard against 100K ceiling
    if offset >= 100000:
        print("WARNING: Hit 100K row ceiling. Use SuiteAnalytics Connect for full dataset.")
        break

print(f"Total records retrieved: {len(all_records)}")
```

**Verify**: `Total records retrieved` matches `totalResults` from first response (if under 100K)

## Code Examples

### Python: Paginated SuiteQL via REST API

```python
# 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

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

/**
 * @NApiVersion 2.1
 * @NModuleScope SameAccount
 */
define(['N/query'], (query) => {
    /**
     * Run SuiteQL with automatic paging for large result sets.
     * @param {string} sql - SuiteQL query
     * @param {Array} [params] - Bind parameters
     * @returns {Array<Object>} All result rows
     */
    function runSuiteQLAll(sql, params) {
        const pagedData = query.runSuiteQLPaged({
            query: sql,
            params: params || [],
            pageSize: 1000
        });
        const results = [];
        pagedData.pageRanges.forEach((range) => {
            const page = pagedData.fetch({ index: range.index });
            results.push(...page.data.asMappedResults());
        });
        return results;
    }
    return { runSuiteQLAll };
});
```

### cURL: Quick SuiteQL test via REST

```bash
# 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",oauth_consumer_key="KEY",oauth_token="TOKEN",oauth_signature_method="HMAC-SHA256",oauth_timestamp="TIMESTAMP",oauth_nonce="NONCE",oauth_version="1.0",oauth_signature="SIG"' \
  -d '{"q": "SELECT id, companyname, email FROM customer WHERE ROWNUM <= 10"}'
```

## Data Mapping

### SuiteQL Record and Field Name Reference

SuiteQL uses internal record type and field names, not UI labels. These names must be discovered via the Records Browser or the `SELECT * FROM ...` exploratory pattern. [src1, src7]

| UI Label | SuiteQL Record/Field | Type | Notes |
|---|---|---|---|
| Customer | `customer` | Record type | Internal name is lowercase [src7] |
| Company Name | `companyname` | String | Not `company_name` — no underscores in standard fields |
| Transaction | `transaction` | Record type | Includes all transaction types (SO, PO, INV, etc.) |
| Transaction Line | `transactionLine` | Record type | JOIN to transaction via `transactionLine.transaction = transaction.id` |
| Item | `item` | Record type | All item types (inventory, service, non-inventory) |
| Sales Order type | `type = 'SalesOrd'` | Filter | Transaction type codes differ from UI names |
| Main Line flag | `mainline = 'T'` or `'F'` | Boolean-like | `'T'` = header, `'F'` = line items [src7] |
| Custom field | `custbody_fieldid` / `custcol_fieldid` | Custom | Body-level vs column-level custom fields |

### Data Type Gotchas

- Boolean fields use `'T'` and `'F'` strings, not `true`/`false` — standard SQL boolean literals do not work. [src7]
- Date fields require Oracle SQL date functions: `TO_DATE('2025-01-01', 'YYYY-MM-DD')`, not ANSI date literals. [src3]
- Column aliases in `asMappedResults()` are always returned in lowercase — `SELECT id AS myID` returns `myid` in the result object. [src4]
- Record type and field name casing may change between NetSuite releases — always normalize to uppercase or lowercase when comparing. [src4]
- Numeric IDs are returned as numbers via SuiteScript but as strings via REST API — type-check before comparison. [src2]

## Error Handling & Failure Points

### Common Error Codes

| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| `INVALID_SEARCH` | Query syntax error | Malformed SQL, unsupported function, or mixed SQL-92/Oracle syntax | Validate query in SuiteAnalytics Workbook first; use Oracle SQL syntax [src4] |
| `SSS_USAGE_LIMIT_EXCEEDED` | Governance units exhausted | Too many queries in one script execution | Move to Map/Reduce script type or reduce query count [src6] |
| `MISSING_REQD_ARGUMENT` | Required parameter missing | Omitted `query` property in runSuiteQL options | Ensure options object has `query` string [src6] |
| `SSS_INVALID_TYPE_ARG` | Invalid parameter type | Non-string/number in params array | Only string, number, or boolean allowed in params [src6] |
| `INSUFFICIENT_PERMISSION` | Role lacks access | Role does not have SuiteAnalytics permissions | Enable SuiteAnalytics Workbook permission on role [src1] |
| HTTP 400 | Bad request (REST) | Missing `Prefer: transient` header or malformed JSON body | Add required header; validate JSON payload [src2] |
| HTTP 401 | Authentication failed | Invalid or expired TBA tokens | Regenerate OAuth tokens; verify role assignment [src2] |

### Failure Points in Production

- **Silent 100K truncation**: REST queries returning >100K rows silently return only the first 100K with no error flag. Fix: `Check totalResults in first response; if >100K, switch to SuiteAnalytics Connect or add WHERE filters to reduce result size`. [src2]
- **Timeout on complex joins**: Multi-table JOINs with large datasets using SQL-92 syntax can cause timeouts that are not operationally remediable. Fix: `Always use Oracle SQL syntax; add ROWNUM or TOP N limits during development; index analysis via SuiteAnalytics Workbook`. [src4]
- **Alias casing breaks code**: `asMappedResults()` lowercases all aliases, breaking code that expects mixed-case field names. Fix: `Always reference result fields in lowercase (e.g., results[0].myid instead of results[0].myID)`. [src4]
- **TransactionStatus query anomalies**: Using `= 'CuTrSale'` in WHERE clause on TranType field returns unexpected results. Fix: `Use LIKE operator: WHERE TranType LIKE 'CuTrSale%'`. [src4]
- **CLOB field sort ordering**: Sorting on large text fields (CLOB) only compares first 250 characters, producing unexpected sort order for records with identical prefixes. Fix: `Sort on a non-CLOB field or truncate in a subquery before sorting`. [src4]

## Anti-Patterns

### Wrong: Fetching all records to filter in code

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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

```sql
-- 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

```sql
-- 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

- **runSuiteQL() 5K silent truncation**: `runSuiteQL()` returns at most 5,000 rows with no error. If you expect more, always use `runSuiteQLPaged()`. Fix: `Check result count against expected; switch to paged execution for any query that might exceed 5K rows`. [src6]
- **Governance budget exhaustion in loops**: Running SuiteQL inside a for-loop burns 10 units per iteration. A loop of 100 iterations = 1,000 units = entire User Event budget. Fix: `Write one query with IN clause or JOIN instead of N queries in a loop`. [src6]
- **REST 100K ceiling with no warning**: The 100K limit applies silently — no error, no truncation flag, just incomplete data. Fix: `Always check totalResults against retrieved count; for datasets >100K, use SuiteAnalytics Connect`. [src2]
- **Mixed SQL syntax errors**: Mixing `TO_DATE()` (Oracle) with `DATE '2025-01-01'` (SQL-92) in one query causes parse errors. Fix: `Standardize on Oracle SQL syntax throughout all SuiteQL queries`. [src4]
- **Field name discovery**: SuiteQL field names differ from UI labels and saved search field names — there is no reliable mapping API. Fix: `Use the Records Browser at Help > Records Browser, or run exploratory SELECT * FROM {recordType} queries with ROWNUM <= 1`. [src7]
- **Role permission gaps**: SuiteQL enforces SuiteAnalytics Workbook permissions, which differ from saved search permissions. A role that can run a saved search may not have SuiteQL access to the same record type. Fix: `Verify role has SuiteAnalytics Workbook permission enabled`. [src1]

## Diagnostic Commands

```bash
# Test SuiteQL connectivity via REST — should return one customer
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"}'

# Check available record types
curl -X POST \
  'https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=100' \
  -H 'Content-Type: application/json' \
  -H 'Prefer: transient' \
  -H 'Authorization: OAuth ...' \
  -d '{"q": "SELECT * FROM metadata_tables WHERE ROWNUM <= 100"}'

# Verify field names for a record type (explore schema)
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 to check if you will hit 100K ceiling
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 / Version | Release | Status | Notes |
|---|---|---|---|
| SuiteQL introduced | 2020.1 | GA | First availability in N/query module [src6] |
| REST /query/v1/suiteql | 2020.1 | GA | POST endpoint for external queries [src2] |
| SuiteAnalytics Connect integration | Pre-2020 | GA | JDBC/ODBC, unlimited rows [src1] |
| runSuiteQLPaged() | 2020.1 | GA | Paged execution, 5-1000 page size [src5] |
| toSuiteQL() conversion | 2020.1 | GA | Convert query.Query objects to SuiteQL [src5] |
| customScriptId parameter | 2021.1+ | GA | Performance tracking identifier [src6] |
| metaDataProvider parameter | 2023.1+ | GA | SUITE_QL or STATIC permission checking [src6] |
| NetSuite.com data source EOL | 2025.1 | Ended | Support ended; migrate to NetSuite2.com data source |
| NetSuite.com data source removal | 2026.1 | Removed | NetSuite.com data source fully removed; NetSuite2.com only |
| OAuth 2.0 promoted as default | 2025.x | GA | OAuth 2.0 now recommended over TBA for REST integrations |
| SOAP Web Services last endpoint | 2025.2 | Last | Last SOAP endpoint; no 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 replacing SuiteSearch for developer use cases — Oracle has not announced any deprecation timeline for SuiteQL. Saved searches remain fully supported but new query 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 When | Don't Use When | Use Instead |
|---|---|---|
| Complex JOINs across multiple record types (>2 joins) | Simple single-record lookups by internal ID | search.lookupFields (5 governance units) |
| UNION queries combining different record types | Need to write/update records | N/record module or SuiteTalk REST API |
| GROUP BY + HAVING aggregations | End-user-facing dashboards and portlets | Saved searches with summary types |
| Subqueries in WHERE or FROM clauses | Result set exceeds 100K rows (via REST) | SuiteAnalytics Connect (JDBC/ODBC) |
| Parameterized queries for security | Non-technical users building reports | SuiteAnalytics Workbook UI |
| External middleware querying NetSuite data | Real-time event notifications on data changes | User Event scripts or SuiteFlow workflows |

## Important Caveats

- SuiteQL result field casing may change between NetSuite releases — never rely on exact casing; normalize before comparison. [src4]
- The 100K row ceiling is absolute for REST Web Services and cannot be raised by contacting NetSuite support. SuiteAnalytics Connect is the only workaround for datasets larger than 100K rows. [src2]
- SuiteQL governance cost (10 units) is higher than search.lookupFields (5 units) — for simple single-record lookups, saved search APIs are more governance-efficient. [src6, src7]
- Performance characteristics differ between Oracle SQL and SQL-92 syntax. Oracle recommends Oracle SQL syntax to avoid "critical performance issues" — this is in the official documentation, not a community recommendation. [src4]
- SuiteQL queries the SuiteAnalytics data source, which has a slight delay compared to live transaction data. For time-critical reads immediately after a write, use N/record.load() instead. [src1]
- The NetSuite.com data source was removed in 2026.1 — all SuiteQL now uses NetSuite2.com exclusively. Some record types and field names changed (e.g., standalone Invoice table merged into Transactions, TimeEntry moved to TimeBill). Review and test all queries after upgrades.
- OAuth 2.0 is now the recommended authentication flow for REST Web Services; TBA (OAuth 1.0) remains supported but is no longer the default recommendation for new integrations.

## Related Units

- [NetSuite SuiteTalk REST API](/business/erp-integration/netsuite-suitetalk-rest-api/2026) — record-level CRUD operations
- [NetSuite Saved Search API](/business/erp-integration/netsuite-saved-search-api/2026) — legacy query interface (SuiteSearch)
- [NetSuite CSV Import](/business/erp-integration/netsuite-csv-import/2026) — file-based bulk data loading
