Showing posts with label SQL Server. Show all posts
Showing posts with label SQL Server. Show all posts

13 May 2011

Bulk upsert to SQL Server from .NET

or, “How inserting multiple records using an ORM should probably work”

Anyone familiar with .NET ORMs should know that one area they’re lacking in is where it comes to updating or inserting multiple objects at the same time. You end up with many individual UPDATE and INSERT statements being executed on the database which can be very inefficient and often results in developers having to extend the ORM or break out of it completely in order to perform particular operations. An added complication is that, where identities are being used in tables, each INSERT command the ORM performs must immediately be followed by a SELECT SCOPE_IDENTITY() call to retrieve the identity value for the newly inserted row so that the CLR object may be amended.

It’s possible to drastically improve on this by making use of a couple of features already supported in the .NET Framework and SQL Server and I’m hoping that a similar solution will feature in future releases of the major ORMs.

  • The .NET Framework’s SqlBulkCopy class allowing you to take advantage of BULK operations supported by SQL Server.
  • SQL Server temporary tables.
  • SQL Server 2008’s MERGE command which allows upsert operations to be performed on a table and in particular its ability, using the OUTPUT command, to return identities for inserted rows.

The process

The main steps of the process are as follows:

  1. Using ADO.NET create a temporary table in SQL Server whose schema mirrors your source data and whose column types match the types in the target table.
  2. Using SqlBulkCopy populate the temporary table with the source data.
  3. Execute a MERGE command via ADO.NET on the SQL Server which upserts data from the temporary table into the target table, outputting identities.
  4. Read the row set of inserted identities.
  5. Drop the temporary table.

So instead of n INSERT statements to insert n records that’s four SQL commands in all to insert or update n records.

There’s already a blog post on this technique that goes into more detail by Kelias which you can read here. The only part missing from Kelias’ post is the piece utilising the OUTPUT modifier to retrieve the inserted identities from the MERGE command. This is simply an additional line in the merge command e.g.

OUTPUT $action, INSERTED.$IDENTITY

and the small matter of reading those returned identities out of a SqlDataReader.

This is the crucial piece, however, as it is this which allows us to tie the inserted row back to the original CLR “entity” item that formed part of our source data. Updating our CLR object with this identity will allow us to save subsequent changes away as an UPDATE to the now existing database row.

Performance

I did some brief testing to get rough timings of this technique versus individual INSERT calls using a parameterised ADO.NET command. With a variety of numbers and sizes of rows from 100 to 10,000 and with row sizes from 1k to 10k roughly the upsert technique nearly always executed in less than half the time of the individual INSERT statements. For example, 1,000 rows of about 1k each took individual INSERTs an average of just over 500ms versus bulk upsert’s 150ms on my quite old desktop with not very much RAM.

That’s pretty cool considering the upsert could be performing either an INSERT or an UPDATE command in the same number of calls whereas if I were to factor that into the individual SQL statements method it would be a lot of extra commands to try an UPDATE and then check whether any rows had been affected etc.

Github project

I decided to have a go at wrapping the upsert technique up in a library which would automatically generate the SQL necessary for creating the temporary table and running the MERGE. I pushed an initial version of this SqlBulkUpsert project to github which can be found here:
https://github.com/dezfowler/SqlBulkUpsert

Usage would be something like this:

using (var connection = DatabaseHelper.CreateAndOpenConnection())
{
 var targetSchema = SqlTableSchema.LoadFromDatabase(connection, "TestUpsert", "ident");

 var columnMappings = new Dictionary<string, Func<TestDto, object>>
       {
        {"ident", d => d.Ident},
        {"key_part_1", d => d.KeyPart1},
        {"key_part_2", d => d.KeyPart2},
        {"nullable_text", d => d.Text},
        {"nullable_number", d => d.Number},
        {"nullable_datetimeoffset", d => d.Date},
       };

 Action<TestDto, int> identUpdater = (d, i) => d.Ident = i;

 var upserter = new TypedUpserter<TestDto>(targetSchema, columnMappings, identUpdater);

 var items = new List<TestDto>();

 // Populate items with TestDto instances
 
 upserter.Upsert(connection, items);

 // Ident property of TestDto instances updated
}

with TestDto just being a simple class like this:

public class TestDto
{
 public int? Ident { get; set; }
 public string KeyPart1 { get; set; }
 public short KeyPart2 { get; set; }
 public string Text { get; set; }
 public int Number { get; set; }
 public DateTimeOffset Date { get; set; }
}

