I'm thinking of caching permissions for every user on our application server. Is it a good idea to use a SqlCacheDependency for every user?
The query would look like this
SELECT PermissionId, PermissionName From Permissions Where UserId = #UserId
That way I know if any of those records change then to purge my cache for that user.
If you read how Query Notifications work you'll see why createing many dependency requests with a single query template is good practice. For a web app, which is implied by the fact that you use SqlCacheDependency and not SqlDependency, what you plan to do should be OK. If you use Linq2Sql you can also try LinqToCache:
var queryUsers = from u in repository.Users
where u.UserId = currentUserId
select u;
var user= queryUsers .AsCached("Users:" + currentUserId.ToString());
For a fat client app it would not be OK. Not because of the query per-se, but because SqlDependency in general is problematic with a large number of clients connected (it blocks a worker thread per app domain connected):
SqlDependency was designed to be used in ASP.NET or middle-tier
services where there is a relatively small number of servers having
dependencies active against the database. It was not designed for use
in client applications, where hundreds or thousands of client
computers would have SqlDependency objects set up for a single
database server.
Updated
Here is the same test as #usr did in his post. Full c# code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;
using DependencyMassTest.Properties;
using System.Threading.Tasks;
using System.Threading;
namespace DependencyMassTest
{
class Program
{
static volatile int goal = 50000;
static volatile int running = 0;
static volatile int notified = 0;
static int workers = 50;
static SqlConnectionStringBuilder scsb;
static AutoResetEvent done = new AutoResetEvent(false);
static void Main(string[] args)
{
scsb = new SqlConnectionStringBuilder(Settings.Default.ConnString);
scsb.AsynchronousProcessing = true;
scsb.Pooling = true;
try
{
SqlDependency.Start(scsb.ConnectionString);
using (var conn = new SqlConnection(scsb.ConnectionString))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand(#"
if object_id('SqlDependencyTest') is not null
drop table SqlDependencyTest
create table SqlDependencyTest (
ID int not null identity,
SomeValue nvarchar(400),
primary key(ID)
)
", conn))
{
cmd.ExecuteNonQuery();
}
}
for (int i = 0; i < workers; ++i)
{
Task.Factory.StartNew(
() =>
{
RunTask();
});
}
done.WaitOne();
Console.WriteLine("All dependencies subscribed. Waiting...");
Console.ReadKey();
}
catch (Exception e)
{
Console.Error.WriteLine(e);
}
finally
{
SqlDependency.Stop(scsb.ConnectionString);
}
}
static void RunTask()
{
Random rand = new Random();
SqlConnection conn = new SqlConnection(scsb.ConnectionString);
conn.Open();
SqlCommand cmd = new SqlCommand(
#"select SomeValue
from dbo.SqlDependencyTest
where ID = #id", conn);
cmd.Parameters.AddWithValue("#id", rand.Next(50000));
SqlDependency dep = new SqlDependency(cmd);
dep.OnChange += new OnChangeEventHandler((ob, qnArgs) =>
{
Console.WriteLine("Notified {3}: Info:{0}, Source:{1}, Type:{2}", qnArgs.Info, qnArgs.Source, qnArgs.Type, Interlocked.Increment(ref notified));
});
cmd.BeginExecuteReader(
(ar) =>
{
try
{
int crt = Interlocked.Increment(ref running);
if (crt % 1000 == 0)
{
Console.WriteLine("{0} running...", crt);
}
using (SqlDataReader rdr = cmd.EndExecuteReader(ar))
{
while (rdr.Read())
{
}
}
}
catch (Exception e)
{
Console.Error.WriteLine(e.Message);
}
finally
{
conn.Close();
int left = Interlocked.Decrement(ref goal);
if (0 == left)
{
done.Set();
}
else if (left > 0)
{
RunTask();
}
}
}, null);
}
}
}
After 50k subscriptions are set up (takes about 5 min), here are the stats io of a single insert:
set statistics time on
insert into Test..SqlDependencyTest (SomeValue) values ('Foo');
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 0 ms.
SQL Server Execution Times:
CPU time = 16 ms, elapsed time = 16 ms.
Inserting 1000 rows takes about 7 seconds, which includes firing several hundred notifications. CPU utilization is about 11%. All this is on my T420s ThinkPad.
set nocount on;
go
begin transaction
go
insert into Test..SqlDependencyTest (SomeValue) values ('Foo');
go 1000
commit
go
the documentation says:
SqlDependency was designed to be used in ASP.NET or middle-tier
services where there is a relatively small number of servers having
dependencies active against the database. It was not designed for use
in client applications, where hundreds or thousands of client
computers would have SqlDependency objects set up for a single
database server.
It tells us not to open thousands of cache dependencies. That is likely to cause resource problems on the SQL Server.
There are a few alternatives:
Have a dependency per table
Have 100 dependencies per table, one for every percent of rows. This should be an acceptable number for SQL Server yet you only need to invalidate 1% of the cache.
Have a trigger output the ID of all changes rows into a logging table. Create a dependency on that table and read the IDs. This will tell you exactly which rows have changed.
In order to find out if SqlDependency is suitable for mass usage I did a benchmark:
static void SqlDependencyMassTest()
{
var connectionString = "Data Source=(local); Initial Catalog=Test; Integrated Security=true;";
using (var dependencyConnection = new SqlConnection(connectionString))
{
dependencyConnection.EnsureIsOpen();
dependencyConnection.ExecuteNonQuery(#"
if object_id('SqlDependencyTest') is not null
drop table SqlDependencyTest
create table SqlDependencyTest (
ID int not null identity,
SomeValue nvarchar(400),
primary key(ID)
)
--ALTER DATABASE Test SET ENABLE_BROKER with rollback immediate
");
SqlDependency.Start(connectionString);
for (int i = 0; i < 1000 * 1000; i++)
{
using (var sqlCommand = new SqlCommand("select ID from dbo.SqlDependencyTest where ID = #id", dependencyConnection))
{
sqlCommand.AddCommandParameters(new { id = StaticRandom.ThreadLocal.GetInt32() });
CreateSqlDependency(sqlCommand, args =>
{
});
}
if (i % 1000 == 0)
Console.WriteLine(i);
}
}
}
You can see the amount of dependencies created scroll through the console. It gets slow very quickly. I did not do a formal measurement because it was not necessary to prove the point.
Also, the execution plan for a simple insert into the table shows 99% of the cost being associated with maintaining the 50k dependencies.
Conclusion: Does not work at all for production use. After 30min I have 55k dependencies created. Machine at 100% CPU all the time.
Related
I am seeing constant deadlocks in my app, even though it performs no select statements, no delete statements, and no update statements. It is only inserting completely new data.
TL;DR: It seems to be related to the foreign key. If I remove that then I don't get any deadlocks at all. But that is not an acceptable solution for obvious reasons.
Given the following table structure
CREATE TABLE [dbo].[IncomingFile]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[ConcurrencyVersion] RowVersion NOT NULL,
CONSTRAINT [PK_IncomingFile] PRIMARY KEY CLUSTERED([Id])
)
GO
CREATE TABLE [dbo].[IncomingFileEvent]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[ConcurrencyVersion] RowVersion NOT NULL,
[IncomingFileId] UNIQUEIDENTIFIER NOT NULL,
CONSTRAINT [PK_IncomingFileEvent] PRIMARY KEY CLUSTERED([Id]),
CONSTRAINT [FK_IncomingFileEvent_IncomingFileId]
FOREIGN KEY ([IncomingFileId])
REFERENCES [dbo].[IncomingFile] ([Id])
)
GO
When I hit a number of concurrent tasks inserting data, I always see a deadlock. READ_COMMITTED_SNAPSHOT is enabled in my DB options (even though I am not reading anyway).
Here is the code that will reproduce the problem. If you do not experience the problem, increase the NumberOfTasksPerCpu constant at the top of the program.
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SqlServerDeadlockRepro
{
class Program
{
private const int NumberOfTasksPerCpu = 8; // Keep increasing this by one if you do not get a deadlock!
private const int NumberOfChildRows = 1_000;
private const string MSSqlConnectionString = "Server=DESKTOP-G05BF1U;Database=EFCoreConcurrencyTest;Trusted_Connection=True;";
private static int NumberOfConcurrentTasks;
static async Task Main(string[] args)
{
NumberOfConcurrentTasks = Environment.ProcessorCount * NumberOfTasksPerCpu;
var readySignals = new Queue<ManualResetEvent>();
var trigger = new ManualResetEvent(false);
var processingTasks = new List<Task>();
for (int index = 0; index < NumberOfConcurrentTasks; index++)
{
var readySignal = new ManualResetEvent(false);
readySignals.Enqueue(readySignal);
var task = CreateDataWithSqlCommand(trigger, readySignal);
processingTasks.Add(task);
}
Console.WriteLine("Waiting for tasks to become ready");
while (readySignals.Count > 0)
{
var readySignalBatch = new List<WaitHandle>();
for(int readySignalCount = 0; readySignals.Count > 0 && readySignalCount < 64; readySignalCount++)
{
readySignalBatch.Add(readySignals.Dequeue());
}
WaitHandle.WaitAll(readySignalBatch.ToArray());
}
Console.WriteLine("Saving data");
var sw = Stopwatch.StartNew();
trigger.Set();
await Task.WhenAll(processingTasks.ToArray());
sw.Stop();
Console.WriteLine("Finished - " + sw.ElapsedMilliseconds);
}
private static int TaskNumber = 0;
private static async Task CreateDataWithSqlCommand(ManualResetEvent trigger, ManualResetEvent readySignal)
{
await Task.Yield();
using var connection = new SqlConnection(MSSqlConnectionString);
await connection.OpenAsync().ConfigureAwait(false);
var transaction = (SqlTransaction)await connection.BeginTransactionAsync(System.Data.IsolationLevel.ReadCommitted).ConfigureAwait(false);
Console.WriteLine("Task " + Interlocked.Increment(ref TaskNumber) + $" of {NumberOfConcurrentTasks} ready ");
readySignal.Set();
trigger.WaitOne();
Guid parentId = Guid.NewGuid();
string fileCommandSql = "insert into IncomingFile (Id) values (#Id)";
using var fileCommand = new SqlCommand(fileCommandSql, connection, transaction);
fileCommand.Parameters.Add("#Id", System.Data.SqlDbType.UniqueIdentifier).Value = parentId;
await fileCommand.ExecuteNonQueryAsync().ConfigureAwait(false);
using var fileEventCommand = new SqlCommand
{
Connection = connection,
Transaction = transaction
};
var commandTextBulder = new StringBuilder("INSERT INTO [IncomingFileEvent] ([Id], [IncomingFileId]) VALUES ");
for (var i = 1; i <= NumberOfChildRows * 2; i += 2)
{
commandTextBulder.Append($"(#p{i}, #p{i + 1})");
if (i < NumberOfChildRows * 2 - 1)
commandTextBulder.Append(',');
fileEventCommand.Parameters.AddWithValue($"#p{i}", Guid.NewGuid());
fileEventCommand.Parameters.AddWithValue($"#p{i + 1}", parentId);
}
fileEventCommand.CommandText = commandTextBulder.ToString();
await fileEventCommand.ExecuteNonQueryAsync().ConfigureAwait(false);
await transaction.CommitAsync().ConfigureAwait(false);
}
}
}
UPDATE
Also tried making the primary key NONCLUSTERED and adding a CLUSTERED index based on the current date and time.
CREATE TABLE [dbo].[IncomingFile]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[ConcurrencyVersion] RowVersion NOT NULL,
[CreatedUtc] DateTime2 DEFAULT GETDATE(),
CONSTRAINT [PK_IncomingFile] PRIMARY KEY NONCLUSTERED([Id])
)
GO
CREATE CLUSTERED INDEX [IX_IncomingFile_CreatedUtc] on [dbo].[IncomingFile]([CreatedUtc])
GO
CREATE TABLE [dbo].[IncomingFileEvent]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[ConcurrencyVersion] RowVersion NOT NULL,
[IncomingFileId] UNIQUEIDENTIFIER NOT NULL,
[CreatedUtc] DateTime2 DEFAULT GETDATE(),
CONSTRAINT [PK_IncomingFileEvent] PRIMARY KEY NONCLUSTERED([Id]),
CONSTRAINT [FK_IncomingFileEvent_IncomingFileId]
FOREIGN KEY ([IncomingFileId])
REFERENCES [dbo].[IncomingFile] ([Id])
)
GO
CREATE CLUSTERED INDEX [IX_IncomingFileEvent_CreatedUtc] on [dbo].[IncomingFileEvent]([CreatedUtc])
GO
UPDATE 2
I tried a sequential guid taken from here, which made no difference.
UPDATE 3
It seems to be related to the foreign key. If I remove that then I don't get any deadlocks at all.
UPDATE 4
A reply from Sql Server Product Group with some suggestions has been posted on my original github issue.
https://github.com/dotnet/efcore/issues/21899#issuecomment-683404734
The deadlock is due to the execution plan needed to check referential integrity. A full table scan of the IncomingFile table is performed when inserting a large number (1K) rows into the related IncomingFileEvent table. The scan acquires a shared table lock that's held for the duration of the transaction and leads to the deadlock when different sessions each hold an exclusive row lock on the just inserted IncomingFile row and are blocked by another sessions exclusive row lock.
Below is the execution plan that shows this:
One way to avoid the deadlock is with an OPTION (LOOP JOIN) query hint on the IncomingFileEvent insert query:
var commandTextBulder = new StringBuilder("INSERT INTO [IncomingFileEvent] ([Id], [IncomingFileId]) VALUES ");
for (var i = 1; i <= NumberOfChildRows * 2; i += 2)
{
commandTextBulder.Append($"(#p{i}, #p{i + 1})");
if (i < NumberOfChildRows * 2 - 1)
commandTextBulder.Append(',');
fileEventCommand.Parameters.AddWithValue($"#p{i}", Guid.NewGuid());
fileEventCommand.Parameters.AddWithValue($"#p{i + 1}", parentId);
}
commandTextBulder.Append(" OPTION (LOOP JOIN);");
This is the plan with the hint:
On a side note, consider the changing the existing primary key to the one below. This is more correct from a data modeling perspective (identifying relationship) and will improve performance of both insert and selects since related rows are physically clustered together.
CONSTRAINT [PK_IncomingFileEvent] PRIMARY KEY CLUSTERED(IncomingFileId, Id)
I wrote the following extension to solve the problem for EF Core.
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
base.OnConfiguring(options);
options.UseLoopJoinQueries();
}
Using this code...
public static class UseLoopJoinQueriesExtension
{
public static DbContextOptionsBuilder UseLoopJoinQueries(this DbContextOptionsBuilder builder)
{
if (builder is null)
throw new ArgumentNullException(nameof(builder));
builder.AddInterceptors(new OptionLoopJoinCommandInterceptor());
return builder;
}
}
internal class OptionLoopJoinCommandInterceptor : DbCommandInterceptor
{
public override Task<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default)
{
AppendOptionToSql(command);
return Task.FromResult(result);
}
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
AppendOptionToSql(command);
return result;
}
private static void AppendOptionToSql(DbCommand command)
{
const string OPTION_TEXT = " OPTION (LOOP JOIN)";
string[] commands = command.CommandText.Split(";");
for (int index = 0; index < commands.Length; index++)
{
string sql = commands[index].Trim();
if (sql.StartsWith("insert into ", StringComparison.InvariantCultureIgnoreCase)
|| sql.StartsWith("select ", StringComparison.InvariantCultureIgnoreCase)
|| sql.StartsWith("delete ", StringComparison.InvariantCultureIgnoreCase)
|| sql.StartsWith("merge ", StringComparison.InvariantCultureIgnoreCase))
{
commands[index] += OPTION_TEXT;
}
}
#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities
command.CommandText = string.Join(";\r\n", commands);
#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities
}
}
I'm fairly new to using Nunit as a test framework and have come across something I don't really understand.
I am writing an integration test which inserts a row into a database table. As I want to repeatedly run this test I wanted to delete the row out once the test has completed. The test runs fine, however when I look in the database table the row in question is still there even though the delete command ran. I can even see it run when profiling the database whilst running the test.
Does Nunit somehow rollback database transaction? If anyone has any ideas why I am seeing this happen please let me know. I have been unable to find any information about Nunit rolling back transactions which makes me think I'm doing something wrong, but the test runs the row appears in the database it is just not being deleted afterwards even though profiler shows the command being run!
Here is the test code I am running
[Test]
[Category("Consultant split integration test")]
public void Should_Save_Splits()
{
var splitList = new List<Split>();
splitList.Add(new Split()
{
UnitGroupId = 69,
ConsultantUserId = 1,
CreatedByUserId = 1,
CreatedOn = DateTime.Now,
UpdatedByUserId = 1,
UpdatedOn = DateTime.Now,
Name = "Consultant1",
Unit = "Unit1",
Percentage = 100,
PlacementId = 47
});
var connection = Helpers.GetCpeDevDatabaseConnection();
var repository = new Placements.ConsultantSplit.DAL.PronetRepository();
var notebookManager = Helpers.GetNotebookManager(connection);
var userManager = Helpers.GetUserManager(connection);
var placementManager = Helpers.GetPlacementManager(connection);
var sut = new Placements.ConsultantSplit.ConsultantSplitManager(repository, connection, notebookManager, userManager, placementManager);
IEnumerable<string> errors;
sut.SaveSplits(splitList, out errors);
try
{
using (connection.Connection)
{
using (
var cmd = new SqlCommand("Delete from ConsultantSplit where placementid=47",
connection.Connection))
{
connection.Open();
cmd.CommandType = CommandType.Text;
connection.UseTransaction = false;
cmd.Transaction = connection.Transaction;
cmd.ExecuteNonQuery();
connection.Close();
}
}
}
catch (Exception exp)
{
throw new Exception(exp.Message);
}
Is there any improvements or magical way when a SqlCommand is executed from a .net console application ?
I see huge improvements and consistent times when running the command from .net based application but its completely different when executed in Sql server management studio.
.NET Code
static void Main(string[] args)
{
try
{
string strConn = "Data Source=.;Initial Catalog=school;Integrated Security=True";
using (SqlConnection conn = new SqlConnection(strConn))
{
conn.Open();
SqlCommand command = new SqlCommand();
command.CommandType = System.Data.CommandType.Text;
command.CommandText = "SET NOCOUNT OFF;SET STATISTICS IO,TIME ON; Exec spStudents #type='SELECTALL';SET STATISTICS IO,TIME OFF;";
conn.InfoMessage += conn_InfoMessage;
command.Connection = conn;
using (var reader = command.ExecuteReader())
{
do
{
while (reader.Read())
{
//grab the first column to force the row down the pipe
var x = reader[0];
}
} while (reader.NextResult());
}
Console.Read();
}
}
catch (Exception ex)
{
throw;
}
}
static void conn_InfoMessage(object sender, SqlInfoMessageEventArgs e)
{
foreach (SqlError item in e.Errors)
{
Console.WriteLine(item.Message);
}
}
I see sql reports consistent times of CPU time = 130 ~ 150 ms and elapsed time = 250ms ~ 300 ms. But when running same in SSMS it reports me CPU time = 900ms, elapsed time = 1s. So which one is reliable result ?
another related question that i asked yesterday SQL query performance statistics messages returned multiple times
I have a simple application that uses a single connection, the flow is something like this:
SELECT QUERY
CONNECTION CLOSE
for(..) //thousands of iterations
{
SIMPLE SELECT QUERY
}
BUSINESS LOGIC
CONNECTION CLOSE
for(..) //thousands of iterations
{
SIMPLE SELECT QUERY
}
BUSINESS LOGIC
CONNECTION CLOSE
When i use the embedded connection mode, the application ends in about 20 seconds, but when i switch to the server mode, the performances deteriorate:
localhost: 110 seconds
remote server (LAN): more than 30 minutes
Each query retrieves a small amount of data.
Is there an explanation for such a poor performance? How can i speed up the application without rewriting the code?
Any help is appreciated
compare your problem like accessing a cup of coffee.
If you access your coffe on your table/workdesk it is comparable like a in memory access to your database (H2 embedded)
It takes about seconds (coffee) and microseconds (h2 embedded)
if you need to go to the kitchen to access a cup of coffe it will take you the travel time from your chair to the kitchen + back (response).
The kitchen is comparable like TCP accessing or file accessing your local database.
It takes you minutes (coffee) and one digit milliseconds (h2 tcp localhost or local file)
if you need to go to a coffeshop outside to access a cup of coffee it will take you at least 15 mins to get a cup of coffee (and ages ( at least 2 digits milliseconds) in h2 tcp on remote machine)
now the question, how many times do you want to travel to a coffeshop?
If I give you a iteration (for loop) over 1000 times to the coffeshop, would you just asking me after the second or the third time if I am kidding? Why do you bother a IO dependent system on long travel times over the network?
So in your case, if you reduce your second for-loop into one SQL query, you will get a great performance locally and of course especially on the remote way.
I hope I could explain you the situation with the coffee, since it explains the problem better.
So to answer the last question, you have to rewrite your "thousands of iterations" for-loops.
Update 1
Forgot, if your for-loops are writing loops (update/insert) you can use batch queries. How to use them depends on your language you are using. Batches are to provide the database a bunch (e.g. multiple hundreds) of insert/updates before the operation takes in place on the database.
I did following test with your provided H2 version.
create a test database with 500.000 records, database size 990 MB
java -cp h2-1.3.168.jar;. PerfH2 create
select random 8.000 records on the indexed column
in embedded mode -> 1.7 sec
java -cp h2-1.3.168.jar;. PerfH2 embedded
in server (localhost) mode -> 2.6 sec
java -cp h2-1.3.168.jar;. PerfH2 server
If you face the problem already using server mode as jdbc:h2:tcp://localhost/your_database, then something with your environment or the way you access the server mode seems to be wrong. Try with a stripped done application and check if the problem still exists. If you have can reproduce the problem also with a stripped down version please post the code.
Find the code used for the test below.
public class PerfH2 {
public static void main(String[] args) throws SQLException {
if (null == args[0]) {
showUsage();
return;
}
long start = System.currentTimeMillis();
switch (args[0]) {
case "create":
createDatabase();
break;
case "embedded":
try (Connection conn = getEmbeddedConnection()) {
execSelects(conn);
}
break;
case "server":
try (Connection conn = getServerConnection()) {
execSelects(conn);
}
break;
default:
showUsage();
}
System.out.printf("duration: %d%n", System.currentTimeMillis() - start);
}
private static Connection getServerConnection() throws SQLException {
return DriverManager.getConnection("jdbc:h2:tcp://localhost/perf_test", "sa", "");
}
private static Connection getEmbeddedConnection() throws SQLException {
return DriverManager.getConnection("jdbc:h2:d:/temp/perf_test", "sa", "");
}
private static void execSelects(final Connection conn) throws SQLException {
Random rand = new Random(1);
String selectSql = "SELECT * FROM TEST_TABLE WHERE ID = ?";
PreparedStatement selectStatement = conn.prepareStatement(selectSql);
int count = 0;
for (int i = 0; i < 8000; i++) {
selectStatement.setInt(1, rand.nextInt(500_000));
ResultSet rs = selectStatement.executeQuery();
while (rs.next()) {
count++;
}
}
System.out.printf("retrieved rows: %d%n", count);
}
private static void createDatabase() throws SQLException {
try (Connection conn = DriverManager.getConnection("jdbc:h2:d:/temp/perf_test", "sa", "")) {
String createTable = "CREATE TABLE TEST_TABLE(ID INT, NAME VARCHAR(1024))";
conn.createStatement().executeUpdate(createTable);
String insertSql = "INSERT INTO TEST_TABLE VALUES(?, ?)";
PreparedStatement insertStmnt = conn.prepareStatement(insertSql);
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1024 / 10; i++) {
sb.append("[cafebabe]");
}
String value = sb.toString();
int count = 0;
for (int i = 0; i < 50; i++) {
insertStmnt.setInt(1, i);
insertStmnt.setString(2, value);
count += insertStmnt.executeUpdate();
}
System.out.printf("inserted rows: %d%n", count);
conn.commit();
String createIndex = "CREATE INDEX TEST_INDEX ON TEST_TABLE(ID)";
conn.createStatement().executeUpdate(createIndex);
}
}
private static void showUsage() {
System.out.println("usage: PerfH2 [create|embedded|server]");
}
}
I have mvc application. In action I have Dictionary<string,int>. The Key is ID and Value is sortOrderNumber. I want to create stored procedure that will be get key(id) find this record in database and save orderNumber column by value from Dictionary. I want to call stored procedure once time and pass data to it, instead of calling many times for updating data.
Have you any ideas?
Thanks!
The accepted answer of using a TVP is generally correct, but needs some clarification based on the amount of data being passed in. Using a DataTable is fine (not to mention quick and easy) for smaller sets of data, but for larger sets it does not scale given that it duplicates the dataset by placing it in the DataTable simply for the means of passing it to SQL Server. So, for larger sets of data there is an option to stream the contents of any custom collection. The only real requirement is that you need to define the structure in terms of SqlDb types and iterate through the collection, both of which are fairly trivial steps.
A simplistic overview of the minimal structure is shown below, which is an adaptation of the answer I posted on How can I insert 10 million records in the shortest time possible?, which deals with importing data from a file and is hence slightly different as the data is not currently in memory. As you can see from the code below, this setup is not overly complicated yet highly flexible as well as efficient and scalable.
SQL object # 1: Define the structure
-- First: You need a User-Defined Table Type
CREATE TYPE dbo.IDsAndOrderNumbers AS TABLE
(
ID NVARCHAR(4000) NOT NULL,
SortOrderNumber INT NOT NULL
);
GO
SQL object # 2: Use the structure
-- Second: Use the UDTT as an input param to an import proc.
-- Hence "Tabled-Valued Parameter" (TVP)
CREATE PROCEDURE dbo.ImportData (
#ImportTable dbo.IDsAndOrderNumbers READONLY
)
AS
SET NOCOUNT ON;
-- maybe clear out the table first?
TRUNCATE TABLE SchemaName.TableName;
INSERT INTO SchemaName.TableName (ID, SortOrderNumber)
SELECT tmp.ID,
tmp.SortOrderNumber
FROM #ImportTable tmp;
-- OR --
some other T-SQL
-- optional return data
SELECT #NumUpdates AS [RowsUpdated],
#NumInserts AS [RowsInserted];
GO
C# code, Part 1: Define the iterator/sender
using System.Collections;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using Microsoft.SqlServer.Server;
private static IEnumerable<SqlDataRecord> SendRows(Dictionary<string,int> RowData)
{
SqlMetaData[] _TvpSchema = new SqlMetaData[] {
new SqlMetaData("ID", SqlDbType.NVarChar, 4000),
new SqlMetaData("SortOrderNumber", SqlDbType.Int)
};
SqlDataRecord _DataRecord = new SqlDataRecord(_TvpSchema);
StreamReader _FileReader = null;
// read a row, send a row
foreach (KeyValuePair<string,int> _CurrentRow in RowData)
{
// You shouldn't need to call "_DataRecord = new SqlDataRecord" as
// SQL Server already received the row when "yield return" was called.
// Unlike BCP and BULK INSERT, you have the option here to create an
// object, do manipulation(s) / validation(s) on the object, then pass
// the object to the DB or discard via "continue" if invalid.
_DataRecord.SetString(0, _CurrentRow.ID);
_DataRecord.SetInt32(1, _CurrentRow.sortOrderNumber);
yield return _DataRecord;
}
}
C# code, Part 2: Use the iterator/sender
public static void LoadData(Dictionary<string,int> MyCollection)
{
SqlConnection _Connection = new SqlConnection("{connection string}");
SqlCommand _Command = new SqlCommand("ImportData", _Connection);
SqlDataReader _Reader = null; // only needed if getting data back from proc call
SqlParameter _TVParam = new SqlParameter();
_TVParam.ParameterName = "#ImportTable";
// _TVParam.TypeName = "IDsAndOrderNumbers"; //optional for CommandType.StoredProcedure
_TVParam.SqlDbType = SqlDbType.Structured;
_TVParam.Value = SendRows(MyCollection); // method return value is streamed data
_Command.Parameters.Add(_TVParam);
_Command.CommandType = CommandType.StoredProcedure;
try
{
_Connection.Open();
// Either send the data and move on with life:
_Command.ExecuteNonQuery();
// OR, to get data back from a SELECT or OUTPUT clause:
SqlDataReader _Reader = _Command.ExecuteReader();
{
Do something with _Reader: If using INSERT or MERGE in the Stored Proc, use an
OUTPUT clause to return INSERTED.[RowNum], INSERTED.[ID] (where [RowNum] is an
IDENTITY), then fill a new Dictionary<string, int>(ID, RowNumber) from
_Reader.GetString(0) and _Reader.GetInt32(1). Return that instead of void.
}
}
finally
{
_Reader.Dispose(); // optional; needed if getting data back from proc call
_Command.Dispose();
_Connection.Dispose();
}
}
Using Table Valued parameters is really not that complex.
given this SQL:
CREATE TYPE MyTableType as TABLE (ID nvarchar(25),OrderNumber int)
CREATE PROCEDURE MyTableProc (#myTable MyTableType READONLY)
AS
BEGIN
SELECT * from #myTable
END
this will show how relatively easy it is, it just selects out the values you sent in for demo purposes. I am sure you can easily abstract this away in your case.
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
namespace TVPSample
{
class Program
{
static void Main(string[] args)
{
//setup some data
var dict = new Dictionary<string, int>();
for (int x = 0; x < 10; x++)
{
dict.Add(x.ToString(),x+100);
}
//convert to DataTable
var dt = ConvertToDataTable(dict);
using (SqlConnection conn = new SqlConnection("[Your Connection String here]"))
{
conn.Open();
using (SqlCommand comm = new SqlCommand("MyTableProc",conn))
{
comm.CommandType=CommandType.StoredProcedure;
var param = comm.Parameters.AddWithValue("myTable", dt);
//this is the most important part:
param.SqlDbType = SqlDbType.Structured;
var reader = comm.ExecuteReader(); //or NonQuery, etc.
while (reader.Read())
{
Console.WriteLine("{0} {1}", reader["ID"], reader["OrderNumber"]);
}
}
}
}
//I am sure there is a more elegant way of doing this.
private static DataTable ConvertToDataTable(Dictionary<string, int> dict)
{
var dt = new DataTable();
dt.Columns.Add("ID",typeof(string));
dt.Columns.Add("OrderNumber", typeof(Int32));
foreach (var pair in dict)
{
var row = dt.NewRow();
row["ID"] = pair.Key;
row["OrderNumber"] = pair.Value;
dt.Rows.Add(row);
}
return dt;
}
}
}
Produces
0 100
1 101
2 102
3 103
4 104
5 105
6 106
7 107
8 108
9 109
Stored procedures do not support arrays as inputs. Googling gives a couple of hacks using XML or comma separated strings, but those are hacks.
A more SQLish way to do this is to create a temporary table (named e.g. #Orders) and insert all the data into that one. Then you can call the sp, using the same open Sql Connection and insie the SP use the #Orders table to read the values.
Another solution is to use Table-Valued Parameters but that requires some more SQL to setup so I think it is probably easier to use the temp table approach.