デベロッパー 2006年6月20日 10:00

ASP.NET 2.0とSQL Server 2005によるカスタムのページング処理

著者: Scott Mitchell  オリジナル版を読む
2006年6月20日 10:00 付の記事
■海外internet.com発の記事

SQL Server 2000を使ったカスタムのページング処理
 SQL Server 2005では、データをページング処理して特定のページのみを取得することが非常に簡単になりました。本稿では、この新機能に焦点を当てることにします。SQL Server 2005への切り替えをまだ終えていない方は、『A More Efficient Method for Paging Through Large Result Sets』の記事をお読みください。本稿で紹介するストアドプロシージャは、その記事で紹介されているストアドプロシージャに置き換えることができます。

はじめに

 Web開発では、ページング処理を行ったデータにアクセスするという方式が一般的です。レポートやデータベーステーブルの内容全体をエンドユーザーに表示するのではなく、通常は、ページ間を移動するコントロールを用意して、Webページ1ページ分のレコードのサブセットだけを表示します。ASP.NET 1.xでは、DataGridのおかげでページング処理が非常に簡単になりました。DataGridのAllowPagingプロパティをTrueに設定し、PageIndexChangedイベントハンドラに数行のコードを追加するだけで終わりです。ASP.NET 2.0GridViewでは、このプロセスがさらに簡単になっています。GridViewのスマートタグで[ページングを有効にする]チェックボックスをオンにするだけです。コーディングは必要ありません。

 もちろん、どのようなことにも代償は付きもので、[ページングを有効にする]チェックボックスをオンにする(DataGridの場合は、コードを数行書く)という簡便さを選んだ場合は、パフォーマンス面での妥協が生じます。何もしなければ、DataGridとGridViewは既定のページング処理を行います。これは、1ページ分のデータを表示するたびにすべてのレコードを返す単純なページング処理モデルです。ページング処理対象のデータが少なければ(レコード数が10個から100個程度であれば)、パフォーマンスが悪くてもこの機能を設定する簡便さの方が勝るでしょう。しかし、数千、数万、数十万のレコードをページング処理する必要がある場合は、既定のページング処理モデルは現実的ではありません。

 既定のページング処理に代わる手段がカスタムのページング処理です。この場合は、データの適切なサブセットをインテリジェントに取得するコードを書かなければなりません。仕事が少し増えますが、非常に大きいサイズのデータを処理する場合には不可欠です。ASP.NET 1.xでカスタムのページング処理を実装する方法については、拙書『ASP.NET Data Web Controls Kick Start』をご覧ください。本稿では、SQL Server 2005の新機能ROW_NUMBER()を使ってASP.NET 2.0でカスタムのページング処理を実装する方法を説明します(ROW_NUMBER()など、SQL Serverの新しい順序付け機能については、『Returning Ranked Results with Microsoft SQL Server 2005』を参照してください)。

 それでは、始めましょう。

既定のページング処理とカスタムのページング処理の違い

 ASP.NET 2.0のGridView(およびASP.NET 1.xのDataGrid)には、既定のページング処理とカスタムのページング処理の2つのページング処理モデルがあります。この2つのモデルでは、パフォーマンスを選ぶか、設定/構成/使用の簡便さを選ぶかという判断が強いられます。SqlDataSourceコントロールは既定のページング処理を使用します(カスタムのページング処理を使うことも可能ですが、非常に手間がかかります)。ObjectDataSourceコントロールも通常は既定のページング処理を使いますが、メカニズムが簡単なので、カスタムのページング処理を使うようにしてもそれほど負担は大きくありません。GridViewは単にデータを表示するだけであることを忘れないでください。データベースから実際にデータを取得するのは、GridViewのデータソースコントロールです。

 既定のページング処理では、新しいページのデータをGridViewに表示するたびに、GridViewのデータソースからすべてのデータが取得されて返されます。すべてのデータが返されると、GridViewは、ユーザーに表示するページと1ページに表示するレコード数に基づいて、すべてのデータの中からデータの一部を選択して表示します。ここで大切なことは、1ページ分のデータを読み込むたびに、つまり、最初のページアクセスで1ページ目のデータを表示するときでも、ユーザーが別のページを表示するよう要求した後でポストバックしたときでも、必ずデータ結果全体が取得されるということです。

 例えば、eCommerceという会社で、この会社が販売している150製品の一覧をページング処理してユーザーに表示するとしましょう。特に、ページあたり10個のレコードを表示したいと思います。さて、ユーザーがWebページにアクセスすると、データソースコントロールによって150個のレコードすべてが返されますが、GridViewには最初の10製品(製品1〜10)が表示されます。次に、ユーザーが次のページに移動するとします。これによりポストバックが発生し、GridViewはデータソースコントロールに150個のレコードすべてを再び要求しますが、このときに表示されるのは2セット目の10製品(製品11〜20)だけです。

