はじめに
業務アプリの中で、DB アクセス層のパフォーマンスが気になってきたのがきっかけで記事にしてみました。
単純な CRUD だけならいいのですが、JOIN や集計が増えるとクエリが重くなることがあり、もっと軽くできないか?と思ったわけです。
EntityFrameworkCore の特徴と課題
LINQ でクエリを書けるので、SQL を詳しく知らなくてもデータ操作ができるのが最大の利点だと思います。
ただし、以下のような場面では少しストレスを感じます。
- トラッキングのオーバーヘッドが重い
- GroupBy や Include を多用すると SQL が複雑化して最適化されにくい
- 実際どんなクエリが書かれているか確認しづらい
生産性は高いですが、パフォーマンスをシビアに求める環境では厳しいこともあると思います。
Dapper とは?
Dapper はマイクロORMと呼ばれるカテゴリ軽量ライブラリです。
ORM というよりは SQL マッピングヘルパーに近い存在で、自分で書いた SQL を .NET オブジェクトにマッピングしてくれます。
特徴をざっくり挙げるとこんな感じ
- SQL は自分で書く(最適化しやすい)
- ORM 的なトラッキングはなし(その分速い)
- シンプルなAPI(
Query<T>()/Execute()など)
要するに、SQL は自分で書くけど、その分高速ってことです。
パフォーマンス比較
「Dapper は速い」みたいな話、よく聞くと思います。
実際に自身の環境で軽く計測してみました。
計測環境
| 項目 | 内容 |
|---|---|
| CPU | Intel Core i7-14700K |
| メモリ | 32GB(4400 [MT/s]) |
| DB | SQL Server 2022(ローカル) |
| .NET | 9.0 |
| ORM | EntityFrameworkCore 9.0 / Dapper 2.1.66 |
テーブル構造は単純な社員情報を持つEmployeesテーブル(約10万件)を想定。
単純な SELECT
EFCore
using var context = new AppDBContext();
var employee = await context.Employees.ToListAsync();
Dapper
using var connection = new SqlConnection(ConnectionStr);
var employee = await connection.QueryAsync<EmployeeEntity>("SELECT * FROM Employees");
結果
| ORM | 実行時間 |
|---|---|
| EFCore | 約 530 ms |
| Dapper | 約 120 ms |
→約 4 倍速い
Dapper は毎回ほぼ同じ速度で安定。EFCore はコンテキスト生成やトラッキングのオーバーヘッドが効いているのではないかと思います。
JOIN + 条件付き SELECT
EFCore
using var context = new AppDbContext();
var list = await context.Employees
.Include(e => e.Department)
.Where(e => e.Salary > 5000000)
.ToListAsync();
Dapper
using var connection = new SqlConnection(ConnectionStr);
var sql = @"SELECT e.*, d.Name AS DepartmentName
FROM Employees e
INNER JOIN Departments d ON e.DepartmentId = d.Id
WHERE e.Salary > @MinSalary";
var list = await connection.QueryAsync<EmployeeEntity>(sql, new { MinSalary = 5000000 });
結果
| ORM | 実行時間 |
|---|---|
| EFCore | 約 650 ms |
| Dapper | 約 180 ms |
JOIN が絡んでも Dapper が圧勝。
SQL を自分で書く分、最適化の自由度が高いです。
Include は便利ですが、裏側の SQL がどうなっているのか見えにくいのがネック。
INSERT (1000件追加)
EFCore
using var context = new AppDbContext();
context.Employees.AddRange(newEmployees);
await context.SaveChangesAsync();
Dapper
using var connection = new SqlConnection(ConnectionStr);
var sql = "INSERT INTO Employees (Name, Age, DepartmentId, Salary) VALUES (@Name, @Age, @DepartmentId, @Salary)";
await connection.ExecuteAsync(sql, newEmployees);
結果
| ORM | 実行時間 |
|---|---|
| EFCore | 約 780 ms |
| Dapper | 約 290 ms |
またもや Dapper が速いですね。
EFCore が一括挿入非対応という構造的な違いも影響していると思います。
高速化を考えるなら、EFCore.BulkExtensionsを使うのが現実的かも。
AsNoTracking()を使用してトラッキングしないようにもできますが、Entity を通している場合、Update() はできなくなってしまう点には注意してください。
結論
| 内容 | EFCore | Dapper | コメント |
|---|---|---|---|
| 単純SELECT | ✗ 遅い | ◎ 速い | トラッキング不要ならDapperが有利 |
| JOIN付きSELECT | △ 普通 | ◎ 速い | SQLチューニングがしやすい |
| INSERT(バッチ) | △ 普通 | ○ 速い | EFは拡張ライブラリ前提 |
| メンテナンス性 | ◎ 高い | △ 手間あり | チーム開発ではEFも悪くない |
結局のところ、
- Dapperは「速くてシンプル」
- EF Coreは「楽で安全」
という印象です。
個人的には、性能が重要な箇所だけDapperに置き換えるハイブリッド構成が一番現実的だと感じました。
設計面から見た検討ポイント
- Repository / UnitOfWork パターンの扱い
- Entity の再利用( DTO 分離 or 使い回し)
- クエリの配置場所(リポジトリ内直書き or SQLファイル管理)
Dapper に変えると、「設計で迷う余地」が増えるかなと思いました。
自由度が高い分、ルールを明確にしておかないとすぐスパゲッティになっちゃいそうです。
採用判断のまとめ
| 観点 | Dapperを選ぶべきケース |
|---|---|
| パフォーマンス重視 | 解析・ログ・大量バッチ処理系 |
| SQLを細かく最適化したい | 複雑な結合・分析系 |
| チームにSQLに強い人がいる | Dapper を活かしやすい環境 |
まとめ
- Dapper は EFCore の約 3 ~ 4倍の速度差を確認
- クエリを自分で最適化する手間は増える
- チーム開発なら、ハイブリッド構成もおすすめ
最終的には、「性能」よりも「チームの体力」と「保守方針」で決めるのが適切かなという結論に至りました。
