navigation
 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 [0]
 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 [0]
 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 [0]

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 [0]
 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 [0]
 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 [0]
 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 [0]
 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 [0]
 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 [0]
 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 [2]
 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 [0]
 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 [1]
 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 [0]
 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 [0]
 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 [0]
 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 [2]
 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