キャッシングとSqlDataSource
 SqlDataSourceでは、EnableCachingプロパティを設定するだけで、返されるDataSetをキャッシュに格納することができます。DataSetのキャッシングにより、ページング処理対象のデータはメモリにキャッシュされるので、別のページに移動するときにデータベースを要求する必要がなくなります。ただし、最初のページを読み込むときは同じ問題が生じます。つまり、すべてのデータをキャッシュ内のDataSetに読み込まなければなりません。さらに、このアプローチには、古いデータが表示されるという問題もあります(ただし、SQLキャッシュ依存を使った場合はこの点について議論の余地があります)。
 私のあまり正確とは言えないテストでは、DataSetをキャッシュしても、カスタムのページング処理の方が2倍高速であることが判明しました。しかし、後でパフォーマンス値を検討するときに詳しく説明しますが、このキャッシングアプローチは非キャッシングアプローチよりもはるかに優れています(ただし、カスタムのページング処理アプローチにはかないません)。
 SqlDataSourceから返されるDataSetのキャッシングの詳細については、『SqlDataSourceコントロールによるデータのキャッシュ』を参照してください。

 カスタムのページング処理では、開発者の仕事がもう少し増えます。GridViewをデータソースコントロールにただバインドして[ページングを有効にする]チェックボックスをオンにするのではなく、特定のページに表示する必要があるレコードのみを選択して取得するように、データソースコントロールを設定しなければなりません。このアプローチのメリットは、1ページ目のデータを表示するときに150個すべてのレコードを取得するのではなく、SQLステートメントを使って1〜10番目の製品だけを取得できる点です。ただし、このSQLステートメントは150個のレコードの中から正しいサブセットだけを取り出せるくらい「賢く」なければなりません。

カスタムのページング処理のパフォーマンスの威力
 カスタムのページング処理は既定のページング処理よりもパフォーマンスが優れています。これは、表示する必要があるデータベースレコードしか取得しないためです。この製品ページの例では、150の製品があり、1ページあたり10製品を表示することにしました。カスタムのページング処理では、ユーザーが15ページすべてを表示するとしても、データベースに対してクエリするレコードの数は150件だけです。しかし、既定のページング処理では、1ページごとに150件のレコードにアクセスすることになるので、取得するレコードの総数は150の15倍、つまり2,250件になります。
 カスタムのページング処理はパフォーマンスが優れているのに対し、既定のページング処理は使いやすさに優れています。そのため、ページング処理対象のデータが比較的少ない場合や、データベースサーバのトラフィックがあまり大きくない場合には、既定のページング処理を使うことをお勧めします。ページング処理対象のレコード数が数百、数千、数万に及ぶ場合は、もちろん、カスタムのページング処理を使用してください。ただし、現時点でまだFAQの数が200個にも満たないASPFAQs.comデータベースのようなデータベースをページング処理する場合は、既定のページング処理で十分です。
 (もちろん、レコード数が75個しかないような小さいテーブルで既定のページング処理を使う場合は、この先もずっとそのテーブルの行数が少ないままであることが前提になります。今は小さいけれども後でレコード数が7,500件にも増加するテーブルで既定のページング処理を使うと、お客様の不満を招くことになります!)

SQL Server 2005で1ページ分のデータを効率よく返す

 4Guysへの以前の投稿『Returning Ranked Results with Microsoft SQL Server 2005』で紹介したように、SQL Server 2005では、順序付けした結果を返す新しいキーワードがいくつか導入されました。特に、ROW_NUMBER()キーワードを指定すると、返される結果に連番の行番号が付けられます。そこで、ROW_NUMBER()を使って、次のようなクエリを実行することで特定のページのデータを取得することができます。