In this TypedUpserter example we:

  1. define the schema of the target table either in code or by loading it from the database (shown in the example)
  2. define mappings from column names of the target to a lambda retrieving the appropriate property value from the TestDto class
  3. define an action to be called to allow setting the the new identity to a property of the DTO
  4. instantiate the Upserter and call Upsert() with a list of items and a database connection
  5. the identity properties of the TestDto instances will have been updated using the defined action so the CLR objects will now be consistent with the database rows.

Next step

The object model could probably do with some refinement and it needs lots more tests adding but it’s in pretty good shape so next I’m going to look at integrating it into Mark Rendle’s Simple.Data project which should mean that, to my knowledge, it’s the only .NET ORM doing proper bulk loading of multiple records.

14 July 2010

UTC gotchas in .NET and SQL Server

After doing some work with DateTime recently I stumbled across the interesting behaviour that a DateTime which is DateTimeKind.Unspecified will be treated as a DateTimeKind.Local whenever you try to perform some operation upon it. You get an “unspecified” DateTime whenever you don’t explicitly say it is Utc or Local. This makes sense because, when you do the following, in most cases what you intended was to use local time:

DateTime d1 = new DateTime(2010, 07, 01, 12, 0 ,0, 0);

If the current timezone is UTC +01:00 here's what I get when working with the DateTime created above:

d1.Kind; // => Unspecified
d1; // => 01/07/2010 12:00:00
d1.ToUniversalTime(); // => 01/07/2010 11:00:00
TimeZoneInfo.Local.GetUtcOffset(d1); // => 01:00:00

Note it’s applied an offset when calculating the UTC value which as we can see for clarification is +1 hour.

If what we actually wanted was a UTC time we need to explicitly specify the kind e.g.

DateTime d2 = new DateTime(2010, 07, 01, 12, 0 ,0, 0, DateTimeKind.Utc);
DateTime d2 = DateTime.UtcNow;

If you need to work with timezones other than UTC or the system timezone then you'll want to use DateTimeOffset rather than DateTime.

SQL Server and SqlDataReader

Another interesting gotcha arising from this is that the SQL Server datetime data type is also timezone agnostic. Any datetime values retrieved through the SqlDataReader will be an “unspecified” kind DateTime. This means that, even if you're correctly using the C# DateTime.UtcNow or the SQL GETUTCDATE() to produce the values in the database, when you try to retrieve them they will be shifted incorrectly according to the local timezone. Yikes!

There are two ways to deal with this.

DateTime.SpecifyKind()

The first is in C# using DateTime.SpecifyKind():

DateTime d3 = DateTime.SpecifyKind(d1, DateTimeKind.Utc);
d3.Kind; // => Utc
d3; // => 01/07/2010 12:00:00
d3.ToUniversalTime(); // => 01/07/2010 12:00:00

Which could be wrapped up in an extension method for ease of use e.g.

public static class SqlDataReaderExtensions
{
 public static DateTime GetDateTimeUtc(this SqlDataReader reader, string name)
 {
  int fieldOrdinal = reader.GetOrdinal(name);
  DateTime unspecified = reader.GetDateTime(fieldOrdinal);
  return DateTime.SpecifyKind(unspecified, DateTimeKind.Utc);
 }
}

SQL Server 2008 datetimeoffset

If you're using SQL Server 2008 you have the option of using the datetimeoffset data type instead. This will store the +00:00 timezone internally and the SqlDataReader will then retrieve the value correctly as a DateTimeOffset. No need to muck about with Kind.

If you have an existing database using datetime you can CAST these as a datetimeoffset in your query which usefully uses an offset of +00:00 in this case. (It treats "unspecified" as UTC – tut!)

16 February 2010

SQL Server Error Severity and .NET

I actually wrote this almost a year ago but forgot to post it.

The other day I noticed some oddness in the Messages window of SQL Server Management Studio when using RAISERROR with different error severities. Running the following:

PRINT 'Start.'
RAISERROR (N'Error.', 16, 1)
PRINT 'End.'

produces...

Start.
Msg 50000, Level 16, State 1, Line 2
Error.
End.

However if you increase the severity to 17 or 18 you get this...

Start.
End.
Msg 50000, Level 18, State 1, Line 2
Error.

Odd as the error message has now moved from between the "Start" and "End" to after the "End". Wondering what the significance of the change from severity 16 to 17 was and why Management Studio should treat them differently I headed over to SQL Server Books Online which says:

  • 0-10 are informational messages
  • 11-16 are errors that can be corrected by the user
  • 17-19 are application errors that the user can't correct
  • 20 and above are fatal errors

So there is a difference but that still doesn't explain Management Studio's behaviour. Management Studio uses the .NET SqlClient for running queries so a brief look at the docs shows the SqlConnection class has a FireInfoMessageEventOnUserErrors property which, when set to true, reports any errors of severity less than 17 to the InfoMessage handler rather than throwing a SqlException.

