C# MVCで一覧画面を実装する際、ページング処理は欠かせない機能の一つです。データ件数が増えると、全件取得は表示速度やサーバー負荷の面で現実的ではありません。
SQL Serverでは、SQL Server 2012以降で利用できる OFFSET / FETCH 構文を使用することで、シンプルにページングを実装できます。しかし、使い方を誤ると「並び順が安定しない」「ページを移動するとデータが重複する」「大量データで性能が低下する」といった問題が発生する場合があります。
本記事では、SQL Serverの OFFSET / FETCH を利用したページングについて、基本構文からC# MVCでの実装方法、パフォーマンス上の注意点までを整理して解説します。
SQL Serverにおけるページングの基本
SQL Server 2012以降では、OFFSET / FETCH を利用することでページングを実現できます。
SELECT *
FROM dbo.Users
ORDER BY Id
OFFSET @Skip ROWS
FETCH NEXT @Take ROWS ONLY;
OFFSETとFETCHの意味
- OFFSET:先頭からスキップする行数
- FETCH NEXT:取得する行数
例えば、1ページあたり20件表示する場合は以下のようになります。
| ページ | Skip |
|---|---|
| 1ページ目 | 0 |
| 2ページ目 | 20 |
| 3ページ目 | 40 |
MVCでは一般的に以下のように計算します。
int skip = (page - 1) * pageSize;
OFFSET / FETCHとORDER BYの関係
OFFSET / FETCH を使用する場合、ORDER BY は必須です。
例えば以下のように記述します。
SELECT Id, Name, CreatedAt
FROM dbo.Users
WHERE IsDeleted = 0
ORDER BY CreatedAt DESC, Id DESC
OFFSET @Skip ROWS
FETCH NEXT @Take ROWS ONLY;
ORDER BYを安定させる重要性
ページングでは、取得順序が毎回同じであることが重要です。
例えば以下のような並び順の場合を考えます。
ORDER BY CreatedAt DESC
同じ日時のデータが複数存在すると、SQL Serverはそれらのレコードの並び順を保証しません。その結果、
- 同じレコードが複数ページに表示される
- 一部のレコードが表示されない
といった問題が発生する可能性があります。
そのため実務では、
ORDER BY CreatedAt DESC, Id DESC
のように、最後にユニークキー(主キー)を追加して並び順を固定することが推奨されます。
C# MVCでの安全なSQL実装
ページング処理ではSQLを動的に組み立てたくなりますが、文字列連結によるSQL生成は避けるべきです。
文字列連結の問題点
- SQLインジェクションのリスクがある
- 型チェックが行われない
- 実行計画の再利用効率が低下する可能性がある
推奨される実装方法
SQLは固定文字列として定義し、可変値はパラメータで渡します。
int currentPage = Math.Max(1, page);
int pageSizeValue = Math.Clamp(pageSize, 1, 200);
int skip = (currentPage - 1) * pageSizeValue;
var sql = @"
SELECT Id, Name, CreatedAt
FROM dbo.Users
WHERE IsDeleted = 0
ORDER BY CreatedAt DESC, Id DESC
OFFSET @Skip ROWS
FETCH NEXT @Take ROWS ONLY;
";
cmd.Parameters.Add("@Skip", SqlDbType.Int).Value = skip;
cmd.Parameters.Add("@Take", SqlDbType.Int).Value = pageSizeValue;
この方法であれば、安全かつ保守しやすい実装になります。
Dapperを利用したページング実装
Dapperを利用している場合は、さらに簡潔に記述できます。
int skip = (page - 1) * pageSize;
var sql = @"
SELECT Id, Name, CreatedAt
FROM dbo.Users
WHERE IsDeleted = 0
ORDER BY CreatedAt DESC, Id DESC
OFFSET @Skip ROWS
FETCH NEXT @Take ROWS ONLY;
";
var list = await conn.QueryAsync<UserDto>(
sql,
new
{
Skip = skip,
Take = pageSize
});
MVCとDapperの組み合わせは、
- 実装がシンプル
- SQLが読みやすい
- パラメータ化しやすい
というメリットがあり、多くの業務システムで採用されています。
ページャ表示に必要な総件数の取得
ページ番号や総ページ数を表示する場合は、全件数の取得が必要になります。
一般的には、
- 一覧取得用SQL
- 件数取得用SQL
を分けて実行します。
件数取得
SELECT COUNT(*)
FROM dbo.Users
WHERE IsDeleted = 0;
一覧取得
SELECT Id, Name, CreatedAt
FROM dbo.Users
WHERE IsDeleted = 0
ORDER BY CreatedAt DESC, Id DESC
OFFSET @Skip ROWS
FETCH NEXT @Take ROWS ONLY;
実装が分かりやすく、保守もしやすいため、多くの現場で採用されている方法です。
パフォーマンス上の注意点
OFFSETの特性
OFFSET は指定された行数を読み飛ばしてから結果を返します。
そのため、
OFFSET 10 ROWS
よりも、
OFFSET 100000 ROWS
の方が処理コストは高くなります。
ページ番号が大きくなるほど、レスポンスが悪化しやすい点に注意が必要です。
実務での対策
以下の対策が有効です。
- ORDER BY列に対応したインデックスを作成する
- 管理画面や中規模データではOFFSET方式を利用する
- 超大量データではKeyset Pagination(Seek方式)を検討する
例えばKeyset Paginationでは、
WHERE Id < @LastId
ORDER BY Id DESC
のように前回取得したキーを利用して次ページを取得します。
大規模テーブルでは、OFFSET方式よりも高速に動作するケースがあります。
まとめ
SQL Serverの OFFSET / FETCH は、C# MVCの一覧画面におけるページング処理をシンプルに実装できる便利な機能です。
ただし、正しく利用するためには以下のポイントを押さえておく必要があります。
- ORDER BYは必須
- 並び順を安定させるためユニークキーを含める
- SQLは必ずパラメータ化する
- 大量データではOFFSETの性能特性を理解する
- 必要に応じてKeyset Paginationも検討する
これらを意識することで、安全で保守しやすく、性能面でも安定したページング機能を実装できます。


コメント