はじめに
大量のデータをユーザーに表示する場合は、すべての情報を一気に表示するのではなく、ある程度のまとまりごとに表示することが重要です。例えば、Googleで「Microsoft」という単語を検索すると約2,190,000,000もの結果が返されますが、一度に表示されるのはそのうちの10個です。特に大量の結果セット(1万、10万、または100万のレコードから成るテーブル)にページングアクセスを提供する場合は、要求された特定のページのデータだけが返されるように、データのクエリに注意を払う必要があります。
ASP.NET 1.xのDataGridとASP.NET 2.0のGridViewは、2種類のページング方法を備えています。
種類 | 特徴 |
既定ページング | 実装は簡単ですが、データベースのすべてのレコードを単純に取得し、結果をトリムすることで、要求されたページのレコードのみを表示します。 |
カスタムページング | 開発者が、現在のページに表示するレコードのサブセットのみを適切に取得する必要があります。実装に手間がかかります。 |
既定ページングは短時間で簡単に実装できますが、必要な大きさの結果セットだけを取得したい場合はカスタムページングが不可欠です。私は2006年3月に、カスタムページングを利用して大量の結果セットを効率的にページングする方法についての記事を本サイト(4GuysFromRolla.com)にいくつか投稿しましたが、そこでは、ObjectDataSourceとSQL Server 2005の新しいROW_NUMBER()
キーワードを使ってGridViewでカスタムページングを行いました。ROW_NUMBER()
という新しいキーワードは、行番号で並べられたデータの特定のサブセットを効率的に取得するものです。3月の投稿の後、多くの読者から、ROW_NUMBER()
のないSQL Server 2000でこのような効率的なカスタムページングを実装する方法について問い合わせがありました。
本サイト(4GuysFromRolla.com)に以前に投稿された「Paging through Records using a Stored Procedure」(Daniel Anderson著)は、既定ページングの実装をはるかにしのぐ1つの方法を示しています。しかし、パフォーマンスという点では改善の余地があります。本稿では、Danielのストアドプロシージャに手を加えて、さらに効率的な方法を示します。本稿の最後に示すストアドプロシージャは、典型的なASPアプリケーションや、ASP.NET 1.xのDataGridによるカスタムページングで使用できます。また、ObjectDataSourceで使用してASP.NET 2.0アプリケーションのGridViewでカスタムページングを実現することもできます。詳細については、以下を参照してください。
Danielの方法
「Paging through Records using a Stored Procedure」の中で、Daniel Andersonは、sp_PagedResults
という名前のストアドプロシージャを作成しました。これは、2つの整数型の入力パラメータ(@Page
および@RecsPerPage
)を受け取り、基になるデータベーステーブルから、指定ページのデータに属するレコードを返します。例えば、1ページが10レコードの場合に2ページ目のデータを表示するときは、ストアドプロシージャsp_PagedRecords
を呼び出して@Page
と@RecsPerPage
にそれぞれ2と10の値を渡します。
テーブルの一意キー(ID)が連番になっていて、番号に抜けがない場合、このようなテーブルからページ単位でデータを取得するのは簡単です。単純なWHERE
句を指定すればよいのです。例えば、次のようなデータのテーブルがあるものとします。
EmployeeID | LastName | FirstName | DepartmentID | Salary | HireDate |
1 | Mitchell | Scott | ... | ... | ... |
2 | Lee | Sam | ... | ... | ... |
3 | Smith | Hans | ... | ... | ... |
4 | Johnson | Ernie | ... | ... | ... |
5 | Gonzalez | Laura | ... | ... | ... |
6 | Wang | Hugo | ... | ... | ... |
7 | Jackson | Tito | ... | ... | ... |
8 | Maher | Todd | ... | ... | ... |
... | ... | ... | ... | ... | ... |
EmployeeIDの値は連続していて番号に抜けがないため、汎用的なクエリで特定のページのデータを簡単に取得できます。
SELECT ... FROM Employees WHERE EmployeeID BETWEEN (''page * recsPerPage'') + 1 AND (''page * recsPerPage'') + ''recsPerPage ''
例えば、1ページに表示する従業員が3人の場合、1ページ目のデータを表示するには、次のように指定します。
SELECT ... FROM Employees WHERE EmployeeID BETWEEN (0 * 3) + 1 AND (0 * 3) + 3 -- Which is tantamount to... SELECT ... FROM Employees WHERE EmployeeID BETWEEN 1 AND 3
これにより、最初の3人の従業員が返されます。2番目のページを表示するには、次のように指定します。
SELECT ... FROM Employees WHERE EmployeeID BETWEEN (1 * 3) + 1 AND (1 * 3) + 3 -- Which is tantamount to... SELECT ... FROM Employees WHERE EmployeeID BETWEEN 4 AND 6
これにより、2ページ目のデータであるEmployeeID
4、5、6が返されます。
この方法は、ページングされるデータの主キー値に抜けがなければうまくいきます。しかし、レコードが削除されると、欠番が生じます。例えば、Sam LeeとHans Smithが解雇されると、「Employees」テーブルは次のようになります。
EmployeeID | LastName | FirstName | DepartmentID | Salary | HireDate |
1 | Mitchell | Scott | ... | ... | ... |
4 | Johnson | Ernie | ... | ... | ... |
5 | Gonzalez | Laura | ... | ... | ... |
6 | Wang | Hugo | ... | ... | ... |
7 | Jackson | Tito | ... | ... | ... |
8 | Maher | Todd | ... | ... | ... |
... | ... | ... | ... | ... | ... |
EmployeeID
1と4の間に欠番があるため、先ほど使用した単純なアルゴリズムではうまくいきません。
このような欠番に対処するため、Daniel Andersonのストアドプロシージャでは偽の連番IDを作成しています。この処理は、次のアルゴリズムによって行われます。
- まず、ページング対象のデータの各列に対応する列と、IDという名前の列を含んだ一時テーブルを作成します。このID列を「IDENTITY列」としてマークします(IDENTITY列としてマークされた列の値は、レコードが挿入されるたびに自動的に増分します)。
- ページング対象のテーブルの内容全体を、ステップ1で作成した一時テーブルにダンプします。これによって、ID列の値は1、2、3、...に設定されます。
- この一時テーブルに対して
SELECT
クエリを行い、WHERE
句に前述の単純な式を適用します。
Danielのストアドプロシージャの詳細については、「Paging through Records using a Stored Procedure」を参照してください。ストアドプロシージャの具体的なコードは、ここで参照できます。
一見するとこの方法は、すべてのレコードをそのまま返すより良いように思えます。しかし、結局は一時テーブルにダンプしているだけで、「Employees」のすべてのレコードを処理していることに変わりありません。とはいえ、Danielの方法は、データベースサーバーとWebサーバーとの間のトラフィックを大きく削減します。「Employees」テーブルのデータ全部を処理するという点は同じですが、ストアドプロシージャが返すのは10レコード(または、設定されている1ページ当たりのレコード数)だけです。