I've put together a quick Snippet Compiler script to test this out which you can download here. The script has a connection string at the top which is looking for a local SQL Express instance with integrated security by default so you may need to amend this.

Results

Queries 3 and 4

These two queries show the differences observed in Management Studio quite nicely with the InfoMessage handler being fired for the severity 16 error but a SqlException being thrown for the severity 18.

-------------------------------------
Executing Query 3:


PRINT 'Start.'
RAISERROR (N'Error.', 16, 1)
PRINT 'End.'


Messages:

Info message fired: Start.
Info message fired: Error.
Info message fired: End.


Result:
Success.
-------------------------------------
Executing Query 4:


PRINT 'Start.'
RAISERROR (N'Error.', 18, 1)
PRINT 'End.'


Messages:

Info message fired: Start.
Info message fired: End.


Result:
SqlException.
Error severity: 18
Message: Error.
-------------------------------------

Query 5

Query 5 causing a severity 20 error (a fatal error) displays some slightly different behaviour as it both fires the InfoMessage handler and throws a SqlException.

-------------------------------------
Executing Query 5:


PRINT 'Start.'
RAISERROR (N'Error.', 20, 1) WITH LOG
PRINT 'End.'


Messages:

Info message fired: Start.
Info message fired: Process ID 51 has raised user error 50000, severity 20. SQL
Server is terminating this process.


Result:
SqlException.
Error severity: 20
Message: Error.
A severe error occurred on the current command.  The results, if any, should be
discarded.
-------------------------------------

Queries 6, 7 and 8

These show how the RAISERROR interacts with a SQL TRY...CATCH block. The RAISERROR within the TRY block only fires the InfoMessage handler in the severity 20 case with the RAISERROR in the CATCH block only firing the handler in the severity 16 case.

Conclusion

It seems like you could build in some quite nice fine grain logging into your SQL statements, stored procs etc by using hooking up a SqlConnection InfoMessage handler and setting FireInfoMessageEventOnUserErrors to true. What's more is that you could write this information out to your application's log file along with the Debug or Trace calls from your code. Considering in some cases you may have quite a lot of logic in a stored procedure especially it may turn out to be really helpful having all your debugging information in one place.

07 January 2009

ISNULL vs COALESCE

Consider this SQL Server statement:

SELECT ISNULL(2, 2.0), COALESCE(2, 2.0)

You might assume, as I did, that they're equivalent and will produce the same result, however this is not the case.

They differ in the way they determine what data type to return. ISNULL always returns the data type of the first argument and the second argument must implicitly cast to that type. COALESCE on the other hand returns the data type of the argument with highest data type precedence.

As a result what you get out of the example above is:

22.0

The reason being that the constant "2" is treated as an integer while "2.0" is treated as a decimal. Therefore ISNULL will return an integer because the first argument is an integer but COALESCE returns a decimal because decimal has higher precedence.

One possible issue I can envisage here is if you were to inadvertently COALESCE a float column in with your decimals and then try to do some arithmetic. You'd end up doing dodgy float arithmetic where you thought you were doing safe decimal arithmetic.

05 February 2007

XML and SQL Server

In this post i'll cover how you can get SQL Server to return data as XML by using the FOR XML command and how you can use XML as input for updating records and as a rich argument for row returning stored procedures using the OPENXML command.

There are lots of reasons you may want to get data out of a database as XML:

  • You may be building an AJAX app and want to send XML to the client directly for processing by your client-side JavaScript
  • You may want to use XSL to transform your data into some format such as HTML or a CSV
  • You may want to export data and store it in a form which retains its original structure

These reasons also give you reasons for needing to pass XML into your database for example with the AJAX app you may want to receive changes as XML from the client and post them straight to a stored proc that updates your tables.

Examples are in VBScript using ASP and ADO.

Getting SQL Server to return XML

The key to getting SQL Server to return XML is the FOR XML command. It comes in three flavours:

FOR XML RAW
The least useful, RAW mode simply outputs the rows returned by your query as <row> nodes with the columns being either elements within this node or attributes of it as you define.
FOR XML AUTO
Automagically translates your SQL query, joins and all into suitable nested XML elements and attributes. For example if you are joining Orders to OrderItems the XML output will be OrderItem nodes nested within the associated Order node. You can alter the naming of the nodes by aliasing your table and column names but that's about it.
FOR XML EXPLICIT
Explicit mode allows the most customisability but it's also the most fiddly requiring you to alias all your columns names to a specific format which describes which nodes they should belong to.

You'll mostly use AUTO mode because it gives you the most useful results in the least amount of time so here it is in an example:

