Apex Cursors

Use Apex cursors to break up the processing of a SOQL query result into pieces that can be processed within the bounds of a single transaction. Cursors provide you with the ability to work with large query result sets, while not actually returning the entire result set. You can traverse a query result in parts, with the flexibility to navigate forward and back in the result set. Package developers and advanced developers can use cursors to work with high-volume and high-resource processing jobs. Cursors combined with chained queueable Apex jobs are a powerful alternative to batch Apex and address some of Batch Apex’s limitations.

Apex cursors are stateless and generate results from the offset position that is specified in the Cursor.fetch(integer position, integer count) method. You must track the offsets or positions of the results within your particular processing scenario.

A cursor is created when a SOQL query is executed on a Database.getCursor() or Database.getCursorWithBinds() call. When a Cursor.fetch(integer position, integer count) method is invoked with an offset position and the count of records to fetch, the corresponding rows are returned from the cursor. The maximum number of rows per cursor is 50 million, regardless of whether the operation is synchronous or asynchronous. To get the number of cursor rows returned from the SOQL query, use Cursor.getNumRecords().

Apex Cursors establish a fixed set of resulting record IDs when they’re created. Subsequent record updates after the cursor is established don’t affect the existing set of record IDs. For example, if an object field is updated such that the record no longer meets the initial WHERE clause of the SOQL query, the record ID remains in the result set. Similarly, subsequent changes to record sharing rules don’t alter the result set.

Note

Calling the Cursor.fetch() method counts against the SOQL query limit, and the rows fetched count against the SOQL query row limit. You can make a maximum of 100 Cursor.fetch() calls per transaction.

Apex cursors throw these new System exceptions: System.FatalCursorException and System.TransientCursorException. Transactions that fail with System.TransientCursorException can be retried.

Apex Cursor Example

1public with sharing class QueryChunkingQueueable implements Queueable {
2    private Database.Cursor locator;
3    private Integer position;
4
5    public QueryChunkingQueueable() {
6        locator = Database.getCursor(
7            'SELECT Id FROM Contact WHERE LastActivityDate = LAST_N_DAYS:400',
8            AccessLevel.USER_MODE);
9        position = 0;
10    }
11
12    public void execute(QueueableContext ctx) {
13        Integer remainingRows = locator.getNumRecords() - position;
14        if (remainingRows == 0) {
15            return; // Nothing to do
16        }
17
18        // Take the minimum of batch size and remaining rows to avoid over-fetching
19        Integer fetchSize = Math.min(200, remainingRows);
20
21        List<Contact> scope = locator.fetch(position, 200);
22        position += scope.size();
23        // do something, like archive or delete the scope list records
24        if (position < locator.getNumRecords()) {
25            // process the next chunk
26            System.enqueueJob(this);
27        }
28    }
29}

Use Apex Cursors with Data 360 Data Model Objects (Beta)

Apex Cursors for Data 360 Data Model Objects is a pilot or beta service that is subject to the Beta Services Terms at Agreements - Salesforce.com or a written Unified Pilot Agreement if executed by Customer, and applicable terms in the Product Terms Directory. Use of this pilot or beta service is at the Customer's sole discretion.

Important

You can use Database.getCursor() to create an Apex cursor for a SOQL query against a Data 360 data model object (DMO). Use Cursor.fetch() to retrieve records in chunks and Cursor.getNumRecords() to get the number of records in the result set.

Running SOQL queries against DMOs can result in Data Services credits being consumed from your Data 360 subscription. For more information on how usage is billed, see Data 360 Billable Usage Types.

Warning

For example, this code creates an Apex cursor for the Individual__dlm DMO.

1public with sharing class DataChunkProcessor implements Queueable {
2    private Database.Cursor cursor;
3    private Integer position;
4
5    public DataChunkProcessor() {
6        // Initialize cursor with DC SOQL
7        this.cursor = Database.getCursor('SELECT Name__c, Email__c FROM Individual__dlm');
8        this.position = 0;
9    }
10
11    public void execute(QueueableContext context) {
12        // Fetch a chunk of 200 records without hitting SOQL query row limits or heap limits
13        List<Individual__dlm> records = cursor.fetch(position, 200);
14
15        // Process records
16        for (Individual__dlm rec : records) {
17            // Business logic
18        }
19
20        this.position += records.size();
21
22        // Chain the next job if more records are left
23        if (this.position < cursor.getNumRecords()) {
24            System.enqueueJob(this);
25        }
26    }
27}

Apex cursors for Data 360 DMOs expire 24 hours after creation. If the cursor expires before processing is complete, a System.InvalidQueryLocatorException is thrown. Otherwise, Apex cursors for Data 360 DMOs have the same transaction and daily limits as standard Apex cursors.

To get limits on Apex cursors for Data 360 DMOs, use these methods in the Limits class.

  • Limits.getDataCloudCursorRows and its upper bound method Limits.getLimitDataCloudCursorRows.
  • Limits.getDataCloudCursors and its upper bound method Limits.getLimitDataCloudCursors.
  • Limits.getDataCloudFlexCredits and its upper bound method Limits.getLimitDataCloudFlexCredits.

Pagination Cursors

Like a standard Apex cursor, an Apex pagination cursor provides a pointer to a large SOQL query result set. However, an Apex pagination cursor is designed for UI-based pagination, such as multipage record lists.

Apex pagination cursors don’t support queries against Data 360 Data model objects.

Important

To create a pagination cursor, call Database.getPaginationCursor() or Database.getPaginationCursorWithBinds() with a SOQL query as an argument. A single Database.PaginationCursor instance can have a maximum of 100,000 rows, regardless of whether the operation is synchronous or asynchronous. This size limit is lower than that of a regular Apex cursor, as pagination cursors are designed for human-readable data.

