navigation
 Sunday, March 22, 2009
Yesterday, I discovered Mars, and that made my life a whole lot easier! Find out how setting MultipleActiveResultSets in your connection string can solve thorny issues with multiple active SqlDataReaders using the same Connection...
posted on March 22, 2009  #    by John Waters  Comments [1]

Yesterday I was debugging some SQL Server transaction logic, and came across the following handy command to see which transactions are running:

SELECT * 
FROM sys.dm_tran_session_transactions
posted on March 22, 2009  #    by John Waters  Comments [0]
 Tuesday, January 06, 2009

Sometimes you want to clear out a database, but you can't just drop the entire database and create a new one. Recently, a colleague needed to do just that, but couldn't because the database was hosted elsewhere. Here is a simple script that works to drop all objects in any database I've tested it against so far. If your database contains object types that mine don't, you might need to add to this script using the same patterns established here.

declare @n char(1)
set @n = char(10)

declare @stmt nvarchar(max)

-- procedures
select @stmt = isnull( @stmt + @n, '' ) +
    'drop procedure [' + name + ']'
from sys.procedures

-- check constraints
select @stmt = isnull( @stmt + @n, '' ) +
    'alter table [' + object_name( parent_object_id ) + '] drop constraint [' + name + ']'
from sys.check_constraints

-- functions
select @stmt = isnull( @stmt + @n, '' ) +
    'drop function [' + name + ']'
from sys.objects
where type in ( 'FN', 'IF', 'TF' )

-- views
select @stmt = isnull( @stmt + @n, '' ) +
    'drop view [' + name + ']'
from sys.views

-- foreign keys
select @stmt = isnull( @stmt + @n, '' ) +
    'alter table [' + object_name( parent_object_id ) + '] drop constraint [' + name + ']'
from sys.foreign_keys

-- tables
select @stmt = isnull( @stmt + @n, '' ) +
    'drop table [' + name + ']'
from sys.tables

-- user defined types
select @stmt = isnull( @stmt + @n, '' ) +
    'drop type [' + name + ']'
from sys.types
where is_user_defined = 1

exec sp_executesql @stmt
posted on January 6, 2009  #    by Adam Anderson  Comments [3]
 Monday, November 10, 2008
Learn about a way to obtain SCOPE_IDENTITY() values from inside an INSTEAD OF INSERT trigger for use in subsequent SQL statements, without using an alternate key.
posted on November 10, 2008  #    by Adam Anderson  Comments [2]
 Monday, September 29, 2008
Imagine you have a table that contains the names of employees. You want to build a string that lists all of the names, separated by commas. You could do it the long, clunky way of declaring a cursor or implementing your own while loop, or you could do it this much simpler way.
posted on September 29, 2008  #    by Adam Anderson  Comments [2]
 Monday, September 22, 2008
How to invert a bit without using conditional logic
posted on September 22, 2008  #    by Adam Anderson  Comments [2]
 Wednesday, August 27, 2008
See some business rules can be enforced using only declarative referential integrity.
posted on August 27, 2008  #    by Adam Anderson  Comments [1]
 Tuesday, August 05, 2008
Writing code that builds SQL is a powerful technique, but it can also be risky. Failing to implement these techniques could leave your database open to attack by SQL injection. Read on to learn how to secure dynamic SQL from SQL injection vulnerability.
posted on August 5, 2008  #    by Adam Anderson  Comments [1]
 Thursday, July 10, 2008
Complete listing of a handy little function you can use to cleanse infinity from float data
posted on July 10, 2008  #    by Adam Anderson  Comments [2]
 Tuesday, July 08, 2008
Learn how to develop for MSSQL using built-in stored procedures (and a few custom ones) as a complete replacement for Visual Studio's Server Explorer.
posted on July 8, 2008  #    by Adam Anderson  Comments [1]
 Tuesday, June 17, 2008
What on Earth are SQL Server Hypothetical Indices, where do they come from, and how do you get rid of them?
posted on June 17, 2008  #    by John Waters  Comments [1]
 Monday, June 16, 2008

Lately I've needed to compare the schemas of two or more databases and highlight the differences. Based on internet research, I settled on two contenders: AdeptSQL Diff and Red Gate SQL Compare. Both are reasonably fast. AdeptSQL's UI looks very 16-bit and has limited reporting options. Red Gate's UI is very attractive and offers more reporting options. However, Red Gate's product currently has a bug that considers quoted and unquoted trigger identifiers to be different, even if the identifier itself is the same. This resulted in a large number of false differences, forcing me to go with AdeptSQL for now. If Red Gate can fix this problem, I would be inclined to switch to their product in the future.

posted on June 16, 2008  #    by Adam Anderson  Comments [0]
 Thursday, May 29, 2008
See how to rewrite T-SQL to reduce the use of cursors, which consume server resources and tend to be slower than set-based operations. Before and after example code included.
posted on May 29, 2008  #    by Adam Anderson  Comments [0]
 Friday, May 16, 2008
Learn how to speed up queries by breaking them down into smaller parts
posted on May 16, 2008  #    by Adam Anderson  Comments [3]
 Wednesday, May 07, 2008

This is actually right in the SQL Server Books Online, but not easily found. Given a temp table named #temp:

if ( object_id( 'tempdb..#temp' ) is null )
    -- table doesn't exist
else
    -- table exists
posted on May 7, 2008  #    by Adam Anderson  Comments [1]

Here is an interesting article on a way to turn many looping row-based operations into set-based ones: http://www.sqlservercentral.com/articles/TSQL/62867/

I think the idea is a clever one, and I love the "Row By Agonizing Row" (RBAR) acronym!

posted on May 7, 2008  #    by Adam Anderson  Comments [0]
 Wednesday, April 30, 2008
When writing queries that group and aggregate, it is not uncommon for there to be many columns in the SELECT clause that are neither in the GROUP BY clause nor being aggregated. Learn two different ways to write such queries without compromising performance.
posted on April 30, 2008  #    by Adam Anderson  Comments [1]
 Wednesday, April 23, 2008
SQL Server provides three different ways to retrieve identity values generated by the database. Here is a detailed description of how they differ.
posted on April 23, 2008  #    by Adam Anderson  Comments [1]
 Monday, April 21, 2008
