Slide 1

Slide 1 text

© 2024 estie, inc. SWE 野村 大樹 GraphQLにおける ページネーションベストプラクティス 1

Slide 2

Slide 2 text

© 2024 estie, inc. 野村 大樹 (@h_noson) estie Software Engineer 2018年に株式会社リクルートホールディングスに新卒 入社しIndeedへ出向。1年後にIndeedに転籍。 2022年よりestieに参画。テックリードとしてestie マーケット調査の開発をリード。 自己紹介 2

Slide 3

Slide 3 text

© 2024 estie, inc. Agenda • Paginationとは • Offset based と Cursor based の概要と利点・欠点 • どちらを使うべきなの? • Offset basedの欠点を補う方法 3

Slide 4

Slide 4 text

© 2024 estie, inc. • 一定量以上の情報を表示する際にページを分割する方法 • 大きく2種類 o 任意のページに移動できる(例:Google)これ↑ o スクロールなどで前後のページを取得する(例:X, Googleモバイル版) Paginationとは 4

Slide 5

Slide 5 text

© 2024 estie, inc. Offset based と Cursor based 5

Slide 6

Slide 6 text

© 2024 estie, inc. • よく使われるシンプルなPaginationの方式 • offset または page と limit を指定する • 例:https://estie.jp/buildings?page=2&limit=10 o 10棟毎に区切ったときの2ページ目 • 2ページ目から5ページ目など、ページ移動が自由 6 Offset based Pagination

Slide 7

Slide 7 text

© 2024 estie, inc. Offset based Pagination 7 type Query { buildings(page: Int! = 1, limit: Int! = 50): BuildingPaginationPayload } type BuildingPaginationPayload { nodes: [Building!]! pageInfo: OffsetPageInfo! } type OffsetPageInfo { """ 1-indexed """ page: Int! totalPages: Int! totalCount: Int! } GraphQL Schema定義例

Slide 8

Slide 8 text

