OData Query Options and Server Side Paging
OData query options such as $top, $skip, $filter, $orderby, $count and $expand let a client shape a result set. Server side paging is the backend deciding page size when the client omits $top. For managed RAP these are translated automatically into a database select with OFFSET/FETCH; for unmanaged RAP the developer's READ implementation must interpret and apply them, including computing the total record count, or paging silently breaks under volume.
This page covers how OData query options are actually handled inside a RAP or classic Gateway service, where the framework does the work automatically and where a developer has to reproduce it by hand. It focuses on the failure patterns that only appear once a list report is loaded with realistic data volumes rather than a handful of test rows.
Published 16 Sept 2026· 1,491 words
What it is
OData query options are standard URL parameters ($top, $skip, $filter, $select, $expand, $orderby, $count or $inlinecount) that let a client shape a result set without a bespoke API for every list variant. The one structural fact that explains most confusion: whether these options are handled automatically depends entirely on whether the exposing business object is managed or unmanaged. For a managed RAP business object built on a CDS view entity, the framework translates $top/$skip/$filter/$orderby into a real database select, pushing offset and fetch to the database engine. For an unmanaged business object, the READ operation implementation receives the requested paging and filter information as part of the incoming request and has to apply it itself against whatever internal table or legacy call it is wrapping, including working out and returning the total count if $count was requested. Server side paging simply means a default page size applies when the client does not send an explicit $top, so an unbounded select never happens by accident, but that default is set independently at several layers.
When to use it
Query option handling is not optional; it happens on essentially every list-returning OData operation, so the real decision is how much of it is built in versus hand coded. Rely on the framework's automatic handling whenever the underlying data comes from a CDS view entity a managed RAP business object can select from directly. Implement paging logic explicitly when building an unmanaged business object around legacy code, a remote-enabled function module, or a BAPI that returns a full internal table with no native offset concept. The wrong reflex is disabling or bypassing server side paging, or forcing an artificially large $top, to make a list report 'show everything' - that just moves the unbounded select into application memory and shifts the failure from a paginator glitch to a timeout or dump. Equally wrong is writing custom paging logic inside a managed business object; the framework already does this and a hand rolled version will conflict with it or simply be ignored.
How it fits the stack
Above this layer sits the OData consumer: a Fiori Elements list report deciding its growing threshold, or a freestyle UI5 list binding, both of which translate scroll or page-size settings into $top and $skip on the request. Below it sits the persistence: for managed RAP, the generated service adaptation layer converts the query options into a select against the CDS view entity, including pushing WHERE conditions from $filter and OFFSET/FETCH from $top/$skip to the database. For unmanaged RAP, that translation step does not exist and the developer's ABAP code stands in for it. This structure supersedes classic Gateway service implementation classes, where every entity set's paging, filtering and counting logic had to be written by hand in the DPC/MPC redefinition; CDS based exposure gets equivalent behavior generated for standard read and CRUD operations, leaving hand written logic only for genuinely non-standard sources.
A worked example
A managed RAP business object exposes a read-only list report over sales order items via a CDS view entity. The UI, after the user scrolls, requests $top=30&$skip=60 with a filter and $count=true. Nothing is coded for paging on the managed side: the framework builds a select against the view with the filter turned into a WHERE clause, applies offset and fetch for the skip and top, and issues a separate count select to satisfy $count. Contrast the same business object built unmanaged, wrapping an existing function module that returns a full internal table. The READ operation implementation now has to read the paging information supplied on the request (offset and page size), slice the internal table returned by the function module accordingly, and if a count was requested and has not already been produced, run separate logic to determine the full count and set it explicitly on the response data. Skipping that last step is exactly why a UI paginator ends up showing an unknown or incorrect total - the OData layer only reports what the READ implementation tells it, nothing more.
How to choose
- Managed versus unmanaged persistence: can the source support OFFSET/FETCH natively on the database, or is it legacy code that already loads a full internal table into memory - the latter forces hand written paging and is the most common reason a list works with two hundred test rows and times out at production volume
- Where filtering happens: push filter conditions into the CDS view's WHERE clause and associations so the database reduces the set before paging is applied, rather than filtering an ABAP internal table after a full select, which defeats the purpose of paging entirely
- Whether an exact total is actually needed: computing $count often means running the query twice, once for the count and once for the data - decide whether the consumer needs an exact total or whether a simple 'more records exist' indicator is sufficient, since an exact count over a large table is not free
- Protocol version semantics: $inlinecount in OData V2 and $count in OData V4 differ in syntax and in what total value clients expect back; check the behavior against odata-v2-versus-odata-v4-for-sap-developers before assuming identical client handling across versions
- Expand behavior under paging: confirm whether $expand on a to-many association respects the parent's $top without issuing one query per parent row - verify with an actual trace of the generated SQL rather than assuming the CDS association resolution is always single pass
Common pitfalls
- Default page size configured differently at the UI, the gateway, and the backend, so a developer testing with a plain REST client and no $top sees the full result set and never notices the production app truncates at the UI's own default
- $count computed by re-running the unfiltered query logic instead of the filtered one, producing a paginator total that does not match what the filtered list actually contains
- An unmanaged READ implementation ignoring $skip entirely and always returning from the beginning of the internal table, so every scroll silently returns the same first page again
- Filtering applied after the paging slice instead of before it, so fewer rows come back than requested because the filter was run against an already truncated subset
- $expand combined with $top triggering one database round trip per parent row instead of a single optimized fetch - invisible with a handful of rows in development, expensive once volumes grow
- No stable sort order for paging (missing $orderby, or sorting on a non-unique field), causing rows to repeat or disappear between successive page requests when the underlying data changes concurrently
- Assuming a CDS view entity pushes every filter condition down automatically - a filter on a client-computed virtual element cannot be pushed to the database and forces a full materialization of the result before it can be applied
ECC, S/4HANA and clean core
On S/4HANA, exposing data through managed RAP business objects on CDS view entities is the supported, clean-core-aligned way to get correct query option and paging behavior without writing it by hand. Hand written paging logic in an unmanaged business object, or in a classic Gateway service implementation class, still works technically but is discouraged for anything a customer will extend or that needs to survive upgrades cleanly, because that logic lives outside the generated framework and has to be maintained manually release after release. Where an unmanaged wrapper around existing RFC-based logic cannot be avoided in the short term, the resulting paging and counting code should be tracked explicitly as technical debt rather than assumed to behave the same as a managed object.
Whose problem this is
The developer implements and tests paging and count logic, especially in unmanaged scenarios, and is responsible for verifying behavior at realistic data volumes, not just in a dev system with sample records. The architect decides managed versus unmanaged exposure and whether filter pushdown to the database is achievable given the underlying persistence. Functional consultants should specify expected list sizes during test planning so paging problems surface before go live rather than after.
Related SAP objects
Reviewed pages this object connects to in the ERPClimb knowledge graph.
Source: ERPClimb — https://erpclimb.com/sap-technical-topics/odata-query-options-and-server-side-pagingERPClimb is an independent platform and is not affiliated with SAP SE. Reference pages are written and reviewed by SAP consultants for learning and troubleshooting.