SELECT ...
FROM
   (SELECT ...
         ’’ROW_NUMBER() OVER(ORDER BY ColumnName) as RowNum’’
    FROM Employees e
   ) as <DerivedTableName>
WHERE ’’RowNum BETWEEN @startRowIndex AND (@startRowIndex + @maximumRows) - 1’’

 ここで、@startRowIndexは開始行のインデックスで、@maximumRowsはページあたりの表示レコードの最大数です。このクエリは、開始行のインデックスから「開始行のインデックス+ページあたりのレコード数」のインデックスまでのROW_NUMBER()を持つレコードのサブセットを返します。

 この概念を理解するために、具体的な例を紹介しましょう。ここに、5,000個のレコードから成る「Employees」テーブルがあるとします(事業が好調ですね)。次のクエリを実行します。

SELECT RowNum, EmployeeID, LastName, FirstName
FROM
   (SELECT EmployeeID, LastName, FirstName
       ’’ROW_NUMBER() OVER(ORDER BY EmployeeID) as RowNum’’
    FROM Employees e
   ) as EmployeeInfo

 この場合は、次のような結果が返されます。

RowNumEmployeeIDLastNameFirstName
11000SmithFrank
21001JacksonLucy
31011LeeSam
41012MitchellJisun
51013YatesScott
61016PropsKathryn
……
50006141JordanDJ

 EmployeeIDフィールドが連番ではなく、しかも1から始まっていない場合でも、ROW_NUMBER()値は1つ目のレコードに対する1から始まり、連番になっていることに注意してください。そのため、ページあたりのレコード数を10個として、3ページ目を表示する場合は、31〜40のレコードが必要であることが分かるので、簡単なWHERE句でそれを実現することができます。

カスタムのページング処理に合わせてObjectDataSourceを設定する

 既に説明したように、SqlDataSourceはカスタムの並べ替え機能に向いていませんが、ObjectDataSourceはこの機能をサポートするように作られています。ObjectDataSourceは、オブジェクトの提供データにアクセスするためのデータソースコントロールです。データ提供側のオブジェクトは、自身が提供するデータをさまざまな手段で取得することができます(Webサービス、データベース、ファイルシステム、XMLファイルなど、多様な方法が考えられます)。ObjectDataSourceは、オブジェクトがどのような手段でデータを取得しているかに関係なく、ただオブジェクトとそのデータを使用するデータWebコントロール(GridViewコントロールなど)との間のプロキシの役割を果たします(ObjectDataSourceの詳細については、『ObjectDataSourceコントロールの概要』を参照してください)。

 データWebコントロールをObjectDataSourceにバインドする場合には[ページングを有効にする]チェックボックスをオンにします。このとき、特にカスタムのページング処理をサポートするようにObjectDataSourceを設定していないと、既定のページング処理が有効になります。カスタムのページング処理をサポートするようにObjectDataSourceを設定するには、次の機能を提供するオブジェクトを使う必要があります。

  1. 最後の2つの入力パラメータとして2つの整数値を取るメソッド。1つ目の整数値ではデータの取得先の開始インデックス(ゼロベース)を指定し、2つ目の整数値ではページあたりに取得するレコードの最大数を指定します。このメソッドは、要求されているデータの正しいサブセット、つまり指定されたインデックスを先頭として、指定されたレコードの合計数を超えないデータを返す必要があります。
  2. ページング処理の対象となるレコードの合計数を表す整数値を返すメソッド(ページ番号を表示するときや[次のページ]リンクを有効にするかどうかを指定するときにはデータの総ページ数を認識する必要があるので、ページング処理コントロールのレンダリング時にデータWebコントロールでこの情報が使用されます)。

 これらの機能を提供するベースオブジェクトを用いれば、カスタムのページング処理をサポートするようにObjectDataSourceを設定することは非常に簡単です。必要なのは、次に示すObjectDataSourceのプロパティを設定することだけです。

  • EnablePagingプロパティをTrueに設定する。
  • SelectMethodプロパティを、開始インデックスと最大行数を入力パラメータとして受け取るメソッドに設定する。
  • StartRowIndexParameterNameプロパティを、SelectMethodメソッドの開始インデックスを受け取る整数型入力パラメータの名前に設定する。この値を省略すると、既定でstartRowIndexが設定される。
  • MaximumRowsParameterNameプロパティを、SelectMethodメソッドの最大行数を受け取るの整数型入力パラメータの名前に設定する。この値を省略すると、既定でmaximumRowsが設定される。
  • SelectCountMethodプロパティを、ページング処理対象のレコードの合計数を返すメソッドに設定する。

 これだけです。この設定が終わると、ObjectDataSourceはカスタムのページング処理機能を使うようになります。もちろん、ここでの山場は、データの正しいサブセットを賢く取得する基となるオブジェクトの作成です。しかし、そのオブジェクトを作成してしまえば、後はいくつかのプロパティを設定するだけです。

