All insights

Data 360

Querying Data 360 from Apex: the new sfsqlquery namespace, and the SQL-vs-SOQL line that trips everyone

Winter '27 adds a first-class way to run Data 360 SQL from Apex, the sfsqlquery namespace, with synchronous, resumable, and Queueable-paged workflows that replace hand-rolled ConnectApi.CdpQuery loops. Here's what the new classes do, the ConnectApi path they supersede, why it's SQL on the Hyper engine and not SOQL, and the governor limits and credits that decide how you write the query.

Querying Data 360 from Apex: the new sfsqlquery namespace, and the SQL-vs-SOQL line that trips everyone, article illustration

We wrote about the Query API as the door your external systems use to read Data 360: a pricing service, a notebook, a warehouse load, anything that isn’t Salesforce, reaching in over HTTPS with a two-token handshake. That post ended on a line worth cashing in: from Apex, you don’t hand-roll the HTTP flow, because Salesforce ships Apex classes so an org can read its own unified data from a trigger or a batch. This post is what’s behind that sentence, and in the Winter ‘27 release it got a real upgrade worth building on.

The upgrade is a new Apex namespace, sfsqlquery, that Salesforce documents as the recommended approach for querying Data 360 data from Apex. It replaces the pattern most Data Cloud developers have been living with, a manual ConnectApi.CdpQuery call with a hand-written pagination loop bumping into the synchronous CPU wall, with three cleaner workflows, including one built specifically to page large result sets asynchronously without blowing a governor limit. If you’ve ever written the “run the SQL, read nextBatchId, loop, watch the clock” dance by hand, this is the abstraction you wanted.

One naming note first, because it’s in every class name you’ll type. Salesforce rebranded Data Cloud to Data 360 in October 2025; the platform and objects are the same, and older APIs, scopes, and Apex classes still carry the cdp/c360a prefix.ConnectApi.CdpQuery, the cdp_query_api OAuth scope. Read “Data Cloud” and “Data 360” as synonyms; we kept the full rebrand history separate.

First, the correction almost everyone gets wrong: it’s SQL, and it’s Hyper

The single most common stumble when developers reach into Data 360 from Apex is assuming they’re writing SOQL. They’re not, or at least, not for the interesting queries. Data 360 speaks ANSI SQL, and the engine underneath is Tableau Hyper, the same columnar engine that powers Tableau and CRM Analytics. Not Trino, not Presto, not a Salesforce dialect of SOQL. Hyper-flavored ANSI SQL. That matters the moment your query needs a JOIN, a window function, a CTE, or aggregation across more than one object, because those are things SQL does and SOQL structurally cannot.

Salesforce also added static SOQL against Data 360 data model objects in Apex, and it’s useful for the simple case, reading fields off a single DMO with native Apex ergonomics and the compiler checking your query. But it comes with two limits that push you back to SQL fast:

  • No SELECT *. You either name every field or use FIELDS(ALL); there’s no wildcard the way SQL gives you.
  • No joins. Relationship traversal across DMOs isn’t supported in the DMO-SOQL implementation, so the instant your question spans two objects, “purchases joined to the unified individual joined to loyalty tier”, SOQL can’t express it and you need Data 360 SQL.

The rule that follows is clean: SOQL for a single-DMO field read from Apex; Data 360 SQL for anything analytical, multi-object, or aggregated. We covered the SOQL side and its SET OPTIONS/dataspace quirks separately; this post is the SQL side, because that’s where the new namespace lives and where the joins you need happen. And note the FROM clause names the modeled objects. DMOs carry the __dlm suffix, which is exactly the data-lake-vs-data-model mapping that determines what your query can even reference.

The path you’re probably on today: ConnectApi.CdpQuery

Before the new namespace, the on-platform Apex path was ConnectApi.CdpQuery. It’s a thin, low-level mirror of the REST Query API: you set a SQL string on an input object, call a static method, and read rows, metadata, and a pagination cursor off the output. It still works, and it’s worth showing because it’s the concrete, well-established shape the new namespace abstracts over.

// The low-level path: ConnectApi.CdpQuery, V2 recommended over V1 for
// larger responses and subsequent-batch paging.
ConnectApi.CdpQueryInput queryInput = new ConnectApi.CdpQueryInput();
queryInput.sql =
    'SELECT ui.ssot__Id__c, ui.ssot__FirstName__c, l.loyalty_tier__c ' +
    'FROM UnifiedIndividual__dlm ui ' +
    'JOIN loyalty_member__dlm l ON l.individual_id__c = ui.ssot__Id__c ' +
    'WHERE l.loyalty_tier__c = \'Platinum\' LIMIT 5000';

