Skip to content

Commit 1c5ea13

Browse files
committed
Add benchmarks for raw bulk inserts and updates benchmark results
1 parent eb4b8c4 commit 1c5ea13

6 files changed

Lines changed: 204 additions & 39 deletions

File tree

README.md

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,23 +73,34 @@ await dbContext.ExecuteInsertWithIdentityAsync(entities, options => {});
7373
- [ ] Add support for TPH (Table Per Hierarchy) inheritance
7474

7575
## Benchmarks
76+
7677
Benchmark projects are available in the [`tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark`](tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/LibComparator.cs) directory.
77-
Run them to compare performance with other libraries (https://github.com/borisdj/EFCore.BulkExtensions and https://entityframework-extensions.net/bulk-extensions),
78-
using optimized configuration (local Docker is required).
78+
Run them to compare performance with raw bulk insert methods and other libraries (https://github.com/borisdj/EFCore.BulkExtensions
79+
and https://entityframework-extensions.net/bulk-extensions), using optimized configuration (local Docker is required).
80+
81+
Legend :
82+
* `PhenX_EntityFrameworkCore_BulkInsert`: this library
83+
* `RawInsert`: no library, using the native provider API (SqlBulkCopy for SQL Server, BeginBinaryImport for PostgreSQL, raw inserts for SQLite)
84+
* `Z_EntityFramework_Extensions_EFCore`: https://entityframework-extensions.net/bulk-extensions
85+
* `EFCore_BulkExtensions`: https://github.com/borisdj/EFCore.BulkExtensions
86+
* `EFCore_SaveChanges`: EF Core SaveChanges classic method
87+
88+
SQL Server results with 500 000 rows :
7989

80-
There is no need to compare with basic EF Core SaveChangesAsync, as it is significantly slower.
90+
![bench-sqlserver.png](https://raw.githubusercontent.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/refs/heads/master/images/bench-sqlserver.png)
8191

82-
SQL Server results :
83-
![bench-sqlserver.png](images/bench-sqlserver.png)
92+
PostgreSQL results with 500 000 rows :
8493

85-
PostgreSQL results :
86-
![bench-postgresql.png](images/bench-postgresql.png)
94+
![bench-postgresql.png](https://raw.githubusercontent.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/refs/heads/master/images/bench-postgresql.png)
8795

88-
SQLite results :
89-
![bench-sqlite.png](images/bench-sqlite.png)
96+
SQLite results with 500 000 rows :
97+
98+
![bench-sqlite.png](https://raw.githubusercontent.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/refs/heads/master/images/bench-sqlite.png)
9099

91100
## Contributing
101+
92102
Contributions are welcome! Please open issues or submit pull requests for bug fixes, features, or documentation improvements.
93103

94104
## License
105+
95106
MIT License. See [LICENSE](LICENSE) for details.

images/bench-postgresql.png

-9.23 KB
Loading

images/bench-sqlite.png

-10.4 KB
Loading

images/bench-sqlserver.png

-10.6 KB
Loading

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/LibComparator.cs

Lines changed: 183 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
1+
using System.Data;
2+
13
using BenchmarkDotNet.Attributes;
24

35
using DotNet.Testcontainers.Containers;
46

57
using EFCore.BulkExtensions;
68

9+
using Microsoft.Data.SqlClient;
10+
using Microsoft.Data.Sqlite;
11+
using Microsoft.EntityFrameworkCore;
12+
13+
using Npgsql;
14+
715
using PhenX.EntityFrameworkCore.BulkInsert.Extensions;
816

917
namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark;
1018

1119
public abstract class LibComparator
1220
{
13-
[Params(100_000/*, 1_000_000/*, 10_000_000*/)]
21+
[Params(500_000/*, 1_000_000/*, 10_000_000*/)]
1422
public int N;
1523

1624
private IList<TestEntity> data = [];
@@ -54,37 +62,45 @@ public async Task PhenX_EntityFrameworkCore_BulkInsert()
5462
{
5563
await DbContext.ExecuteInsertAsync(data);
5664
}
57-
58-
[Benchmark]
59-
public void PhenX_EntityFrameworkCore_BulkInsert_Sync()
60-
{
61-
DbContext.ExecuteInsert(data);
62-
}
6365
//
6466
// [Benchmark]
65-
// public async Task PhenX_EntityFrameworkCore_BulkInsertWithIdentity()
67+
// public void PhenX_EntityFrameworkCore_BulkInsert_Sync()
6668
// {
67-
// await DbContext.ExecuteInsertWithIdentityAsync(data);
68-
// }
69-
//
70-
// [Benchmark]
71-
// public async Task PhenX_EntityFrameworkCore_BulkInsertWithIdentityMoveRows()
72-
// {
73-
// await DbContext.ExecuteInsertWithIdentityAsync(data, options => options.MoveRows = true);
69+
// DbContext.ExecuteInsert(data);
7470
// }
7571

7672
[Benchmark]
77-
public async Task Z_EntityFramework_Extensions_EFCore()
73+
public void RawInsert()
7874
{
79-
await DbContext.BulkInsertOptimizedAsync(data, options => options.IncludeGraph = false);
75+
if (DbContext.Database.ProviderName!.Contains("SqlServer", StringComparison.InvariantCultureIgnoreCase))
76+
{
77+
// Use SqlBulkCopy for SQL Server
78+
RawInsertSqlServer();
79+
}
80+
else if (DbContext.Database.ProviderName!.Contains("Sqlite", StringComparison.InvariantCultureIgnoreCase))
81+
{
82+
// Use raw sql insert statements for SQLite
83+
RawInsertSqlite();
84+
}
85+
else if (DbContext.Database.ProviderName!.Contains("Npgsql", StringComparison.InvariantCultureIgnoreCase))
86+
{
87+
// Use BeginBinaryImport for PostgreSQL
88+
RawInsertPostgreSql();
89+
}
8090
}
8191

8292
[Benchmark]
83-
public void Z_EntityFramework_Extensions_EFCore_Sync()
93+
public async Task Z_EntityFramework_Extensions_EFCore()
8494
{
85-
DbContext.BulkInsertOptimized(data, options => options.IncludeGraph = false);
95+
await DbContext.BulkInsertOptimizedAsync(data, options => options.IncludeGraph = false);
8696
}
8797

98+
// [Benchmark]
99+
// public void Z_EntityFramework_Extensions_EFCore_Sync()
100+
// {
101+
// DbContext.BulkInsertOptimized(data, options => options.IncludeGraph = false);
102+
// }
103+
88104
[Benchmark]
89105
public async Task EFCore_BulkExtensions()
90106
{
@@ -95,20 +111,157 @@ await DbContext.BulkInsertAsync(data, options =>
95111
});
96112
}
97113

114+
// [Benchmark]
115+
// public void EFCore_BulkExtensions_Sync()
116+
// {
117+
// DbContext.BulkInsert(data, options =>
118+
// {
119+
// options.IncludeGraph = false;
120+
// options.PreserveInsertOrder = false;
121+
// });
122+
// }
123+
98124
[Benchmark]
99-
public void EFCore_BulkExtensions_Sync()
125+
public async Task EFCore_SaveChanges()
100126
{
101-
DbContext.BulkInsert(data, options =>
127+
DbContext.AddRange(data);
128+
await DbContext.SaveChangesAsync();
129+
}
130+
131+
private void RawInsertPostgreSql()
132+
{
133+
using var connection = (NpgsqlConnection)DbContext.Database.GetDbConnection();
134+
if (connection.State != ConnectionState.Open)
102135
{
103-
options.IncludeGraph = false;
104-
options.PreserveInsertOrder = false;
105-
});
136+
connection.Open();
137+
}
138+
139+
const string copyCommand = $"""
140+
COPY "{nameof(TestEntity)}" (
141+
"Name",
142+
"Price",
143+
"Identifier",
144+
"CreatedAt",
145+
"UpdatedAt",
146+
"StringEnumValue",
147+
"NumericEnumValue"
148+
) FROM STDIN (FORMAT BINARY)
149+
""";
150+
151+
using var writer = connection.BeginBinaryImport(copyCommand);
152+
foreach (var entity in data)
153+
{
154+
writer.StartRow();
155+
writer.Write(entity.Name);
156+
writer.Write(entity.Price);
157+
writer.Write(entity.Identifier);
158+
writer.Write(entity.CreatedAt);
159+
writer.Write(entity.UpdatedAt);
160+
writer.Write(entity.StringEnumValue.ToString());
161+
writer.Write((int)entity.NumericEnumValue);
162+
}
163+
164+
writer.Complete();
106165
}
107166

108-
// [Benchmark]
109-
// public async Task EFCore_SaveChanges()
110-
// {
111-
// DbContext.AddRange(data);
112-
// await DbContext.SaveChangesAsync();
113-
// }
167+
private void RawInsertSqlite()
168+
{
169+
var connection = (SqliteConnection)DbContext.Database.GetDbConnection();
170+
if (connection.State != ConnectionState.Open)
171+
{
172+
connection.Open();
173+
}
174+
using var transaction = connection.BeginTransaction();
175+
using var command = connection.CreateCommand();
176+
command.CommandText = $"""
177+
INSERT INTO "{nameof(TestEntity)}" (
178+
"Name",
179+
"Price",
180+
"Identifier",
181+
"CreatedAt",
182+
"UpdatedAt",
183+
"StringEnumValue",
184+
"NumericEnumValue"
185+
) VALUES (@Name, @Price, @Identifier, @CreatedAt, @UpdatedAt, @StringEnumValue, @NumericEnumValue)
186+
""";
187+
188+
command.Parameters.Add(new SqliteParameter("@Name", DbType.String));
189+
command.Parameters.Add(new SqliteParameter("@Price", DbType.Decimal));
190+
command.Parameters.Add(new SqliteParameter("@Identifier", DbType.Guid));
191+
command.Parameters.Add(new SqliteParameter("@CreatedAt", DbType.DateTime));
192+
command.Parameters.Add(new SqliteParameter("@UpdatedAt", DbType.DateTime2));
193+
command.Parameters.Add(new SqliteParameter("@StringEnumValue", DbType.String));
194+
command.Parameters.Add(new SqliteParameter("@NumericEnumValue", DbType.Int32));
195+
196+
foreach (var entity in data)
197+
{
198+
command.Parameters["@Name"].Value = entity.Name;
199+
command.Parameters["@Price"].Value = entity.Price;
200+
command.Parameters["@Identifier"].Value = entity.Identifier;
201+
command.Parameters["@CreatedAt"].Value = entity.CreatedAt;
202+
command.Parameters["@UpdatedAt"].Value = entity.UpdatedAt;
203+
command.Parameters["@StringEnumValue"].Value = entity.StringEnumValue.ToString();
204+
command.Parameters["@NumericEnumValue"].Value = (int)entity.NumericEnumValue;
205+
206+
command.ExecuteNonQuery();
207+
}
208+
209+
transaction.Commit();
210+
}
211+
212+
private void RawInsertSqlServer()
213+
{
214+
var connection = (SqlConnection)DbContext.Database.GetDbConnection();
215+
if (connection.State != ConnectionState.Open)
216+
{
217+
connection.Open();
218+
}
219+
220+
using var bulkCopy = new SqlBulkCopy(connection);
221+
222+
bulkCopy.DestinationTableName = nameof(TestEntity);
223+
bulkCopy.BatchSize = 50_000;
224+
bulkCopy.BulkCopyTimeout = 60;
225+
226+
bulkCopy.ColumnMappings.Add("Name", "Name");
227+
bulkCopy.ColumnMappings.Add("Price", "Price");
228+
bulkCopy.ColumnMappings.Add("Identifier", "Identifier");
229+
bulkCopy.ColumnMappings.Add("CreatedAt", "CreatedAt");
230+
bulkCopy.ColumnMappings.Add("UpdatedAt", "UpdatedAt");
231+
bulkCopy.ColumnMappings.Add("StringEnumValue", "StringEnumValue");
232+
bulkCopy.ColumnMappings.Add("NumericEnumValue", "NumericEnumValue");
233+
234+
var dataTable = new DataTable();
235+
dataTable.Columns.Add("Name", typeof(string));
236+
dataTable.Columns.Add("Price", typeof(decimal));
237+
dataTable.Columns.Add("Identifier", typeof(Guid));
238+
dataTable.Columns.Add("CreatedAt", typeof(DateTime));
239+
dataTable.Columns.Add("UpdatedAt", typeof(DateTimeOffset));
240+
dataTable.Columns.Add("StringEnumValue", typeof(string));
241+
dataTable.Columns.Add("NumericEnumValue", typeof(int));
242+
243+
foreach (var entity in data)
244+
{
245+
var row = dataTable.NewRow();
246+
row["Name"] = entity.Name;
247+
row["Price"] = entity.Price;
248+
row["Identifier"] = entity.Identifier;
249+
row["CreatedAt"] = entity.CreatedAt;
250+
row["UpdatedAt"] = entity.UpdatedAt;
251+
row["StringEnumValue"] = entity.StringEnumValue.ToString();
252+
row["NumericEnumValue"] = (int)entity.NumericEnumValue;
253+
dataTable.Rows.Add(row);
254+
255+
if (dataTable.Rows.Count >= 50_000)
256+
{
257+
bulkCopy.WriteToServer(dataTable);
258+
dataTable.Clear();
259+
}
260+
}
261+
262+
if (dataTable.Rows.Count > 0)
263+
{
264+
bulkCopy.WriteToServer(dataTable);
265+
}
266+
}
114267
}

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/TestEntity.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace PhenX.EntityFrameworkCore.BulkInsert.Benchmark;
66

77
[PrimaryKey(nameof(Id))]
8+
[Table(nameof(TestEntity))]
89
public class TestEntity
910
{
1011
public int Id { get; set; }

0 commit comments

Comments
 (0)