みなさんこんにちは!ヒロポンです!
C# で業務アプリを作ってると、DataTable は毎日のように触りますよね。なのに、その横に DataSet ってやつが居座ってて「これ、いつ使うん??」ってなりませんか??
俺も最初、ここがずっと曖昧なまま DataTable だけで押し切ってた時期があったんですよね。
で、マスタと明細みたいな親子データが出てきた瞬間に、急に DataSet の出番が来た。「あれ、構造どうなってんの」って詰まった。
今回は、その DataSet と DataTable が「そもそも何が違うのか」を箱の構造から積み上げます。業務 SE がどっちを選ぶかの判断軸まで、一気に腹落ちさせます。
忙しい人向けに最初にまとめ
- DataTable は「1枚の表」、DataSet は「複数の DataTable を親子関係ごと抱える箱の箱」 です
- 単一テーブルを軽く回すだけなら DataTable、マスタ詳細のように複数テーブルのリレーションが要るなら DataSet
- 判断軸は ①テーブル数と親子関係 ②メモリの軽さ ③型安全とメンテ性 の3つ。比較表は記事中盤に置きました
- コピペで動く親子テーブルの DataSet サンプルは「仕組みを見る」の章に置いてます
そもそも DataSet と DataTable は何が違うのか
最初に定義をはっきりさせておきます。
DataTable は、行(DataRow)と列(DataColumn)からなる「1枚の表」を表す ADO.NET のクラスです。 Excel のシート1枚をイメージすると、ほぼ合ってます。
DataSet は、複数の DataTable とそのリレーションをまとめて1つのメモリ上に保持する、いわばインメモリの小さなデータベースです。 こっちは Excel の「ブック」。シートを何枚も束ねた箱のほう。
違いはこの一段の入れ子だけ、と言ってもいいくらいです。DataTable が「表」で、DataSet が「表を何枚も入れる箱」。
だから DataSet の中には DataTable が複数いて、その表どうしの親子関係(DataRelation)まで一緒に持てる。
ここで押さえてほしいのは、DataSet は DataTable の上位概念で、両者は対立じゃなく入れ子の関係 だということ。「どっちが偉い」じゃない。「どの粒度でデータを抱えたいか」の話なんですよね。
なぜ DataSet という「箱の箱」が生まれたのか
そもそも、なんでわざわざ箱の箱が要るんでしょう。
ADO.NET より前、いわゆる ADO の時代は Recordset っていう「DB に繋ぎっぱなしでカーソルを動かす」モデルが主流でした。接続を握ったまま1行ずつ進む。
これ、Web みたいに接続をすぐ手放したい世界だと、相性が悪い。
そこで .NET が出した答えが「いったん全部メモリに持ってきて、接続は切る」という発想です。DB から取ったデータをローカルのメモリにごっそりコピーして、後はオフラインで読んだり書いたりする。
この「持ってきたデータの置き場」が DataTable で、複数テーブルをまとめて持つ器が DataSet。
DataSet が複数テーブルを抱えられるのは、この設計思想の自然な結論なんですよね。1回の往復で「注文ヘッダも明細も全部」まとめて取ってきて、画面側ではもう DB に触らず親子をたどる。接続を握る時間を最短にするための「箱の箱」だったわけです。
つまり、DataSet は「接続を切ってオフラインで扱う」ための器として生まれた。この出自を頭に入れておくと、後の判断軸がスッと入ってきます。
用語を整理する — DataSet / DataTable / DataRow / DataRelation
コードに入る前に、登場人物を整理しておきます。図にするとこんな感じです。

