Archive
Needing a change… Modify your Filename
A friend of mine told me that he felt a need for a change. Without question I said cool. Change is always inviting as it keeps things interesting. Little did I know or realize he was referring to the changing of his database file name. So he issued the simple ALTER DATABASE MODIFY FILE statement and specified a new name for his database.
ALTER DATABASE AdventureWorks MODIFY FILE (NAME = AdventureWorks_data, FILENAME = 'C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\DATA\AdventureWorks_data.mdf') ALTER DATABASE AdventureWorks MODIFY FILE (NAME = AdventureWorks_Log, FILENAME = 'C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\DATA\AdventureWorks_log.ldf')
and in the results were
The file "AdventureWorks_data" has been modified in the system catalog. The new path will be used the next time the database is started.
The file "AdventureWorks_Log" has been modified in the system catalog. The new path will be used the next time the database is started.
So he took this message as needing to restart the SQL Server Engine Service. Which he did; however, that did not satisfy the underlying issue. After refreshing the databases he attempted to expand the db and received a nice little error indicating that the database was not accessible. Which is when I received the call.
Having experienced this myself I remembered that:
1. the database needed to be taken offline
ALTER DATABASE AdventureWorks SET OFFLINE WITH ROLLBACK IMMEDIATE
2. he would need to physically rename the files accordingly
3. bring the database back online
ALTER DATABASE AdventureWorks SET ONLINE WITH ROLLBACK IMMEDIATE
Once these steps were completed he was back in business. On a side note this permitted the changing of the file names but you can also change the actual file path location for each of the files as well using the same syntax. If you wanted to update the logical name for both the data and log file below is the syntax for doing so.
ALTER DATABASE [AdventureWorks] MODIFY FILE (NAME=N'AdWks', NEWNAME=N'AdventureWorks_data') ALTER DATABASE [AdventureWorks] MODIFY FILE (NAME=N'AdWks_log', NEWNAME=N'AdventureWorks_log')
Dropping a subscriber database
Usually I can simply run an ALTER statement against the database and SET SINGLE_USER WITH NO_WAIT like so:
ALTER DATABASE MyDBName SET SINGLE_USER WITH NO_WAIT
but I ran into a problem with active processes that prevented me from switching to SINGLE_USER mode. So I executed the sp_Who2 stored procedure and identified the spids responsible and killed them!
EXEC sp_Who2

I essentially ran the kill command for every spid that was associated to the specific database. So you can see it was very inefficient and a great waste of time.
KILL 54 KILL 58
Now imagine if I had to do this will 200 subscribers… not a fun time!

Then I re-ran the stored procedure sp_Who2 again to make sure they were dead.
EXEC sp_Who2