© 2024 estie, inc. • GraphQLにおけるベストプラクティス (https://graphql.org/learn/pagination/) • Relay connectionsが標準仕様 (https://relay.dev/graphql/connections.htm) o "cursor” (after, before) と取得する要素数 (first, last) を指定する o 例: https://estie.jp/buildings?after=MTIzCg&first=10 • レスポンスに始点と終点の “cursor” が含まれており、前後のページへ のみ遷移できる “cursor” とは • IDなどレコードを特定する何かしらの値 • Base64などでencodeして中身を曖昧するべき (“MTIzCg==“ = Base64(“123”)) o クライアントがその値に依存せず仕様変更できるようにする目的 Cursor based Pagination 8

Slide 9

Slide 9 text

© 2024 estie, inc. GraphQL Schema定義例 Cursor based Pagination 9 type Query { buildings( after: String, before: String, first: Int, last: Int ): BuildingConnection } type BuildingConnection { edges: [BuildingEdge!]! pageInfo: PageInfo! } type BuildingEdge { node: Building! cursor: String! } type PageInfo { startCursor: String! endCursor: String! hasNextPage: Boolean! hasPreviousPage: Boolean! }

Slide 10

Slide 10 text

© 2024 estie, inc. • 一定量以上の情報を表示する際にページを分割する方法 • 大きく2種類 o 任意のページに移動できる(例:Google)これ↑ o スクロールなどで前後のページを取得する(例:X, Googleモバイル版) Paginationとは 10 前者がOffset based、後者がCursor based でよいのでは?

Slide 11

Slide 11 text

© 2024 estie, inc. • 一定量以上の情報を表示する際にページを分割する方法 • 大きく2種類 o 任意のページに移動できる(例:Google)これ↑ o スクロールなどで前後のページを取得する(例:X, Googleモバイル版) Paginationとは 11 前者がOffset based、後者がCursor based でよいのでは? 大体そう だがそれぞれの利点を理解した上で選択しましょう

Slide 12

Slide 12 text

© 2024 estie, inc. それぞれの利点・欠点 12

Slide 13

Slide 13 text

© 2024 estie, inc. それぞれの利点・欠点 13 Offset based Cursor based 利点 • 仕様や実装がシンプル • 連続していないページにアクセスできる • 条件を満たしたときクエリ実行が高速 • 更新に強い • GraphQLのライブラリに標準実装されているこ とが多い • 例:async-graphql (Rust), graphql-ruby 欠点 • テーブルが大きい場合、パフォーマンスに 悪影響がある • 更新に弱い • 連続したページにのみアクセスできる • offset basedに比べて仕様や実装が複雑

Slide 14

Slide 14 text

© 2024 estie, inc. それぞれの利点・欠点 14 Offset based Cursor based 利点 • 仕様や実装がシンプル • 連続していないページにアクセスできる • 条件を満たしたときクエリ実行が高速 • 更新に強い • GraphQLのライブラリに標準実装されているこ とが多い • 例:async-graphql (Rust), graphql-ruby 欠点 • テーブルが大きい場合、パフォーマンスに 悪影響がある • 更新に弱い • 連続したページにのみアクセスできる • offset basedに比べて仕様や実装が複雑

Slide 15

Slide 15 text

© 2024 estie, inc. Offset basedで発行されるSQLクエリ SELECT * FROM buildings WHERE ... LIMIT 10 OFFSET 100; Cursor basedで発行されるSQLクエリ idがcursorのとき(高速) SELECT * FROM buildings WHERE ... AND id > 123 LIMIT 10; offsetがcursorのとき(低速) SELECT * FROM buildings WHERE ... LIMIT 10 OFFSET 100; 複数キーからcursorが生成されるとき(indexを張ると高速) SELECT * FROM buildings WHERE updated_at > '2024-01-01' OR (updated_at = '2024-01-01' AND id > 123) ORDER BY updated_at, id LIMIT 10; パフォーマンス 15 OFFSETが大きいと遅くなる O(N)

Slide 16

Slide 16 text

© 2024 estie, inc. それぞれの利点・欠点 16 Offset based Cursor based 利点 • 仕様や実装がシンプル • 連続していないページにアクセスできる • 条件を満たしたときクエリ実行が高速 • 更新に強い • GraphQLのライブラリに標準実装されているこ とが多い • 例:async-graphql (Rust), graphql-ruby 欠点 • テーブルが大きい場合、パフォーマンスに 悪影響がある • 更新に弱い • 連続したページにのみアクセスできる • offset basedに比べて仕様や実装が複雑

Slide 17

Slide 17 text

© 2024 estie, inc. Offset based データが更新されるとどうなるか 17 ID=1 2 3 4 5 page=1&limit=3 page=2&limit=3 ID=3を削除 ID=1 2 4 5 ID=1 2 4 5 スキップされる

Slide 18

Slide 18 text

© 2024 estie, inc. Cursor based データが更新されるとどうなるか 18 ID=1 2 3 4 5 first=3 after=“3”&first=3 ID=3を削除 ID=1 2 4 5 ID=1 2 4 5 スキップされない

Slide 19

Slide 19 text

© 2024 estie, inc. 方式の選択基準 19

Slide 20

Slide 20 text

© 2024 estie, inc. Cursor based • データ量が膨大で絞り込みをあまりせずに取得する(OFFSETが大きくなり得る) • 追加や削除が起こる • 例:X, Facebook Offset based • ページを跨いで遷移したい(2ページ目→5ページ目) • Cursor basedの恩恵をあまり受けられない • OFFSETが大きくならない(Googleでは1000件以上表示できない) • 追加や削除がない(or 検索インデックスのバージョンを固定してページ遷移する) • 様々な項目でソートしたい(estieではこのケースが多い) • ソートキーをcursorにしてindexを張ればCursor basedでも実装可能 • 例:Google, Amazon, LinkedIn 20 方式の選択基準

Slide 21

Slide 21 text

© 2024 estie, inc. 追加や削除に対応した Offset based Pagination 21

Slide 22

Slide 22 text

© 2024 estie, inc. 22 追加や削除に対応した Offset based Pagination • ページを跨いで遷移したいかつ追加・削除が起こるケースは存在する • ページを跨いで遷移したい → Offset based • 追加・削除が起こる → Subscriptionで変更を監視して調整する Subscription • WebSocketまたはHTTPを使って同期的にデータを取得できる subscription UserUpdated { userUpdated { id } }

Slide 23

Slide 23 text

© 2024 estie, inc. 23 追加や削除に対応した Offset based Pagination(例:順位表) type User { id: ID! score: Int! } type Query { rankedUsers(page: Int! = 1, limit: Int! = 50): UserPaginationPayload } type Mutation { updateScore(userId: ID!, score: Int!): UpdateScorePayload } type Subscription { userUpdated: User }

Slide 24

Slide 24 text

© 2024 estie, inc. 24 まとめ • GraphQLではCursor based Paginationが公式に推奨されているが、欠点 もありOffset basedが適しているケースもある • Cursor basedは高速で更新に強い • Offset basedは仕様がシンプルで自由にページ遷移できる

Slide 25

Slide 25 text

© 2024 estie, inc. • GraphQL Foundation • https://graphql.org/learn/pagination/ • Relay - GraphQL Cursor Connections Specification • https://relay.dev/graphql/connections.htm • Production Ready GraphQL • https://book.productionreadygraphql.com/ • Cursor based pagination with arbitrary ordering • https://medium.com/@george_16060/cursor-based-pagination-with-arbitrary- ordering-b4af6d5e22db 参考 25