You can't run MSI or CAB files "As Administrator" directly from the explorer in Vista, but there's a workaround.
posted on April 21, 2008  #    by Noel Rice  Comments [0]
 Monday, April 07, 2008
Learn how and why to convert a scalar user-defined function to a table-valued one
posted on April 7, 2008  #    by Adam Anderson  Comments [0]
 Thursday, April 03, 2008
Prior to 2005, if you needed to access the value of a newly inserted IDENTITY column, you were bound to row-based operations so you could access each value using the SCOPE_IDENTITY() function. But now, there is another way...
posted on April 3, 2008  #    by Adam Anderson  Comments [3]
 Monday, March 10, 2008
Today I learned how to compare text columns in different SQL server databases that use different collations.
posted on March 10, 2008  #    by John Waters  Comments [2]
 Wednesday, February 06, 2008
Turns out be be very straight forward. Read on to find out how..
posted on February 6, 2008  #    by John Waters  Comments [1]
 Friday, November 30, 2007
Read about how to alter a column in a replicated table without breaking replication.
posted on November 30, 2007  #    by John Waters  Comments [1]
 Wednesday, November 21, 2007
What is a sparse column and when do I need one? This blog will bring you up to speed.
posted on November 21, 2007  #    by John Waters  Comments [3]
 Tuesday, November 20, 2007

Falafel is partnering with Microsoft to offer this free half day seminar at the beautiful Saint Claire hotel in downtown San Jose, CA to celebrate the release of Visual Studio 2008, LINQ, WPF, WCF, WF and other exciting technologies.

ActiveFocus Hosting

Please join us on December 10th from 9:00 AM to 1:00 PM
Register on the Microsoft event site ASAP as space is limited.

Charlie Calvert, the C# Community Project Manager will be there to talk about LINQ and Lino Tadros will present the usefulness of the new technologies.
Hope to see you there!
 |  |  |  |  |  |  |  |  |  | 
posted on November 20, 2007  #    by Lino Tadros  Comments [0]
 Thursday, October 04, 2007
Inside, I'll discuss a common SQL query pattern and a simple change that can increase query performance by a factor of two or more.
posted on October 4, 2007  #    by Adam Anderson  Comments [2]
 Wednesday, September 26, 2007

Wow!  I was told so many times that Visual Studio 2008 will ship in 2007, well it does not sound like that is a possibility anymore.

Microsoft released a new date, February 27th 20008 to release Windows Server 2008, Visual Studio 2008 and SQL Server 2008

2008 Global Launch Wave
963
Days
Windows Server 2008 • Microsoft Visual Studio 2008 • Microsoft SQL Server 2008
posted on September 26, 2007  #    by Lino Tadros  Comments [0]
 Friday, September 21, 2007
I often find myself having to upload data in Excel files into temp tables in SQL Server in order to do ad hoc updates or comparisons. Users will commonly submit an Excel file containing data to verify, insert, update or what not. I know there is a way to have SQL Server read data directly form Excel, so finally I got round to trying that out to make these ad hoc tasks easier. Here is how you do it:
posted on September 21, 2007  #    by John Waters  Comments [2]
 Saturday, July 14, 2007

You may run into the 15023 error if you restore a MS SQL database from backup.  You expect a restored database to be in exactly the same state as the backup, but the login fails for a user that had permissions in the backed up database.  When you use the "User Mapping" SQL Management Studio functionality to allow the user permissions to the new database, you receive the 15023 error.  This is caused by Security identification numbers (SID) that are mismatched or 'orphaned' in the sysusers table. 

The SQL Server stored proc sp_change_users_login locates and fixes these records.  Run it with a single parameter 'Report' to get a listing of abandoned user names and corresponding SIDs:

exec sp_change_users_login Report

The 'Update_One' parameter will reconnect a single login:

exec sp_change_users_login Update_One, 'MyLogin', 'MyLogin'

You can find more info about this issue at:

http://support.microsoft.com/kb/246133

http://support.microsoft.com/kb/240872

This next blog expands on the available parameters for sp_change_users_login:

http://blog.sqlauthority.com/2007/02/15/sql-server-fix-error-15023-user-already-exists-in-current-database/

Also, try checking out the source for sp_change_users_login found in the Sql Server Management Studio under Databases | System Databases | Master | Programmability | Stored Procedures | sp_change_users_login.

posted on July 14, 2007  #    by Noel Rice  Comments [0]
 Sunday, July 08, 2007

SQL Management Objects (SMO) is something of a Swiss army knife that lets you traverse meta data, automate backup and restore, and otherwise manage SQL Server 2005 through .NET code.  For instance, to perform the classic database hierarchy walk use the SmoApplication object and enumerate the servers.  First you need to reference the Smo assemblies:

Microsoft.SqlServer.ConnectionInfo.dll
Microsoft.SqlServer.Smo.dll
Microsoft.SqlServer.SmoEnum.dll

Then call the SmoApplication EnumAvailableSqlServers() method, passing 'true' to list only local servers:

1
DataTable tblServers = SmoApplication.EnumAvailableSqlServers(true);

EnumAvailableSqlServers() may not work if you don't have a network connection, but you can use the RegisteredServers collection property instead. This will work without network connection and will pick up server instances for SQL 2000/2005, SQL Express and MSDE. Here the heirarchy follows the expected pattern of Server/Database/Table:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
foreach (RegisteredServer registeredServer in SmoApplication.SqlServerRegistrations.RegisteredServers)
{
Server server = new Server(registeredServer.Name);
Console.WriteLine("Server: {0} Version: {1}",
registeredServer.Name, server.PingSqlServerVersion(server.Name).Major);

foreach (Database database in server.Databases)
{
if ((!database.IsSystemObject) && (database.IsAccessible))
{
Console.WriteLine("Database: {0}", database.Name);

foreach (Table table in database.Tables)
{
Console.WriteLine("Table: {0} Rows: {1}", table.Name, table.RowCount);
}
}
}
}

There are a large number of collections and enumerating methods, for example: stored procedures, user defined types, roles, rules, schemas, locks and permissions.  SMO works against Sql Server earlier than 2005, but some methods may not be supported.  You can handle that by checking the sql version:

