Deploying signed assemblies in a single script

I’ve been writing a set of routines to control my database backups recently and one part of this was a CLR User Defined Data Type I’d created that allows some rudimentary file handling (with the appropriate security). By it’s very nature this assembly required the EXTERNAL ACCESS privilege and this is where the head-scratching came about.

Liking clean install scripts with minimal payloads (single scripts are easier to check into Source Code and deploy across multiple environments) I needed some method of encapsulating the deploy and ensuring the appropriate asymmetric key, users etc. were created. However normally this would require either the key file or the assembly DLL to be copied along with the .Sql script – and this would break my 1 script rule! So next idea was to create the asymmetric key from the assembly directly within Sql server – but I couldn’t do that until I’d installed the assembly which would fail because it required the EXTERNAL ACCESS privilege; Thus leading to another game of Chicken and Egg.

The solution was remarkably obvious – use a temporary SAFE assembly to create the asymmetric key then dispose of it directly afterwards – after all the assembly is only required at this juncture to create the asymmetric key.

The first section of the script deals with previous execution cleanups etc:

  1. Use RandomThunks
  2.  
  3. ------------/ Start stage 1 : Drop previous objects /--------------------------------------------------
  4.  
  5. If Exists ( Select 1 From sys.assembly_types As T Inner Join sys.schemas As S On T.schema_id = S.schema_id Where T.name = N'SqlFile' And S.Name = N'Maintenance.Database.Archives')
  6.     Drop Type [Maintenance.Database.Archives].SqlFile;
  7.  
  8. If Exists (Select 1 From sys.assemblies Where name = N'RandomThunks.CLR.SqlServerFileUDT' and is_user_defined = 1)
  9.     Drop Assembly [Skipjack.CLR.SqlServerFileUDT];
  10.  
  11. -- This next key should never exist when we run this as it's only use to create the asymmetric key before we Grant the External Access right to it.
  12.  
  13. If Exists (Select 1 From sys.assemblies Where name = N'TEMPORARY. DO NOT USE!' and is_user_defined = 1)
  14.     Drop Assembly [TEMPORARY. DO NOT USE!];
  15.     
  16. If Exists ( Select 1 From sys.server_principals Where name = 'SqlServerFileUDTUser' )
  17.     Drop Login SqlServerFileUDTUser;
  18.     
  19. If Exists ( Select 1 From sys.asymmetric_keys Where name = 'SqlServerFileUDTKey' )
  20.     Drop Asymmetric Key SqlServerFileUDTKey;
  21.  
  22. ------------/ End stage 1 : Drop previous objects /----------------------------------------------------

And the second part deploys the assembly with the requisite privilege:

  1. ------------/ Start stage 2 : Object Creation /--------------------------------------------------------
  2.  
  3. Declare @Assembly VarBinary(MAX);
  4. Set @Assembly = 0x4D5A9000030000000...
  5.  
  6. If Not Exists ( Select 1 From sys.schemas Where Name = 'Maintenance.Database.Archives')
  7.     Exec sp_ExecuteSql N'Create Schema [Maintenance.Database.Archives]';
  8.  
  9. -- Create a temporary assembly that we'll use to build the Asymmetric Key from
  10.  
  11. Create Assembly [TEMPORARY. DO NOT USE!] Authorization dbo
  12.     From @Assembly
  13.     With Permission_Set = Safe;
  14.  
  15. Create Asymmetric Key SqlServerFileUDTKey
  16.     From Assembly [TEMPORARY. DO NOT USE!];
  17.  
  18. -- Drop the temporary assembly - it's use is over.
  19.  
  20. Drop Assembly [TEMPORARY. DO NOT USE!];
  21.  
  22. Create Login SqlServerFileUDTUser
  23.     From Asymmetric Key SqlServerFileUDTKey;
  24.  
  25. -- This needs to be run at the Server level - encapsulating it in a sp_ExecuteSql allows the script to be DB name agnostic.
  26.  
  27. Exec sp_executeSql N'
  28.     Use master;
  29.  
  30.     Grant External Access Assembly
  31.         To SqlServerFileUDTUser;';
  32.  
  33. -- This is the real assembly
  34.  
  35. Create Assembly [Skipjack.CLR.SqlServerFileUDT] Authorization dbo
  36.     From @Assembly
  37.     With Permission_Set = External_Access;
  38.  
  39. Create Type [Maintenance.Database.Archives].SqlFile
  40.     External Name [RandomThunks.CLR.SqlServerFileUDT].SqlFile;
  41.  
  42. ------------/ End stage 2 : Object Creation /----------------------------------------------------------

