I was really interested in performance of using a classic for loop for inserting multiple rows into a database, or to be more precise, I was interested how bad the performance of the for loop actually is.
I wanted to compare Entity framework, ADO.NET, SQL Server stored procedure and SqlBulkCopy class to see how they handle inserting 10, 100, 1000 and 10000 rows at once. So I wrote a really simple app to test them. I used EF v.6.01 and SQL Server 2012.
The database I created is also simple, it has only one column.
CREATETABLE TestTable( RowKey intNOTNULL, CONSTRAINT PK_TestTable PRIMARYKEYCLUSTERED ( RowKey ASC ))
The stored procedure accepts a XML parameter with values for inserting:
CREATEPROCEDURE XMLInsert @pXml Xml ASBEGINBEGINTRANSACTION; SET NOCOUNT ON; select T.node.value('.', 'INT') as Num into #temp from @pXml.nodes('/root/element') as T (node) insertinto TestTable (RowKey) (select Num from #temp) if@@ERROR <> 0 ROLLBACKTRANSACTION; elseCOMMITTRANSACTION; END
EF supports atomic transactions, i.e. if exception is raised during SaveChanges call, the whole transaction is rolled back and none of the rows will be inserted. I presume this doesn't bring a big performance hit, but nevertheless the stored procedure is under the transaction, so is the code using ADO.NET.
Here's the complete code for testing them:
privatevoid test_Click(object sender, RoutedEventArgs e) { Task.Run(() => TestBulkInsert(1)) .ContinueWith(t => TestBulkInsert(10)) .ContinueWith(t => TestBulkInsert(100)) .ContinueWith(t => TestBulkInsert(1000)) .ContinueWith(t => TestBulkInsert(10000)); } privatevoid TestBulkInsert(int rows) { List<TestTable> addRangeRows = GetAddRangeRows(rows); List<TestTable> forLoopRows = GetForLoopRows(rows); XElement xml = GetXml(rows); DataTable table = GetDataTable(rows); Stopwatch sw = new Stopwatch(); using (var context = new TestInsertEntities()) { sw.Start(); context.TestTable.AddRange(addRangeRows); context.SaveChanges(); Console.WriteLine(string.Format("EF AddRange insert for {0} rows: {1} ", rows, sw.Elapsed)); sw.Restart(); foreach (var t in forLoopRows) { context.TestTable.Add(t); } context.SaveChanges(); sw.Stop(); Console.WriteLine(string.Format("EF for loop insert for {0} rows: {1} ", rows, sw.Elapsed)); sw.Restart(); context.XMLInsert(xml.ToString()); sw.Stop(); Console.WriteLine(string.Format("Stored procedure XML insert for {0} rows: {1} ", rows, sw.Elapsed)); sw.Stop(); } string insertCommand = "INSERT INTO TESTTABLE VALUES(@param);"; using (SqlConnection connection = new SqlConnection(connectionString)) { SqlCommand command = new SqlCommand(insertCommand, connection); command.CommandType = CommandType.Text; command.Parameters.Add("@param", SqlDbType.Int); sw.Start(); connection.Open(); var transaction = connection.BeginTransaction(); command.Transaction = transaction; try { for (int i = 3 * rows + 1; i < 4 * rows + 1; i++) { command.Parameters["@param"].Value = i; command.ExecuteNonQuery(); } } catch (Exception) { transaction.Rollback(); } transaction.Commit(); connection.Close(); sw.Stop(); Console.WriteLine(string.Format("ADO.net for loop insert for {0} rows: {1} ", rows, sw.Elapsed)); sw.Reset(); sw.Start(); connection.Open(); using (SqlBulkCopy bulkCopy = new SqlBulkCopy(connection)) { bulkCopy.DestinationTableName = "TestTable"; bulkCopy.WriteToServer(table); } connection.Close(); sw.Stop(); Console.WriteLine(string.Format("SqlBulkCopy insert for {0} rows: {1} ", rows, sw.Elapsed)); connection.Open(); command = new SqlCommand("DELETE FROM TESTTABLE;", connection); command.ExecuteNonQuery(); connection.Close(); } } privatestatic DataTable GetDataTable(int rows) { DataTable table = new DataTable(); table.Columns.Add("RowKey", typeof(int)); for (int i = 4 * rows + 1; i < 5 * rows + 1; i++) { table.Rows.Add(i); } return table; } privatestatic XElement GetXml(int rows) { XElement xml = new XElement("root"); for (int i = 2 * rows + 1; i < 3 * rows + 1; i++) { XElement x = new XElement("element"); x.Add(i); xml.Add(x); } return xml; } privatestatic List<TestTable> GetForLoopRows(int rows) { List<TestTable> forLoopRows = new List<TestTable>(1000); for (int i = 1 + rows; i < 2 * rows + 1; i++) { TestTable t = new TestTable() { RowKey = i }; forLoopRows.Add(t); } return forLoopRows; } privatestatic List<TestTable> GetAddRangeRows(int rows) { List<TestTable> addRangeRows = new List<TestTable>(rows); for (int i = 1; i < 1 + rows; i++) { TestTable t = new TestTable() { RowKey = i }; addRangeRows.Add(t); } return addRangeRows; }
Results
- EF AddRange : 00.8875315 sec
- EF for loop : 00.0109685 sec
- Stored procedure : 00.0605031 sec
- ADO.NET for loop : 00.0665507 sec
- SqlBulkCopy : 00.0070696 sec
- EF AddRange : 00.0865438 sec
- EF for loop : 00.0878373 sec
- Stored procedure : 00.0099671 sec
- ADO.NET for loop : 00.0509694 sec
- SqlBulkCopy : 00.0068580 sec
- EF AddRange : 00.6969128 sec
- EF for loop : 00.7041873 sec
- Stored procedure : 00.0152227 sec
- ADO.NET for loop : 00.3681421 sec
- SqlBulkCopy : 00.0065223 sec
- EF AddRange : 06.8745769 sec
- EF for loop : 08.1898612 sec
- Stored procedure : 00.0192753 sec
- ADO.NET for loop : 03.7156545 sec
- SqlBulkCopy : 00.0163246 sec
- EF AddRange : 67.2680139 sec
- EF for loop : 193.0319509 sec
- Stored procedure :00.1364101 sec
- ADO.NET for loop : 34.9175692 sec
- SqlBulkCopy : 00.0533580 sec
OK, no surprises here, if you are not using a file based databased, like SQLite, SqlBulkCopy or Stored procedures are the way to go.
It's also worth noting how ADO.NET really outperforms the EF when inserting 10 or more rows.