📄

カーソルページングとオフセットページングの比較(Flutterの例)

2024/08/03に公開

大量のデータを効率的に扱うために、一般的に使われる二つのページング手法「カーソルページング」と「オフセットページング」のメリット・デメリットを比較します。

カーソルページングとは

カーソルページングは、データベースクエリで前回の取得位置(カーソル)を利用して、次のデータを効率的に取得する手法です。

メリット

カーソルページングのメリットとしてまず挙げられるのは、高速なデータアクセスが可能で、データ変更にも強い安定性があります。リソース効率も良く、特定の範囲に限定されたデータを扱うため、メモリやCPUの消費が少ないです。

デメリット

まず、カーソルの管理が必要であり、後述するオフセットページングに比べて実装が複雑です。適切なインデックスが設定されていない場合、クエリパフォーマンスが低下する可能性があります。データの並び順に影響を受けやすく、ソート順が変更されるとカーソルが無効になることがあります。

Flutterでの実装例

Future<List<Post>> fetchPosts(String? lastId, int limit) async {
  final query = lastId == null
      ? FirebaseFirestore.instance.collection('posts').orderBy('id').limit(limit)
      : FirebaseFirestore.instance.collection('posts').orderBy('id').startAfter([lastId]).limit(limit);

  final snapshot = await query.get();
  return snapshot.docs.map((doc) => Post.fromFirestore(doc)).toList();
}

// 使用例
String? lastId = '1003';
List<Post> posts = await fetchPosts(lastId, 3);
if (posts.isNotEmpty) {
  lastId = posts.last.id;
}

オフセットページングとは

オフセットページングは、データベースクエリで「オフセット」と「リミット」を利用して、特定の位置から一定数のレコードを取得する手法です。

メリット

オフセットページングのメリットとしては、実装が簡単で、データの任意の位置から取得でき、特定のページ番号を指定しやすいです。

デメリット

しかし、オフセットページングにはパフォーマンスの低下が問題となります。大量のデータセットでは後半のページにアクセスする際に多くのデータをスキャンする必要があり、パフォーマンスが低下します。(オフセットページングのパフォーマンス向上のためにキャッシングを行うことができますが、キャッシュの管理を適切に行う必要があります。)

仕組みと具体例

オフセットページングは、データベースクエリに対して「オフセット」と「リミット」を使用することで、特定の位置から一定数のレコードを取得します。

Flutterアプリでの実装例

Future<List<Product>> fetchProducts(int offset, int limit) async {
  final response = await http.get(Uri.parse('https://api.example.com/products?offset=$offset&limit=$limit'));
  if (response.statusCode == 200) {
    List jsonResponse = json.decode(response.body);
    return jsonResponse.map((product) => Product.fromJson(product)).toList();
  } else {
    throw Exception('Failed to load products');
  }
}

// 使用例
int offset = 3;
int limit = 3;

List<Product> products = await fetchProducts(offset, limit);
offset += limit;

オフセットページングでは、特に後半のページでパフォーマンスが低下する理由は、データベースがオフセット分のデータをスキャンしなければならないためです。
例えば、次のようなSQLクエリを考えます。

SELECT * FROM products ORDER BY name ASC LIMIT 10 OFFSET 90000;

このクエリは、productsテーブルから名前順に並べたデータの90001番目から90010番目までの10件を取得するものです。
このとき内部処理は以下のようになっています。

  1. 全データの読み込み:
    データベースエンジンはまず、productsテーブル全体を対象にします。ここで、全データがメモリ上にロードされるか、ディスク上で順次読み込まれます。
  2. ソート処理:
    まず、100,000件のデータがソートされます。データベースエンジンは、指定されたソートキー(ここではname)に基づいてデータを並べ替えます。インデックスが適切に設定されている場合は、このソートはインデックスを使用して効率的に行われますが、インデックスがない場合は全データのソートが必要です。
  3. オフセットの適用:
    ソートが完了すると、データベースエンジンはオフセット値(ここでは90000)を適用します。つまり、ソートされたデータセットの最初の90000件をスキップします。
  4. リミットの適用:
    オフセットの後、データベースエンジンはリミット値(ここでは10)を適用し、スキップした後のデータセットから10件を取得します。

この内部処理のフローからわかるように、オフセットページングでは、データベースエンジンがオフセット分のデータをスキャンしてスキップしなければならないため、オフセット値が大きくなるとスキャンするデータ量も増えます。

これにより、次のような問題が発生します。

  1. I/Oコストの増加:
    ディスクから読み込むデータ量が増えるため、ディスクI/Oのコストが増大します。
  2. メモリ使用量の増加:
    スキャンするデータ量が増えることで、メモリの使用量も増加し、メモリが逼迫する可能性があります。
  3. CPU使用率の増加:
    スキップするデータのフィルタリングやソートのために、CPUリソースが多く消費されます。

この処理を繰り返すことで、オフセットが大きくなると、非常に多くのリソースが消費されます。これがオフセットページングで後半のページのパフォーマンスが低下する主な理由です。

パフォーマンス比較とデータベースの違い

カーソルページングはインデックスを活用して大量のデータを高速で扱える一方、オフセットページングは前のデータをスキャンする必要があるため、特に後半のページでパフォーマンスが低下します。リレーショナルデータベース(MySQL, PostgreSQL)ではインデックスを活用したカーソルページングが有効であり、NoSQLデータベース(MongoDB)ではドキュメントの位置情報を利用したカーソルページングが効率的です。Flutterアプリでは、データベースの特性を理解し、適切なページング手法を選ぶことが重要です。

FlutterのFirebase Firestoreでは、ドキュメントのタイムスタンプや他のフィールドを基にしたカーソルページングが効果的です。Firestoreはリアルタイムデータベースであり、効率的にデータを取得するための強力なクエリ機能を提供します。これにより、アプリケーションはリアルタイムでデータの変更に対応しつつ、高速なデータアクセスを実現できます。

まとめ

カーソルページングとオフセットページングはそれぞれ異なる利点と欠点があります。適切な手法の選択は、具体的なユースケース、データベースの特性、パフォーマンス要件に基づいて行うべきです。具体的には、カーソルページングは大規模データセットに適しており、オフセットページングは実装の簡単さが求められる小規模データセットに適している傾向があります。

適切なページング手法を選択し、効果的に実装することで、アプリのパフォーマンスを最適化し、優れたユーザエクスペリエンスを提供していくことができます。

Discussion