Apex Cursors
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().
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}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.
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 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());