SQL Serverで日付範囲の金額合計を正しく取得する方法(SUMのNULL/DBNull例外・C# ExecuteScalarでLabel表示)

SQL Serverで日付範囲を指定して金額合計をLabelに表示する処理は、SUMがNULLを返す挙動、money→int変換による端数切り捨て、datetimeの時刻取りこぼしが重なると「合計が合わない」「DBNull例外」が起きがちです。原因の切り分けから、SQL/C#両側の定番対策、さらに設計改善までをまとめます。

目次

起きている症状を整理する(合計が合わない/DBNull例外)

よくある実装として、SQL側で次のように合計を作り、C#側で ExecuteScalar() で受け取ってLabelに表示します。

SELECT SUM(CAST(due AS money))
FROM TableRecords
WHERE Uid = @Uid
  AND DateCreated BETWEEN @StartDate AND @EndDate;

ところが実運用では、次の2種類の問題が出やすいです。

  • 合計が期待値と一致しない(一部のデータが含まれていない/端数が消える/丸めが入る、など)
  • 「DBNull を他の型にキャストできない」(該当期間が0件のとき、2回目以降の検索で例外が出る、など)

この2つは別々の原因に見えますが、実は「SQLの集計の仕様」と「C#の型の受け方」と「日付の範囲指定」が絡み合って同時に発生することが多いです。

原因になりやすいポイントと解決策

SUMは該当行が0件だとNULLを返す(DBNull例外の正体)

SQL Serverの SUM() は、条件に一致する行が1件もない場合、0ではなくNULL を返します。C#側でその戻り値を int や decimal に変換しようとすると、DBNull が混ざって例外になります。

解決策はシンプルで、SQL側で必ず0に寄せることです。ISNULL か COALESCE を使います。

SELECT ISNULL(SUM(CAST(due AS money)), 0)
FROM TableRecords
WHERE Uid = @Uid
  AND DateCreated BETWEEN @StartDate AND @EndDate;

より一般的に書くなら COALESCE でもOKです。

SELECT COALESCE(SUM(CAST(due AS money)), 0)
FROM TableRecords
WHERE Uid = @Uid
  AND DateCreated BETWEEN @StartDate AND @EndDate;

これで、該当件数0でもSQLの結果が0になり、C#側でDBNull例外が起きにくくなります。

C#側でintにしていると小数(端数)が消えて合計がズレる

SQLで money に変換しているのに、C#側で int に変換してしまうと、小数点以下が切り捨てられます。例えば、次のような合計が返ってきても…

  • SQLの結果:1234.56
  • C#でint化:1234(.56が消える)

「日本円だから小数は出ないはず」と思っていても、データが文字列(nvarchar)で入っていると、過去の入力や他システム連携で小数が混入することは珍しくありません。さらに money 型は内部的に固定小数点(小数4桁)なので、集計後の値は小数があり得ます。

金額はC#側でdecimalで受けるのが基本です。表示用に丸めたい場合は、最後に書式指定で調整します。

var result = cmd.ExecuteScalar();  // object が返る
decimal total = Convert.ToDecimal(result); // SQL側で0にしていれば DBNull は来にくい

TotalRev.Text = total.ToString("#,##0.##");

「常に小数2桁で表示したい」なら N2 のような定番書式でも構いません。

TotalRev.Text = total.ToString("N2");

なお、SQL側で money を返しても、C#側では decimal として受けるのが実務上いちばん事故が少ないです。

日付入力が「日付のみ(00:00:00)」になり、当日のデータを取りこぼす

DateCreated が datetime / datetime2 のように時刻を持つ場合、UI側で「2024-11-23」のように日付だけを指定すると、C#の DateTime は通常 2024-11-23 00:00:00 を指します。

この状態で次の条件を使うと、@EndDate 当日の午後や夜に登録されたデータが範囲外になり、合計が小さく見えることがあります。

DateCreated BETWEEN @StartDate AND @EndDate

特に「TOに当日を入れたのに当日分が含まれない」というズレは、現場で最も多いパターンのひとつです。

対策は主に2つあります。

対策A:DateCreatedをDATEにして比較する(シンプルだが注意点あり)

WHERE Uid = @Uid
  AND CAST(DateCreated AS date) BETWEEN @StartDate AND @EndDate

これは直感的で分かりやすい一方、列に対してCASTをかけるため、インデックスが効きにくくなる(パフォーマンスが落ちる)ことがあります。データ量が増える想定なら、次の対策Bが定番です。

対策B:開始以上〜終了日の翌日未満にする(時刻取りこぼしを防ぐ定番)

WHERE Uid = @Uid
  AND DateCreated >= @StartDate
  AND DateCreated < DATEADD(day, 1, @EndDate)

この書き方は、終了日が日付のみでも、当日23:59:59.xxxまでを確実に含められます。さらに、列側に加工をしないためインデックスが効きやすく、実運用で強いです。

まずはこれが安全:SQL側でNULLを0にし、日付範囲も確実にする

今回の症状(DBNull例外+合計ズレ)をまとめて潰すなら、次のSQLに寄せるのが堅いです。

SELECT COALESCE(SUM(CAST(due AS money)), 0) AS TotalDue
FROM TableRecords
WHERE Uid = @Uid
  AND DateCreated >= @StartDate
  AND DateCreated < DATEADD(day, 1, @EndDate);

これにより、

  • 該当0件でも合計は0(NULLが返らない)
  • 終了日当日の時刻付きデータも漏れにくい

という状態になります。

C#側の実装例(ExecuteScalar+decimal+Label表示)

SQL側で COALESCE / ISNULL を入れていても、C#側でも「null/DBNullだったら0」にフォールバックする癖を付けると、改修や別クエリ追加のときに事故りにくくなります。

using (var conn = new SqlConnection(connectionString))
using (var cmd = conn.CreateCommand())
{
    cmd.CommandText = @"
SELECT COALESCE(SUM(CAST(due AS money)), 0)
FROM TableRecords
WHERE Uid = @Uid
  AND DateCreated >= @StartDate
  AND DateCreated < DATEADD(day, 1, @EndDate);";

    cmd.Parameters.Add("@Uid", SqlDbType.NVarChar, 50).Value = uid;

    // UIが日付のみなら Date 部分を明示して渡す(余計な時刻が混ざるのを防ぐ)
    cmd.Parameters.Add("@StartDate", SqlDbType.DateTime).Value = startDate.Date;
    cmd.Parameters.Add("@EndDate", SqlDbType.DateTime).Value = endDate.Date;

    conn.Open();
    object result = cmd.ExecuteScalar();

    decimal total = 0m;
    if (result != null && result != DBNull.Value)
    {
        total = Convert.ToDecimal(result);
    }

    // 表示書式は要件に合わせて(例:小数なし、桁区切り)
    TotalRev.Text = total.ToString("#,##0.##");
}

ポイントは次の通りです。

  • decimalで受ける(金額計算にfloat/doubleは避ける)
  • startDate.Date / endDate.Date のように日付成分を明確にして渡す
  • AddWithValueを避け、SqlDbTypeを明示して型推論の罠を回避する

「合計が合わない」をもう一段深掘り:nvarchar(max)の金額は地雷が多い

今回のテーブルでは、金額が Due 列(nvarchar(max))に文字列として入っています。この設計だと、表面上は CAST(due AS money) で動いて見えても、データが少し汚れるだけで合計の正確性が崩れたり、将来的に変換エラーが出たりします。

代表的な混入パターンを表にまとめます。

Dueの例(nvarchar)起こりがちな問題現実的な対策
1,234.56カンマがあるとCASTに失敗することがある(環境・形式次第)REPLACEでカンマ除去、または数値列へ移行
¥1234 / 1234円通貨記号・文字が混ざると変換できない入力段階で数値だけに制限、移行時にクレンジング
1234.567桁数・小数の扱いで丸めが発生decimal(19,2)等に統一、表示で丸め
(1234)会計形式の負数が混ざると変換失敗入力規則の統一、取り込み時に正規化
空文字 / NULLNULL/空の扱いでSUMがNULL、または変換失敗NULLは0扱い、空はNULLに寄せる

「今はたまたま動いている」状態になりやすいので、できる範囲で変換の安全性を高めるのがおすすめです。

文字列金額を安全に集計する(TRY_CONVERTを使う)

SQL Serverでは、変換に失敗したときにエラーで止めず、NULLを返してくれる TRY_CONVERT / TRY_CAST が使えます。Dueが汚れている可能性があるなら、こちらの方が「集計処理が落ちる」事故を防げます。

SELECT COALESCE(
         SUM(
           TRY_CONVERT(decimal(19,2),
             REPLACE(due, ',', '')
           )
         ),
         0
       ) AS TotalDue
FROM TableRecords
WHERE Uid = @Uid
  AND DateCreated >= @StartDate
  AND DateCreated < DATEADD(day, 1, @EndDate);

この例では、

  • カンマを除去(REPLACE)
  • decimal(19,2) に変換(TRY_CONVERT)
  • 合計がNULLなら0(COALESCE)

という順番で安全に集計します。

ただし、TRY_CONVERTは変換できないデータを黙ってNULLにするため、「本当は入力ミスがあるのに気付けない」状態にもなり得ます。運用面では、次のように「変換できないデータを洗い出すクエリ」を用意して、データ品質をチェックできるようにしておくと安心です。

SELECT due, DateCreated, Uid
FROM TableRecords
WHERE TRY_CONVERT(decimal(19,2), REPLACE(due, ',', '')) IS NULL
  AND due IS NOT NULL
  AND LTRIM(RTRIM(due)) <> '';

日付範囲の指定で迷ったら:BETWEENより「開始以上〜翌日未満」

日付範囲で集計する検索は、要件上「日付」しか指定できないことが多いです。そんなときは、次のルールに統一しておくと、バグが減ります。

書き方メリット落とし穴
BETWEEN @Start AND @End短くて読みやすい@Endが日付のみだと当日分の時刻付きデータを漏らす
>= @Start AND < DATEADD(day,1,@End)当日分の取りこぼしが起きにくい/インデックスに優しい「Endは日付で渡す」という前提を守る必要がある
CAST(DateCreated AS date)で比較直感的で分かりやすい列側に関数をかけるため性能が落ちることがある

データ量が増える可能性があるシステムでは、基本は「開始以上〜翌日未満」に寄せるのがおすすめです。

AddWithValueを避ける理由(合計ズレの“間接原因”になることも)

AddWithValue 自体が必ず悪いわけではありませんが、SQL Serverのパラメータ型推論が想定とズレると、次のような問題につながることがあります。

  • 日付が文字列として渡ってしまい、暗黙変換が発生する
  • 暗黙変換のせいでインデックスが使われず、検索が遅くなる
  • 遅いので「期間を変えたら結果が変」など、調査が難しくなる

集計のように“正確性と再現性”が重要な処理ほど、C#側で SqlDbType を明示して渡すのが安全です。

cmd.Parameters.Add("@StartDate", SqlDbType.DateTime).Value = startDate.Date;
cmd.Parameters.Add("@EndDate", SqlDbType.DateTime).Value = endDate.Date;

根本改善:Dueをnvarchar(max)で持つのをやめ、数値型にする

今回のように「金額を合計したい」要件があるなら、Dueを文字列で保持するのは根本的に不利です。代表的なデメリットは次の通りです。

  • 正確性:フォーマット混入(カンマ、通貨記号、空白)で変換に失敗する/気付かない
  • 性能:集計のたびに文字列→数値変換が走る。nvarchar(max) は特に重い
  • 保守性:C#、SQL、入力画面など各所で「金額文字列の取り扱いルール」が増殖する

最もおすすめなのは、DB側の列を decimal(19,2) などの数値型に変更し、アプリ側もdecimalで統一することです(日本円で小数不要なら decimal(19,0) なども検討できます)。

移行の進め方(既存データがある場合)

いきなり列型変更が難しい場合は、「新しい数値列を追加して段階的に移行」する方法が安全です。

-- 1) 新しい数値列を追加
ALTER TABLE TableRecords
ADD DueAmount decimal(19,2) NULL;

