.NET - Fastest Way to Load Text File To SQL - SqlBulkCopy
An exercise in coding. Loading a text file to SQL. There are a billion different ways to do it, and depending on your source data format and such, you can parse the file a million ways too. But how fast can you get data from disk to SQL using .NET code? (VB.NET or C#) . This post is going to show the differences in some of the ways you can load data from disk to SQL.
I am sure I could do more, but this is a good sampling. Lets assume a 1,00,000 row file, comma separated with 3 columns, string, int, string of variable length. Lets assume our destination is SQL Server 2005, table already created, no keys or anything on the table.
We will call our table LoadedData. Our test app will be a VB.NET Console Application, running on the same box as SQL 2005 is loaded. Now, there are many ways to load files. A few are: Reading them line by line, ReadToEnd() and also using the JET engine to read in a CSV, etc. From the testing I have been doing, all of these seem to work fairly fast, maybe a comparison on these is for another blog post, but for brevity’s sake, lets just say they are all comparable. Now, I chose 3 methods of inserting data.
1) StreamReader.ReadLine, Insert Line By Line
Sub Method1()
Dim i As Long = 0
Dim sr As StreamReader = New StreamReader(filename)
Dim line As String = sr.ReadLine()
Dim dbConn As SqlConnection = New SqlConnection(ConfigurationManager.ConnectionStrings("MyDB").ToString())
Dim dbCmd As SqlCommand = New SqlCommand()
dbCmd.Connection = dbConn
Dim wholeFile As String = sr.ReadToEnd()
Do
Dim fields() As String = line.Split(",")
dbCmd.CommandText = "INSERT INTO dbo.TestData (Column1,Column2,Column3) " & _
" VALUES (’" & fields(0) & "’," & fields(1) & ",’" & fields(2) & "’)"
dbConn.Open()
dbCmd.ExecuteNonQuery()
dbConn.Close()
i = i + 1
line = sr.ReadLine()
Loop While Not line = String.Empty
End Sub
2) StreamReader.ReadLine, Batch Insert With DataAdapter
Sub Method2()
Dim i As Long = 0
Dim dbConn As SqlConnection = New SqlConnection(ConfigurationManager.ConnectionStrings("MyDB").ToString())
Dim sr As StreamReader = New StreamReader(filename)
Dim line As String = sr.ReadLine()
Dim strArray As String() = line.Split(",")
Dim dt As DataTable = New DataTable()
Dim row As DataRow
For Each s As String In strArray
dt.Columns.Add(New DataColumn())
Next
Do
row = dt.NewRow()
row.ItemArray = line.Split(",")
dt.Rows.Add(row)
i = i + 1
line = sr.ReadLine()
Loop While Not line = String.Empty
Dim dataAdapter As New SqlDataAdapter()
dataAdapter.SelectCommand = New SqlCommand("SELECT TOP 1 Column1,Column2,Column3 from dbo.TestData", dbConn)
Dim cmdBuilder As SqlCommandBuilder = New SqlCommandBuilder(dataAdapter)
dbConn.Open()
Dim ds As DataSet = New DataSet
dataAdapter.Fill(dt)
dataAdapter.UpdateBatchSize = 1000
dataAdapter.Update(dt)
dbConn.Close()
End Sub
3) StreamReader.ReadLine, SqlBulkCopy
Sub Method3()
Dim i As Long = 0
Dim dbConn As SqlConnection = New SqlConnection(ConfigurationManager.ConnectionStrings("MyDB").ToString())
Dim sr As StreamReader = New StreamReader(filename)
Dim line As String = sr.ReadLine()
Dim strArray As String() = line.Split(",")
Dim dt As DataTable = New DataTable()
Dim row As DataRow
For Each s As String In strArray
dt.Columns.Add(New DataColumn())
Next
Do
row = dt.NewRow()
row.ItemArray = line.Split(",")
dt.Rows.Add(row)
i = i + 1
line = sr.ReadLine()
Loop While Not line = String.Empty
Dim bc As SqlBulkCopy = New SqlBulkCopy(dbConn, SqlBulkCopyOptions.TableLock, Nothing)
bc.DestinationTableName = "TestData"
bc.BatchSize = dt.Rows.Count
dbConn.Open()
bc.WriteToServer(dt)
dbConn.Close()
bc.Close()
End Sub
The results of the 3 methods are surprising. The thing is, most people are going to use Method1 because it just is the first thing you think of doing, and maybe the easiest to code (everyone learns loops in school, etc) - now, nitpickers will say "use a stored proc" etc - that will save minimal time, and in best practice yes, but for the sake of the example bear with it..
Method2 is less intuitive, and really tricky to get working (at least I had some issues with it) but once it works, it makes a little bit more sense then Method1.
Method3 is something that no one ever hears or uses, but once they do, they never go back.
Side note: about 5 years ago I worked on a program that inserted huge files, and they were taking 10-20 minutes a piece. I was using VB6, and converted the line by line insert to use BCP from code and got it down to 2-3 minutes, which was good. So I know about BCP and BULK INSERT. I just didn’t know it was built into .NET, now I do..anyways, on to the results.
Method 1- 14.69 minutes to insert 1 million records
Method 2 - 7.88 minutes to insert 1 million records
Method 3 - 0.28 minutes to insert 1 million records
So yeah. Wow. That is not a typo for Method 3. Roughly 17 seconds. Now, give Method2 so credit, it reduced the time from Method1 by 50% but Method3 just KILLS them both. Man, just awesome. When you run something like that and see that kind of performance, you can’t help but smile.
A few caveats and gotchas:
Method2 - the BatchSize property I have set to 1000. If you set to 0 it uses the max. I tried this and locked my machine up. Yikes.
Method3 - The SqlBulkCopyOptions makes a difference - TableLock speeds up the operation. The BatchSize here I have set to the # of rows, It might run differently with different batch sizes, I really didn’t experiment with it, but adding the Copy Options and BatchSize, it sped up the operations.
So, the fastest way I have found in .NET to load data from files to sql - hands down - SqlBulkCopy. Since it took 17 seconds, The next step is to try different file reading methods and see what time times are there, like I said, maybe that will be my next post. :)
p.s. yes I am up at 2:00 AM posting this, insomnia anyone? In any event I got to watch the repeat of the Nevada Democratic Debate :)
Simlar Posts