Sql Agent Session Storage

I’ve been fiddling around with some jobs recently that required values to be kept from one job step to another. Not finding anything much exciting I went for plan Z – roll my own.

They’re simple procedures but do work nicely.

First I create a Schema that everything will be bundled under:

  1. If Not Exists ( Select 1 From sys.schemas Where Name = 'Aracs.Utilities.Jobs')
  2.     Exec sp_ExecuteSql N'Create Schema [Aracs.Utilities.Jobs]';

 

Now we’ll need a backing store:

  1. Create Table [Aracs.Utilities.Jobs].SessionSaverStore
  2.     (
  3.          JobId                UniqueIdentifier        Not Null
  4.         ,ParameterName        NVarChar(128)            Not Null
  5.         ,Value                Sql_Variant                Null
  6.     );

 

Next I create the SessionDestroyer Procedure (since it can be called from the next one this keeps the dependencies happy and calm):

  1. If Object_Id('[Aracs.Utilities.Jobs].SessionDestroyer') Is Null
  2.     Exec sp_ExecuteSql N'Create Procedure [Aracs.Utilities.Jobs].SessionDestroyer As';
  3.  
  4. Go
  5.  
  6. Alter Procedure [Aracs.Utilities.Jobs].SessionDestroyer
  7.  
  8. As
  9.  
  10.     Declare     @JobHex                    NVarChar(32)    
  11.             ,@JobId                        UniqueIdentifier
  12.     ;
  13.     
  14.     Select    @JobHex = Substring(Program_Name, 32, 32)
  15.       From    sys.dm_Exec_Sessions
  16.       Where    Session_Id = @@Spid;
  17.  
  18.     Delete    [Aracs.Utilities.Jobs].SessionSaverStore
  19.       Where    JobId = Cast('' as xml).value('xs:hexBinary(sql:variable("@JobHex") )', 'VarBinary(MAX)');

 

Along comes the SessionSaverStore next:

  1. If Object_Id('[Aracs.Utilities.Jobs].SessionSaverStore') Is Null
  2.     Create Table [Aracs.Utilities.Jobs].SessionSaverStore
  3.         (
  4.              JobId                        UniqueIdentifier
  5.             ,ParameterName                NVarChar(128)
  6.             ,Value                        Sql_Variant
  7.         );
  8.         
  9. If Object_Id('[Aracs.Utilities.Jobs].SessionSaver') Is Null
  10.     Exec sp_ExecuteSql N'Create Procedure [Aracs.Utilities.Jobs].SessionSaver As';
  11.  
  12. Go
  13.  
  14. Alter Procedure [Aracs.Utilities.Jobs].SessionSaver
  15.              @ParameterName                NVarChar(128)
  16.             ,@Value                        Sql_Variant
  17.             ,@StartFresh                Bit = 0
  18.  
  19. As
  20.  
  21.     Declare     @JobHex                    NVarChar(32)
  22.             ,@JobId                        UniqueIdentifier
  23.     ;
  24.     
  25.     Select    @JobHex = Substring(Program_Name, 32, 32)
  26.       From    sys.dm_Exec_Sessions
  27.       Where    Session_Id = @@Spid;
  28.  
  29.     Set @JobId = Cast('' as xml).value('xs:hexBinary(sql:variable("@JobHex") )', 'VarBinary(MAX)');      
  30.     If Exists ( Select 1 From MSDB.dbo.sysJobs Where Job_Id = @JobId )
  31.     Begin
  32.         If @StartFresh = 1
  33.             Exec [Aracs.Utilities.Jobs].SessionDestroyer;
  34.             
  35.         If Exists (
  36.             Select    1
  37.               From    [Aracs.Utilities.Jobs].SessionSaverStore
  38.               Where JobId = @JobId
  39.                 And ParameterName = @ParameterName
  40.         )
  41.             Update    [Aracs.Utilities.Jobs].SessionSaverStore
  42.               Set    Value = @Value
  43.               Where    JobId = @JobId
  44.                 And    ParameterName = @ParameterName;
  45.         
  46.         Else
  47.             Insert
  48.               Into    [Aracs.Utilities.Jobs].SessionSaverStore
  49.                     (
  50.                          JobId
  51.                         ,ParameterName
  52.                         ,Value
  53.                     )
  54.                 Values
  55.                     (
  56.                          @JobId
  57.                         ,@ParameterName
  58.                         ,@Value
  59.                     );
  60.     End

 