This worked for me at the time, but later in the future I don’t want to deal with the hassles of running through all the steps involved. I turned to BOL and then I remembered I could easily query the sysprocesses and mock up a script but I didn’t really have the time to run through the whole Q/A process. So I searched the interwebs and stumbled upon the blog post [Script to drop all connections to a Database] by Tijo. It was exactly what I was looking for. So I borrowed it and merged it with some additional items that are specific to our clean up steps. In a nutshell here is the final outcome.
-- KILL ACTIVE PROCESSES
DECLARE @db NVARCHAR(50)
DECLARE @spid int
SET @db = 'myDBName'
SELECT @spid = MIN(spid)
FROM sys.sysprocesses
WHERE dbid = db_id(@db)
WHILE @spid IS NOT NULL
BEGIN
SELECT @spid
EXEC ('KILL ' + @spid)
SELECT @spid = MIN(spid)
FROM sys.sysprocesses
WHERE dbid = db_id(@db)
END
-- DROP DATABASE
DECLARE @cmd1 NVARCHAR(75)
DECLARE @cmd2 NVARCHAR(50)
SET @cmd1 = 'ALTER DATABASE ' + @db + ' SET SINGLE_USER WITH NO_WAIT'
SET @cmd2 = 'DROP DATABASE ' + @db
EXEC(@cmd1)
EXEC(@cmd2)
-- DROP USER ACCOUNT
DECLARE @cmd3 NVARCHAR(65)
IF EXISTS (SELECT * FROM sys.syslogins WHERE name = + HOST_NAME() + '\MergeAgent')
SET @cmd3 = 'DROP LOGIN [' + HOST_NAME() + '\MergeAgent]'
EXEC(@cmd3)
GO
Also thinking about the future I combined this sql script with a batch file using SQLCMD and provided it to the onsite support staff so that they can handle the reinitialization for subscriptions that fall outside of their threshold.
@echo off color 17 Title Device Reinit Clean Up . . . :: ========================= SET srv=(local)\Instance :: ========================= SQLCMD -S %srv% -E -i "Device.ReinitCleanUp.sql" PAUSE
Both files need to reside in the same directory in order to work; however, if you decide to call the sql script from a network share be sure to make the necessary adjustments. Reason being is that we don’t deploy management studio on the subscribers and some of the onsite technical support staff are unfamiliar with SQL Server. So I would rather not take any chances which is why I am going to add an additional condition.
IF HOST_NAME() = 'servername'
BEGIN
PRINT '**** WARNING ****'
PRINT 'You attempted to apply the cleanup script against the Production Server!'
PRINT 'Terminating process!'
END
ELSE
BEGIN
-- KILL ACTIVE PROCESSES
DECLARE @db NVARCHAR(50)
DECLARE @spid int
SET @db = 'myDBName'
SELECT @spid = MIN(spid)
FROM sys.sysprocesses
WHERE dbid = db_id(@db)
WHILE @spid IS NOT NULL
BEGIN
SELECT @spid
EXEC ('KILL ' + @spid)
SELECT @spid = MIN(spid)
FROM sys.sysprocesses
WHERE dbid = db_id(@db)
END
-- DROP DATABASE
DECLARE @cmd1 NVARCHAR(75)
DECLARE @cmd2 NVARCHAR(50)
SET @cmd1 = 'ALTER DATABASE ' + @db + ' SET SINGLE_USER WITH NO_WAIT'
SET @cmd2 = 'DROP DATABASE ' + @db
EXEC(@cmd1)
EXEC(@cmd2)
-- DROP USER ACCOUNT
DECLARE @cmd3 NVARCHAR(65)
IF EXISTS (SELECT * FROM sys.syslogins WHERE name = + HOST_NAME() + '\MergeAgent')
SET @cmd3 = 'DROP LOGIN [' + HOST_NAME() + '\MergeAgent]'
EXEC(@cmd3)
END
Calculate Subscription Expiration Threshold
While I was piecing together a few queries for a report intended to deliver subscription information about subscribers that are nearing the expiration threshold for replication I found myself needing to calculate warning threshold levels. In one case I found not only do I need to calculate the threshold warning criteria but I also needed to round the outcome to the nearest whole integer.
As you probably know the default retention period for replication defaults to 14 days; however, in replication monitor you can set up warnings to indicate if a subscriber is nearing the threshold. Typically this is 80% so in short I am going to take the retention value and multiple it by .8 and round to the nearest whole integer. For this example I really only need to query data from one table [MSreplication_monitordata] which resides within the distribution database. If you have replication setup then you will have a distribution database, but if you don’t then the distribution database will not be available. Also note this example is based on a single publication so if you have multiple publications then you will see more than one entry per subscriber per publication.
Step 1: Simple Query
I just want the retention period…
SELECT retention FROM [Distribution]..[MSreplication_monitordata]

Now we know what the retention period is set to and that is 14 days. Now I need to multiply the retention value by .8 and the result will be 11.2. So 80% of 14 = 11.2.
Step 2: Simple Calculation
SELECT retention
,(retention * .8)
FROM [Distribution]..[MSreplication_monitordata]

Side Note:
Keep in mind if you want to get the threshold value of the metric being monitored from the database JOIN [Distribution]..[MSpublicationthresholds] t ON m.publication_id = t.publication_id and pull the t.[value] which will return the value of 80 which is the default. If it was changed then of course the value will differ. Just note that this is a sql_variant data type so you will need to CAST or CONVERT it to perform the calculation.
SELECT m.retention ,t.[value] -- These steps are just for illustration ,(CAST(t.[value] AS DECIMAL(10,2))/100) '1' ,(m.[retention] * (CAST(t.[value] AS DECIMAL(10,2))/100)) '2' ,CAST((m.[retention] * (CAST(t.[value] AS DECIMAL(10,2))/100)) AS INT) '3' -- The following is faster and easier ,(m.retention * CAST(t.[value] AS INT)/100) '4' FROM [Distribution]..[MSreplication_monitordata] m JOIN [Distribution]..[MSpublicationthresholds] t ON m.publication_id = t.publication_id

