|
2 | 2 | title: "Pagination"
|
3 | 3 | ---
|
4 | 4 |
|
5 |
| -Hi, |
| 5 | +import { ExampleTabs } from "../../../components/mdx/example-tabs"; |
6 | 6 |
|
7 |
| -We're currently working on the version 11 documentation. Probably right now at this very moment. However, this is an open-source project, and we need any help we can get! You can jump in at any time and help us improve the documentation for hundreds or even thousands of developers! |
| 7 | +Pagination is one of the most common problems that we have to solve when implementing our backend. Often, sets of data are too large to pass them directly to the consumer of our service. |
8 | 8 |
|
9 |
| -In case you might need help, check out our slack channel and get immediate help from the core contributors or the community itself. |
| 9 | +Pagination solves this problem by giving the consumer the ability to fetch a set in chunks. |
10 | 10 |
|
11 |
| -Sorry for any inconvenience, and thank you for being patient! |
| 11 | +# Connections |
12 | 12 |
|
13 |
| -The ChilliCream Team |
| 13 | +_Connections_ are a standardized way to expose pagination to clients. |
| 14 | + |
| 15 | +Instead of returning a list of entries, we return a _Connection_. |
| 16 | + |
| 17 | +```sdl |
| 18 | +type Query { |
| 19 | + users(first: Int after: String last: Int before: String): UserConnection |
| 20 | +} |
| 21 | +
|
| 22 | +type UserConnection { |
| 23 | + pageInfo: PageInfo! |
| 24 | + edges: [UserEdge!] |
| 25 | + nodes: [User!] |
| 26 | +} |
| 27 | +
|
| 28 | +type UserEdge { |
| 29 | + cursor: String! |
| 30 | + node: User! |
| 31 | +} |
| 32 | +
|
| 33 | +type PageInfo { |
| 34 | + hasNextPage: Boolean! |
| 35 | + hasPreviousPage: Boolean! |
| 36 | + startCursor: String |
| 37 | + endCursor: String |
| 38 | +} |
| 39 | +``` |
| 40 | + |
| 41 | +You can learn more about this in the [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). |
| 42 | + |
| 43 | +> Note: _Connections_ are often associated with _cursor-based_ pagination, due to the use of a _cursor_. Since the specification describes the _cursor_ as opague though, it can be used to faciliate an _offset_ as well. |
| 44 | +
|
| 45 | +## Usage |
| 46 | + |
| 47 | +Adding pagination capabilties to our fields is a breeze. All we have to do is add the `UsePaging` middleware. |
| 48 | + |
| 49 | +<ExampleTabs> |
| 50 | +<ExampleTabs.Annotation> |
| 51 | + |
| 52 | +```csharp |
| 53 | +public class Query |
| 54 | +{ |
| 55 | + [UsePaging] |
| 56 | + public IEnumerable<User> GetUsers([Service] IUserRespository repository) |
| 57 | + => repository.GetUsers(); |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +If we need to specify the concrete node type of our pagination, we can do so by passing a Type as the constructor argument `[UsePaging(typeof(User))]`. |
| 62 | + |
| 63 | +The `UsePaging` attribute also allows us to configure some other properties, like `DefaultPageSize`, `MaxPageSize` and `IncludeTotalCount`. |
| 64 | + |
| 65 | +```csharp |
| 66 | +[UsePaging(MaxPageSize = 50)] |
| 67 | +``` |
| 68 | + |
| 69 | +</ExampleTabs.Annotation> |
| 70 | +<ExampleTabs.Code> |
| 71 | + |
| 72 | +```csharp |
| 73 | +public class QueryType : ObjectType |
| 74 | +{ |
| 75 | + protected override void Configure(IObjectTypeDescriptor descriptor) |
| 76 | + { |
| 77 | + descriptor |
| 78 | + .Field("users") |
| 79 | + .UsePaging() |
| 80 | + .Resolve(context => |
| 81 | + { |
| 82 | + var repository = context.Service<IUserRespository>(); |
| 83 | + |
| 84 | + return repository.GetUsers(); |
| 85 | + }); |
| 86 | + } |
| 87 | +} |
| 88 | +``` |
| 89 | + |
| 90 | +If we need to specify the concrete node type of our pagination, we can do so via the generic argument: `UsePaging<UserType>()`. |
| 91 | + |
| 92 | +We can also configure the `UsePaging` middleware further, by specifying `PagingOptions`. |
| 93 | + |
| 94 | +```csharp |
| 95 | +descriptor.UsePaging(options: new PagingOptions |
| 96 | +{ |
| 97 | + MaxPageSize = 50 |
| 98 | +}); |
| 99 | +``` |
| 100 | + |
| 101 | +</ExampleTabs.Code> |
| 102 | +<ExampleTabs.Schema> |
| 103 | + |
| 104 | +⚠️ Schema-first does currently not support pagination! |
| 105 | + |
| 106 | +</ExampleTabs.Schema> |
| 107 | +</ExampleTabs> |
| 108 | + |
| 109 | +For the `UsePaging` middleware to work, our resolver needs to return an `IEnumerable<T>` or an `IQueryable<T>`. The middleware will then apply the pagination arguments to what we have returned. In the case of an `IQueryable<T>` this means that the pagination operations can be directly translated to native database queries, through database drivers like EntityFramework or the MongoDB client. |
| 110 | + |
| 111 | +## Customization |
| 112 | + |
| 113 | +If we need more control over the pagination process we can do so, by returning a `Connection<T>`. |
| 114 | + |
| 115 | +<ExampleTabs> |
| 116 | +<ExampleTabs.Annotation> |
| 117 | + |
| 118 | +```csharp |
| 119 | +public class Query |
| 120 | +{ |
| 121 | + [UsePaging] |
| 122 | + public Connection<User> GetUsers(string? after, int? first, string sortBy) |
| 123 | + { |
| 124 | + // get users using the above arguments |
| 125 | + IEnumerable<User> users = null; |
| 126 | + |
| 127 | + var edges = users.Select(user => new Edge<User>(user, user.Id)) |
| 128 | + .ToList(); |
| 129 | + var pageInfo = new ConnectionPageInfo(false, false, null, null); |
| 130 | + |
| 131 | + var connection = new Connection<User>(edges, pageInfo, |
| 132 | + ct => ValueTask.FromResult(0)); |
| 133 | + |
| 134 | + return connection; |
| 135 | + } |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +</ExampleTabs.Annotation> |
| 140 | +<ExampleTabs.Code> |
| 141 | + |
| 142 | +```csharp |
| 143 | +public class QueryType : ObjectType |
| 144 | +{ |
| 145 | + protected override void Configure(IObjectTypeDescriptor descriptor) |
| 146 | + { |
| 147 | + descriptor |
| 148 | + .Field("users") |
| 149 | + .UsePaging() |
| 150 | + .Argument("sortBy", a => a.Type<NonNullType<StringType>>()) |
| 151 | + .Resolve(context => |
| 152 | + { |
| 153 | + var after = context.ArgumentValue<string?>("after"); |
| 154 | + var first = context.ArgumentValue<int?>("first"); |
| 155 | + var sortBy = context.ArgumentValue<string>("sortBy"); |
| 156 | + |
| 157 | + // get users using the above arguments |
| 158 | + IEnumerable<User> users = null; |
| 159 | + |
| 160 | + var edges = users.Select(user => new Edge<User>(user, user.Id)) |
| 161 | + .ToList(); |
| 162 | + var pageInfo = new ConnectionPageInfo(false, false, null, null); |
| 163 | + |
| 164 | + var connection = new Connection<User>(edges, pageInfo, |
| 165 | + ct => ValueTask.FromResult(0)); |
| 166 | + |
| 167 | + return connection; |
| 168 | + }); |
| 169 | + } |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | +If we need to work on an even lower level, we could also use `descriptor.AddPagingArguments()` and `descriptor.Type<ConnectionType<UserType>>()` to get rid of the `UsePaging` middleware. |
| 174 | + |
| 175 | +</ExampleTabs.Code> |
| 176 | +<ExampleTabs.Schema> |
| 177 | + |
| 178 | +⚠️ Schema-first does currently not support pagination! |
| 179 | + |
| 180 | +</ExampleTabs.Schema> |
| 181 | +</ExampleTabs> |
| 182 | + |
| 183 | +## Total count |
| 184 | + |
| 185 | +Sometimes we might want to return the total number of pageable entries. |
| 186 | + |
| 187 | +For this to work we need to enable the `IncludeTotalCount` flag on the `UsePaging` middleware. |
| 188 | + |
| 189 | +<ExampleTabs> |
| 190 | +<ExampleTabs.Annotation> |
| 191 | + |
| 192 | +```csharp |
| 193 | +[UsePaging(IncludeTotalCount = true)] |
| 194 | +``` |
| 195 | + |
| 196 | +</ExampleTabs.Annotation> |
| 197 | +<ExampleTabs.Code> |
| 198 | + |
| 199 | +```csharp |
| 200 | +descriptor.UsePaging(options: new PagingOptions |
| 201 | +{ |
| 202 | + IncludeTotalCount = true |
| 203 | +}); |
| 204 | +``` |
| 205 | + |
| 206 | +</ExampleTabs.Code> |
| 207 | +<ExampleTabs.Schema> |
| 208 | + |
| 209 | +⚠️ Schema-first does currently not support pagination! |
| 210 | + |
| 211 | +</ExampleTabs.Schema> |
| 212 | +</ExampleTabs> |
| 213 | + |
| 214 | +This will add a new field called `totalCount` to our _Connection_. |
| 215 | + |
| 216 | +```sdl |
| 217 | +type UserConnection { |
| 218 | + pageInfo: PageInfo! |
| 219 | + edges: [UserEdge!] |
| 220 | + nodes: [User!] |
| 221 | + totalCount: Int! |
| 222 | +} |
| 223 | +``` |
| 224 | + |
| 225 | +If our resolver returns an `IEnumerable<T>` or an `IQueryable<T>` the `totalCount` will be automatically computed, if it has been specified as a subfield in the query. |
| 226 | + |
| 227 | +If we have customized our pagination and our resolver now returns a `Connection<T>`, we have to explicitly declare how the `totalCount` value is computed. |
| 228 | + |
| 229 | +```csharp |
| 230 | +var connection = new Connection<User>( |
| 231 | + edges, |
| 232 | + pageInfo, |
| 233 | + getTotalCount: cancellationToken => ValueTask.FromResult(0)); |
| 234 | +``` |
| 235 | + |
| 236 | +<!-- ## Custom Edges |
| 237 | +
|
| 238 | +_Edges_ are not only there to hold the _cusor_, they can also be used to include information about the relation between the parent and one of the _nodes_. |
| 239 | +
|
| 240 | +```sdl |
| 241 | +type User { |
| 242 | + id: ID! |
| 243 | + name: String! |
| 244 | + friends: FriendConnection |
| 245 | +} |
| 246 | +
|
| 247 | +type FriendConnection { |
| 248 | + pageInfo: PageInfo! |
| 249 | + edges: [FriendEdge!] |
| 250 | + nodes: [User!] |
| 251 | +} |
| 252 | +
|
| 253 | +type FriendEdge { |
| 254 | + cursor: String! |
| 255 | + # this is a relation specific property |
| 256 | + friendsSince: DateTime! |
| 257 | + node: User! |
| 258 | +} |
| 259 | +
|
| 260 | +type PageInfo { |
| 261 | + hasNextPage: Boolean! |
| 262 | + hasPreviousPage: Boolean! |
| 263 | + startCursor: String |
| 264 | + endCursor: String |
| 265 | +} |
| 266 | +``` --> |
| 267 | + |
| 268 | +# Offset Pagination |
| 269 | + |
| 270 | +> Note: While we support _offset-based_ pagination, we highly encourage the use of [_Connections_](#connections) instead. _Connections_ provide an abstraction which makes it easier to switch to another pagination mechanism later on. |
| 271 | +
|
| 272 | +Besides _Connections_ we can also expose a more traditional _offset-based_ pagination. |
| 273 | + |
| 274 | +```sdl |
| 275 | +type Query { |
| 276 | + users(skip: Int take: Int): UserCollectionSegment |
| 277 | +} |
| 278 | +
|
| 279 | +type UserCollectionSegment { |
| 280 | + items: [User!] |
| 281 | + pageInfo: CollectionSegmentInfo! |
| 282 | +} |
| 283 | +
|
| 284 | +type CollectionSegmentInfo { |
| 285 | + hasNextPage: Boolean! |
| 286 | + hasPreviousPage: Boolean! |
| 287 | +} |
| 288 | +``` |
| 289 | + |
| 290 | +## Usage |
| 291 | + |
| 292 | +To add _offset-based_ pagination capabilties to our fields we have to add the `UseOffsetPaging` middleware. |
| 293 | + |
| 294 | +<ExampleTabs> |
| 295 | +<ExampleTabs.Annotation> |
| 296 | + |
| 297 | +```csharp |
| 298 | +public class Query |
| 299 | +{ |
| 300 | + [UseOffsetPaging] |
| 301 | + public IEnumerable<User> GetUsers([Service] IUserRespository repository) |
| 302 | + => repository.GetUsers(); |
| 303 | +} |
| 304 | +``` |
| 305 | + |
| 306 | +If we need to specify the concrete node type of our pagination, we can do so by passing a Type as the constructor argument `[UseOffsetPaging(typeof(User))]`. |
| 307 | + |
| 308 | +The `UseOffsetPaging` attribute also allows us to configure some other properties, like `DefaultPageSize`, `MaxPageSize` and `IncludeTotalCount`. |
| 309 | + |
| 310 | +```csharp |
| 311 | +[UseOffsetPaging(MaxPageSize = 50)] |
| 312 | +``` |
| 313 | + |
| 314 | +</ExampleTabs.Annotation> |
| 315 | +<ExampleTabs.Code> |
| 316 | + |
| 317 | +```csharp |
| 318 | +public class QueryType : ObjectType |
| 319 | +{ |
| 320 | + protected override void Configure(IObjectTypeDescriptor descriptor) |
| 321 | + { |
| 322 | + descriptor |
| 323 | + .Field("users") |
| 324 | + .UseOffsetPaging() |
| 325 | + .Resolve(context => |
| 326 | + { |
| 327 | + var repository = context.Service<IUserRespository>(); |
| 328 | + |
| 329 | + return repository.GetUsers(); |
| 330 | + }); |
| 331 | + } |
| 332 | +} |
| 333 | +``` |
| 334 | + |
| 335 | +If we need to specify the concrete node type of our pagination, we can do so via the generic argument: `UseOffsetPaging<UserType>()`. |
| 336 | + |
| 337 | +We can also configure the `UseOffsetPaging` middleware further, by specifying `PagingOptions`. |
| 338 | + |
| 339 | +```csharp |
| 340 | +descriptor.UseOffsetPaging(options: new PagingOptions |
| 341 | +{ |
| 342 | + MaxPageSize = 50 |
| 343 | +}); |
| 344 | +``` |
| 345 | + |
| 346 | +</ExampleTabs.Code> |
| 347 | +<ExampleTabs.Schema> |
| 348 | + |
| 349 | +⚠️ Schema-first does currently not support pagination! |
| 350 | + |
| 351 | +</ExampleTabs.Schema> |
| 352 | +</ExampleTabs> |
| 353 | + |
| 354 | +For the `UseOffsetPaging` middleware to work, our resolver needs to return an `IEnumerable<T>` or an `IQueryable<T>`. The middleware will then apply the pagination arguments to what we have returned. In the case of an `IQueryable<T>` this means that the pagination operations can be directly translated to native database queries, through database drivers like EntityFramework or the MongoDB client. |
| 355 | + |
| 356 | +## Customization |
| 357 | + |
| 358 | +If we need more control over the pagination process we can do so, by returning a `CollectionSegment<T>`. |
| 359 | + |
| 360 | +<ExampleTabs> |
| 361 | +<ExampleTabs.Annotation> |
| 362 | + |
| 363 | +```csharp |
| 364 | +public class Query |
| 365 | +{ |
| 366 | + [UseOffsetPaging] |
| 367 | + public CollectionSegment<User> GetUsers(int? skip, int? take, string sortBy) |
| 368 | + { |
| 369 | + /// get users using the above arguments |
| 370 | + IEnumerable<User> users = null; |
| 371 | + |
| 372 | + var pageInfo = new CollectionSegmentInfo(false, false); |
| 373 | + |
| 374 | + var collectionSegment = new CollectionSegment<User>( |
| 375 | + users, |
| 376 | + pageInfo, |
| 377 | + ct => ValueTask.FromResult(0)); |
| 378 | + |
| 379 | + return collectionSegment; |
| 380 | + } |
| 381 | +} |
| 382 | +``` |
| 383 | + |
| 384 | +</ExampleTabs.Annotation> |
| 385 | +<ExampleTabs.Code> |
| 386 | + |
| 387 | +```csharp |
| 388 | +public class QueryType : ObjectType |
| 389 | +{ |
| 390 | + protected override void Configure(IObjectTypeDescriptor descriptor) |
| 391 | + { |
| 392 | + descriptor |
| 393 | + .Field("users") |
| 394 | + .UseOffsetPaging() |
| 395 | + .Argument("sortBy", a => a.Type<NonNullType<StringType>>()) |
| 396 | + .Resolve(context => |
| 397 | + { |
| 398 | + var skip = context.ArgumentValue<int?>("skip"); |
| 399 | + var take = context.ArgumentValue<int?>("take"); |
| 400 | + var sortBy = context.ArgumentValue<string>("sortBy"); |
| 401 | + |
| 402 | + // get users using the above arguments |
| 403 | + IEnumerable<User> users = null; |
| 404 | + |
| 405 | + var pageInfo = new CollectionSegmentInfo(false, false); |
| 406 | + |
| 407 | + var collectionSegment = new CollectionSegment<User>( |
| 408 | + users, |
| 409 | + pageInfo, |
| 410 | + ct => ValueTask.FromResult(0)); |
| 411 | + |
| 412 | + return collectionSegment; |
| 413 | + }); |
| 414 | + } |
| 415 | +} |
| 416 | +``` |
| 417 | + |
| 418 | +If we need to work on an even lower level, we could also use `descriptor.AddOffsetPagingArguments()` and `descriptor.Type<CollectionSegmentType<UserType>>()` to get rid of the `UseOffsetPaging` middleware. |
| 419 | + |
| 420 | +</ExampleTabs.Code> |
| 421 | +<ExampleTabs.Schema> |
| 422 | + |
| 423 | +⚠️ Schema-first does currently not support pagination! |
| 424 | + |
| 425 | +</ExampleTabs.Schema> |
| 426 | +</ExampleTabs> |
| 427 | + |
| 428 | +## Total count |
| 429 | + |
| 430 | +Sometimes we might want to return the total number of pageable entries. |
| 431 | + |
| 432 | +For this to work we need to enable the `IncludeTotalCount` flag on the `UseOffsetPaging` middleware. |
| 433 | + |
| 434 | +<ExampleTabs> |
| 435 | +<ExampleTabs.Annotation> |
| 436 | + |
| 437 | +```csharp |
| 438 | +[UseOffsetPaging(IncludeTotalCount = true)] |
| 439 | +``` |
| 440 | + |
| 441 | +</ExampleTabs.Annotation> |
| 442 | +<ExampleTabs.Code> |
| 443 | + |
| 444 | +```csharp |
| 445 | +descriptor.UseOffsetPaging(options: new PagingOptions |
| 446 | +{ |
| 447 | + IncludeTotalCount = true |
| 448 | +}); |
| 449 | +``` |
| 450 | + |
| 451 | +</ExampleTabs.Code> |
| 452 | +<ExampleTabs.Schema> |
| 453 | + |
| 454 | +⚠️ Schema-first does currently not support pagination! |
| 455 | + |
| 456 | +</ExampleTabs.Schema> |
| 457 | +</ExampleTabs> |
| 458 | + |
| 459 | +This will add a new field called `totalCount` to our _CollectionSegment_. |
| 460 | + |
| 461 | +```sdl |
| 462 | +type UserCollectionSegment { |
| 463 | + pageInfo: CollectionSegmentInfo! |
| 464 | + items: [User!] |
| 465 | + totalCount: Int! |
| 466 | +} |
| 467 | +``` |
| 468 | + |
| 469 | +If our resolver returns an `IEnumerable<T>` or an `IQueryable<T>` the `totalCount` will be automatically computed, if it has been specified as a subfield in the query. |
| 470 | + |
| 471 | +If we have customized our pagination and our resolver now returns a `CollectionSegment<T>`, we have to explicitly declare how the `totalCount` value is computed. |
| 472 | + |
| 473 | +```csharp |
| 474 | +var collectionSegment = new CollectionSegment<User>( |
| 475 | + items, |
| 476 | + pageInfo, |
| 477 | + getTotalCount: cancellationToken => ValueTask.FromResult(0)); |
| 478 | +``` |
| 479 | + |
| 480 | +# Pagination defaults |
| 481 | + |
| 482 | +If we want to enforce consistent pagination defaults throughout our app, we can do so, by setting the global `PagingOptions`. |
| 483 | + |
| 484 | +```csharp |
| 485 | +public class Startup |
| 486 | +{ |
| 487 | + public void ConfigureServices(IServiceCollection services) |
| 488 | + { |
| 489 | + services |
| 490 | + .AddGraphQLServer() |
| 491 | + // ... |
| 492 | + .SetPagingOptions(new PagingOptions |
| 493 | + { |
| 494 | + MaxPageSize = 50 |
| 495 | + }); |
| 496 | + } |
| 497 | +} |
| 498 | +``` |
| 499 | + |
| 500 | +# Types of pagination |
| 501 | + |
| 502 | +In this section we will look at the most common pagination approaches and their downsides. There are mainly two concepts we find today: _offset-based_ and _cursor-based_ pagination. |
| 503 | + |
| 504 | +> Note: This section is intended as a brief overview and should not be treated as a definitive guide or recommendation. |
| 505 | +
|
| 506 | +## Offset Pagination |
| 507 | + |
| 508 | +_Offset-based_ pagination is found in many server implementations whether the backend is implemented in SOAP, REST or GraphQL. |
| 509 | + |
| 510 | +It is so common, since it is the simplest form of pagination we can implement. All it requires is an `offset` (start index) and a `limit` (number of entries) argument. |
| 511 | + |
| 512 | +```sql |
| 513 | +SELECT * FROM Users |
| 514 | +ORDER BY Id |
| 515 | +LIMIT %limit OFFSET %offset |
| 516 | +``` |
| 517 | + |
| 518 | +### Problems |
| 519 | + |
| 520 | +But whilst _offset-based_ pagination is simple to implement and works relatively well, there are also some problems: |
| 521 | + |
| 522 | +- Using `OFFSET` on the database-side does not scale well for large datasets. Most databases work with an index instead of numbered rows. This means the database always has to count _offset + limit_ rows, before discarding the _offset_ and only returning the requested number of rows. |
| 523 | + |
| 524 | +- If new entries are written to or removed from our database at high frequency, the _offset_ becomes unreliable, potentially skipping or returning duplicate entries. |
| 525 | + |
| 526 | +## Cursor Pagination |
| 527 | + |
| 528 | +Contrary to the _offset-based_ pagination, where we identify the position of an entry using an _offset_, _cursor-based_ pagination works by returning the pointer to the next entry in our pagination. |
| 529 | + |
| 530 | +To understand this concept better, let's look at an example: We want to paginate over the users in our application. |
| 531 | + |
| 532 | +First we execute the following to receive our first page: |
| 533 | + |
| 534 | +```sql |
| 535 | +SELECT * FROM Users |
| 536 | +ORDER BY Id |
| 537 | +LIMIT %limit |
| 538 | +``` |
| 539 | + |
| 540 | +`%limit` is actually `limit + 1`. We are doing this to know wether there are more entries in our dataset and to receive the _cursor_ of the next entry (in this case its `Id`). This additional entry will not be returned to the consumer of our pagination. |
| 541 | + |
| 542 | +To now receive the second page, we execute: |
| 543 | + |
| 544 | +```sql |
| 545 | +SELECT * FROM Users |
| 546 | +WHERE Id >= %cursor |
| 547 | +ORDER BY Id |
| 548 | +LIMIT %limit |
| 549 | +``` |
| 550 | + |
| 551 | +Using `WHERE` instead of `OFFSET` is great, since now we can leverage the index of the `Id` field and the database does not have to compute an _offset_. |
| 552 | + |
| 553 | +For this to work though, our _cursor_ needs to be **unique** and **sequential**. Most of the time the _Id_ field will be the best fit. |
| 554 | + |
| 555 | +But what if we need to sort by a field that does not have the aforementioned properties? We can simply combine the field with another field, which has the needed properties (like `Id`), to form a _cursor_. |
| 556 | + |
| 557 | +Let's look at another example: We want to paginate over the users sorted by their birthday. |
| 558 | + |
| 559 | +After receiving the first page, we create a combined _cursor_, like `"1435+2020-12-31"` (`Id` + `Birthday`), of the next entry. To receive the second page, we convert the _cursor_ to its original values (`Id` + `Birthday`) and use them in our query: |
| 560 | + |
| 561 | +```sql |
| 562 | +SELECT * FROM Users |
| 563 | +WHERE (Birthday >= %cursorBirthday |
| 564 | +OR (Birthday = %cursorBirthday AND Id >= %cursorId)) |
| 565 | +ORDER BY Birthday, Id |
| 566 | +LIMIT %limit |
| 567 | +``` |
| 568 | + |
| 569 | +### Problems |
| 570 | + |
| 571 | +Even though _cursor-based_ pagination can be more performant than _offset-based_ pagination, it comes with some downsides as well: |
| 572 | + |
| 573 | +- When using `WHERE` and `ORDER BY` on a field without an index, it can be slower than using `ORDER BY` with `OFFSET`. |
| 574 | + |
| 575 | +- Since we now only know of the next entry, there is no more concept of pages. If we have a feed or only _Next_ and _Previous_ buttons, this works great, but if we depend on page numbers, we are in a tight spot. |
0 commit comments