1
2
3
4
5
6
7
if (server.PingSqlServerVersion(server.Name).Major > 8)
{
foreach (UserDefinedDataType udf in database.UserDefinedDataTypes)
{
Console.WriteLine("Type: {0}", udf.Name);
}
}

A nifty SMO bonus is that objects Database and downwards in the hiearchy have a Script StringCollection property.  Script is pre-populated with T-SQL and can be used to recreate objects.

SMO provides DBA automation functionality like backup/restore and create/drop databases. These operations depend on the SMO Server object.  The constructor for Server can have a) no parameters for your local server, b) a server name only, or c) a ServerConnection object in case you need to supply user name and password.

1
2
ServerConnection serverConnection = new ServerConnection("MyMachine", "sa", "MyM@ch1n3");
Server raServer = new Server(serverConnection);

Here is an example of creating a new database to be used as a backup location:

1
2
3
4
5
6
7
8
static Database CreateBackupDatabase(Server server, string databaseName)
{
string backupDatabaseName = databaseName + "_" + DateTime.Now.ToString("yyMMddHHmmffffff");
Database backupDatabase = new Database(server, backupDatabaseName);
Console.WriteLine("Creating new database {0}", backupDatabase.Name);
backupDatabase.Create();
return backupDatabase;
}

The backup operation requires a backup "device", in this case a file that will be used to store the backup.  Assign an SMO Database object to be backed up, add the backup device to its list of devices and call the SqlBackup() method.  You may also want to assign event handlers for PercentComplete and Complete events.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void DoBackup(Server server, string databaseName, Database backupDatabase)
{
Console.WriteLine("Backing up {0}", databaseName);

BackupDeviceItem backupDevice =
new BackupDeviceItem(databaseName + ".bak", DeviceType.File);

Backup backup = new Backup();
backup.Database = databaseName;
backup.Devices.Add(backupDevice);
backup.PercentComplete += new PercentCompleteEventHandler(InProgress);
backup.Complete += new ServerMessageEventHandler(Complete);
backup.SqlBackup(server);
}

static void InProgress(object sender, PercentCompleteEventArgs e)
{
Console.WriteLine("Percent complete: {0}", e.Percent);
}

Restoring can sometimes be trickier due to the original database having a strangle-hold on the physical data and log files.  To work around this use the Restore RelocateFiles property to map new file names and locations. Also notice that to get the physical file path we use the Database object's FileGroups property, drill down into the Files and use the path of the first file in the list.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void DoRestore(Server server, string databaseName, Database backupDatabase)
{
Console.WriteLine("Restoring {0}", backupDatabase.Name);
Restore restore = new Restore();
restore.Database = backupDatabase.Name;
restore.ReplaceDatabase = true;
string backupPath = Path.GetDirectoryName(backupDatabase.FileGroups[0].Files[0].FileName);
string dataPath = string.Format("{0}\\{1}.mdf", backupPath, backupDatabase.Name);
restore.RelocateFiles.Add(new RelocateFile("Falafel", dataPath));
restore.RelocateFiles.Add(new RelocateFile("Falafel_log", Path.ChangeExtension(dataPath, ".ldf")));
BackupDeviceItem backupDevice =
new BackupDeviceItem(databaseName + ".bak", DeviceType.File);
restore.Devices.Add(backupDevice);
restore.PercentComplete += new PercentCompleteEventHandler(InProgress);
restore.Complete += new ServerMessageEventHandler(Complete);
restore.SqlRestore(server);
}

Thats a few of the things SMO can do for you. It's not a replacement for T-SQL, but if you need access to meta-data, automation or other DBA tasks from managed code, then the SMO namespace may be worth exploring. BTW, you can also use SMO in PowerShell directly, wrapped in commands or as the infrastructure for PowerShell providers.

posted on July 8, 2007  #    by Noel Rice  Comments [2]
 Thursday, June 21, 2007

Read all about it here. Something that looks very interesting is support for sometimes connected applications...

posted on June 21, 2007  #    by John Waters  Comments [0]
 Sunday, May 06, 2007

I recently ran into an unexpected behavior of SSRS while writing a complex report. The report required various calculations that would refer to specific previous groups and details, and I had decided to solve the problem by writing a custom report function that would evaluate on each detail row, saving the information I would need later in a dictionary. My solution worked fine, as long as my custom calculation in a group footer didn't depend on the contents of that group's details. Upon further investigation, I learned something very interesting about the order in which SSRS evaluates group headers, footers, and details...

Here is how to observe the behavior for yourself: create a stored procedure like this one, that generates some simple test data:

CREATE PROCEDURE dbo.HundredRows
AS
SET NOCOUNT ON

create table #result (
id int,
grp int
)

declare @id int
set @id = 1

while @id <= 100
begin
insert #result
values ( @id, ( @id - 1 ) / 5 )

set @id = @id + 1
end

select *
from #result

This will return a result set whose id column increments in steps of one from 1 to 100, and whose grp column increments in steps of one for every five rows. Next, create a simple report layout with a table, showing the values of each column and grouping by the grp column. In the group header and footer, include the same column references. Next, add this custom code:

Dim eval_order As Integer = 0

Public Function show_eval_order() As String
  eval_order = eval_order + 1
  Return eval_order.ToString()
End Function

Display the result of this function in the group header, footer, and detail rows. Your layout should look like this:

Once you have the report set up, click on the preview tab to see the surprising results:

I've only shown the first two groups here, but notice what has happened! The order column clearly indicates that the group header and footer are both evaluated before the details in between! By looking at the value of the id column in the footer, we can see that the the group's header and footer rows are printed while the "current" row in the dataset is still positioned on the first row in the group.

While this is interesting, it isn't very important as long as you stick to using SSRS's built-in aggregation functions. It is only when you attempt to "roll your own" by saving the dataset in your own storage for later reference that you might run into trouble. In my case, I used the following workarounds:

  1. Move calculations that rely on the details having been traversed from the group footer to the following group's header. It's clunky and less intuitive, but that's what SSRS has forced upon us.
  2. For the final group, move the calculation out of the table entirely, and put it into textboxes just below the table. While there is no way to tell SSRS to evaluate the textboxes after it evaluates the table, it does so at least for the 2005 version.

