|
| 1 | +--- |
| 2 | +title: Guidance for pagination |
| 3 | +description: Learn about paging results and pagination limitations. |
| 4 | +ms.date: 02/06/2026 |
| 5 | +ms.topic: reference |
| 6 | +ms.custom: devx-track-csharp |
| 7 | +--- |
| 8 | + |
| 9 | +# Guidance for pagination |
| 10 | + |
| 11 | +When it's necessary to break a result set into smaller sets of records for processing or because a result set would exceed the maximum allowed value of _1000_ returned records, use paging. The [REST API](/rest/api/azureresourcegraph/resourcegraph(2021-03-01)/resources/resources) `QueryResponse` provides values that indicate a results set was broken up: `resultTruncated` and `$skipToken`. `resultTruncated` is a Boolean value that informs the consumer if there are more records not returned in the response. This condition can also be identified when the `count` property is less than the `totalRecords` property. `totalRecords` defines how many records that match the query. |
| 12 | + |
| 13 | +`resultTruncated` is `true` when there are less resources available than a query is requesting or when paging is disabled or when paging isn't possible because: |
| 14 | + |
| 15 | +- The query contains a `limit` or `sample`/`take` operator. |
| 16 | +- All output columns are either `dynamic` or `null` type. |
| 17 | + |
| 18 | +When `resultTruncated` is `true`, the `$skipToken` property isn't set. |
| 19 | + |
| 20 | +The following examples show how to skip the first 3,000 records and return the `first` 1,000 records after those records skipped with Azure CLI and Azure PowerShell: |
| 21 | + |
| 22 | +```azurecli |
| 23 | +az graph query -q "Resources | project id, name | order by id asc" --first 1000 --skip 3000 |
| 24 | +``` |
| 25 | + |
| 26 | +```azurepowershell |
| 27 | +Search-AzGraph -Query "Resources | project id, name | order by id asc" -First 1000 -Skip 3000 |
| 28 | +``` |
| 29 | + |
| 30 | +> [!IMPORTANT] |
| 31 | +> The response won't contain `$skipToken` if: |
| 32 | +> - The query contains a `limit` or `sample`/`take` operator. |
| 33 | +> - All output columns are either `dynamic` or `null` type. |
| 34 | +
|
| 35 | +For an example, go to [Next page query](/rest/api/azureresourcegraph/resourcegraph(2021-03-01)/resources/resources#next-page-query) in the REST API docs. |
| 36 | + |
| 37 | +## Pagination limitations |
| 38 | + |
| 39 | +Azure Resource Graph provides powerful capabilities for querying resources across your Azure environment. When working with large result sets that require pagination, understanding how pagination behaves in different scenarios helps you retrieve consistent and complete results. |
| 40 | + |
| 41 | +This article explains pagination considerations and provides strategies for scenarios where you might observe duplicate or missing records in your paginated results. |
| 42 | + |
| 43 | +### Pagination limitations scenario: Sorting by non-unique columns |
| 44 | + |
| 45 | +When paginating results sorted by a non-unique column, you might encounter duplicate or missing records even in static environments where resources aren't changing. This occurs because records with identical sort values have no guaranteed order, and their positions can shift between pagination calls. |
| 46 | + |
| 47 | +> [!NOTE] |
| 48 | +> When using `skip or first`, it's recommended to order results by at least one column with asc or `desc`. Without sorting, results are random and not repeatable. |
| 49 | +
|
| 50 | +#### Why this scenario happens |
| 51 | + |
| 52 | +Consider an example scenario, a query that retrieves virtual machines sorted by location: |
| 53 | + |
| 54 | +```kusto |
| 55 | +Resources |
| 56 | +| where type =~ 'microsoft.compute/virtualmachines' |
| 57 | +| order by location asc |
| 58 | +| project name, location, resourceGroup |
| 59 | +``` |
| 60 | + |
| 61 | +If multiple VMs share the same location value (for example, eastus), their relative order isn't deterministic. When paginating: |
| 62 | + |
| 63 | +**Page 1 request:** Retrieve the first five virtual machines. |
| 64 | + |
| 65 | +| Position | Name| Location| |
| 66 | +|----|-----|-----| |
| 67 | +|1|vm-web-01 | eastus| |
| 68 | +|2|vm-db-01 | eastus | |
| 69 | +|3|vm-app-01 | eastus | |
| 70 | +|4| vm-cache-01 | eastus | |
| 71 | +|5| vm-api-01 | eastus | |
| 72 | + |
| 73 | +**Page 2 request:** Skip five records and retrieve the next 5. |
| 74 | + |
| 75 | +Because all these VMs share the same location value (eastus), their relative order isn't guaranteed to remain consistent across pagination calls. The second page might return: |
| 76 | + |
| 77 | +| Position | Name| Location| |
| 78 | +|----|-----|-----| |
| 79 | +|1|vm-app-01 | eastus| |
| 80 | +|2|vm-queue-01 | eastus | |
| 81 | +|3|vm-monitor-01 | westus | |
| 82 | +|4| vm-backup-01 | westus | |
| 83 | +|5| vm-test-01 | westus | |
| 84 | + |
| 85 | +Notice that *vm-app-01* appears in both pages (duplicate). Due to the same reordering when there's lack of sorting, a record for e.g *vm-db-01* might never appear in any subsequent page (missing). |
| 86 | + |
| 87 | +#### Solution: sort by a unique column |
| 88 | + |
| 89 | +Always include a unique column such as id in your sort order to ensure deterministic results: |
| 90 | + |
| 91 | +```kusto |
| 92 | +Resources |
| 93 | +| where type =~ 'microsoft.compute/virtualmachines' |
| 94 | +| order by location asc, id asc |
| 95 | +| project name, location, resourceGroup |
| 96 | +``` |
| 97 | + |
| 98 | +By adding id (which is unique for every resource) as a secondary sort column, you establish a stable, deterministic order that remains consistent across pagination calls. |
| 99 | + |
| 100 | +> [!NOTE] |
| 101 | +> Sorting by a unique column resolves pagination inconsistencies in static environments where resources aren't frequently changing. If you're still experiencing duplicate or missing records after adding a unique sort column, your environment is likely dynamic with resources being created or deleted during pagination. See Scenario 2 for strategies to handle dynamic environments. |
| 102 | +
|
| 103 | +### Pagination limitations scenario: pagination in dynamic environments |
| 104 | + |
| 105 | +If you're experiencing duplicate or missing records despite sorting by a unique column, the cause is likely changes occurring in your Azure environment during pagination. When paginating through large result sets, changes to your Azure environment between requests can affect which records appear on each page. |
| 106 | + |
| 107 | +#### Why this scenario happens |
| 108 | + |
| 109 | +When resources change between pagination requests, the underlying data shifts. Consider the following example scenario: |
| 110 | + |
| 111 | +**Page 1 request:** Retrieve the first 100 virtual machines sorted by id. |
| 112 | + |
| 113 | +```kusto |
| 114 | +Resources |
| 115 | +| where type =~ 'microsoft.compute/virtualmachines' |
| 116 | +| order by id asc |
| 117 | +| take 100 |
| 118 | +``` |
| 119 | + |
| 120 | +This request returns VMs with IDs from vm-001 through vm-100. <br> |
| 121 | +**Between requests:** Resource vm-050 is deleted from your environment. <br> |
| 122 | +**Page 2 request:** Skip the first 100 records and retrieve the next 100. <br> |
| 123 | + |
| 124 | +```kusto |
| 125 | +Resources |
| 126 | +| where type =~ 'microsoft.compute/virtualmachines' |
| 127 | +| order by id asc |
| 128 | +| skip 100 |
| 129 | +| take 100 |
| 130 | +``` |
| 131 | + |
| 132 | +Because vm-050 was deleted, all subsequent resources shifted up by one position. The resource vm-101 is now at position 100, so when you skip 100 records, vm-101 isn't included in the results. |
| 133 | + |
| 134 | +Similarly, if a new resource is created between requests, you might see duplicate records in your results. |
| 135 | + |
| 136 | +### Client-side strategies for dynamic environments |
| 137 | + |
| 138 | +If your scenario requires a more consistent retrieval of resources, consider one of the following approaches. These strategies partition your data in a way that's resilient to changes and can also improve performance through parallel execution. |
| 139 | + |
| 140 | +> [!NOTE] |
| 141 | +> These client-side strategies move the pagination logic to your application, which helps avoid the pagination inconsistencies described previously. However, they don't guarantee complete consistency across calls. Resources might be added or deleted between your initial query (for counting or retrieving IDs) and subsequent data fetches. This can result in discrepancies such as a mismatch between expected count and total resources fetched, or missing results if a resource was deleted during the operation. For scenarios requiring strict consistency, consider whether point-in-time accuracy is critical for your use case. |
| 142 | +
|
| 143 | +#### Option 1: Hash-based data partitioning |
| 144 | + |
| 145 | +This approach partitions your data using a hash function to ensure consistent and non-overlapping results across multiple queries. Each resource belongs to exactly one partition based on its unique identifier. |
| 146 | + |
| 147 | +##### Step 1: Get the total record count |
| 148 | + |
| 149 | +First, determine how many records match your query: |
| 150 | + |
| 151 | +```kusto |
| 152 | +Resources |
| 153 | +| where type =~ 'microsoft.compute/virtualmachines' |
| 154 | +| count |
| 155 | +``` |
| 156 | + |
| 157 | +Use the count to determine the number of partitions needed. For example, if your count returns 7,712 records and since Azure Resource Graph returns a maximum of 1,000 records per query, you would need at least eight partitions. |
| 158 | + |
| 159 | +##### Step 2: Query each position |
| 160 | + |
| 161 | +Use the hash() function to partition data based on the resource ID. Query each partition separately: |
| 162 | + |
| 163 | +**Partition 0** |
| 164 | + |
| 165 | +```kusto |
| 166 | +Resources |
| 167 | +| where type =~ 'microsoft.compute/virtualmachines' |
| 168 | +| where hash(tolower(id)) % 8 == 0 |
| 169 | +``` |
| 170 | + |
| 171 | +**Partition 1** |
| 172 | + |
| 173 | +```kusto |
| 174 | +Resources |
| 175 | +| where type =~ 'microsoft.compute/virtualmachines' |
| 176 | +| where hash(tolower(id)) % 8 == 1 |
| 177 | +``` |
| 178 | + |
| 179 | +Continue for each partition through partition 7: |
| 180 | + |
| 181 | +**Partition 7** |
| 182 | + |
| 183 | +```kusto |
| 184 | +Resources |
| 185 | +| where type =~ 'microsoft.compute/virtualmachines' |
| 186 | +| where hash(tolower(id)) % 8 == 7 |
| 187 | +``` |
| 188 | + |
| 189 | +#### Pseudo code |
| 190 | + |
| 191 | +##### Step 1: Get total count and calculate partitions |
| 192 | + |
| 193 | +```kusto |
| 194 | +totalCount = executeQuery("Resources | where type =~ 'microsoft.compute/virtualmachines' | count") |
| 195 | +
|
| 196 | +numPartitions = ceiling(totalCount / 1000) |
| 197 | +``` |
| 198 | + |
| 199 | +##### Step 2: Build queries for each partition |
| 200 | + |
| 201 | +```kusto |
| 202 | +queries = [] |
| 203 | +
|
| 204 | +for i = 0 to numPartitions - 1: |
| 205 | +
|
| 206 | + queries.append("Resources |
| 207 | +
|
| 208 | + | where type =~ 'microsoft.compute/virtualmachines' |
| 209 | +
|
| 210 | + | where hash(tolower(id)) % {numPartitions} == {i}") |
| 211 | +
|
| 212 | + |
| 213 | +``` |
| 214 | + |
| 215 | +##### Step 3: Execute all queries in parallel and combine results |
| 216 | + |
| 217 | +```kusto |
| 218 | +allResults = executeInParallel(queries) |
| 219 | +``` |
| 220 | + |
| 221 | +##### Benefits |
| 222 | + |
| 223 | +- **No duplicates or missed records:** Each resource ID hashes to exactly one partition. |
| 224 | +- **Parallel execution:** All partition queries can run simultaneously, reducing total query time. |
| 225 | + |
| 226 | +#### Option 2: Batch processing with resource IDs |
| 227 | + |
| 228 | +This approach retrieves all resource IDs first, then queries for complete records in smaller batches. This ensures you have a consistent set of identifiers before retrieving the full resource data. |
| 229 | + |
| 230 | +##### Step 1: Retrieve all resource IDs |
| 231 | + |
| 232 | +Use summarize with make_set() to retrieve all resource IDs: |
| 233 | + |
| 234 | +```kusto |
| 235 | +Resources |
| 236 | +| where type =~ 'microsoft.compute/virtualmachines' |
| 237 | +| summarize make_set(id) |
| 238 | +``` |
| 239 | + |
| 240 | +##### Step 2: Query in batches |
| 241 | + |
| 242 | +Once you have the list of resource IDs, query for full records in batches of 1,000 or fewer: |
| 243 | + |
| 244 | +**Batch 1** |
| 245 | + |
| 246 | +```kusto |
| 247 | +Resources |
| 248 | +| where type =~ 'microsoft.compute/virtualmachines' |
| 249 | +| where id in~ ('id1', 'id2', ... , 'id1000') |
| 250 | +``` |
| 251 | + |
| 252 | +**Batch 2** |
| 253 | + |
| 254 | +```kusto |
| 255 | +Resources |
| 256 | +| where type =~ 'microsoft.compute/virtualmachines' |
| 257 | +| where id in~ ('id1001', 'id1002', ... , 'id2000') |
| 258 | +``` |
| 259 | + |
| 260 | +Continue until all IDs are covered. |
| 261 | + |
| 262 | +##### Benefits |
| 263 | + |
| 264 | +- **Guaranteed completeness:** You have a fixed set of IDs before querying for details. |
| 265 | +- **Parallel execution:** Batch queries can run simultaneously. |
| 266 | + |
| 267 | + |
| 268 | +If the response of the query is huge (> 16 MB) and doesn’t fit in a single call, it's suggested to use the previously mentioned partitioning technique to fetch all the data in multiple calls. |
| 269 | + |
| 270 | +The following is an example of a query that might exceed response size limit of 16 MB: |
| 271 | + |
| 272 | +```kusto |
| 273 | +Resources |
| 274 | +| where type =~ 'microsoft.compute/virtualmachines' |
| 275 | +| summarize make_set(id) |
| 276 | +``` |
| 277 | +In this case, use partitioning: |
| 278 | + |
| 279 | +**Partition 0** |
| 280 | + |
| 281 | +```kusto |
| 282 | +Resources |
| 283 | +| where type =~ 'microsoft.compute/virtualmachines' |
| 284 | +| where hash(tolower(id))%10 == 0 |
| 285 | +| summarize make_set(id) |
| 286 | +``` |
| 287 | + |
| 288 | +**Partition 1** |
| 289 | + |
| 290 | +```kusto |
| 291 | + Resources |
| 292 | + | where type =~ 'microsoft.compute/virtualmachines' |
| 293 | + | where hash(tolower(id))%10 == 1 |
| 294 | + | summarize make_set(id) |
| 295 | +``` |
| 296 | +.... |
| 297 | + |
| 298 | +**Partition 9** |
| 299 | + |
| 300 | +```kusto |
| 301 | + Resources |
| 302 | + | where type =~ 'microsoft.compute/virtualmachines' |
| 303 | + | where hash(tolower(id))%10 == 9 |
| 304 | + | summarize make_set(id) |
| 305 | +``` |
| 306 | + |
| 307 | +## Summary |
| 308 | + |
| 309 | +From this article, you were able to learn: |
| 310 | + |
| 311 | +- How sorting by non-unique columns can cause duplicate or missing records during pagination |
| 312 | +- How dynamic environments with changing resources can affect paginated results |
| 313 | +- Client-side strategies including hash-based partitioning and batch processing with resource IDs |
0 commit comments