ConnectApi.CdpQueryOutputV2 res = ConnectApi.CdpQuery.queryAnsiSqlV2(queryInput);
// res.data      → the rows
// res.metadata  → column names + ordering (placeInOrder)
// res.rowCount  → rows in this batch
// res.nextBatchId → cursor to the next batch, if any

// paginate by hand:
while (res.nextBatchId != null) {
    res = ConnectApi.CdpQuery.nextBatchAnsiSqlV2(res.nextBatchId);
    // ...accumulate res.data...
}

Two things about this are load-bearing. First, use queryAnsiSqlV2, not queryAnsiSql: the V2 methods exist precisely to handle larger responses and the subsequent-batch paging that V1 handled worse. Second, that while loop is your problem: you write it, you accumulate the rows, and you’re doing it inside a single synchronous transaction with a hard 10-second CPU limit. A join across two large DMOs that returns tens of thousands of rows will hit that wall, and the classic symptoms (CPU time limit exceeded, or the batch simply not finishing) are what sent every Data Cloud developer looking for Queueable Apex. That friction is the entire reason the new namespace exists.

The new path: the sfsqlquery namespace

sfsqlquery gives you object-oriented query handling with three documented workflows, and the third one is the reason to switch. The classes are SqlStatement, SqlRowIterator, Row, QueryHandle, and SqlQueueable, and they compose like this:

  • Run a query synchronously: build a SqlStatement, execute it, and iterate the returned SqlRowIterator, pulling typed values off each Row (e.g. Row.getString(...) and the other typed accessors). This is the direct replacement for a queryAnsiSqlV2 call when the result set is small enough to handle in one synchronous transaction.
  • Fetch the results of a query you already ran: a QueryHandle lets you attach to a previously executed query by its id and read its rows, which is how you resume reading a large result without re-executing the SQL.
  • Process a large dataset asynchronously.SqlQueueable is the one that earns its keep. You extend it, and it pages the result set through Queueable Apex, chaining a job per chunk so each page runs inside its own transaction with the 60-second asynchronous CPU budget instead of the 10-second synchronous one. The manual nextBatchId loop that used to blow the CPU limit becomes a framework concern instead of yours.

Conceptually, the async pattern looks like this: a Queueable that handles one page of rows and chains the next:

// Illustrative sfsqlquery async pattern. This feature is new in Winter '27
// (Beta); verify the exact class members and method signatures against the
// current Apex Reference Guide for your API version before you build on it, 
// the namespace name, the class names, and the three workflows are what's
// documented; the precise method shapes are still settling.
public class LoyaltyExport extends sfsqlquery.SqlQueueable {

    // called once per page of results as the framework pages through the set.
    // Method/override names here are illustrative, reconcile with the current
    // sfsqlquery reference for your API version before you build on them.
    protected override void processDataChunk(sfsqlquery.SqlRowIterator rows) {
        while (rows.hasNext()) {
            sfsqlquery.Row r = rows.next();
            String id   = r.getString('ssot__Id__c');
            String tier = r.getString('loyalty_tier__c');
            // ...write to a custom object, enqueue a callout, build a segment feed...
        }
    }

    // the framework calls this to decide whether to chain the next page's job
    protected override Boolean chainNextJob() {
        return true; // keep paging until the result set is exhausted
    }
}

Treat that as the shape, not gospel. The namespace, the five class names, and the three-workflow split are what the Winter ‘27 documentation establishes; the exact method signatures on SqlStatement.create(...), the full set of typed Row accessors, and the precise SqlQueueable override names are the kind of detail that moves while a feature is still settling, and parts of this one are flagged Beta in the release. Author against it, then reconcile with what the Apex Reference returns for your org, the same discipline any evolving metadata or namespace deserves. What you can rely on is the intent: sfsqlquery is the recommended path for new development, and ConnectApi.CdpQuery “remains available as a low-level interface” but is no longer where you should start.

The governor limits decide how you write the query

Querying Data 360 from Apex is a callout, and that framing sets the constraints. Every query counts against the transaction’s callout allowance, the payload lands on your heap, and the whole thing runs under the CPU ceiling of whatever context you’re in. Two rules fall out of that, and they matter more than any class name:

Push the work into the query, not into Apex. The engine is Hyper, and it’s good at filtering and aggregating, so filter in SQL, aggregate in SQL, and return the smallest result you can. A WHERE that eliminates 90% of the rows before they cross the callout boundary is worth more than any amount of clever Apex on the other side. The anti-pattern is SELECT * with no filter against a large DMO, then trimming in a loop: you pay to serialize and heap everything, then throw most of it away. This is the same lesson as the Query API’s “the bill tracks scan, not return”, the engine reasons over what you make it scan.