In summary, it sure would have been nice if SSRS would have evaluated group headers and footers after it evaluated the details that each group contains, but since we have clearly demonstrated that it doesn't, we now know what we will have to do in order to make custom report code produce the output we want: by putting those custom calculations into the following group's header, and by putting the final footer calculation outside the table entirely.

posted on May 6, 2007  #    by Adam Anderson  Comments [1]
 Wednesday, May 02, 2007

If you need to monitor your transactional replication with a custom monitoring service, Microsoft has provided some useful tools to help. Recently I was having trouble reliably monitoring my replication, and then I discovered this page.

 

http://msdn2.microsoft.com/en-us/library/ms146951.aspx

 

If you’re using transactional replication there is no better way to monitor its health than using your own tracer token. This is just like inserting a tracer token using the replication monitor utility in SQL Server Management Studio.

 

First you need to create a connection to the server.

 

   server = new ServerConnection(sci); 

 

then create a TransPublication object.

 

    TransPublication transPublication = new TransPublication(publicationName, publicationDBName, server);

 

Call LoadProperties of the new object to make sure that all publications and subscriptions are loaded.

 

   transPublication.LoadProperties(); 

 

Then post the tracer token and call refresh to send it on its way. You need to save the ID of the token so that you can clean it up later.

 

   id = transPublication.PostTracerToken();
   transPublication.Refresh();
 

 

Now that the tracer token is on its way we need to create a publication monitor to look for its return. You have to go through a couple of layers to get to it.

 

   Microsoft.SqlServer.Replication.ReplicationMonitor monitor =

      new Microsoft.SqlServer.Replication.ReplicationMonitor(server);
   PublisherMonitor pub = monitor.PublisherMonitors[publisherMonitor];
   PublicationMonitor publicationMonitor = pub.PublicationMonitors[publicationDBName, publicationName];

Now we need to enumerate all of the tokens in the publication. You must call LoadProperties to refresh this list.

 

   publicationMonitor.LoadProperties();
   ArrayList tokens = publicationMonitor.EnumTracerTokens();
 

 

You can cast the items in the array list to a TracerToken type to find the token we sent with the ID we saved earlier.

 

   TracerToken token = null;
   foreach (TracerToken t in tokens)
   {
      if (t.TracerTokenId == tokenID)
      {
         token = t;
         break;
      }
   }
 

 

Now that we have our token we need to enumerate the tracer token history with this call.

 

   DataSet tth = publicationMonitor.EnumTracerTokenHistory(token.TracerTokenId); 

 

This returns a dataset that you need to parse through to get the data we are interested in. These include distributor latency, subscriber latency, and overall latency. If these values are blank, then the token has not returned yet. You need to then enumerate the tokens and check again. When the token has returned you then need to cleanup. A simple call will do this for us.

 

   publicationMonitor.CleanUpTracerTokenHistory(tokenID);

 

If any part of the replication fails for any reason, the token will fail to return and you know you have a problem.

posted on May 2, 2007  #    by Bary Nusz  Comments [2]
 Monday, April 09, 2007

I love the new features of ADO.Net, the Object Datasources and the table adapters that the XSD designer can generate. But they kind of have a will of their own! They open and close connections as they see fit, and dont really care about transactions. If you have a set of related tables that you make changes to, and then want to write all of the changes to the database in one transaction, you need to do things a certain way to make it work...

What about the TransactionScope class?

My first attempt was to use the new TransactionScope functionality in ADO.Net 2.0. You can do something like this:

using (TransactionScope ts = new TransactionScope(TransactionScopeOption.Required))
{
   // do stuff that manipulates data
   ts.Complete();
}

While this is a very elegant solution, it is very tricky to get to work. The mechanism uses a Local Transaction Coordinator, but as soon as you execute your second database update command, it escalates the transaction to a distributed transaction (even if you are just doing sequential operations in the same SqlConnection), and tries to do 2 way communication with the DTC (Distributed Transaction Coordinator) running on the SqlServer. This communication turns out to be a major headache to get working. Asides from opening all kinds of firewall holes and configuring lots of complicated DTC options on both the server machine and your local machine, the server needs to be able to do a reverse DNS lookup to call back to your client machine, which proves to be unfeasable if you are developing from home across a VPN or have a dynamic IP address that the server cant lookup. It really only works when your client and the SQL Server are part of the same AD domain.

Long story short, TransactionScope is too complicated right now to actually use in normal development. So I wanted to stick with the traditional transaction support.

Getting the TableAdapters to use the correct Connection object

First of all, you need to make sure all of your TableAdapters use the same Connection object. TableAdapters are not an actual class in ADO.Net, they are code generated classes, kind of like typed DataSets, but unlike typed DataSets they dont inherit from some common TableAdapter class, they just inherit from Component. However, they do expose a Connection property, so I use that to hook up all my TableAdapters to the same Connection object. Something like this:

void InitTableAdapters( SqlConnection conn )
{
  taReportSetGroupAccountNotAssigned.Connection = conn;
  taReportSetGroupAccount.Connection = conn;
  taReportSetGroup.Connection = conn;
  taReportSet.Connection = conn;
}

Setting up the transaction

Now, you can encapsuluate your database operations in a transaction as follows:

SqlConnection conn = new SqlConnection( connStr );

InitTableAdapters( conn );

conn.Open();
SqlTransaction tran = conn.BeginTransaction();
try
{
  // manipulate data

  tran.Commit();
}
catch()
{
  tran.Rollback();
}
finally
{
  conn.Close();
}

However, there is still a problem with this. The SqlCommands inside the TableAdapter need to use the transaction of the Connection object in the Table Adapter, but the generated code does not do that for you. And you don't want to start messing with the generated code, or your changes will be wiped out next time it is regenerated. What to do?

Making the SqlCommand enlist in your transaction

First I tried subclassing the generated TabeAdapter, as I saw there was a protected CommandCollection property. However, it turned out that this collection only contains the Select Command, and I wanted the Insert, Update and Delete commands to participate in the transaction. These are part of the SqlDataAdapter member of the TableAdapter, which is private. Duh!

