C# SqlDataReaderでレコード件数を取得してRazor Pages・Web APIに表示する方法(List.Count/@@ROWCOUNT/COUNT比較)

C#でSqlDataReaderを使ってSQL Serverから一覧を読み込むと、画面に「何件取得できたか」を出したくなります。しかし@@ROWCOUNTやConsole.WriteLineを混ぜると、Webではうまく表示できない/常に0になるなどの落とし穴も。本記事では、最短で正しく件数を出す方法と、用途別の実装パターンを整理します。

目次

よくある状況:SqlDataReaderで映画一覧を読み、Listに追加している

例えば、映画一覧をSQL Serverから読み取り、ListMovies に追加して画面に出す処理は次のようになります。

public class MovieInfo
{
    public int MovieId { get; set; }
    public string Title { get; set; } = "";
    public int ReleaseYear { get; set; }
}

public List<MovieInfo> ListMovies { get; } = new();

public void LoadMovies()
{
    using var con = new SqlConnection(_connectionString);
    using var cmd = new SqlCommand(@"
        SELECT MovieId, Title, ReleaseYear
        FROM dbo.Movies
        ORDER BY MovieId;", con);

    con.Open();

    using var reader = cmd.ExecuteReader();
    while (reader.Read())
    {
        var movie = new MovieInfo
        {
            MovieId = reader.GetInt32(reader.GetOrdinal("MovieId")),
            Title = reader.GetString(reader.GetOrdinal("Title")),
            ReleaseYear = reader.GetInt32(reader.GetOrdinal("ReleaseYear"))
        };

        ListMovies.Add(movie);
    }
}

このとき、次の要望が出がちです。

  • 読み取ったレコード件数(行数)を知りたい
  • その件数をRazor Pages / MVC / Web APIの画面(またはJSON)に表示したい
  • @@ROWCOUNTを足して「2つ目の結果セット」で件数を取ろうとしたが、扱いが分からず表示できない
  • whileループ内でカウンタを増やしたのに、画面上ではなぜか常に0になる

結論:すでにListに追加しているなら、件数はList.Countが最短で確実

「取得した件数=Listに追加した件数」なので、最終的に ListMovies.Count を見ればそれが行数です。クエリに@@ROWCOUNTを足す必要も、SqlDataReaderで複数結果セットを読む必要もありません。

ループ完了後に一度だけCountを取る

public int TotalRecordsRead { get; private set; }

public void LoadMovies()
{
ListMovies.Clear();


using var con = new SqlConnection(_connectionString);
using var cmd = new SqlCommand(@"
    SELECT MovieId, Title, ReleaseYear
    FROM dbo.Movies
    ORDER BY MovieId;", con);

con.Open();
using var reader = cmd.ExecuteReader();

while (reader.Read())
{
    var movie = new MovieInfo
    {
        MovieId = reader.GetInt32(reader.GetOrdinal("MovieId")),
        Title = reader.GetString(reader.GetOrdinal("Title")),
        ReleaseYear = reader.GetInt32(reader.GetOrdinal("ReleaseYear"))
    };
    ListMovies.Add(movie);
}

// 件数は最後に一回だけ確定させる
TotalRecordsRead = ListMovies.Count;


}

Listが最初から空とは限らない場合は差分で取る

「画面の都合でListを使い回している」「読み取り前に別の要素が入っている可能性がある」といったケースでは、読み取り前後の差分を取るのが安全です。

public int TotalRecordsRead { get; private set; }

public void LoadMovies()
{
    var originalCount = ListMovies.Count;

    using var con = new SqlConnection(_connectionString);
    using var cmd = new SqlCommand(@"
        SELECT MovieId, Title, ReleaseYear
        FROM dbo.Movies
        ORDER BY MovieId;", con);

    con.Open();
    using var reader = cmd.ExecuteReader();

    while (reader.Read())
    {
        ListMovies.Add(new MovieInfo
        {
            MovieId = reader.GetInt32(reader.GetOrdinal("MovieId")),
            Title = reader.GetString(reader.GetOrdinal("Title")),
            ReleaseYear = reader.GetInt32(reader.GetOrdinal("ReleaseYear"))
        });
    }

    TotalRecordsRead = ListMovies.Count - originalCount;
}

Webアプリで「Console.WriteLine」は画面に出ない

Webアプリ(ASP.NET CoreのRazor Pages / MVC / Web API)で Console.WriteLine を使っても、基本的にブラウザの画面には表示されません。これは「どこかのコンソールウィンドウに出す」ための出力であり、HTMLのレンダリングとは別物だからです。

Webで件数を表示したいなら、次のいずれかにする必要があります。

  • Razor:PageModel(またはViewModel)のプロパティに件数を入れて、HTMLに埋め込む
  • Web API:レスポンスDTOに TotalCount を含め、JSONとして返す
  • ログ:画面ではなく運用目的なら ILogger でログ出力する

落とし穴:「件数」をMovieInfo(各行)に持たせると混乱しやすい

MovieInfoは「映画1件」を表すデータです。そこに TotalRecordsRead(総件数)を持たせると、設計上も表示上もややこしくなります。

持たせる場所向いているデータ理由
MovieInfo(各行)Title / ReleaseYearなど、その行固有の値1件単位で完結する情報
PageModel / ViewModel / DTO総件数、ページ番号、検索条件、並び替え条件一覧全体に関わる情報で、foreachの外で表示するのが自然

件数は「一覧全体」の情報なので、ページ全体のモデルにプロパティとして持たせ、表示もforeachの外に置くのが定石です。

Razor Pagesで件数を表示する実装例

PageModel側:Listと件数プロパティを用意する

public class MoviesModel : PageModel
{
    private readonly string _connectionString;


public MoviesModel(IConfiguration config)
{
    _connectionString = config.GetConnectionString("DefaultConnection")!;
}

public List<MovieInfo> ListMovies { get; } = new();
public int TotalRecordsRead { get; private set; }

public void OnGet()
{
    ListMovies.Clear();

    using var con = new SqlConnection(_connectionString);
    using var cmd = new SqlCommand(@"
        SELECT MovieId, Title, ReleaseYear
        FROM dbo.Movies
        ORDER BY MovieId;", con);

    con.Open();
    using var reader = cmd.ExecuteReader();

    while (reader.Read())
    {
        ListMovies.Add(new MovieInfo
        {
            MovieId = reader.GetInt32(reader.GetOrdinal("MovieId")),
            Title = reader.GetString(reader.GetOrdinal("Title")),
            ReleaseYear = reader.GetInt32(reader.GetOrdinal("ReleaseYear"))
        });
    }

    TotalRecordsRead = ListMovies.Count;
}


}

Razor側:foreachの外で件数を表示する

「一覧の上に出す」「下に出す」「テーブルのフッターとして出す」など、どれでもOKです。大事なのは、件数表示をforeachの外に置くことです。

<h2>映画一覧</h2>

<div class="notice">
  取得件数:@Model.TotalRecordsRead 件
</div>

<table>
  <thead>
    <tr>
      <th>ID</th>
      <th>タイトル</th>
      <th>公開年</th>
    </tr>
  </thead>
  <tbody>
  @foreach (var item in Model.ListMovies)
  {
    <tr>
      <td>@item.MovieId</td>
      <td>@item.Title</td>
      <td>@item.ReleaseYear</td>
    </tr>
  }
  </tbody>
</table>

<div class="footer">
  総件数:@Model.ListMovies.Count 件
</div>

なお、@Model.TotalRecordsRead と @Model.ListMovies.Count は、読み取り後にListへ追加している限り同じ値になります。どちらを表示しても問題ありません。

Web APIで件数を返す実装例

Web APIでは「HTMLに埋め込む」ではなく「JSONに含める」形になります。フロント側(React/Vue/Blazor/素のJSなど)が一覧と件数を同時に受け取りやすいように、レスポンスDTOを作るのが定番です。

レスポンスDTO(Items + TotalCount)

public class MoviesResponse
{
    public List<MovieInfo> Items { get; set; } = new();
    public int TotalCount { get; set; }
}

Controller例:List.CountをTotalCountに入れて返す

[ApiController]
[Route("api/movies")]
public class MoviesController : ControllerBase
{
    private readonly string _connectionString;


public MoviesController(IConfiguration config)
{
    _connectionString = config.GetConnectionString("DefaultConnection")!;
}

[HttpGet]
public ActionResult<MoviesResponse> Get()
{
    var list = new List<MovieInfo>();

    using var con = new SqlConnection(_connectionString);
    using var cmd = new SqlCommand(@"
        SELECT MovieId, Title, ReleaseYear
        FROM dbo.Movies
        ORDER BY MovieId;", con);

    con.Open();
    using var reader = cmd.ExecuteReader();

    while (reader.Read())
    {
        list.Add(new MovieInfo
        {
            MovieId = reader.GetInt32(reader.GetOrdinal("MovieId")),
            Title = reader.GetString(reader.GetOrdinal("Title")),
            ReleaseYear = reader.GetInt32(reader.GetOrdinal("ReleaseYear"))
        });
    }

    return Ok(new MoviesResponse
    {
        Items = list,
        TotalCount = list.Count
    });
}


}

返ってくるJSONのイメージ

{
  "items": [
    { "movieId": 1, "title": "Example", "releaseYear": 1999 }
  ],
  "totalCount": 1
}

この形にしておくと、クライアント側は「itemsを描画」「totalCountを表示」を分けて実装できます。ページングやフィルタ条件を付けたときにも拡張が容易です。

「whileでカウントしたのに画面では0」になりやすい原因

取得処理の中で count++ しているのに画面で0になる場合、SQLやSqlDataReaderが悪いのではなく、Webの表示モデルに値が渡っていないことがほとんどです。典型例を整理します。

  • ローカル変数のまま
    int totalRecordsRead = 0; をメソッド内で増やしても、それをRazorが参照できるプロパティに代入しないと表示できません。
  • MovieInfoに入れてしまい、表示場所がforeachの中/外でズレている
    各行に同じ総件数を入れる設計になり、表示側が @item.TotalRecordsRead を出したり @Model.TotalRecordsRead を出したりで混乱します。
  • 読み取り後に代入していない/代入が別の分岐にある
    例:例外で途中 return しており、TotalRecordsRead の代入まで到達していない。
  • ListをClearしていない/二重に読み込んでいる
    画面上は0でも実は別のインスタンスを見ている、などの事故が起きやすいです。
  • 非同期処理の結果を待たずに描画している(JS側など)
    APIのレスポンスが来る前に初期値0を描画し、更新処理が書かれていないケース。

対策としてはシンプルで、「表示したい値は、表示モデル(PageModel/DTO)のプロパティに入れる」、そして「foreachの外で表示する」を徹底すると解決しやすいです。

どうしても@@ROWCOUNTを使うなら:複数結果セットをNextResultで読む

「select …; select @@ROWCOUNT;」のように書くと、SqlDataReaderは結果セットを2つ持ちます。1つ目が映画一覧、2つ目が件数です。2つ目を読むには NextResult() が必要です。

ただし、すでに一覧を全部読み取ってListに入れているなら、List.Countで十分です。@@ROWCOUNTを使うと実装が複雑になり、読みやすさも落ちやすいので「どうしても」の場合に限定するのがおすすめです。

SQL例:2つのSELECTで結果セットを分ける

SELECT MovieId, Title, ReleaseYear
FROM dbo.Movies
ORDER BY MovieId;

SELECT @@ROWCOUNT AS TotalCount;

C#例:最初の結果セットをRead、次にNextResultで件数へ

public int TotalRecordsRead { get; private set; }
public List<MovieInfo> ListMovies { get; } = new();

public void LoadMoviesWithRowCount()
{
ListMovies.Clear();
TotalRecordsRead = 0;


using var con = new SqlConnection(_connectionString);
using var cmd = new SqlCommand(@"
    SELECT MovieId, Title, ReleaseYear
    FROM dbo.Movies
    ORDER BY MovieId;

    SELECT @@ROWCOUNT AS TotalCount;", con);

con.Open();
using var reader = cmd.ExecuteReader();

// 1つ目:一覧
while (reader.Read())
{
    ListMovies.Add(new MovieInfo
    {
        MovieId = reader.GetInt32(reader.GetOrdinal("MovieId")),
        Title = reader.GetString(reader.GetOrdinal("Title")),
        ReleaseYear = reader.GetInt32(reader.GetOrdinal("ReleaseYear"))
    });
}

// 2つ目:件数
if (reader.NextResult())
{
    if (reader.Read())
    {
        // 2つ目の結果セットは1列だけの想定なら reader[0] でもOK
        TotalRecordsRead = reader.GetInt32(reader.GetOrdinal("TotalCount"));
    }
}


}

@@ROWCOUNT運用がハマりやすいポイント

  • 「最後に実行された文」の影響を受ける
    途中に別のSQL文が入ると、意図しない行数になることがあります。
  • コード量が増える割にメリットが薄い
    一覧を読み切っているならList.Countで十分で、より明快です。
  • 将来の改修で壊れやすい
    クエリに手が入ると結果セットの順番や列名が変わり、表示が崩れる原因になります。

「件数だけ欲しい」場合はCOUNT(*) + ExecuteScalarが最適

要件を整理すると、「一覧も表示したい」ではなく「件数だけ知りたい」ケースもあります。この場合、全行を読み取ってCountするのは無駄が大きいことがあります。件数だけならSQLでCOUNT(*)を取り、ExecuteScalarで受け取るのが速くて簡単です。

SQL:件数だけ取得

SELECT COUNT(*) 
FROM dbo.Movies;

C#:ExecuteScalarで1つの値を受け取る

public int GetMoviesCount()
{
    using var con = new SqlConnection(_connectionString);
    using var cmd = new SqlCommand(@"SELECT COUNT(*) FROM dbo.Movies;", con);


con.Open();
var result = cmd.ExecuteScalar();

return Convert.ToInt32(result);


}

検索条件(WHERE句)がある場合も同様で、WHERE を揃えれば「該当件数」を正確に出せます。

ページングがあるなら:COUNT(*) OVER()で「一覧+総件数」を一度に返す

一覧画面でよくあるのが「1ページ20件だけ表示したいが、総件数(全体のヒット数)は欲しい」というパターンです。ここで単純にList.Countを表示すると「今ページの件数」になり、総件数とズレます。

この場合はSQL Serverのウィンドウ関数 COUNT(*) OVER() を使うと、各行に総件数を添付した形で返せます(DBへの往復を増やさずに済むのが利点)。

SQL例:総件数をTotalCount列として付与しつつページング

DECLARE @Page int = 1;
DECLARE @PageSize int = 20;

SELECT
MovieId,
Title,
ReleaseYear,
COUNT(*) OVER() AS TotalCount
FROM dbo.Movies
WHERE ReleaseYear >= 2000
ORDER BY MovieId
OFFSET (@Page - 1) * @PageSize ROWS
FETCH NEXT @PageSize ROWS ONLY;

C#例:最初の行でTotalCountを拾い、Itemsは通常通りListへ

public class MoviesResponse
{
    public List<MovieInfo> Items { get; set; } = new();
    public int TotalCount { get; set; }
}

public MoviesResponse GetMoviesPaged(int page, int pageSize)
{
var res = new MoviesResponse();


using var con = new SqlConnection(_connectionString);
using var cmd = new SqlCommand(@"
    SELECT MovieId, Title, ReleaseYear, COUNT(*) OVER() AS TotalCount
    FROM dbo.Movies
    WHERE ReleaseYear >= @minYear
    ORDER BY MovieId
    OFFSET (@page - 1) * @pageSize ROWS
    FETCH NEXT @pageSize ROWS ONLY;", con);

cmd.Parameters.AddWithValue("@minYear", 2000);
cmd.Parameters.AddWithValue("@page", page);
cmd.Parameters.AddWithValue("@pageSize", pageSize);

con.Open();
using var reader = cmd.ExecuteReader();

while (reader.Read())
{
    if (res.TotalCount == 0)
    {
        // どの行にも同じTotalCountが入るので、最初に一度だけ拾う
        res.TotalCount = reader.GetInt32(reader.GetOrdinal("TotalCount"));
    }

    res.Items.Add(new MovieInfo
    {
        MovieId = reader.GetInt32(reader.GetOrdinal("MovieId")),
        Title = reader.GetString(reader.GetOrdinal("Title")),
        ReleaseYear = reader.GetInt32(reader.GetOrdinal("ReleaseYear"))
    });
}

return res;


}

この方式は「今ページの表示件数」と「検索結果の総件数」を両立できます。表示側は Items.Count と TotalCount を分けて使えるため、UIが作りやすくなります。

手段ごとのおすすめ度と使い分け早見表

目的おすすめ手段メリット注意点
一覧を読み取った「取得件数」を出したいList.Count(または差分)最短・確実・読みやすい「総件数」ではなく「読み取った件数」
件数だけ欲しい(一覧不要)COUNT(*) + ExecuteScalar高速・通信量が少ない一覧も必要なら別クエリが必要
ページングしつつ総件数も欲しいCOUNT(*) OVER() + OFFSET/FETCH往復を増やさず総件数を返せるSQLが少し長くなる
どうしても@@ROWCOUNTで取りたい複数結果セット + NextResult()仕組みを理解できれば取得可能実装が複雑・改修で壊れやすい

実務で効く改善ポイント:表示・設計・パフォーマンス

「取得件数」と「総件数」を言葉と変数名で分ける

要件によって意味が変わるので、命名で事故を減らすのが有効です。

  • 取得件数(読み取った件数):FetchedCount / RecordsRead / List.Count
  • 総件数(検索ヒットの総数):TotalCount / MatchedCount

ページングがある画面で List.Count を「総件数」として表示してしまうのは、かなり起きやすいミスです。

PageModel/DTOに「一覧全体の情報」を集約する

件数だけでなく、実務では次のような情報も一緒に画面に出したくなります。

  • 検索条件(例:公開年の下限)
  • 並び順(例:MovieId昇順)
  • ページ番号、ページサイズ
  • 総件数、現在表示件数

これらはMovieInfoのような「行データ」ではなく、画面全体のモデル(ViewModel/DTO)にまとめると、フロント実装も保守も楽になります。

ログに出したいならILoggerを使う

「画面表示ではなく調査のために件数を知りたい」なら、Consoleではなくログフレームワーク(ASP.NET Core標準のILogger)に出すのが運用向きです。

private readonly ILogger<MoviesModel> _logger;

public MoviesModel(IConfiguration config, ILogger logger)
{
_connectionString = config.GetConnectionString("DefaultConnection")!;
_logger = logger;
}

public void OnGet()
{
// ... 読み取り処理 ...
_logger.LogInformation("Movies fetched: {Count}", ListMovies.Count);
}

大量データのときは「全部読んでCount」より「必要な分だけ」に寄せる

一覧が数万件、数十万件になると、すべてをSqlDataReaderで読み取ってListに貯めるだけでも負荷になります。画面表示が目的なら、次の方向が現実的です。

  • ページングを入れて「表示分だけ取得」する
  • 総件数はCOUNT(*)またはCOUNT(*) OVER()で別途返す
  • 必要なら検索条件に合うインデックス設計を見直す(DB側の話)

チェックリスト:この形にしておくと迷いにくい

  • 一覧を読み取ってListに追加しているなら、件数は List.Count
  • Webで表示する値は、ローカル変数ではなく PageModel/DTOのプロパティへ
  • 件数表示はforeachの外に置く(一覧全体の情報として表示)
  • ページングがあるなら List.Count(今ページ) と TotalCount(総件数) を分ける
  • どうしても複数結果セットなら NextResult() を忘れない

件数の取り方は一見小さな話ですが、画面表示・API設計・ページング・運用ログまで繋がる「地味に重要な基礎」です。まずはList.Countで最短ルートを押さえ、必要になったタイミングでCOUNT(*)やCOUNT(*) OVER()へ拡張していくのが、実務では最もトラブルが少ない進め方です。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次