SELECT Order.*, OrderItem.*
FROM Order
INNER JOIN OrderItem
   ON Order.order_key = OrderItem.order_fkey
WHERE Order.customer_fkey = 1
FOR XML AUTO

All you do is tag FOR XML AUTO on to the end of your query, that's it! The output will look something like this:

<Order order_key="1" customer_fkey="48" date_placed="24/08/2006 12:31">
   <OrderItem orderitem_key="123" order_fkey="1" product_fkey="234" list_price="£14" />
   <OrderItem orderitem_key="124" order_fkey="1" product_fkey="64" list_price="£3" />
   <OrderItem orderitem_key="125" order_fkey="1" product_fkey="73" list_price="£27" />
</Order>

If you run this in Query Analyzer you'll notice in the results pane it looks like the XML has been split into rows. We need to use an ADODB.Stream object to get at the output properly, thus:

Set conn = Server.CreateObject("ADODB.Connection")
Set cmd = Server.CreateObject("ADODB.Command")
Set strm = Server.CreateObject("ADODB.Stream")

conn.Open "Provider=SQLOLEDB;Data Source=myServerAddress;" & _
   "Initial Catalog=myDataBase;User Id=myUsername;Password=myPassword;"

strm.Open

Set cmd.ActiveConnection = conn

cmd.Properties("Output Stream").Value = strm
cmd.Properties("Output Encoding") = "UTF-8"
cmd.Properties("XML Root") = "Root"  'this can be anything you want

cmd.CommandType = adCmdText
cmd.CommandText = strSQL

cmd.Execute , , adExecuteStream

Set xmlDoc = Server.CreateObject("Microsoft.XMLDOM")
xmlDoc.async = "false"

xmlDoc.LoadXML(strm.ReadText)

strm.Close : Set strm = Nothing
Set cmd = Nothing

xmlDoc now contains our XML to do with as we will.

Passing XML into SQL Server

The easiest way to get XML into SQL Server is as a parameter of a stored procedure thus:

cmd.Parameters.Append cmd.CreateParameter("somexml", adVarChar,
adParamInput, 8000, xmlDoc.xml)

You then use two System Stored Procedures along with the OPENXML command to SELECT from the contents of the XML parameter as if it were a table:

DECLARE @idoc int
EXEC sp_xml_preparedocument @idoc OUTPUT, @somexml

SELECT * FROM OPENXML (@idoc, '/Root/Order') WITH (Order)

EXEC sp_xml_removedocument @idoc

OPENXML takes the prepared XML document and an XPath expression telling it which nodes it is taking into account. The WITH statement in this case tells OPENXML that the nodes it is working on are of the same schema type as the rows in the Order table.

The result of this call is a list of records of the same schema as the Order table but which have actually come from the passed in XML document. Because the schema is that of Order you can put an INSERT INTO [Order] in front of the SELECT and this will add the rows from the XML to the Order table. You probably wouldn't want to do that but you get the idea.

You don't have to have a table representing the schema of the XML you're passing in in your database. WITH also accepts a normal schema declaration i.e. comma delimited column names with their types and which node this maps to in the XML:

SELECT order_key, customer_fkey, description
FROM OPENXML (@idoc, '/Root/Order') 
WITH (
   order_key     int           '@order_key',
   customer_fkey int           '@customer_fkey',
   description   nvarchar(100) 'description'
)

The advantage of being able to do this is that you can pass complex structured criteria into one of your stored procedures and use OPENXML to turn it into a rowset which you can use to JOIN to the tables in your database. Powerful stuff with a large number of applications in both improving querying data and updating it.

01 February 2007

SQL Server Oddness

I recently came across a rather odd bug in SQL Server 2000 which, although fixed in SP4, i had to find a work-around for as an immediate solution was required that couldn't wait for a scheduled server patching.

In my case it concerned a JOIN subselect which contained a JOIN using a user-defined scalar function as one of its predicates e.g.

...
LEFT JOIN (
   SELECT foo.col1, MIN(bar.col1)
   FROM foo
   INNER JOIN bar
      ON foo.col1 = bar.col2
      AND bar.col3 = dbo.fn_GetVal("derek")
   GROUP BY foo.col1 
)
...

which produced the rather unhelpful error:

Server: Msg 913, Level 16, State 8, Line 4
Could not find database ID 102. Database may not be activated yet or may
be in transition.

The work-around in this case was to move the predicate to the WHERE clause rather than the ON e.g.

...
LEFT JOIN (
   SELECT foo.col1, MIN(bar.col1)
   FROM foo
   INNER JOIN bar
      ON foo.col1 = bar.col2
   WHERE bar.col3 = dbo.fn_GetVal("derek")
   GROUP BY foo.col1 
)
...