Well, partial classes come to the rescue! The TableAdapter class is generated as a partial class, which allows you to extend it by adding in additional methods in your own partial class. One gotcha is that you need to use the same namespace for your partial class as for the generated partial class, and that namespace is actually not the same as that of the typed dataset. It has .XXXTableAdapters added on to the end of it, where XXX is the name of your typed dataset. For instance, if your dataset namespace is Falafel.Data, and its name is FalafelDataset, then your TableAdapter namespace will be Falafel.Data.FalafelDatasetTableAdapters.

Armed with this knowledge, we can extend the partial class of each TableAdapter as follows:

namespace Falafel.Data.FalafelDataSetTableAdapters
{
  public partial class ReportSetGroupAccountTableAdapter
  {
    public void JoinTransaction( SqlTransaction tran )
    {  
      TransactionHelper.JoinTransaction(_adapter, tran);
    }
  }
  public partial class ReportSetGroupTableAdapter
  {
    public void JoinTransaction( SqlTransaction tran )
    { 
      TransactionHelper.JoinTransaction(_adapter, tran);
    }
  }
}

Here I am using my own helper class:

public class TransactionHelper
{
  public static void JoinTransaction( SqlAdapter adapter, SqlTransaction tran)
  {
      CheckTransaction(tran, adapter.InsertCommand);
      CheckTransaction(tran, adapter.DeleteCommand);
      CheckTransaction(tran, adapter.UpdateCommand);
      CheckTransaction(tran, adapter.SelectCommand);
  }
  public static void CheckTransaction( SqlAdapter adapter, SqlCommand cmd)
  {
    if (cmd != null)
      cmd.Transaction = tran;
  }
}

Putting it all together

Now that I have injected a public JoinTransaction method into each TableAdapter, I can get them to enlist in the transaction by surreptitiously calling the trojan horse helper:

void EnlistTableAdaptersInTransaction( SqlTransaction tran )
{
  taReportSetGroupAccountNotAssigned.JoinTransaction( tran );
  taReportSetGroupAccount.JoinTransaction( tran );
  taReportSetGroup.JoinTransaction( tran );
  taReportSet.JoinTransaction( tran );   
}

Adding a call to this in my update logic, this is the final sequence of events:

SqlConnection conn = new SqlConnection( connStr );
InitTableAdapters( conn );
conn.Open();
SqlTransaction tran = conn.BeginTransaction();
try
{
  EnlistTableAdaptersInTransaction( tran );
  // manipulate data
  tran.Commit();
}
catch()
{
  tran.Rollback();
}
finally
{
  conn.Close();
}

It's not the most elegant or compact code, but it gets the job done! I hope this will save you some time and allow you to productively use TableAdapters in transactions until the TransactionScope class becomes more development environment friendly!

posted on April 9, 2007  #    by John Waters  Comments [1]
 Thursday, March 15, 2007

When querying an external data source through a linked server, you have two choices: you can write a typical T-SQL query, specifying the external tables with their four-part name in the FROM clause, or use the OPENQUERY function. The first option has advantages, as long as the external provider supports enough SQL so that SQL Server can take your query and translate it into a query that the external data source can use. On the other hand, if th external provider doesn't support enough SQL for SQL Server to optimize the query, or if the external provider is just too plain buggy, you will have to resort to passing through your own provider-specific queries with OPENQUERY. This gives you a great deal of control over how the external data source is accessed, but comes with a frustrating and puzzling limitation: OPENQUERY requires that the query be a string constant. That means no string variables you built with conditional logic, no expressions that result in a string, and especially no support for parameter substitution!

Fortunately, there is an option besides pulling the entire external table over and then processing it on SQL Server. It's an extremely powerful built-in procedure called sp_executesql. This stored procedure accepts a string containing SQL statements and attempts to execute it. However, with this great power comes great responsibility! Since you are constructing a string of ad-hoc SQL, you must be extremely careful to validate all external input lest you fall victim to a SQL Injection attack. Let's look at a simple example of how to use sp_executesql to create a parameterized OPENQUERY against a linked server named NAVISION.

CREATE PROCEDURE OpenQuery_Customer_ByDate( @date datetime )
AS
set nocount on