What this means in terms of Replication Monitor is that any subscriber that has not synchronized for 11 days with the retention set to 14 days will have a subscription status of “Expiring Soon/Expired“. If the retention was set to 30 days which is also common and the threshold set to the default of 80% then the subscribers would show the same the status when they reach 24 days without synchronizing.
Step 3: Round the Results
11.2 is a good start, but I need a whole number so I will wrap line 2 from (Step 2) inside a ROUND function and supply 0 for the length or precision as it needs one argument.
SELECT retention
,ROUND((retention * .8) ,0)
FROM [Distribution]..[MSreplication_monitordata]

Step 4: Remove the Decimal
The above statement returns a value of 11.0 which is rounded, but now I need to get rid of the decimal place so that 11 is the only thing returned. Again I will take line 2 this time from (Step 3) and wrap it in a CAST function and CAST it as INT.
SELECT retention
,CAST(ROUND((retention * .8) ,0) AS INT)
FROM [Distribution]..[MSreplication_monitordata]

Or you could have saved yourself a few steps and done this…
SELECT retention
,CAST((retention * .8) AS INT)
FROM [Distribution]..[MSreplication_monitordata]

Final Results
When I apply the rounded value to my query as part of the WHERE clause only the records that fall within the expiration threshold (retention * .8) will be returned. Meaning subscriptions that are close to expiration and subscriptions that are expired.
The results are exactly what I wanted. Now I just need to take it a bit further… think email delivery!
I have wrapped this into a stored procedure and added some logic that will check for expiring records and if there are records found it will then tie the results into a nice html formatted message delivered by database mail. Then I setup a sql job which runs the stored procedure daily including weekends and fires off an email only when records are found. Technically I could fire off a report subscription which can be formatted very nicely and just fire off the job using exec sp_start_job @JobName = ‘Report Subscription ID goes here’ when ever records are detected.
For more information about using ROUND please visit: http://msdn.microsoft.com/en-us/library/ms175003.aspx
Download the code
SELECT.GetExpiringSubscriptions_v0.01.sql
/*****************************************************************
** Author : Samson J. Loo (justsamson.com | @just_samson)
** Created : 9/26/2010
** Intent : Notification of subscriptions that are nearing the
** the expiration threshold
** Notes : Requires db mail
** Version : 0.01
*****************************************************************/
IF OBJECT_ID('sp_RP_GetExpiringSubscribers') IS NOT NULL
DROP PROCEDURE sp_RP_GetExpiringSubscribers
GO
CREATE PROCEDURE [dbo].[sp_RP_GetExpiringSubscribers]
AS
DECLARE @cnt INT
SET @cnt = 0
SELECT @cnt = COUNT(a.[subscriber_name])
FROM [Distribution]..[MSmerge_sessions] s
JOIN [Distribution]..[MSmerge_agents] a ON s.agent_id = a.id
JOIN [Distribution]..[MSreplication_monitordata] m ON s.agent_id = m.agent_id
JOIN [Distribution]..[MSpublicationthresholds] t ON m.publication_id = t.publication_id
WHERE s.end_time IN
(
SELECT TOP 1 s1.end_time
FROM [Distribution]..[MSmerge_sessions] s1
WHERE s.agent_id = s1.agent_id
ORDER BY s1.end_time DESC
) AND DATEDIFF(d,s.[start_time],getdate()) >= CAST((CAST(t.[value] AS DECIMAL)/100)* m.[retention] AS INT)
AND t.[isenabled] = 1
GROUP BY s.[start_time]
IF @cnt > 0
BEGIN
DECLARE @tableHTML NVARCHAR(MAX) ;
SET @tableHTML =
N'<H1><font color="#FF0000">Expiring Subscription Report</font></H1>' +
N'<table border="0" cellspacing="2" cellpadding="2">' +
N'<tr><th bgcolor="#BDBDBD">Subscriber</th>' +
N'<th bgcolor="#BDBDBD">Status</th>
<th bgcolor="#BDBDBD">Delivery Rate</th>
<th bgcolor="#BDBDBD">Last Sync</th>' +
N'<th bgcolor="#BDBDBD">Duration</th>
<th bgcolor="#BDBDBD">Conn Type</th>
<th bgcolor="#BDBDBD">Result</th>
<th bgcolor="#BDBDBD">Days Behind</th>
<th bgcolor="#BDBDBD">Subscriber Status</th></tr>' +
CAST ( (
SELECT
td = CASE
WHEN CHARINDEX('\',a.[subscriber_name]) > 0 THEN LEFT(a.[subscriber_name],CHARINDEX('\',a.[subscriber_name])-1)
ELSE a.[subscriber_name]
END
,''
,td = CASE
WHEN s.[runstatus] = 3 THEN 'Synchornizing'
WHEN s.[runstatus] = 5 THEN 'Retrying failed command'
ELSE 'Not Synchronizing'
END
,''
,td = CAST(s.[delivery_rate] AS VARCHAR) + ' rows/sec'
,''
,td = s.[start_time]
,''
,td = CAST((s.[duration]/86400) AS VARCHAR)
+ '.' + CAST(REPLACE(STR(((s.[duration]/3600) - ((s.[duration]/86400) * 24)),2),SPACE(1),0) AS VARCHAR)
+ ':' + CAST(REPLACE(STR((s.[duration] % 3600/60),2),SPACE(1),0) AS VARCHAR)
+ ':' + CAST(REPLACE(STR((s.[duration] % 60),2),SPACE(1),0) AS VARCHAR)
,''
,td = CASE
WHEN s.[connection_type] = 1 THEN 'LAN'
WHEN s.[connection_type] = 2 THEN 'Dialup'
WHEN s.[connection_type] = 3 THEN 'Web Sync'
END
,''
,td = CASE
WHEN s.[runstatus] = 1 THEN 'Start'
WHEN s.[runstatus] = 2 THEN 'Succeed'
WHEN s.[runstatus] = 3 THEN 'In Progress'
WHEN s.[runstatus] = 4 THEN 'Idle'
WHEN s.[runstatus] = 5 THEN 'Retry'
WHEN s.[runstatus] = 6 THEN 'Error'
END
,''
,td = DATEDIFF(d,s.[start_time],getdate())
,''
,td = CASE
WHEN (DATEDIFF(d,s.[start_time],getdate()) < CAST((CAST(t.[value] AS DECIMAL)/100)* m.[retention] AS INT)) THEN 'Good'
WHEN (DATEDIFF(d,s.[start_time],getdate()) <= m.[retention]) THEN 'Expiring Soon'
WHEN (DATEDIFF(d,s.[start_time],getdate()) > m.[retention]) THEN 'Expired'
END
--,m.[retention]
FROM [Distribution]..[MSmerge_sessions] s
JOIN [Distribution]..[MSmerge_agents] a ON s.agent_id = a.id
JOIN [Distribution]..[MSreplication_monitordata] m ON a.id = m.agent_id
JOIN [Distribution]..[MSpublicationthresholds] t ON m.publication_id = t.publication_id
WHERE s.end_time IN
(
SELECT TOP 1 s1.end_time
FROM [Distribution]..[MSmerge_sessions] s1
WHERE s.agent_id = s1.agent_id
ORDER BY s1.end_time DESC
) AND DATEDIFF(d,s.[start_time],getdate()) >= CAST((CAST(t.[value] AS DECIMAL)/100)* m.[retention] AS INT)
AND t.[isenabled] = 1
ORDER BY s.[start_time]
FOR XML PATH('tr'), TYPE
) AS NVARCHAR(MAX) ) +
N'</table>' ;
EXEC msdb.dbo.sp_send_dbmail
@profile_name = 'WorkingNotifier',
@recipients='you@yourdomain.com',
@copy_recipients='someone@somewhere.com',
@subject = 'Expiring Subscription Report',
@body = @tableHTML,
@body_format = 'HTML' ;
END
ELSE
BEGIN
PRINT 'No Records Found!'
END
Date Parameters and Things to Consider
I touched on Reporting Services Date Parameters, but what I did not cover is the underlying importance of understanding how TSQL works in terms of date parameters. You need to know that there is an implicit midnight time-stamp of 00:00:00 when a date is only supplied. So when you are dealing with date parameters in terms of FROM and TO dates, the TO will need or should include all records for that date as well. However, since a date is only supplied your chances of including all records for that end date are very slim. I was able to correct this by adding a modifier in stored procedures that takes the input date and increments it by one day.
For illustration purposes here are some TSQL scripts that will allow you to test a few queries to see what is actually begin returned when you only pass a date to a DATETIME field. This will help you understand what the results of your report will be as well.
Step 1: Create Table
CREATE TABLE #TestDate ( [ID] [int] IDENTITY(1,1) NOT NULL, [StandardDT] [datetime] NOT NULL, [MyKey] [int] NOT NULL, [Notes] [nvarchar](100) NULL, )
Step 2: Populate Table
INSERT INTO #TestDate (MyKey, StandardDT, Notes) SELECT 1, '1/1/2010', 'Note 1' UNION ALL SELECT 2, '2/1/2010', 'Note 2' UNION ALL SELECT 3, '3/1/2010', 'Note 3' UNION ALL SELECT 4, '4/1/2010 10:42:22', 'Note 4' UNION ALL SELECT 5, '4/1/2010 22:42:22', 'Note 5' UNION ALL SELECT 6, '6/1/2010', 'Note 6' UNION ALL SELECT 7, '7/1/2010', 'Note 7' UNION ALL SELECT 8, '7/1/2010 01:10:19', 'Note 8' UNION ALL SELECT 9, '7/1/2010 02:11:20', 'Note 9' UNION ALL SELECT 10, '7/1/2010 21:21:22', 'Note 10'
Step 3: Verify Data
SELECT * FROM #TestDate
As you can see we have 10 records total with some records that have a time-stamp associated that are beyond midnight specifically April and July while the the other months are midnight.

Step 4: Test Queries
Let’s run through some test queries to help better illustrate my point. Let’s query the data using a FROM date of 3/1/2010 and a TO date of 4/1/2010. Looking at the data there is one record for 3/1/2010 and two records from 4/1/2010. How many records do you think will be returned? Well let’s just see for ourselves.
DECLARE @StartDate DATETIME, @EndDate DATETIME SET @StartDate = '3/1/2010' SET @EndDate = '4/1/2010' SELECT * FROM #TestDate WHERE StandardDT BETWEEN @StartDate AND @EndDate SELECT @StartDate SELECT @EndDate
Interesting… there is only one record returned even though 4/1/2010 is within our query parameters. Take a look at lines 8 & 9 from the query above and pay attention to the time-stamps. You can see in the screen cast below that the TO date which is represented by @EndDate parameter is 4/1/2010 00:00:00 which is 4/1/2010 midnight. Well looking through the data proves we do not have any records that meet that specific criteria because there are no records for 4/1/2010 that have a midnight time-stamp associated.

Let’s try a few more test queries just to get a clearer picture. Let’s see how many records are returned when we search FROM 7/1/2010 TO 7/1/2010. We know that our data set contains four records for 7/1/2010, but can you guess how many records will be returned if we want to search FROM 7/1/2010 TO 7/1/2010?
DECLARE @StartDate DATETIME, @EndDate DATETIME SET @StartDate = '7/1/2010' SET @EndDate = '7/1/2010' SELECT * FROM #TestDate WHERE StandardDT BETWEEN @StartDate AND @EndDate SELECT @StartDate SELECT @EndDate
There was only one record returned because there was only one record that met the criteria 100%. If you look at the values for @StartDate and @EndDate they both are returning 7/1/2010 00:00:00.

Now let’s run through one last query, because practice makes perfect. If I were to search our data set for all records from 4/1/2010 and supply a FROM date of 4/1/2010 and a TO date of 4/1/2010 how many records would be returned? The answer is zero, because both you and I know there are no records within the data set that have a time-stamp of 00:00:00 for 4/1/2010.
DECLARE @StartDate DATETIME, @EndDate DATETIME SET @StartDate = '4/1/2010' SET @EndDate = '4/1/2010' SELECT * FROM #TestDate WHERE StandardDT BETWEEN @StartDate AND @EndDate SELECT @StartDate SELECT @EndDate
Perfect. No records returned just as we thought.

Step 5: Solution
The easiest way to rectify this is to ask the end users to use the next day, but that is not a practical answer because you and I both know after the first weekend or vacation the end users will return to their normal mode and begin searching through the data using the date they did before. So in order to satisfy the requirements I decided to add a parameter modifier to my procedure which takes the received input value for the @EndDate and increments the day by one. Meaning if 7/1/2010 were passed as the TO or @EndDate value it would become 7/2/2010 00:00:00 and all records from 7/1/2010 will be returned. Pretty simple.
ALTER PROCEDURE sp_RP_MyReport ( @StartDate DATETIME ,@EndDate DATETIME ) AS SET @EndDate = DATEADD(d,1,@EndDate)
Line #: 9 in the above syntax is what I implemented which makes the adjustment to the @EndDate parameter.
Step 6: Validate Solution
Let’s add the modifier just below the parameter declarations and above the SELECT statement and see what we get when we execute the script. Can you guess what will be returned?
DECLARE @StartDate DATETIME, @EndDate DATETIME SET @StartDate = '3/1/2010' SET @EndDate = '4/1/2010' SET @EndDate = DATEADD(d,1,@EndDate) SELECT * FROM #TestDate WHERE StandardDT BETWEEN @StartDate AND @EndDate SELECT @StartDate SELECT @EndDate
Success! Three records returned. By adding the modifier (Line #: 5) the value was accepted, incremented by 1 and supplied back to the SELECT statement and the records which fell into the criteria were returned.

Step 7: Cleanup
Now that we are all squared away, let’s try not forget that we still need to drop the temp table.
DROP TABLE #TestDate
Side Note
You can always use the DATEADD in the BETWEEN statement like below in line 06.
DECLARE @StartDate DATETIME, @EndDate DATETIME SET @StartDate = '3/1/2010' SET @EndDate = '4/1/2010' SELECT * FROM #TestDate WHERE StandardDT BETWEEN @StartDate AND DATEADD(d,1,@EndDate)
Format Seconds into HH:MM:SS
Without having to use replication monitor I decided to put together a stored procedure that will return the same results. In the MSmerge_sessions table of the Distribution database the field duration is returned in raw seconds. Meaning that if the process took 2 mins and 2 secs it displayed as 122. Well I suppose I could live with it but I am hard-headed and wanted the outcome to look similarly formatted HH:MM:SS. Without really getting into the specifics on joining tables I used the MSmerge_Agents a and MSmerge_Sessions s joining on s.agent_id to a.id and added a.subscriber_name to my query to display the subscriber name but that is beside the point of this post.
Calculating Hours
The first thing you should know is how to calculate seconds into hours and minutes. Below outlines how to do each, but before we jump into that let’s try it manually.
There are 60 seconds in one minute and there are 60 minutes in one hour. So it is 60*60 which equals 3600. 3600 is the amount of seconds in one hour.
Now let’s take 3723 seconds and divide it by 3600.
What do we get?
3723/3600 = 1.03416 just a tad more than one hour
If you run the following in Management Studio you will notice that one is returned. You will not get (.03416) trailing the one. So calculating hours is easy since it returns whole numbers.
SELECT (3723/3600)

Calculating Minutes
In order to calculate minutes you will need to perform a calculation similar to the one above with the exception of asking for the remainder using the modulo operator. Which is stated as: (3723 % 3600). However this alone will not return the results you are looking for. You need to take it a step further and divide it by 60.
Now we have:
((3723 % 3600) / 60) = 2 minutes
If you run the following in Management Studio you will get the same result as I stated above.
SELECT ((3723 % 3600) / 60)

Calculating Seconds
Alright we are almost there. Now we need to get the seconds after everything is all said and done. This is another easy one. We take 3723 and get the remainder by using 3723 % 60.
SELECT (3723 % 60)

Run it together
Here is a quick and non-formatted way to see the results.
DECLARE @secs INT SET @secs = 3723 SELECT (@secs / 3600) 'Hrs' SELECT ((@secs % 3600)/60) 'Mins' SELECT (@secs % 60) 'Secs'

Stop! Format Time!
The purpose of this post was to format this field appropriately and here is what I did.
DECLARE @secs INT SET @secs = 3723 SELECT CONVERT(VARCHAR(10),CONVERT(datetime,CAST((@secs / 3600) AS VARCHAR) + ':' + CAST(((@secs % 3600)/60) AS VARCHAR) + ':' + CAST((@secs % 60) AS VARCHAR)),108)

Final Results Set

Using sp_MSforeachtable
I actually stumbled upon this very stored procedure on the blog post by Suprotim Agarwal of sqlservercurry.com titled: How to skip a Table or Database while using sp_MSforeachdb or sp_MSforeachtable while just doing a little research about SQL Server. I honestly didn’t know this existed. I barely learned about the stored procedure “sp_MSforeachdb” only after attending Brent Ozar’s BLITZ! 60 Minute SQL Server Takeovers presentation during the 24hrs of PASS event. I walked away with valuable information as Brent delivered the content very well. So when I learned of “sp_MSforeachtable” I had to immediately test it.
I have used “sp_spaceused” in the past and of course I constructed a CURSOR that looped through the tables using either INFORMATION_SCHEMA.TABLES or sys.tables. This time around I wanted to achieve the same results without a CURSOR and thankfully “sp_MSforeachtable” with a little creativity allowed me to do so. So here is what I came up with.
CREATE TABLE #tmpTblSpace ( ID INT IDENTITY(1,1) ,name NVARCHAR(75) ,rows INT ,reserved NVARCHAR(50) ,data NVARCHAR(50) ,index_size NVARCHAR(50) ,unsed NVARCHAR(50) ) INSERT INTO #tmpTblSpace EXEC sp_MSforeachtable 'EXEC sp_spaceused ''?''' SELECT * FROM #tmpTblSpace DROP TABLE #tmpTblSpace
Results
If I had just ran (Line: 12) alone the results are not quite as appeasing which is precisely why I created the temp table, better formatting.
EXEC sp_MSforeachtable 'EXEC sp_spaceused ''?'''
Results
Amended
As I was practicing database mirroring with SQL Server 2008 I loaded up the sample databases (AdventureWorks) and needed to change the recovery model from Simple to Full. Then it dawned on me… that you can use the sp_MSForEachDB to change the recovery model for all of the adventureworks databases. Here is how it’s done.
EXEC sp_MSforeachdb
'IF (''?'' NOT IN (''master'', ''tempdb'', ''model'', ''msdb''))
EXECUTE (''ALTER DATABASE [?] SET RECOVERY FULL'')'
UNION and Reporting Services 2008
I have used the UNION operator a few times in the past and during a recent Business Intelligence project I quite possibly used it more then I had in the past combined. Typically you can merge queries together simply by placing a UNION operator between two queries. Well it’s not that simple there are some restrictions.
- the number of fields must be the same in both queries
- the order of the fields must be the same in both queries
- data types must be compatible
What you don’t believe me? Well then visit the TechNet site: UNION Transact-SQL and see it for yourself.
syntax
The syntax is really nothing…
SELECT Column1, Column2 FROM table1 UNION SELECT Column1, Column2 FROM table1
That’s the gist of the it. Kind of boring right? Yeah a little bit. When we incorporate it within a stored procedure that is used for Reporting Services it becomes a tad more interesting. I can’t speak for you but at least it is for me. Now let’s get into some scenarios where it comes handy.
Let’s say I want to offer a report that uses a dropdown list for selecting and passing the parameters. Well typically in a select list you can only choose one, but what if you wanted an option to select all? This is where the UNION comes in handy.
procedure
Typically you would construct your procedure similarly to the one below.
USE [AdventureWorksDW2008] GO IF EXISTS (SELECT name FROM sys.objects WHERE (name = 'sp_RP_GetProspectBuyerData')) DROP PROCEDURE sp_RP_GetProspectBuyerData GO CREATE PROCEDURE sp_RP_GetProspectBuyerData ( @id INT ) AS SELECT ProspectiveBuyerKey ,FirstName ,MiddleName ,LastName ,CONVERT(varchar,BirthDate,101) 'DOB' ,EmailAddress FROM [AdventureWorksDW2008].[dbo].[ProspectiveBuyer] WHERE (ProspectiveBuyerKey = @id)
The basics of the procedure is a simple SELECT statement. So let’s take a look at the results when I just run (lines: 12-18) only.
SELECT ProspectiveBuyerKey ,FirstName ,MiddleName ,LastName ,CONVERT(varchar,BirthDate,101) 'DOB' ,EmailAddress FROM [AdventureWorksDW2008].[dbo].[ProspectiveBuyer]
The only problem with this procedure is with (line: 19). The choice is either one value or another. There is not option for a “show all value” to be passed.
WHERE (ProspectiveBuyerKey = @id)
This is where the UNION operators comes in. Not necessarily in the stored procedure per se but in the dataset you create to populate the select list. However you still need to modify the stored procedure to prepare for whats to come. This is a slight modification to the WHERE clause of the stored procedure.
WHERE (ProspectiveBuyerKey = @id OR @id = -1)
Here is the final base for the “sp_RP_GetProspectBuyerData” stored procedure with the added OR operator.
USE [AdventureWorksDW2008] GO IF EXISTS (SELECT name FROM sys.objects WHERE (name = 'sp_RP_GetProspectBuyerData')) DROP PROCEDURE sp_RP_GetProspectBuyerData GO CREATE PROCEDURE sp_RP_GetProspectBuyerData ( @id INT ) AS SELECT ProspectiveBuyerKey ,FirstName ,MiddleName ,LastName ,CONVERT(varchar,BirthDate,101) 'DOB' ,EmailAddress FROM [AdventureWorksDW2008].[dbo].[ProspectiveBuyer] WHERE (ProspectiveBuyerKey = @id OR @id = -1)
dataset
Now we need to construct the dataset stored procedure that we will use to populate the select list which will be used in our report. This is the basics of the stored procedure that we will be using to drive the select list on the report.
USE [AdventureWorksDW2008] GO IF EXISTS (SELECT name FROM sys.objects WHERE (name = 'sp_RP_ProspectBuyerDDL')) DROP PROCEDURE sp_RP_ProspectBuyerDDL GO CREATE PROCEDURE sp_RP_ProspectBuyerDDL AS SELECT [ProspectiveBuyerKey] 'ID' ,[FirstName] + SPACE(1) + [LastName] 'Full Name' FROM [AdventureWorksDW2008].[dbo].[ProspectiveBuyer]
Well the above procedure is cool and all but it does not meet our needs as we need to have an “ALL” option listed as a selectable item in the select list. In order to achieve this let’s add another SELECT statement that will correspond to the changes we made to the first procedure “sp_RP_GetProspectBuyerData” where we added the following: ( OR @id = -1 ) and use UNION operator to merge them.
To list the “ALL” in our select list we need to add it using the same data type, order and we must have the same amount of fields. So here is what we need.
SELECT -1 AS 'ID' ,'<ALL>' AS 'Full Name'
So now we need to merge the two SELECT statements and this is how its done.
Step 1: First we take…
SELECT -1 AS 'ID' ,'<ALL>' AS 'Full Name'
Step 2: Then we add
UNION
Step 3: And finally we append…
SELECT [ProspectiveBuyerKey] 'ID' ,[FirstName] + SPACE(1) + [LastName] 'Full Name' FROM [AdventureWorksDW2008].[dbo].[ProspectiveBuyer]
Step 4: We wrap them into a stored procedure
USE [AdventureWorksDW2008] GO IF EXISTS (SELECT name FROM sys.objects WHERE (name = 'sp_RP_ProspectBuyerDDL')) DROP PROCEDURE sp_RP_ProspectBuyerDDL GO CREATE PROCEDURE sp_RP_ProspectBuyerDDL AS SELECT -1 AS 'ID' ,'<ALL>' AS 'Full Name' UNION SELECT [ProspectiveBuyerKey] 'ID' ,[FirstName] + SPACE(1) + [LastName] 'Full Name' FROM [AdventureWorksDW2008].[dbo].[ProspectiveBuyer]
Now if we run (lines: 9-15) only here is what the results look like.
As you can see the “ALL” is on top which is what we need and for the sake of testing. Let’s run through two quick scenario of passing a value of 1 and a value of -1 to the @id parameter just to see what the end results look like.
Here I set the @id variable to 1…
USE [AdventureWorksDW2008] DECLARE @id INT = 1 SELECT ProspectiveBuyerKey ,FirstName ,MiddleName ,LastName ,CONVERT(varchar,BirthDate,101) 'DOB' ,EmailAddress FROM [AdventureWorksDW2008].[dbo].[ProspectiveBuyer] WHERE (ProspectiveBuyerKey = @id OR @id = -1)
Now let’s see what happens when we pass a value of -1…
USE [AdventureWorksDW2008] DECLARE @id INT = -1 SELECT ProspectiveBuyerKey ,FirstName ,MiddleName ,LastName ,CONVERT(varchar,BirthDate,101) 'DOB' ,EmailAddress FROM [AdventureWorksDW2008].[dbo].[ProspectiveBuyer] WHERE (ProspectiveBuyerKey = @id OR @id = -1)
Boom! All records are returned. Exactly what I needed. Now let’s run through the report! I already set the available values for the parameter properties so we can just jump right into the report and see it in action.
Business Intelligence Development Studio
Step 1: Select All
Step 2: Run Report
Results when value other than All is selected