-- 2) 既存のDueから変換して埋める(例:カンマ除去)
UPDATE TableRecords
SET DueAmount = TRY_CONVERT(decimal(19,2), REPLACE(due, ',', ''));

-- 3) 今後の集計はDueAmountを使う
SELECT COALESCE(SUM(DueAmount), 0)
FROM TableRecords
WHERE Uid = @Uid
  AND DateCreated >= @StartDate
  AND DateCreated < DATEADD(day, 1, @EndDate);

移行中は、入力・更新処理で Due と DueAmount を二重に更新するか、可能ならアプリ側を先に DueAmount へ寄せて、最後に Due を廃止する流れが現実的です。

実務でのチェックポイント(バグを再発させない)

「直したはずなのに、別条件でまたズレる」を防ぐために、集計機能では次のチェックポイントを押さえておくと強いです。

チェックポイント一覧

チェック項目確認内容おすすめ対応
該当0件のとき合計は0か、DBNull例外が出ないかSQLでCOALESCE/ISNULL、C#でDBNullチェック
小数が混ざるとき端数が消えていないかC#はdecimalで受ける、表示で丸める
終了日当日のデータ当日午後のレコードが含まれるか「開始以上〜翌日未満」に統一
Dueの文字列形式カンマや記号、空文字が混ざっていないかTRY_CONVERTで検出、可能なら数値列へ移行
パラメータの型暗黙変換で条件がブレていないかAddWithValue回避、SqlDbTypeを明示