Finally the SessionRetriever:

  1. If Object_Id('[Aracs.Utilities.Jobs].SessionRetriever') Is Null
  2.     Exec sp_ExecuteSql N'Create Procedure [Aracs.Utilities.Jobs].SessionRetriever As';
  3.  
  4. Go
  5.  
  6. Alter Procedure [Aracs.Utilities.Jobs].SessionRetriever
  7.              @ParameterName                NVarChar(128)
  8.             ,@Value                        Sql_Variant        Output
  9. As
  10.  
  11.     Declare     @JobHex                    NVarChar(32)    
  12.             ,@JobId                        UniqueIdentifier
  13.             ,@Valuei                    Sql_Variant
  14.             ,@DataType                    NVarChar(128)
  15.     ;
  16.     
  17.     Select    @JobHex = Substring(Program_Name, 32, 32)
  18.       From    sys.dm_Exec_Sessions
  19.       Where    Session_Id = @@Spid;
  20.  
  21.     Set @JobId = Cast('' as xml).value('xs:hexBinary(sql:variable("@JobHex") )', 'VarBinary(MAX)');      
  22.     If Exists ( Select 1 From MSDB.dbo.sysJobs Where Job_Id = @JobId )
  23.     Begin
  24.         Select    @Value = Value
  25.           From    [Aracs.Utilities.Jobs].SessionSaverStore
  26.           Where    JobId = @JobId
  27.             And    ParameterName = @ParameterName;
  28.  
  29.     End

 

Use is pretty straightforward. As an Proof of concept I created a Sql Agent Job with two steps – Store’ and ‘Fetch’

Store contained the following:

  1. Exec [Aracs.Utilities.Jobs].SessionSaver 'Name', 'Fred', 1
  2. Exec [Aracs.Utilities.Jobs].SessionSaver 'Age', 37

 

And Fetch:

  1. Declare @Name Sql_Variant
  2. Declare @Age Sql_Variant
  3.  
  4.   Exec[Aracs.Utilities.Jobs].SessionRetriever 'Name', @Name Output;
  5.   Exec[Aracs.Utilities.Jobs].SessionRetriever 'Age', @Age Output;
  6.  
  7. Print Cast(@Name As VarChar(128)) + ' ' + Cast( @Age As VarChar(4));

 

Granted it’s missing some bells and whistles (like exception handling) and I’d wager I could stuff the Retriever in as a function. However as a first hit I was pretty chuffed with it!

Bulk Copy within a Sql Server CLR Stored Procedure

Whilst it’s not immediately obvious, one can perform a Bulk Copy operation as part of a Sql Server CLR Stored Procedure. There are however some gotcha’s.

First off is that you need to stay away from Context Connections for the actual operation. Whilst you can technically use a Context Connection for the first part, I’d personally not recommend you do for reasons that will become apparent shortly.