Match the workflow to the size. A few thousand rows for a record-page computation or a scoring pass: run it synchronously and stay under 10 seconds. Tens of thousands for an export, a segment feed, or a reconciliation: use SqlQueueable and let it page under the 60-second async budget. Reaching for the synchronous path on a big pull is how you meet CPU time limit exceeded in production after it worked fine on a sandbox subset. As with the Query API, a single response is bounded (on the order of tens of thousands of rows per batch), so a large extract is always a paged, asynchronous job, never one synchronous read you trust to return everything. And if you’re moving millions of rows to a warehouse, Apex is the wrong tool entirely; that’s what zero-copy sharing is for.

Permissions and the credit meter

Two operational facts before you ship. First, access is gated. The org has to be provisioned for Data 360, the running user needs the standard Data 360 permissions (Data Cloud Admin, or the appropriate standard permission set rather than a hand-rolled custom one), and the connected app or integration path needs the cdp_query_api scope. A query that 403s or returns nothing where you expected rows is very often a missing permission-set assignment, not a bad SQL statement, the same silent-failure trap the Query API auth flow sets for external callers.

Second, the query is metered. Data 360 querying consumes credits against the platform’s consumption model, and running the SQL from Apex rather than from an external REST client doesn’t change that, the compute is the compute. The cost tracks the data the engine scans to answer you, which is why the “filter hard, project narrow” hygiene above is a budget decision as much as a performance one. If a scheduled Apex job runs a wide Data 360 query every hour, that’s a recurring credit line, and it belongs in the same credit-optimization discipline as your streaming insights and segmentation. Watch it in the Digital Wallet; don’t discover it on the invoice.

The takeaway

The Query API is how the rest of your stack reads Data 360 from outside; sfsqlquery is how your own org reads it from inside a trigger, a batch, or a scheduled job, and in Winter ‘27 that path finally got a first-class shape. Reach for the new namespace, not ConnectApi.CdpQuery, for new work: run small results synchronously, resume large ones with QueryHandle, and page big sets through SqlQueueable so the 60-second async budget does the job the 10-second synchronous wall couldn’t. Remember it’s Hyper-flavored ANSI SQL, not SOQL, so joins and aggregation are on the table, and the DMO-SOQL path is only for the single-object simple case. Push filtering and aggregation into the query, size the workflow to the result set, confirm the cdp_query_api scope and the Data 360 permissions are assigned, and treat every query as a metered scan. Parts of it are Beta as of this release, so pin the exact method signatures to the current Apex Reference before you depend on them, but the capability itself is the one Data Cloud developers have been hand-rolling for years, finally handed to you as a framework.

Understanding the basics

What is the sfsqlquery namespace in Apex?

sfsqlquery is the Apex namespace, introduced in the Winter ‘27 release, that Salesforce documents as the recommended way to run Data 360 (Data Cloud) SQL queries directly from Apex. It provides object-oriented classes (SqlStatement, SqlRowIterator, Row, QueryHandle, and SqlQueueable) supporting three workflows: execute a query synchronously and iterate its rows, fetch the results of a previously executed query by handle, and process a large result set asynchronously by paging it through Queueable Apex. It supersedes the older ConnectApi.CdpQuery approach for new development, though that lower-level class remains available.

Is querying Data 360 from Apex SOQL or SQL?

Both exist, but they’re for different jobs. Data 360 SQL is ANSI SQL running on the Tableau Hyper engine, and it’s what you use for joins, aggregation, window functions, and any query spanning more than one object. Invoked from Apex through sfsqlquery (or the older ConnectApi.CdpQuery). Salesforce also supports static SOQL against Data 360 data model objects in Apex, but that path has no SELECT * (use FIELDS(ALL) or name fields) and no relationship joins, so it only fits reading fields off a single DMO. Rule of thumb: SOQL for a single-object field read, Data 360 SQL for anything analytical or multi-object.

How do you avoid governor limits when querying Data 360 from Apex?

Treat the query as a callout and do the heavy lifting in SQL, not Apex: filter and aggregate in the query so you return the smallest result set possible, and never SELECT * an unfiltered large object. Match the workflow to the size. Run small results synchronously under the 10-second CPU limit, and page large results asynchronously with sfsqlquery’s SqlQueueable, which chains a Queueable job per page so each runs under the 60-second async CPU budget. A single response is bounded to a batch (on the order of tens of thousands of rows), so large extracts are always paged jobs, and massive data movement belongs in zero-copy sharing rather than Apex.


Wiring Data 360 into your org’s Apex: grounding a record page, scoring a batch, or feeding a downstream system from your unified data without melting a governor limit or a credit budget? Talk to us. Getting the data foundation and the code that reads it right is exactly the work we do.

Keep reading

All insights