declare @stmt nvarchar(max)
set @stmt = 'select * from OPENQUERY( NAVISION, ''select * from Customer where "Last Date Modified" >= ' + convert( char(10), @date, 120 ) + ''' )'

exec sp_executesql @stmt

This procedure illustrates the basic idea of the technique: build a string containing the OPENQUERY query, dynamically building the pass-through query within this string. After the string has been built and passed to sp_executesql, the pass-through query appears as if it were a string constant to OPENQUERY. The same technique could be used to dynamically create a view or function with a predictable name that other SQL could use in a SELECT statement. With sp_executesql, you are limited only by what you can write to a string, and that could be anything!

posted on March 15, 2007  #    by Adam Anderson  Comments [3]
 Wednesday, March 14, 2007
Today I battled with some interesting problems writing changes to a hierarchical datatable to a SQL Server database. Hopefully by writing about this I can save you some time.
posted on March 14, 2007  #    by John Waters  Comments [1]
 Tuesday, March 06, 2007

There was a bug in SP2 regarding maintenance plans, see official note below:

An issue was recently discovered with SQL Server 2005 SP2 which might impact customers using cleanup tasks in Maintenance Plans and SQL Server Integration Services packages. The issue is described in detail at http://support.microsoft.com/kb/933508. We have created a General Distribution Release (GDR) #50000939 to address this issue which is available for download and we have included the GDR in a refresh of the SP2 installation package

More information can be found here.

posted on March 6, 2007  #    by John Waters  Comments [1]
 Wednesday, February 28, 2007

While I was writing a stored procedure that did some string manipulation and was debugging some unexpected behavior, I came across a fascinating, infurating, hidden-in-plain-sight "feature" of the LEN function: it returns the length of a string expression excluding trailing blanks. So, for example:

declare @string varchar(10)
set @string = '123456789 '

declare @len int
set @len = len( @string )

print @len
print right( @string, @len - 5 )

returns 9 and '789 ', not the expected 10 and '6789 '. Also, with this code:

declare @string varchar(10)
set @string = '123456789 '

declare @len int, @reverse_len int
set @len = len( @string )
set @reverse_len = len( reverse( @string ) )

print @len
print @reverse_len

One might expect that the length of a string would be the same forwards and backwards, but it isn't! @len = 9 and @reverse_len = 10!

A workaround for non-Unicode character data is to use the DATALENGTH function, which returns the size of an expression in bytes. If you choose to use DATALENGTH with Unicode data, you must divide by 2 to get the actual string length. Another workaround would be to append a non-blank character to the end of the string, get the length, then subtract one. Neither is very nice. If we had wanted to exclude trailing blanks, we could have called RTRIM before LEN ourselves.

posted on February 28, 2007  #    by Adam Anderson  Comments [3]

The T-SQL PRINT statement is the way to output plain text from SQL scripts and stored procedures. Unfortunately, it has a built-in limitation of printing 8000 non-Unicode or 4000 Unicode characters, and the rest is truncated. I was writing some SQL that generated lengthy SQL scripts that exceeded this limitation, and I wanted to create my own version of PRINT that would split my string at the nearest CRLF, print the part preceding the CRLF, then repeat on the rest of the string to the right of the CRLF. The CRLF itself would be omitted, since each PRINT statement appends its own to the output. However, my own string would sometimes contain two consecutive CRLF sequences for formatting purposes. My printing procedure would correctly strip the second one and leave the first, passing it to PRINT. But instead of my CRLF plus PRINT's, I only got one, spoiling my formatting. I ran the following test:

declare @cr char(1), @lf char(1)
set @cr = char(13)
set @lf = char(10)

print 'cr + lf' + @cr + @lf
print '---'

print 'cr only' + @cr
print '---'

print 'lf only' + @lf
print '---'

print 'lf + cr' + @lf + @cr
print '---'

And got the following output:

cr + lf
---
cr only

---
lf only

---
lf + cr

---

Interesting results! Notice that when a string is followed by CRLF, there is only one, not one plus one from PRINT. A solitary CR or LF will result in a new line plus one from PRINT, and LFCR results in two new lines plus one from PRINT. Since SQL Server honors a single CR or LF and they work as I expected, I updated my SQL generation code to use LF only as new line characters.

posted on February 28, 2007  #    by Adam Anderson  Comments [1]

I've been writing a lot of dynamic SQL that generates other SQL lately. While SQL isn't the best string manipulation language, it's got great native access to database schema information. However, my latest project was resulting in some very long scripts that coudn't be printed with PRINT, because it truncates the output after 8000 bytes, which is 8000 chars or 4000 nchars. One solution to this would have been to execute dynamic SQL to create a stored procedure containing the text, but in this case, I wanted to append multiple scripts together, so I chose to use PRINT as a simple way to output them all. The concept was simple: For each 4000 character Unicode block, find the last occurrance of CR, LF, or both, print the left part, and repeat on the right part. However, I ran into a few obstacles on the way; see my last two T-SQL Gotchas. Here is the final result, a general-purpose PRINT replacement that can handle any Unicode string, assuming that it contains at least one CR or LF every 4000 characters. The delimiter(s) could be parameterized, but I'll leave that as an exercise for you.

-- Works around the 4000/8000 character limit of the print statement
CREATE PROCEDURE dbo.LongPrint( @string nvarchar(max) )
AS
SET NOCOUNT ON

set @string = rtrim( @string )

declare @cr char(1), @lf char(1)
set @cr = char(13)
set @lf = char(10)

declare @len int, @cr_index int, @lf_index int, @crlf_index int, @has_cr_and_lf bit, @left nvarchar(4000), @reverse nvarchar(4000)
set @len = 4000

while ( len( @string ) > @len )
begin
   set @left = left( @string, @len )
   set @reverse = reverse( @left )
   set @cr_index = @len - charindex( @cr, @reverse ) + 1
   set @lf_index = @len - charindex( @lf, @reverse ) + 1
   set @crlf_index = case when @cr_index < @lf_index then @cr_index else @lf_index end
   set @has_cr_and_lf = case when @cr_index < @len and @lf_index < @len then 1 else 0 end
   print left( @string, @crlf_index - 1 )
   set @string = right( @string, len( @string ) - @crlf_index - @has_cr_and_lf )
end

print @string

posted on February 28, 2007  #    by Adam Anderson  Comments [2]
 Tuesday, February 20, 2007

...and with it a long missed friend returns!

When I first switched from 2000 to 2005, I was dismayed to find that the scripting options were drastically reduced in functionality. I am glad to announce that in SP2, the old 2000 options are all back!

The service pack contains a lot of new functionality, read all about it here. The service pack can be downloaded here.

posted on February 20, 2007  #    by John Waters  Comments [1]
 Wednesday, February 14, 2007

How do you find if a table is referenced in a stored proc?  That can be quite the Easter egg hunt without some sort of tool.  The following SQL uses the syscomments table (contains the stored proc text) and joins to the sysobjects table to determine which syscomment records belong to stored procs.  CHARINDEX returns the starting position of an expression in a character string, so the fact that it returns greater than zero signals a hit.

DECLARE @SEARCHSTRING VARCHAR(255)

SELECT @SEARCHSTRING = 'MyTable'

SELECT DISTINCT sysobjects.name
FROM sysobjects,syscomments
WHERE sysobjects.id = syscomments.id
-- look for stored procs only
AND sysobjects.type = 'P'
AND sysobjects.category = 0
-- what you are looking for, what you're looking in
AND CHARINDEX(@SEARCHSTRING,syscomments.text)>0

This is a reduced version taken from an example at:

http://wiki.ittoolbox.com/index.php/HOWTO:Search_the_text_of_triggers_and_stored_procedures

Another handy related query from Adam Anderson's toolbox of SQL techniques finds references to column names.  This one lists the table names where tables contain "Created" and "Modified" users and dates:

select distinct so.name
from sysobjects so
where exists (
select *
from syscolumns sc
where sc.id = so.id
and sc.name in ( 'CreatedBy', 'CreatedOn', 'ModifiedBy', 'ModifiedOn' )
)

More info about CHARINDEX at http://msdn2.microsoft.com/en-us/library/ms186323.aspx.

posted on February 14, 2007  #    by Noel Rice  Comments [1]
 Thursday, November 16, 2006

This is an interesting article about the performance and scalability of temp table and table variable in SQL Server 2000 and 2005. http://www.lakesidesql.com/articles/?p=12

posted on November 16, 2006  #    by Adam Anderson  Comments [1]
 Tuesday, September 05, 2006

I recently ran into a problem with a large DataSet when I set the EnforceConstraints to true. It gave me the error “Failed to enable constraints. One or more rows contain values violating non-null, unique, or foreign-key constraints.” without any clue as to where to look or what to do. Googling the problem, I came across a blog by Dave Lloyd with a good method to figure out exactly where the error is. The only problem was the example code was Dimmed. Naturally I un-Dimmed the code and put it in a proper C# format. I then encapsulated it into a nice little static function that returns a NameValueCollection loaded with all of the relevant error info. The info returned tells you what table, what row/column, and what the constraint is.

private static NameValueCollection BuildDataSetErrorInfo(DataSet dataSet)     
        {     
            NameValueCollection errorInfo = new NameValueCollection();     
            errorInfo.Add("DataSetName: ", dataSet.DataSetName);     
            foreach (DataTable table in dataSet.Tables)     
            {     
                DataRow[] rows = table.GetErrors();     
                if ((rows != null) && (rows.Length > 0))     
                {     
                    errorInfo.Add("Table: ", table.TableName);     
                    foreach (DataRow row in rows)     
                    {     
                        errorInfo.Add("Row Error: ", row.RowError);     
                        DataColumn[] cols = row.GetColumnsInError();     
                        if ((cols != null) && (cols.Length > 0))     
                        {     
                            foreach (DataColumn col in cols)     
                            {     
                                errorInfo.Add("Column: ", col.ColumnName);     
                                errorInfo.Add("Column Error: ", row.GetColumnError(col));     
                            }     
                        }     
                    }     
                }     
            }     
            return errorInfo;     
        }   
 
posted on September 5, 2006  #    by Bary Nusz  Comments [0]
 Monday, July 24, 2006

As an application developer, I often need to generate scripts for mutiple database objects, each in its own file, with both a DROP and a CREATE statement. In SQL Server 2000, there was a robust set of scripting options to choose from which allowed exactly this kind of use. However, in SQL Server 2005, these options have vanished. All selected objects get put into a single script file, and there is no way to generate DROP and CREATE in a single script. Some options can be accessed by choosing the Tasks | Generate Scripts... menu item at the database level, but they don't provide the desired functionality, and the wizard itself is cumbersome to navigate.

In response to this loss of functionality, Bill Graziano wrote a utility that mimics these commonly-used options of SQL scripting. The original blog, which contains links to an installer as well as source code, can be found here.

posted on July 24, 2006  #    by Adam Anderson  Comments [1]
 Monday, June 05, 2006

A lot has been written about the merits of multiple narrow indexes versus single larger compound indexes. This blog by Grant Fritchey yields juicy food for thought on that very topic, backed by some very thorough testing with real-world data.

posted on June 5, 2006  #    by Adam Anderson  Comments [2]
 Tuesday, May 30, 2006

I encountered a troubling message while playing around with SQL Server 2005 and its new Integration Services. It read, “Failed to save package file "C:\Documents and Settings\Adam Anderson\Local Settings\Temp\tmp44F.tmp" with error 0x80029C4A "Error loading type library/DLL.".”

I first saw the error when I tried to create an Integration Services project in Visual Studio 2005, but the project was created anyway. I opened the project and ignored the error, hoping it was merely a glitch. However, after setting up a data source, I tried to add a package, and was blocked by the same error message. Now I knew I was in trouble. This was a brand-new installation, and it was annoying to see such an uninformative message, which is usually a sign that something has gone wrong at a fundamental level. Fortunately, my situation was not unique, and a search on Google quickly brought me to the correct solution.

It turns out that the MSXML 6.0 Parser was either incompletely installed or the installation was damaged.

To fix:

  1. Start Menu | Control Panel | Add or Remove Programs
  2. Locate MSXML 6.0 Parser in the list
  3. Click Change. The Setup dialog will appear.
  4. Click Next.
  5. There will be two options: Repair and Remove. Select Repair, then click Next.
  6. The Setup dialog will display a progress bar and then (hopefully) report success.
posted on May 30, 2006  #    by Adam Anderson  Comments [1]
 Wednesday, April 05, 2006

The SQL Server functions ISNULL and COALESCE seem to occupy the same space in terms of functionality, only ISNULL is restricted to only two parameters, while COALESCE can take any number of parameters. So why ever use ISNULL? Answers and trivia lie within!

First of all, the simple answer for why to prefer ISNULL over COALESCE when given the choice is that ISNULL tends to produce query plans that are more efficient than COALESCE's. Examine the query plans for the two queries given below to see the difference:

-- These two yield different execution plans. 
select a.au_id,
isnull( (select price from titles where title_id = ta.title_id), 0 )
from authors a
join titleauthor ta on a.au_id = ta.au_id
select a.au_id,
coalesce( (select price from titles where title_id = ta.title_id), 0 )
from authors a
join titleauthor ta on a.au_id = ta.au_id

The first query uses one less nested loop than the second, resulting in a lower overall cost. I wouldn't recommend writing queries that nest subqueries within functions anyway, but the example was only intended to show the difference in generated execution plans, not to recommend a certain T-SQL coding style.

The other bit of trivia most people don't know about ISNULL and COALESCE is that the return data type for ISNULL is guaranteed to be the same as the data type of the first parameter. However, the return data type of COALESCE is determined by data type precedence rules (see the Books Online topic "Data Type Precedence"). Therefore, the following queries will produce different output:

declare @Example char(2) 
set @Example = null
select isnull( @Example, 'abcde' ), coalesce( @Example, 'abcde' )
The first expression using ISNULL will return 'ab', which is the declared datatype of the first parameter, a char(2). The second expression using COALESCE will return the highest precedence data type, which is the longer string 'abcde.'

Take-home lessons:
  1. When you only need to coalesce two arguments, use ISNULL instead.
  2. When using COALESCE, use explicit casting to ensure you get a consistent return data type and also to make the meaning of the code clear to those who are less knowledgeable about the quirks of ISNULL and COALESCE than you are now. ;)
posted on April 5, 2006  #    by Adam Anderson  Comments [0]
 Thursday, March 16, 2006

Here's a mysterious bit of code that doesn't look like it ought to work, but yet it does

select top 1 *
from table
order by newid()

posted on March 16, 2006  #    by Adam Anderson  Comments [2]
 Thursday, February 23, 2006

My colleague John recently blogged about the unexpected results of using the T-SQL IN operator on a set that contains NULL. Here is another solution to the problem.There are three main ways to test for existence in another table, listed here in order of increasing performance

  1. LEFT JOIN and look for NULLs in the right table
  2. Use the IN operator
  3. Use the EXISTS operator

In general, the EXISTS operator will generate the most efficient query plan, and it also isn't subject to the very NULL pitfall that plagues the IN operator. The query that John posted:

SELECT COUNT(*)
FROM TableTarget
WHERE PrimaryKeyField NOT IN (
  SELECT ForeignKeyField
  FROM TableSource
  WHERE ForeignKeyField IS NOT NULL
)

Could be writtten like this:

SELECT COUNT(*)
FROM TableTarget t
WHERE NOT EXISTS (
  SELECT *
  FROM TableSource
  WHERE ForeignKeyField = t.PrimaryKeyField
)

posted on February 23, 2006  #    by Adam Anderson  Comments [1]
 Friday, January 27, 2006

Being able to compare the results of two different SQL queries and verify that they match 100% is a useful ability for any database application developer. I've encountered two main reasons for wanting to compare query results in the course of development:

  1. After importing data from an older system, verifying that equivalent reports in the old system and the new one agree
  2. When refactoring a complex query, to verify that the changes made haven't altered the output

In both cases, I find it convenient to run data comparisons of queries run in SQL Server Query Analyzer by using MS Excel. The technique is simple, but the payoff is huge: 100% confidence that every row and every column matches. Here are the steps I use:

  1. Create a new 3-page workbook and rename the tabs "Old", "New", and "Compare" 
  2. Hand-enter column names in the top row of each worksheet. I like to make them bold and underlined so Excel can tell that they're headers. 
     
  3. Run the "old" query in Query Analyzer. Once the results come back, click in the result pane and select all using Ctrl+A or Edit | Select All
  4. In the "Old" worksheet, select the leftmost cell in the row under the header and paste the results.
  5. Repeat the same steps for the "New" query and worksheet.
  6. If the queries don't explicitly set their own sort order, I recommend sorting both the "Old" and "New" data in a way that should guarantee that they are ordered the same on the two worksheets.
  7. In the "Compare" worksheet, enter the following formula in the leftmost cell under the header row: =Old!A2=New!A2. This formula will return TRUE if the cells match, and FALSE if they don't. 
  8. To make mismatches easier to spot, I like to apply some conditional formatting as well.

    1. Select the cell and then select Format | Conditional Formatting...
    2. Set up the condition so that it reads "Cell Value Is" "equal to" "FALSE"
    3. Click the Format... button. On the Patterns tab, select a nice bright color that will stand out, like red. Click OK.
    4. Click OK on the Conditional Formatting dialog
  9. Select the span from this initial cell to the rightmost column in the result set and press Ctrl+R or Edit | Fill | Right.
  10. Check the "Old" and "New" worksheets and note the number of the last row containing query results.
  11. In the "Compare" worksheet, select all the cells that contain data in the same row and column on the other two sheets and press Ctrl+D or Edit | Fill | Down.
  12. Differences in data will result in a value of FALSE (optionally highlighted if you used conditional formatting). The values can be scanned for visually, or you can use the Find dialog to search for them. When using the Find dialog, make sure to click the Options button and change "Look in" from "Formulas" to "Values" 
  13. Once differences are found, the old and new data can be examined to determine the cause for the difference.
posted on January 27, 2006  #    by Adam Anderson  Comments [0]
 Friday, January 20, 2006

I love working with MS SQL Server. Oh sure, Oracle is supposedly faster and all that jazz, but when it comes to rapid development, MSSQL's ease of use just can't be beat. However, there are still some things to watch out for. Today I want to warn you all off of using UNION.

Conceptually, there's nothing wrong with UNION. It takes two result sets with congruent columns and returns them as a single result set. There are two potential performance issues, however, one of which can be quite serious.

The first problem is that the UNION operator automatically performs a DISTINCT operation on the combined result set. If it's unnecessary, or worse, if it is necessary, but you've also used SELECT DISTINCT in one or both of the UNIONed queries, then UNION is causing a loss of performance for nothing. The workaround to this is simple; use UNION ALL instead.

The second problem can be far worse. I've run into situations many times where the UNION (or UNION ALL) operator caused inexplicably poor performance. In one recent scenario, I used UNION ALL to merge the results of two queries, each of which ran in about 7 seconds. When merged using UNION ALL, query execution time leapt up to 1:20. Neither the estimated execution plan nor the actual one reflected this; the total subtree cost was exactly the same as the sum of the two queries' costs when run separately. The fix is a little more complex, but extremely effective.

My solution to the problem is to use a table variable. In general, I recommend the use of table variables over temp tables unless you absolutely need a feature that temp tables support but table variables do not.

Example

The example below is not intended to demonstrate good database design; it merely serves to illustrate the solution. Given the original query:

SELECT FirstName, LastName, Salary
FROM Employees
UNION ALL
SELECT FirstName, LastName, Salary
FROM Managers

First declare a table variable with columns that match that of the result set:

DECLARE @Result TABLE (
  FirstName varchar(50),
  LastName varchar(50),
  Salary money,
  -- Example of how to declare a PK within a table variable
  PRIMARY KEY ( LastName, FirstName )
)

Next, insert each query into the result table:

INSERT @Result
SELECT FirstName, LastName, Salary
FROM Employee

INSERT @Result
SELECT FirstName, LastName, Salary
FROM Manager

Finally, select the results:

SELECT *
FROM @Result

This workaround will not only avoid potential performance problems with UNION, but will actually allow you to improve join performance through the definition of a good primary key, if the UNION result is joined to other data.

posted on January 20, 2006  #    by Adam Anderson  Comments [1]