The code below is taken from one of my procedures using a homebrew class that provides me with information regarding a table – including the various elements of a fully qualified 4 part name (Server.Database.Schema.Table).

The first part of the code handles the bulk import:

  1. using (SqlConnection sourceCx = new SqlConnection(string.Format("Data Source={0};Initial Catalog={1};Integrated Security=SSPI;", sourceTable.ServerName, sourceTable.DatabaseName)))
  2.             {
  3.                 sourceCx.Open();
  4.  
  5.                 using (SqlCommand command = new SqlCommand(string.Format("Select * From {0};", sourceTable.FqTableName2Part), sourceCx))
  6.                 {
  7.                     using (SqlDataReader reader = command.ExecuteReader())
  8.                     {
  9.                         using (SqlBulkCopy bulkCopy = new SqlBulkCopy(string.Format("Data Source={0};Initial Catalog={1};Integrated Security=SSPI;", destinationTable.ServerName, destinationTable.DatabaseName)))
  10.                         {
  11.                             bulkCopy.DestinationTableName = destinationTable.FqTableName2Part;
  12.                             bulkCopy.SqlRowsCopied += new SqlRowsCopiedEventHandler(bulkCopy_SqlRowsCopied);
  13.                             bulkCopy.DestinationTableName = destination;
  14.                             bulkCopy.BatchSize = 10000;
  15.                             bulkCopy.NotifyAfter = bulkCopy.BatchSize;
  16.                             bulkCopy.WriteToServer(reader);
  17.                             bulkCopy.Close();
  18.                         }
  19.                     }
  20.                 }
  21.             }

 

As you can see I’m using two SSPI connections in place of the Context based Connection that’s normally provided. I decided to use SSPI to avoid needing to store user names and passwords. Your mileage and methodology may vary.

The next section is the reason why I advise not using a Context Connection for the DataReader:

  1. static void bulkCopy_SqlRowsCopied(object sender, SqlRowsCopiedEventArgs e)
  2.      {
  3.          string call = string.Format("RaisError('{0} rows copied', 0, 0) With NoWait", e.RowsCopied);
  4.          SqlContext.Pipe.ExecuteAndSend(new SqlCommand(call));  
  5.      }

 

In order for this event to work correctly the Context Connection must be free – which will not be the case if you use it for the DataReader. Running the RaisError via ExecuteAndSend ensures the results are not gobbled up by the any special pipe that SqlCommand.ExecuteNoQuery creates (see Vadim Tryshev’s excellent post on RAISERROR in CLR Routines). Finally I use a 0 for both the State and Severity to avoid any subsequent messages that RaisError would otherwise produce.

Sql Server 2008, SP1

Wither ‘Go’

Wow, it’s been another time and a bit since my last post. Still here, still banging my head against the proverbial wall…

Came across this Oddness today.

I had the following script segment:

    If Exists ( Select 1 From sys.tables Where Object_Id = Object_Id('Security.Dictionary') )
Drop Table [Security].Dictionary;

Create Table [Security].Dictionary
(
Id Int Identity(1, 1) Not Null
,
HashValue Int Not Null
,
ItemType TinyInt Null
,
TextData NVarChar(MAX)
);


Alter Table
[Security].Dictionary With NoCheck
Add Constraint
PK_Dictionary Primary Key NonClustered (
HashValue
)
With
(
FillFactor = 50
,Online = On
,Pad_Index = On
,Allow_Row_Locks = On
,Allow_Page_Locks = Off
)
;

I needed to Add ItemType to the clustered key, so I dutifully made it a Not Null and tried again.
    If Exists ( Select 1 From sys.tables Where Object_Id = Object_Id('Security.Dictionary') )
Drop Table [Security].Dictionary;

Create Table [Security].Dictionary
(
Id Int Identity(1, 1) Not Null
,
HashValue Int Not Null
,
ItemType TinyInt Not Null
,
TextData NVarChar(MAX)
);

