Configuring OFFSET‑FETCH pagination in Azure SQL Database when Row‑Level Security is enabled
0 reputation · 09 May 2026, 22:25 UTC
0 reputation · 09 May 2026, 22:25 UTC
When using Azure SQL Database to paginate a large result set with the OFFSET‑FETCH clause, you may also have Row‑Level Security (RLS) policies that filter rows before the offset is applied. The goal is to retrieve a predictable page size (e.g., 20 rows) regardless of how many rows are hidden by RLS, while keeping the query plan efficient across different offset values. It is unclear whether the OFFSET value must account for the variable number of rows removed by RLS, whether OPTION (RECOMPILE) is required to prevent plan caching problems when the offset changes, and what practical limits exist for the offset before performance degrades under RLS.
Does the OFFSET value need to be increased to compensate for rows filtered by RLS to maintain a fixed page size? Should OPTION (RECOMPILE) be added to avoid plan reuse issues when the offset varies across executions? Is there a recommended maximum offset value beyond which keyset pagination becomes preferable when RLS is active?
33275 reputation · 10 May 2026, 08:09 UTC
In Azure SQL Database, the OFFSET-FETCH clause is applied after the Row-Level Security (RLS) filter predicate has been evaluated. Because the security predicate acts as an implicit WHERE clause, the engine filters out unauthorized rows before determining which rows to skip (OFFSET) and which to return (FETCH).
OFFSET value to compensate for filtered rows. Since RLS removes rows before the offset is calculated, an OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY will always return the first 20 rows the user is actually authorized to see.OFFSET-FETCH can lead to parameter sniffing issues if the distribution of data varies wildly across pages, OPTION (RECOMPILE) is generally not required specifically because of RLS. However, if you notice significant performance degradation as the offset increases (deep paging), recompilation may help the optimizer choose a better plan for that specific offset value.WHERE ID > @last_seen_id).ORDER BY clause. Without it, OFFSET-FETCH is non-deterministic, and RLS filtering may result in inconsistent pages across requests.To confirm that RLS is filtering rows before the pagination occurs, run the following check as a restricted user:
-- Verify that the page size is consistent regardless of hidden rows SELECT * FROM dbo.YourSecureTable ORDER BY YourSortColumn OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY;Inspect the Execution Plan. You should see a Filter operator (the RLS predicate) appearing before the Fetch operator in the plan tree. If the Filter appears after the Fetch, your security is not being applied correctly to the result set.
Diagnostic Detail Needed: Are you accessing the RLS-protected table through a View or a Common Table Expression (CTE)? This may change the recommendation regarding how the optimizer handles the predicate.
Use comments to ask for clarification. Post a solution as an answer.
5,330 reputation · 10 May 2026, 07:04 UTC
When the Row‑Level Security predicate contains non‑sargable expressions (e.g., calls to SESSION_CONTEXT, USER_NAME(), or functions applied to the secured column), the optimizer cannot accurately estimate how many rows will survive the filter. This can lead to stale cached plans that misjudge the cost of skipping rows, especially as the OFFSET value grows. Adding OPTION (RECOMPILE) forces a fresh plan each execution, allowing the optimizer to incorporate the current predicate selectivity and choose a more appropriate access path for the given offset.