However, pagination cursors have a higher instance daily limit than that of regular Apex cursors. Whereas standard cursors are limited to 10,000 instances per org per 24-hour period, pagination cursors are limited to 200,000 instances per org per 24-hour period. This higher instance limit supports many users accessing records lists that rely on smaller pagination cursors.

To retrieve a page of rows from a pagination cursor, call PaginationCursor.fetchPage(integer start, integer pageSize). The start parameter is the zero-based index from which to begin fetching rows, and the pageSize is the maximum number of rows to retrieve for this page. The maximum pageSize value is 2000 rows.

Unlike a standard Apex cursor, a pagination cursor retrieves a complete page of records, where record rows deleted after the creation of the cursor are skipped over by default. This way, the number of rows displayed per page is consistent.

For example, let’s say that you create a standard cursor and a pagination cursor on the same SOQL query, where the result set is 100 rows. After the cursors are created, you delete the first five rows in the set, indexed 0-4. If you then call Cursor.fetch(0, 20), only 15 rows are retrieved—rows indexed 5-19. However, if you call PaginationCursor.fetchPage(0, 20), 20 rows are retrieved—rows indexed 5-24. The fetchPage() method automatically skips over the five deleted records so that a complete page is retrieved.

To manage this handling of deleted records, the fetchPage() method returns a Database.CursorFetchResult object instead of only the list of results. The Database.CursorFetchResult object encapsulates the rows retrieved and information for the next pagination call.

  • To retrieve the rows as a list of sObjects, call CursorFetchResult.getRecords().
  • To retrieve the number of deleted rows that the cursor skipped in the fetchPage() operation, call CursorFetchResult.getDeletedRows().
  • To retrieve the next page of results, first call CursorFetchResult.getNextIndex(), and then use the return value as the start parameter in the next fetchPage() call.
  • To determine whether to make subsequent calls to fetchPage(), use the CursorFetchResult.isDone() method. The method returns true if the specified pageSize is reached, which indicates that a full page of results is retrieved. It also returns true if the pagination cursor reaches the end of a result set before the specified pageSize is reached, which indicates that a partial, final page of results is retrieved.

Calling the PaginationCursor.fetchPage() and PaginationCursor.fetchDeleted() methods count against the SOQL query limit, and the rows fetched count against the SOQL query row limit.

Apex pagination cursors throw these System exceptions: System.FatalCursorException and System.TransientCursorException. Transactions that fail with System.TransientCursorException can be retried.

Cursors and Pagination Cursor Limits

To get limits on Apex cursors and Apex pagination cursors, use these methods in the Limits class.

  • Limits.getApexCursorRows() and its upper bound Limits.getLimitApexCursorRows() method
  • Limits.getFetchCallsOnApexCursor() and its upper bound Limits.getLimitFetchCallsOnApexCursor() method
  • Limits.getApexCursors() and its upper bound Limits.getLimitApexCursors() method
  • Limits.getApexPaginationCursors() and its upper bound Limits.getLimitApexPaginationCursors() method
  • Limits.getApexPaginationCursorRows() and its upper bound Limits.getLimitApexPaginationCursorRows() method

To get limits on Apex cursors for Data 360 objects, see Use Apex Cursors with Data 360 Data Model Objects (Beta).

To view transaction and daily limits for Apex cursors and Apex pagination cursors, see Execution Governors and Limits.

Apex cursors and pagination cursors have the same expiration limits as API Query cursors. See API Query Cursor Limits.

Apex Cursor and Pagination Cursor Limits Example

1// Create a standard cursor
2Database.Cursor cursor = Database.getCursor('SELECT Id, Name FROM Account LIMIT 20');
3System.debug('Standard Cursors: ' + Limits.getApexCursors() + '/' + Limits.getLimitApexCursors());
4System.debug('Standard Cursor Rows: ' + Limits.getApexCursorRows() + '/' + Limits.getLimitApexCursorRows());
5
6// Fetch records
7List<Account> batch1 = cursor.fetch(0, 10);
8List<Account> batch2 = cursor.fetch(10, 10);
9
10// Create a pagination cursor
11Database.PaginationCursor pagCursor = Database.getPaginationCursor('SELECT Id, Name FROM Account LIMIT 15');
12System.debug('Pagination Cursors: ' + Limits.getApexPaginationCursors() + '/' + Limits.getLimitApexPaginationCursors());
13System.debug('Pagination Cursor Rows: ' + Limits.getApexPaginationCursorRows() + '/' + Limits.getLimitApexPaginationCursorRows());
14
15// Fetch a page
16Database.CursorFetchResult page = pagCursor.fetchPage(0, 5);
17
18// Check shared fetch call limit
19System.debug('Fetch Calls: ' + Limits.getFetchCallsOnApexCursor() + '/' + Limits.getLimitFetchCallsOnApexCursor());
20
21// Get daily limits map
22Map<String, System.OrgLimit> limitMap = OrgLimits.getMap();
23
24// Standard cursor daily limit
25System.OrgLimit dailyCursorLimit = limitMap.get('DailyApexCursorLimit');
26System.debug('Daily Cursors: ' + dailyCursorLimit.getValue() + '/' + dailyCursorLimit.getLimit());
27
28// Pagination cursor daily limit
29System.OrgLimit dailyPCursorLimit = limitMap.get('DailyApexPCursorLimit');
30System.debug('Daily Pagination Cursors: ' + dailyPCursorLimit.getValue() + '/' + dailyPCursorLimit.getLimit());
31
32// Shared daily rows limit
33System.OrgLimit dailyRowsLimit = limitMap.get('DailyApexCursorRowsLimit');
34System.debug('Daily Cursor Rows: ' + dailyRowsLimit.getValue() + '/' + dailyRowsLimit.getLimit());