- DataSet … 複数の DataTable とリレーションをまとめて持つ箱
- DataTable … 1枚の表。
Tables["Orders"]のように名前で引く - DataColumn … 表の列。型(
typeof(int)等)を持つ - DataRow … 表の1行。
row["列名"]で値にアクセスする - DataRelation … DataTable どうしの親子関係。これが DataSet の主役
この5つの関係さえ掴めば、DataSet 周りのコードはほぼ読めます。
逆に言うと、DataRelation を知らないと「DataSet を使う意味」が半分しか見えない。次でそこを動かします。
仕組みを見る — マスタ詳細を DataSet で持つ(コピペで動く)
百聞は一見にしかず。親子2テーブルを DataSet で組む最小サンプルです。
注文ヘッダ(親)と注文明細(子)を OrderId で繋ぎます。DB 接続は要らない、インメモリだけで完結するので、そのままコピペで動きます。
using System;
using System.Data;
class Program
{
static void Main()
{
// DataSet は「複数の DataTable をまとめて持つ箱」
var ds = new DataSet("SalesDb");
// 親テーブル: 注文ヘッダ
var orders = ds.Tables.Add("Orders");
orders.Columns.Add("OrderId", typeof(int));
orders.Columns.Add("Customer", typeof(string));
orders.Rows.Add(1, "みどり商店");
orders.Rows.Add(2, "あおぞら物産");
// 子テーブル: 注文明細
var details = ds.Tables.Add("OrderDetails");
details.Columns.Add("OrderId", typeof(int));
details.Columns.Add("Item", typeof(string));
details.Columns.Add("Qty", typeof(int));
details.Rows.Add(1, "りんご", 10);
details.Rows.Add(1, "みかん", 5);
details.Rows.Add(2, "ぶどう", 3);
// ★ ここが DataSet の本体: 2 テーブルを OrderId で親子関係に
ds.Relations.Add("Order_Details",
orders.Columns["OrderId"],
details.Columns["OrderId"]);
// 親をたどって子を引く
foreach (DataRow order in orders.Rows)
{
Console.WriteLine($"注文 {order["OrderId"]} ({order["Customer"]})");
foreach (DataRow line in order.GetChildRows("Order_Details"))
{
Console.WriteLine($" - {line["Item"]} x {line["Qty"]}");
}
}
}
}
実行結果:

注文ごとに、明細がちゃんとぶら下がって出てますよね!!
ポイントは ds.Relations.Add(...) の1行と、order.GetChildRows("Order_Details") の親→子ナビゲーション。
リレーションさえ張れば、親の DataRow から子の行を直接たどれる。自前で OrderId を突き合わせる for ループを書かなくて済むんですよね。いい感じに親子が繋がります。
ちなみに、これを DataTable だけでやろうとすると、明細側を OrderId で毎回フィルタする処理を自分で書くことになる。
それが悪いわけじゃない。ただ、リレーションを ADO.NET に預けられるのが DataSet の旨味、という感覚を持っておくといいです。
ここまでで「DataSet が親子をどう繋ぐか」は腹落ちしたはず。次はこの繋ぎ方で踏みやすい罠を見ます。
実装で踏みやすい2つの罠
DataSet で最初にハマる場所は、だいたいこの2つに集約されます。先に総数を言っておくと2つ。順番に潰します。
罠その1:DataRelation の張り方(親キーは一意じゃないとコケる)
DataRelation を張ると、ADO.NET は裏で「親キーは一意であるべき」という制約(UniqueConstraint)を自動で作ります。
なので親テーブルに同じキーが重複してると、リレーションを張る瞬間に例外で落ちる。
using System;
using System.Data;
class Program
{
static void Main()
{
var ds = new DataSet();
var orders = ds.Tables.Add("Orders");
orders.Columns.Add("OrderId", typeof(int));
orders.Rows.Add(1);
orders.Rows.Add(1); // ← OrderId=1 が重複している
var details = ds.Tables.Add("OrderDetails");
details.Columns.Add("OrderId", typeof(int));
// 親キーが一意じゃないので、この行で例外が飛ぶ
ds.Relations.Add("Order_Details",
orders.Columns["OrderId"],
details.Columns["OrderId"]);
}
}
エラー再現:

これ、System.ArgumentException: These columns don't currently have unique values. で死にます。
DB から取ってきた親テーブルにうっかり重複が混ざってると、画面表示の手前で落ちる。「え、SELECT は通ったのに??」ってなるやつ。
親キーの一意性は DataRelation を張る前に確認、と覚えておくと安全です。
罠その2:メモリ消費(DataSet は全テーブルを抱えっぱなし)
もう1つは地味だけど効いてくるやつ。DataSet は中の DataTable を全部メモリに保持し続けます。
マスタ詳細の「詳細」が数万行あると、画面で使うのは1ページ分なのに、裏では全部抱えてる、という状態になりがち。
俺の現場でも、一覧画面の裏で巨大な DataSet を握りっぱなしにしてたことがありました。画面を何枚か開いたら、メモリがじわじわ膨らんでいったんですよね。
原因は「使い終わった DataSet を Dispose せず参照を持ち続けてた」こと。複数テーブルを束ねてる分、解放のインパクトも大きい。
運用メモ:DataSet を画面に持たせるなら「いつ捨てるか」をセットで設計する。表示が終わったら参照を切る、もしくは必要なテーブルだけ DataTable で持つ。箱が大きいほど、置きっぱなしのコストも大きい。
この2つさえ意識できれば、DataSet 実装の事故はだいぶ減らせます。
じゃあ最後、DataTable と DataSet をどう選び分けるか。判断軸でまとめます。
どっちを使う?3つの判断軸
ここまでの話を、業務 SE が現場で即決できる形に落とします。軸は3つ。①テーブル数と親子関係 ②メモリの軽さ ③型安全とメンテ性 です。