よくある質問(つまずきポイントの補足)

SQL側で0にしているのに、C#でDBNull例外が出ることがあるのは?

SQL文のどこかで ISNULL/COALESCE が外れている、または別のクエリ(条件分岐したSQLなど)で SUM() の生値を返している可能性があります。アプリの改修でSQLが分岐していくほど混入しがちなので、C#側でも DBNull を保険として潰しておくのが安全です。

moneyではなくdecimalにした方がいい?

一般に、SQL Serverでは金額を money で持つケースもありますが、アプリ側(C#)では decimal が標準で、丸め規則や桁数を明確に制御できるのが強みです。特に「Dueが文字列」のような状況では、変換先をdecimal(19,2)に統一すると挙動が読みやすくなります。

Labelに表示するときのおすすめフォーマットは?

要件次第ですが、日本語UIで「合計金額」を見せる場合は、桁区切り+小数は必要なときだけ表示、が読みやすいことが多いです。

  • 小数不要:total.ToString("#,##0")
  • 小数があるときだけ表示:total.ToString("#,##0.##")
  • 常に小数2桁:total.ToString("N2")

「円」を付けるなら文字列連結でOKです。

TotalRev.Text = total.ToString("#,##0.##") + " 円";

まとめ:ズレとDBNullを一度に潰す最短ルート

SQL Serverで「指定した日付範囲の金額合計」を正しくLabelに表示するには、次の3点をセットで押さえるのが近道です。

  • SUMは0件でNULLになる → SQL側で COALESCE/ISNULL によって0を返す
  • 金額をintで受けない → C#は decimal で受け、表示は書式で整える
  • datetimeの終端を取りこぼさない → BETWEEN ではなく「開始以上〜翌日未満」を使う

さらに、Dueが nvarchar(max) である限り、変換・性能・運用の面で不利が残ります。可能なら、段階的にでも数値列へ移行し、SQL/C#ともにdecimalで統一すると、合計の正確性と保守性が一気に上がります。

この記事を書いた人

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

コメント

コメントする

目次