Alter Table
[Security].Dictionary With NoCheck
Add Constraint
PK_Dictionary Primary Key NonClustered (
HashValue
,ItemType
)
With
(
FillFactor = 50
,Online = On
,Pad_Index = On
,Allow_Row_Locks = On
,Allow_Page_Locks = Off
)
;


Imagine my puzzlement when I received the following error:

Msg 8111, Level 16, State 1, Line 12
Cannot define PRIMARY KEY constraint on nullable column in table 'Dictionary'.
Msg 1750, Level 16, State 0, Line 12
Could not create constraint. See previous errors.


This threw me for a loop since ItemType was quite clearly Not Null (Line 9). So I ran each segment in turn using Select and F5 and was even more non-plussed when each segment ran fine. I then ran the script again and once more, it ran without failure. With my trusty imaginary ‘WTF?’ Red Fez hat firmly planted on my noggin, I attempted to recreate the situation and low and behold I found that adding a Go batch separator thus separating the Create Table And the Alter Table made all the difference for the first run through:
    If Exists ( Select 1 From sys.tables Where Object_Id = Object_Id('Security.Dictionary') )
Drop Table [Security].Dictionary;

Create Table [Security].Dictionary
(
Id Int Identity(1, 1) Not Null
,
HashValue Int Not Null
,
ItemType TinyInt Not Null
,
TextData NVarChar(MAX)
);

Go

Alter Table
[Security].Dictionary With NoCheck
Add Constraint
PK_Dictionary Primary Key NonClustered (
HashValue
,ItemType
)
With
(
FillFactor = 50
,Online = On
,Pad_Index = On
,Allow_Row_Locks = On
,Allow_Page_Locks = Off
)
;

If I were to guess what just happend here I'd say that I think the Alter Table sans the Go seperator was parsed based off the previous table specification, however embedding the Go seperator forced the parser to re-evaluate the new layout. Or something...

Sql Server 2008, SP1

We interrupt your schedule to bring you this late breaking news

Off topic, however worthy of keeping for posterity. From the Department of Homeland Security comes this press release from the Office of the Press Secretary, and a statement on just how serious the US Government is on eradicating the 2009 H1N1 ‘Swine Flu’ virus.

And just incase they redact their statement, here’s the most chilling section:

image

One hopes this was a simple typo!

There can be only 1

Our resident architect came over today and posed me this little puzzler – he wanted a query that would return if 1 row or nothing based of a certain criteria. In other words if two rows met the criteria he wanted nothing pulled back.

A strange request I’ll grant ye, and one that took me some time to bang my head against, however the answer was pretty neat:

Create Schema Test;
Go

Create Table
Test.Only1RowTest
(
Key1 Int
,Key2 Int
)
Go

Insert into
Test.Only1RowTest Values (1,1),(2,2),(3,3),(1,2);


-- Demonstrate pulling back no rows because 2 rows meet the criteria

With Cte As ( Select Top(2) * From Test.Only1RowTest where Key1 = 1 )
Select Cte.* from Cte Inner Join ( Select Count(*) As c From Cte) As CteC On CteC.c = 1;

-- Expand the criteria to further limit the row count and we get 1

With Cte As ( Select Top(2) * From Test.Only1RowTest where Key1 = 1 And Key2 = 1 )
Select Cte.* from Cte Inner Join ( Select Count(*) As c From Cte) As CteC On CteC.c = 1;


To break it down further, we create a common table expression and start off on the basis that returning 2 rows is just as bad as returning 2 million, so we limit the rows pulled back with a Top(2), we then bring back the results of the CTE as long as the # of rows returned =1 (no-one ever said an Inner Join had to join two columns together).



Sql Server 2008 Build 10.0.1798.0 (RTM-CU4)

Fritzin’ with the Guv’nor

It’s a feature I for one have been screaming desperate for – the Sql Server 2008 Resource Governor – the means to finally provide a T-SQL based process to throttle how much CPU and Memory certain activities can utilize.