軸① テーブル数と親子関係
ここが一番効く軸です。扱うのが1枚の表で完結するなら DataTable、複数テーブルの親子関係を抱えたいなら DataSet。
マスタ詳細、ヘッダ明細、ツリー構造みたいに「表どうしの関係」が主役になった瞬間、DataSet の DataRelation が活きます。
逆に一覧をグリッドに出すだけなら、DataSet は過剰。
軸② メモリの軽さ
罠その2で見たとおり、DataSet は中身を全部抱えます。
だから軽く取り回したいなら DataTable 1枚のほうが素直。1枚で済む処理にわざわざ箱の箱を被せると、解放のことまで気を使うハメになる。
軸③ 型安全とメンテ性
最後は地味だけど、保守でボディブローのように効いてくる軸。
DataTable も DataSet も、素のままだと値は object で持っていて、row["Qty"] のように文字列キーでアクセスします。
これ、列名のタイプミスがコンパイルで弾かれない。値を取り出すたびに型変換のコストもかかる。
この「object でぜんぶ持つ箱」のコストについて、『Effective C#』(Bill Wagner 著・項目9 ボックス化およびボックス化解除を最小限に抑える / p.43-44・英語原書の日本語版より) にこう書かれています。
.NET Framework がリリースされた当初、コレクションには System.Object インスタンスへの参照が保持されるようになっていた。値型をコレクションに追加すると、毎回ボックス内に格納される。コレクションからオブジェクトを取り出すと、常にコピーが作成される。(中略)1.x のオブジェクトベースのコレクションではなく、.NET BCL 2.0 以降で追加されたジェネリック版のコレクションを使用すべき。
DataRow は、まさにこの「object でぜんぶ持つ箱」なんですよね。
row["Qty"] は object を返すので、(int)row["Qty"] と書くたびにボックス化解除のコストを払ってる。しかも中身が DBNull なら例外で死ぬ。
俺が現場でこれを避けるためにやってるのは、型付きアクセサ Field<T>() に統一すること。コードで比べるとこうです。
using System;
using System.Data;
class Program
{
static void Main()
{
var dt = new DataTable();
dt.Columns.Add("Qty", typeof(int));
dt.Rows.Add(10);
dt.Rows.Add(DBNull.Value); // 値が入ってない行
var row0 = dt.Rows[0];
var row1 = dt.Rows[1];
// object を cast する書き方。値があれば動くが、DBNull 列ではこの形が InvalidCastException
int qty = (int)row0["Qty"];
Console.WriteLine($"cast 版: {qty}");
// 型付きアクセサ: null 許容で DBNull を素直に扱える
int qty2 = row0.Field<int>("Qty");
int? qty3 = row1.Field<int?>("Qty"); // DBNull は null になる
Console.WriteLine($"Field<int> 版: {qty2}");
Console.WriteLine($"Field<int?> 版(DBNull行): {(qty3.HasValue ? qty3.ToString() : "null")}");
}
}
実行結果:

