0

I have a c# application that is taking data from one database, making the necessary transformations, and inserting the data into a table in another database. I am doing this by inserting my source data into a queue and then processing the queue to insert the data into the destination table. I have two separate threads to read the source data and write the destination data. The reading thread is operating MUCH faster than the writing thread so my queue fills up pretty quickly.

As you can see in the reading thread I am using SqlCommand.ExecuteReader() to read in the data. I am then looping through the queue and doing separate INSERT statements for each row. What I am envisioning is, instead of inserting row by row, doing some sort of (possibly a Linq statement). Does anyone have any ideas how to do my inserts more quickly?

Queue Definition:

private static readonly BlockingCollection<HistorianData> ValueQueue = new BlockingCollection<HistorianData>(new ConcurrentQueue<HistorianData>(), 1000000);

Reading:

    public static void EnqueueHistorianData(SqlConnection connection, int idToAdd, DateTime minDatetime, DateTime maxDateTime, string cluster, string dbName, string dataTable, string idTable, string mainIdColumn, string foreignIdColumn, string dateColumn, string nameColumn, string valueColumn)
    {
        StringBuilder select = new StringBuilder();
        HistorianData values;

        select.Append(String.Format("SELECT {0}.{1},", dataTable, dateColumn));
        select.Append(String.Format(" '{0}.' + {1}.{2},", cluster, idTable, nameColumn));
        select.Append(String.Format(" {0}.{1}", dataTable, valueColumn));
        select.Append(String.Format(" FROM {0}.{1}", dbName, dataTable));
        select.Append(String.Format(" JOIN {0}.{1}", dbName, idTable));
        select.Append(String.Format(" ON {0}.{1} = {2}.{3}", dataTable, foreignIdColumn, idTable, mainIdColumn));
        select.Append(String.Format(" INNER JOIN Runtime.dbo.Tag"));
        select.Append(String.Format(" ON Runtime.dbo.Tag.TagName = '{0}.' + {1}.{2}", cluster, idTable, nameColumn));
        select.Append(String.Format(" WHERE {0}.{1} >= '{2}'", dataTable, dateColumn, minDatetime.ToString()));
        select.Append(String.Format(" AND {0}.{1} = {2}", dataTable, foreignIdColumn, idToAdd.ToString()));
        select.Append(String.Format(" AND {0}.{1} >= '{2}'", dataTable, dateColumn, minDatetime.ToString()));
        select.Append(String.Format(" AND {0}.{1} < '{2}'", dataTable, dateColumn, maxDateTime.ToString()));

        using (var command = new SqlCommand(select.ToString(), connection))
        {
            command.CommandTimeout = 1000;

            using (var reader = command.ExecuteReader())
            {
                if (reader.HasRows)
                {
                    while (reader.Read())
                    {
                        values = new HistorianData();

                        values.SampleDate = reader.GetDateTime(0);
                        values.TagName = reader.GetString(1);
                        values.TagValue = reader.GetDouble(2);

                        ValueQueue.Add(values);
                    }

                    values = null;
                    reader.Close();
                }
            }
        }
    }

Writing:

    public static void WriteQueueValuesToHistorian(string connectionString)
    {   
        HistorianData values;

        using (var connection = new SqlConnection(connectionString))
        {
            connection.Open();

            using (SqlCommand insertCommand = connection.CreateCommand())
            {
                insertCommand.CommandType = CommandType.Text;
                insertCommand.CommandText = "INSERT INTO Runtime.dbo.History (DateTime, TagName, Value, QualityDetail) VALUES (@P1, @P2, @P3, 192)";
                insertCommand.CommandTimeout = 1000;

                var param1 = new SqlParameter("@P1", SqlDbType.DateTime);
                insertCommand.Parameters.Add(param1);

                var param2 = new SqlParameter("@P2", SqlDbType.NVarChar, 512);
                insertCommand.Parameters.Add(param2);

                var param3 = new SqlParameter("@P3", SqlDbType.Float);
                insertCommand.Parameters.Add(param3);

                insertCommand.Prepare();

                while (!ValueQueue.IsCompleted && ValueQueue.TryTake(out values, System.Threading.Timeout.Infinite))
                {
                    int retries = 0;

                    while (retries < 3)
                    {
                        insertCommand.Parameters["@P1"].Value = values.SampleDate.ToLocalTime();
                        insertCommand.Parameters["@P2"].Value = values.TagName;
                        insertCommand.Parameters["@P3"].Value = values.TagValue;

                        try
                        {
                            insertCommand.ExecuteNonQuery();
                            retries = 4;
                        }
                        catch (SqlException)
                        {
                            retries += 1;
                            sw.WriteLine("SQLException - Values: " + insertCommand.Parameters["@P1"].Value + ", " + insertCommand.Parameters["@P2"].Value + ", " + insertCommand.Parameters["@P3"].Value);
                        }
                    }
                }
            }
        }
    }
clintperry
  • 182
  • 1
  • 8
  • The [SqlBulkCopy](http://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlbulkcopy(v=vs.110).aspx) class does have optimizations from reading from a SqlDataReader built in. Instead of reading the records to a concurrent queue then writing them back out, would it be possible to just use a SqlBulkCopy on `reader` from the first function? – Scott Chamberlain Oct 27 '14 at 15:10
  • I tried going this route. My destination database is actually proprietary and referenced via a linked server using a custom OLEDB interface. Bulk insert doesn't work for a linked server. – clintperry Oct 27 '14 at 16:10
  • Are the connection strings going to the same SQL server for both functions (Is the linked server linked on the same server as your source database)? if so why do you pass through C# at all, why not have the query go directly there in a single query? – Scott Chamberlain Oct 27 '14 at 16:26

1 Answers1

0

You can create a DataTable from the data you are retrieving and use the SqlBulkCopy method. Please check the following URL;

http://msdn.microsoft.com/en-us/library/ex21zs8x.aspx

Hozikimaru
  • 1,144
  • 1
  • 9
  • 20
  • I tried going this route. My destination database is actually proprietary and referenced via a linked server using a custom OLEDB interface. Bulk insert doesn't work for a linked server. – clintperry Oct 27 '14 at 16:11
  • What about using 2 different connection strings with a DatabaseHelper class? If that does not work, then you can try BulkInsert method as well : http://msdn.microsoft.com/en-us/library/ms175915.aspx – Hozikimaru Oct 27 '14 at 16:13