For me I need to it because I hate bringing the server down every time I need to run a fragmentation scan; with a 1.5TB DB on an undersized server, I need most of the resources dedicated to keeping the application happy. However I don’t want anything I do to kill the performance – and likewise I don’t want some schmuck running a badly written adhoc query in Management Studio preventing me from doing my job.

And this is where the Resource Governor comes in to play by offering Resource Pools and Work Groups. Each Pool can be though of almost as an individual Sql Server instance operating on shared data.And to a certain extent this is true, however the pools do share some meta-data which I’ll explain later.

image

Out of the box there are 2 Pools and 2 Groups pre-defined – One called Internal which is static and immutable and where Sql Server processes its internal processes, the other called Default which contains a undeletable but otherwise configurable group, also called Default.

In this configuration and user processing is performed in the Default Pool and all internal processing is carried on in the Internal pool – even to the detriment of any other pool out there. This allows Sql Server to continue processing without other pools pressurizing it and as a result, no user processes are allowed in the Internal pool.

Pools are the first level of control and are used to define the minimum and maximum CPU and memory workloads running inside them are ever allowed to use. Work Groups take it to the next level and allow not only a finer level of control but also the ability to move them between pools. Groups, for example can be defined to specify how much of a % they take out of the pool, the priority level and also the MAXDOP setting. Finally, users are associated with pools via use of a Classification Function.

However, it is important to understand that the Resource Governor really only limits resources when there is a pinch point in total resource availability. Running identical queries on an otherwise silent server, one in a High priority pool and the other in a low one will have no obvious effect. However once the server is being pushed the resource governor provides a nice guarantee of resource availability to those sessions that require it and a good throttle to those that don’t.

image In the examples that follows I use two new pools and one associated work group in each pool. One pool will be used to process high priority tasks and the other low priority. Two users will be created, one will be assigned to the High Work Group, the other the Low Work Group.

(NB: for brevities sake the users I’m creating here do not have any passwords; it goes without saying that this is exceptionally bad form and should not be practiced on any server that you value data, schema or any other objects on!)

Create Resource Pool Low With ( Min_CPU_Percent = 0, Max_CPU_Percent = 30 ) ;
Create Resource Pool High With ( Min_CPU_Percent = 60, Max_CPU_Percent = 100
) ;

Create Workload Group Low With ( Importance = Low ) Using Low
;
Create Workload Group High With ( Importance = High, Request_Max_Memory_Grant_Percent = 100 ) Using High
;

Alter Resource Governor Reconfigure
;

Use AdventureWorks2008
;
Create Login LowUser With Password=N'', Default_Database=AdventureWorks2008, Check_Expiration=Off, Check_Policy=Off
;
Create User LowUser For Login LowUser
;
Exec sp_AddRoleMember N'db_owner',
N'LowUser'
Create Login HighUser With Password=N'', Default_Database=AdventureWorks2008, Check_Expiration=Off, Check_Policy=Off
;
Create User HighUser For Login HighUser
;
Exec sp_AddRoleMember N'db_owner',
N'HighUser'




As can be seen, the Low pool is only given 30% of CPU to play with – however this is not a true maximum but for these examples it’s good enough. I’ll explain these figures and how they interact later. Notice also the statement reconfiguring the governor; no changes are ever made until this Reconfiguration has taken place.

Next we need to be ability associate users with workgroups – something that’s facilitated with a Scalar-based classification function. This function can make its decision based on numerous factors, including (but not limited to) session user name, host name and application name. For now we’ll keep it very simple and base it off the user name.

Use master;
Go

Create Function
dbo.ResourceGovernorWorkgroupClassifier
()
Returns sysname
With SchemaBinding
As
Begin

Declare
@WorkgroupName SysName =
'default'
;

If SUser_Name() =
'LowUser'
Set @WorkgroupName = 'Low'
;

If SUser_Name() =
'HighUser'
Set @WorkgroupName = 'High'
;

Return @WorkGroupName
;

End
Go