April 14th, 2008 at 4:49 am
Steve,
I do recommend you try the union approach. Not sure why it wouldn’t get used under the hood in 2), but I have certainly found it to be very quick, and unlike 3) it will work without particularly weird coding and against a variety of database systems.
I suspect also that your test is a bit flattering to bulk in this case since it has no keys, which is hardly useful in practice for a million rows.
April 14th, 2008 at 8:36 am
not sure what the UNION approach is, but yeah, even if I put I primary key, I think the bulk (#3) would still beat out the others, especially if the # of records was higher. There is just no way you are going to be make sequential inserts faster.
April 15th, 2008 at 1:17 am
You go:
insert t ()
select ()
union select ()
union select ()
union …
SQLServer bulk should be fastest but I suspect it has a setup overhead and will be a lot less optimised with indices and triggers involved.
The statement above can be used anywhere a simple insert can be used, so you can insert to several tables in a single TSQL batch - or from a trigger, stored proc etc.
And the performance can be rather impressive.
Bulk has its place - and its easier on SQLServer than Sybase - but also some limitations.
April 15th, 2008 at 1:17 am
Darn, its stripped some of the text with angle brackets. :-(
Should still be understandable.
April 15th, 2008 at 6:36 am
Yeah, I dont know how you would really use that in a stored proc unless you knew the values in advance. If I get any motivation, I might open up the project I made to test this and add that, but Id bet money it doesn’t perform as well as SqlBulk.
April 15th, 2008 at 2:13 pm
I suspect it won’t be as fast as bulk - but as you add indices and any triggers, the gap will narrow, and you don’t have any awkward setup tasks that don’t use simple statements. I think you’ll find
I don’t understand your comment about knowing values in advance? Same for everything - in this case you can at least prepare a statement that inserts 10 rows, for example.
Admitedly you’re not going to _prepare_ an insert that does 1000 rows like this (seem to remember I’ve executed statements like that though), but it makes it easy to use one code path that builds insertion batches for master detail - providing you can preallocate the master keys rather than expecting an insert trigger to do it for you.
The parse and plan time seems low enough that its reasonable
to send and execute without a prep.
You can send a complex batch like this:
begin tran
insert master (…) select (…) union select (…) union select (…)
insert detail (…) select (…) union select (…) union select (…) … etc
insert sub_detail (…) select (…) union select (…) union select (…) … etc
commit
as one RPC to the server. Its very flexible.
Like I said, a million rows into a table with no index isn’t particularly representative - it can change the amount of logging performed for example - I do encourage you try the tests with a more representative schema.
April 15th, 2008 at 2:19 pm
knowing the values in advance? well with a stored proc, passing in values, or are you just saying pass in a string and execute it in a stored proc? otherwise you need to parameterize your proc and pass in values, not sure how you could do that without knowing in advance.
The test wasn’t on master/detail/subdetail values, just a bulk log file of values. The only key that I would possibly add would be a primary key, and in this case (in the test), it would have probably just been an auto index, I don’t see how that would hinder the performance too much.
This test is just inserting a ton of values from a log file to a database. With master/detail things would have to be done differently, of course, you couldn’t really bulk insert without doing something different, but for what I was trying to represent, the tests themselves speak for what I was trying to convey.
April 15th, 2008 at 4:40 pm
Well, the data can be parameters, or calculated from them, or expanded from a blob param or whatever. No matter - we have to decide the data values at some point.
Granted that bulk insert of a log file is a ’standard’ BCP scenario, but even then I’d normally expect to see an index in place before its useful. Of course, creating the index after import is quite normal - partly becauuse the bulk import is so much slower if you have indices in place.
If all you care about is bulk log record insert then so be it. I’m really not arguing that BCP or the SQLServer bulk facility isn’t the fastest way to do this - but its just not very generalised and a multi-row insert that can be done anywhere you can do a normal insert statement is a whole lot more flexible. And it can be surprisingly fast.