Reputation: 4928
Within a C# method, I execute the following SQL query that returns multiple rows:
SELECT [Data], [Version]
FROM [dbo].[Table]
WHERE [Id]=@uniqueId AND [ReferenceId] IS NULL
ORDER BY [Version] Asc
I then iterate the results and call a method that is supposed to update the table:
while (sqlDataReader.Read())
{
SqlBytes data = sqlDataReader.GetSqlBytes(0);
SqlInt64 version = sqlDataReader.GetSqlInt64(1);
UpdateReference(data, version);
}
UpdateReference(data, version)
{
// do database unrelated stuff with data
UPDATE [dbo].[Table]
SET [dbo].[Table].[ReferenceId]=..., [dbo].[Table].[Data]=...
WHERE [dbo].[Table].[Id]=@uniqueId AND [dbo].[Table].[Version]=@version
}
For a while this worked fine, but suddenly (after executing some SELECT ... INNER JOIN
queries on the same table) stopped. I create a transaction scope over the first SELECT (in the same method that calls UpdateReference()
):
using (TransactionScope scope = new TransactionScope())
SELECT ...
while (sqlDataReader.Read()) ... UpdateReference();
I get the following exception:
The transaction has aborted.
If I remove the the transaction scope, timeout exception occurs after some time while calling UPDATE:
Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding.
But this doesn't seem to be an SQL Server problem. What is also odd is that for some records, there are no such problems - they only occur when the first SELECT is used on certain table records.
Here's what I found out so far:
One solution that seems to work (for now?) is to store results of the first query to lists, then call update on list elements after SELECT
has finished:
List<long> versionList = new List<long>();
List<byte[]> dataList = new List<byte[]>();
using (TransactionScope scope = new TransactionScope())
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
// Execute SELECT ...
using (SqlCommand sqlCommand = new SqlCommand(selectStatement, connection))
{
...
using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader())
{
while (sqlDataReader.Read())
{
SqlBytes data = sqlDataReader.GetSqlBytes(0);
SqlInt64 version = sqlDataReader.GetSqlInt64(1);
// Store result to lists
versionList.Add(version.Value);
dataList.Add((byte[])data.ToSqlBinary(););
}
}
}
}
// Everything works as expected if this loop is placed here; but if it is placed within the above SqlConnection using clause, an exception is thrown:
// "Network access for Distributed Transaction Manager (MSDTC) has been disabled. Please enable DTC for network access in the security configurationfor MSDTC using the Component Services Administrative tool."
for (int i = 0; i < versionList.Count; i++)
{
UpdateReference(dataList[i], versionList[i]);
}
scope.Complete();
}
I'm not sure if this solution is any good (besides using more memory than optimal) or what other potential problems it may cause. I would be grateful for any insight into what is going on here and how best to resolve it.
UPDATE 1
For clarity sake, this is how I fixed the problem:
execute SELECT outside TransactionScope, store results into lists;
iterate these lists and feed their content to UPDATE, which is enclosed in a TransactionScope
Feel free to criticize/improve this solution:
Method1()
{
List<long> versionList = new List<long>();
List<byte[]> dataList = new List<byte[]>();
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
// Execute SELECT ...
using (SqlCommand sqlCommand = new SqlCommand(selectStatement, connection))
{
...
using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader())
{
while (sqlDataReader.Read())
{
SqlBytes data = sqlDataReader.GetSqlBytes(0);
SqlInt64 version = sqlDataReader.GetSqlInt64(1);
// Store result to lists
versionList.Add(version.Value);
data.Add((byte[])data.ToSqlBinary());
}
}
}
// Call update
for (int i = 0; i < versionList.Count; i++)
{
UpdateReference(dataList[i], versionList[i]);
}
}
}
UpdateReference(data, version)
{
...
using (TransactionScope scope = new TransactionScope())
{
using (SqlConnection connection = new SqlConnection(this.ConnectionString))
{
connection.Open();
UPDATE [dbo].[Table]
SET [dbo].[Table].[ReferenceId]=..., [dbo].[Table].[Data]=...
WHERE [dbo].[Table].[Id]=... AND [dbo].[Table].[Version]=@version
}
scope.Complete();
}
}
Upvotes: 3
Views: 3000
Reputation: 1062945
Yes, select
usually takes locks; during the query itself, for stability; but if there is a transaction (depending on the isolation level), those locks can persist after the query for the entire transaction; key-range locks in particular. Of course, code in the same transaction won't be adversely impacted by these locks. Of particular importance is where exactly your connections are created opened, and how many you are using:
dtcping
can help)However! Personally, I suspect the simplest option in your case is to do the query first outside of the transaction and into a list (or similar) - i.e. not lazily spooling. Then do the work, and apply any updates. If possible, I would try to avoid a single transaction that spans hundreds/thousands of separate commands - if you can possibly batch that work, that would be preferable.
Upvotes: 5