こんな感じで型付きアクセサに寄せておくと、DBNull 行でも落ちずに素直に処理が流れます。
Field<int?> なら DBNull がそのまま null に落ちる。(int)row["Qty"] で踏むあの例外を、構造的に避けられるんですよね。
型付き DataSet(.xsd から生成するやつ)まで行けば、列名もコンパイルでチェックされます。とはいえスキーマ変更のたびに再生成が要るので、変更の多いテーブルではメンテコストと天秤にかける。ここは現場の事情次第です。
3軸まとめると、「表が1枚か、複数の親子か」で大枠を決めて、メモリと型安全で微調整する。これが一番ブレない選び方だと思ってます。
俺の現場ではこう使い分けてる
正直に言うと、業務の保守現場だと9割は DataTable 1枚で足ります。
一覧を取ってグリッドに流す、検索結果を出す、CSV に吐く。このへんは表が1枚で完結するので、わざわざ箱の箱を持ち出さない。
DataSet を引っ張り出すのは、はっきり「親子をまとめて1往復で取りたい」ときだけ。受発注のヘッダ明細を1画面で編集する、みたいな場面ですね。
ここで DataRelation を張っておくと、親行を選んだら子行がスッと出る。画面側のコードがめっちゃ素直になります。
逆にやらかしがちなのが、「なんとなく DataSet で取っといた」パターン。1テーブルしか使わないのに DataSet で包んで、ds.Tables[0] で取り出してる。
これ、箱が無駄に1段増えてるだけなんですよね。「ん?これ DataTable でよくない?」と一回立ち止まると、コードがシンプルになります。
動作確認メモ:今回のコードは DB 接続を伴わないインメモリ処理だけなので、
System.Dataが使える環境ならそのまま動きます。実際の業務では DataAdapter で SELECT 結果を DataSet/DataTable に流し込む形になりますが、その Fill/Update 周りの罠は範囲外なので、関連記事のほうで触れてます。
まとめ
DataSet と DataTable の違いは、突き詰めると「箱の入れ子が1段あるかどうか」でした。
- DataTable は1枚の表、DataSet は表を束ねる箱の箱。両者は対立じゃなく入れ子の関係
- 選ぶ軸は ①テーブル数と親子関係 ②メモリの軽さ ③型安全とメンテ性 の3つ
- 親子データなら DataSet + DataRelation、1枚で足りるなら DataTable
- 値は
objectで持つので、(int)row["Qty"]よりField<T>()型付きアクセサが安全
「DataSet っていつ使うん??」がモヤッとしてた人は、この「箱が1段増えるだけ」のイメージを持ち帰ってください。
次に親子データが来たとき、迷わず手が動くはず!!
よくある質問
DataSet と DataTable はどちらが速いですか?
単一テーブルなら DataTable が速くて軽いです。DataSet は複数の DataTable とリレーションを抱える分、オーバーヘッドがあります。
速さと軽さだけで選ぶなら DataTable、構造が要るなら DataSet。そういう棲み分けになります。
DataSet を使わず DataTable だけで親子データを扱えますか?
扱えます。ただし親子の突き合わせを、自前の LINQ や Dictionary で書くことになります。
リレーションの管理を ADO.NET 側に任せて画面コードを薄くしたいなら、DataSet の DataRelation を使うほうが楽です。「自分で繋ぐか、ADO.NET に繋がせるか」の選択ですね。
なぜ DataTable じゃなく DataSet で取ったほうがいいと言われる場面があるのですか?
複数テーブルを1往復で取って、接続をすぐ切りたいときです。
DataSet は元々「接続を切ってオフラインで扱う」ための器なので、ヘッダ明細をまとめて持って画面側で完結させたい場面では理にかなっています。逆に1テーブルなら、DataSet で包む意味は薄いです。
今から DataSet を覚える意味はありますか?EF や Dapper でいいのでは?
新規開発なら ORM が主流なのは事実です。
ただ、レガシーな業務システムの保守では DataSet と DataTable がまだ現役で、読めないと改修に入れません。新しく選ぶための知識と、既存を読むための知識は別物。保守で食ってる業務 SE ほど、ここは押さえておいて損はないです。
この記事の参考文献
今回の「型付きアクセサとボックス化のコスト」の話は、以下の書籍の知見を下敷きにしています。
『Effective C# 6.0/7.0』(Bill Wagner 著 / 翔泳社 / 2018年 / ISBN 978-4798153827・英語原書の日本語版を参照)
C# 開発者が押さえるべき50のベストプラクティス集。今回は「項目9 ボックス化およびボックス化解除を最小限に抑える(Item 9: Minimize Boxing and Unboxing)」を、DataRow が値を
objectとして持つことのコストの根拠として引用しました。業務 SE が C# で型の扱いを判断するときの土台になる一冊です。
執筆者
バイブス父さん — 業務 SE 7 年(正社員 2 / フリーランス 5)。現職は SEO 直轄部の AI アドバイザー兼 PL、副業で中小 SIer の CTO。SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」の技術 + キャリア記事を書いています。
🐦 X: @hiro_progra0524(日々の現場メモ更新中)
📝 About Me で経歴詳細を見る
次に読むべき記事





以上!