Alter Resource Governor With
( Classifier_Function = dbo.ResourceGovernorWorkgroupClassifier);



This really simple classifier function simply assumes user will be allocated to the “default” work group unless either LowUser or HighUser logs in, at which point they’ll be allocated to the relevant Work Groups. This function will now be executed for every subsequent user login – so as a result you’ll always want to keep this function as short and sweet as possible (a reconfiguration is not required for this to take a effect).

So, now we’re ready to demonstrate the Resource Governor in action. Now, remember earlier I mentioned the governor only kicks in when there’s a pressure point? This being the case we need a method of creating an artificial resource hog – something that will thrash the DB nicely. The AdventureWorks2008.Sales.SalesOrderDetail table is a nice enough size for us to run some hairy and not so hairy queries against. Caveat emptor – the queries themselves are totally nonsensical – they are written to provide example load usage only.


Run the following in a session as a ‘normal’ user (sa, your Windows account or anything else):

Select COUNT(*)
From Sales.SalesOrderDetail As Sod1
Inner Join Sales.SalesOrderDetail As Sod2 On Sod1.SalesOrderDetailID != Sod2.SalesOrderDetailID;




Even on a relatively beefy PC this should begin to the thrash the living bejezus out of Sql Server and take the CPU close to 100%. Now connect a session as LowUser and run:

Set Statistics Time On;
Select COUNT(*)
From Sales.SalesOrderDetail As Sod1
Inner Join Sales.SalesOrderDetail As Sod2 On Sod1.SalesOrderDetailID != Sod2.SalesOrderDetailID;
And Sod1.SalesOrderID < 43800
;





Finally, connect a third session as HighUser and run the same query. If all goes well, this last query should finish relatively quickly (for me it takes between 10 and 15 seconds), however the second query (running as LowUser) should take considerably longer (for me it was a shade over 2 minutes). If all is well the first control query should still be running (and still consuming the very soul out of your CPUs). If I were you I’d kill this initial query off now if it’s still going. It’ll do nothing for the performance of your PC!


image


To see the true affect of the Governor, one simply has to compare the results produced by the time statistics. Whilst the actual CPU used by both queries is broadly the same, the elapsed time for low user is almost exponentially higher. This is directly the effect of running under the pool where the maximum CPU is capped at 30%. The figures here also show running the same queries under no stress to show that without putting the server under load, the governor makes very little difference.


Finally for this post (and don’t worry – there will be a follow-up posting detailing some of the more intricate aspects of the Resource Governor) we’ll demonstrate another nice feature – moving Work Groups to different pools.

image



Moving a work group can be performed a any time, however the effect will only be felt with a Reconfiguration. Executing the following will result in an error:

Alter Workload Group Low Using High;
Alter Resource Governor Reconfigure;


Msg 10904, Level 16, State 2, Line 2

Resource governor configuration failed. There are active sessions in workload groups being dropped or moved to different resource pools. Disconnect all active sessions in the affected workload groups and try again.




What is needed here is to disconnect any sessions currently utilizing the Low Work Group. We can utilize the following query to find out which sessions are affected and kill them off by hand (and don’t forget to run the reconfiguration!)

Select    Session_Id, Status
From
sys.dm_exec_sessions As S
Inner Join sys.dm_resource_governor_workload_groups As G
On S.group_id = G.group_id
Where G.name = 'Low';



Now repeat the experiment by starting the Stress query first followed by the Low and finally the High.

image

As you can see the elapsed time is significantly less when the Work Group is moved into the High Resource Pool. The reason for the discrepancy between the two is down to the configuration of the two groups; the High Group is set at a High Priority and allowed to utilize all of the RAM the pool has been allocated, whereas the Low Work Group is limited to being able to grab a maximum of 25% of memory from the pool (the default) and a relative importance setting of Low.

That’s all for now – keep tuned for part two where we delve deeper into the mind of the guv’nor.

Sql Server 2008 Build 10.0.1600.22 (RTM)