カスタムのページング処理をサポートするオブジェクトを作成する

 ObjectDataSourceをGridViewにバインドするためには、まずObjectDataSourceが使う基となるオブジェクトが必要であり、このオブジェクトには、データの特定のサブセットにアクセスしてページング処理の対象となる行数を返すメソッドが必要です。Joseph Chancellorの記事『Using Strongly-Typed Data Access in Visual Studio 2005 and ASP.NET 2.0』とBrian Noyesの記事『Build a Data Access Layer with the Visual Studio 2005 DataSet Designer』で述べられているように、Visual Studio 2005ではObjectDataSourceに接続できるオブジェクトを簡単に作成できます。まず、これらのオブジェクトのメソッドから返される厳密に型指定されたDataSet(Strongly-typed DataSet)にデータを取り込む場合に使うストアドプロシージャ(またはSQLクエリ)を定義します。

 本稿のサンプルファイルには、50,000人分の社員レコード(および、追加レコードをまとめて追加できる簡単な方法)から成るサンプルデータベースが収められています。このデータベースに、カスタムのページング処理の2つのデモで使う次の3つのストアドプロシージャが含まれています。

  • GetEmployeesSubset(@startRowIndex int, @maximumRows int)
  • EmployeeID順に並べ替えて、@startRowIndexから始まる最大@maximumRows個のレコードを「Employees」テーブルから返します。
  • GetEmployeesRowCount
  • 「Employees」テーブル内のレコードの合計数を返します。
  • GetEmployeesSubsetSorted(@sortExpression nvarchar(50), @startRowIndex int, @maximumRows int)
  • 指定の並べ替え式(@sortExpression)に基づいて並べ替えられた1ページ分のデータを返します。このストアドプロシージャでは、例えば給与順に並べ替えられた1ページ分のデータを返すことができます(GetEmployeesSubsetは常にEmployeeID順に並べ替えられたレコードを返します)。カスタムのページング処理と並べ替えを併用するGridViewを作成する場合にはこの柔軟性が必要です。
 本稿ではカスタムのページング処理と並べ替えを併用するGridViewの実装方法については説明しませんが、例はサンプルファイルに入っています。カスタムのページング処理と双方向の並べ替えを併用するUIの作成方法については、『ASP.NET 2.0でカスタムのページング処理と並び替えを併用する』を参照してください。

 これらのストアドプロシージャを作成した後、型指定されたDataSetをプロジェクト(Employees.xsd)に追加して、基となるオブジェクトを作成しました。次に、前述の各ストアドプロシージャに対して1つずつ、合計3つのメソッドを追加しました。これで、ObjectDataSourceのプロパティに接続できるGetEmployeesSubset(startRowIndex、maximumRows)メソッドおよびGetEmployeesRowCount()メソッドを持つEmployeesTableAdapterオブジェクトが出来上がりました(型指定されたDataSetの作成手順については、『Using Strongly-Typed Data Access in Visual Studio 2005 and ASP.NET 2.0』とScott Guthrieのブログの『Building a DAL using Strongly Typed TableAdapters and DataTables in VS 2005 and ASP.NET 2.0』を参照してください)。

既定のページング処理とカスタムのページング処理のパフォーマンスを比較する

 本稿のサンプルファイルに収められている(50,000個のレコードから成るテーブルを持つ)データベースで既定のページング処理とカスタムのページング処理のパフォーマンスを比較するために、私はSQLプロファイラとASP.NETのトレース機能の両方を使って相対的なパフォーマンスの差を突き止めました(このテストは、私のコンピュータ上で、バックグラウンドで他のプロセスを実行したりしながら行ったものなので、あまり厳密とは言えません。この結果を結論とは言い難いですが、2つのアプローチにおけるパフォーマンスの差を見れば、カスタムのページング処理が優れていることがはっきり分かると思います)。

SQLプロファイラの結果
既定のページング処理カスタムのページング処理
(Employeesからすべてのレコードを選択する)(Employeesから1ページ分のレコードを選択する)
時間(秒)読み取り回数時間(秒)読み取り回数
1.4553830.00329
1.4053830.00029
1.4343830.00029
1.3943830.00329
1.3653830.00329
平均:1.411平均:383平均:0.002平均:29
ASP.NETのトレース結果
既定のページング処理カスタムのページング処理SqlDataSourceのキャッシング
(Employeesからすべてのレコードを選択する)(Employeesから1ページ分のレコードを選択する)(すべてのレコードを選択してキャッシュに保管する)
ページ読み込み時間(秒)ページ読み込み時間(秒)ページ読み込み時間(秒)
2.341368525888070.02596112075696772.39666633608461
2.357722280345690.02800467657202240.0431529705591074
2.433682772531150.03590540138481290.0443528437273452
2.432375623158810.02955347676869550.0442313199023898
2.331670645291510.03000968000122920.0491523364002967
平均:2.379363969平均:0.029886871平均:0.515511161

 お分かりのように、カスタムのページング処理は既定のページング処理よりも、およそ2桁速いです。データベースレベルでは、GetEmployeesSubset(@startRowIndex int, @maximumRows int)ストアドプロシージャの処理速度は、「Employees」テーブルからすべてのレコードを返すシンプルなSELECTステートメントの約470倍です。ASP.NETレベルでは、カスタムのページング処理の速度は既定のページング処理の約120倍です。時間の短縮、つまりパフォーマンスの向上は、おそらく両方のアプローチに共通する高い作業負荷、すなわち、データベース接続を確立したりコマンドを実行したりする処理によるものでしょう。とはいえ、2桁という差はパフォーマンスの世界では非常に大きい差です。この差は、データ容量がもっと大きかったり、サーバで読み込みのようなことを実行したりすると、一層拡大すると思われます。

 SqlDataSourceのキャッシングでは、キャッシュが空であるとデータベースにアクセスしてすべてのレコードを取得しなければならないため、それだけ時間がかかります。キャッシュの再読み込みの頻度は、Webサーバ上の空きリソース(利用できるリソースが少ないと、キャッシュ内のDataSetが削除されることがあります)とキャッシュの有効期限ポリシーによって異なります。ただし、データをキャッシュした後は、パフォーマンスが著しく向上し、カスタムのページング処理のアプローチに匹敵します。0.516秒という平均時間は、キャッシュ内のデータで処理できる要求の数が増えるにつれて、0.05秒に近づきます。

まとめ

 ASP.NET 1.xのDataGridと同様、2.0のGridViewでは、既定とカスタムの2つのページング処理を利用することができます。既定のページング処理は、簡単に設定できますが、1ページ分のデータを表示するたびにデータベースに対してクエリを再実行しなければなりません。しかし、カスタムのページング処理は、表示する必要があるレコードのみを賢く取得するので、パフォーマンスを大幅に向上させることができます。SQL Server 2005では、ROW_NUMBER()機能を含め、結果を順序付けする機能を使うことで、任意のページに対するレコードの正しいサブセットの取得を簡易化しています。

 作成中のWebアプリケーションを将来的に拡張する必要がある場合や、ページング処理してユーザーに表示するデータセットが遅かれ早かれ大きくなる可能性が高い場合には、カスタムのページング処理を実装する必要があります。

 それでは、ハッピープログラミング!

参考資料

著者紹介

Scott Mitchell(Scott Mitchell)


トップページ | 画面トップ

Copyright 2008 Jupitermedia Corporation All Rights Reserved. http://www.internet.com/