![]() |
|
Identify and resolve SQL Server problems before they happen

|
|
By: Aaron Bertrand | Read Comments (15) | Related Tips: More > Scripts |
Problem
While the system procedure sp_MSforeachdb is neither documented nor officially supported, most SQL Server professionals have used it at one time or another. This is typically for ad hoc maintenance tasks, but many people (myself included) have used this type of looping activity in permanent routines. Sadly, I have discovered instances where, under heavy load and/or with a large number of databases, the procedure can actually skip multiple catalogs with no error or warning message. Since this situation is not easily reproducible, and since Microsoft typically has no interest in fixing unsupported objects, this may be happening in your environment right now.
Solution
In my environment, the minute I discovered the issue, I promptly wrote a replacement. (I blogged about my replacement earlier this year.) While I was writing the new stored procedure, it struck me that, while I was making my maintenance processes more reliable, I could also make them more flexible.
For example, I could have the procedure operate only on databases that:
Some other handy options I thought to add, which aren't in sp_MSforeachdb, include an option to print the database name before each result, or even to only print the command instead of executing. This can be very handy if you are trying to set a slew of databases to SINGLE_USER and don't want the operations to happen serially; you can print the commands and split the output across multiple Management Studio windows.
With all that said, here is the stored procedure in its current form:
USE [master];
GO
CREATE PROCEDURE dbo.sp_foreachdb
@command NVARCHAR(MAX),
@replace_character NCHAR(1) = N'?',
@print_dbname BIT = 0,
@print_command_only BIT = 0,
@suppress_quotename BIT = 0,
@system_only BIT = NULL,
@user_only BIT = NULL,
@name_pattern NVARCHAR(300) = N'%',
@database_list NVARCHAR(MAX) = NULL,
@recovery_model_desc NVARCHAR(120) = NULL,
@compatibility_level TINYINT = NULL,
@state_desc NVARCHAR(120) = N'ONLINE',
@is_read_only BIT = 0,
@is_auto_close_on BIT = NULL,
@is_auto_shrink_on BIT = NULL,
@is_broker_enabled BIT = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE
@sql NVARCHAR(MAX),
@dblist NVARCHAR(MAX),
@db NVARCHAR(300),
@i INT;
IF @database_list > N''
BEGIN
;WITH n(n) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY s1.name) - 1
FROM sys.objects AS s1
CROSS JOIN sys.objects AS s2
)
SELECT @dblist = REPLACE(REPLACE(REPLACE(x,'</x><x>',','),
'</x>',''),'<x>','')
FROM
(
SELECT DISTINCT x = 'N''' + LTRIM(RTRIM(SUBSTRING(
@database_list, n,
CHARINDEX(',', @database_list + ',', n) - n))) + ''''
FROM n WHERE n <= LEN(@database_list)
AND SUBSTRING(',' + @database_list, n, 1) = ','
FOR XML PATH('')
) AS y(x);
END
CREATE TABLE #x(db NVARCHAR(300));
SET @sql = N'SELECT name FROM sys.databases WHERE 1=1'
+ CASE WHEN @system_only = 1 THEN
' AND database_id IN (1,2,3,4)'
ELSE '' END
+ CASE WHEN @user_only = 1 THEN
' AND database_id NOT IN (1,2,3,4)'
ELSE '' END
+ CASE WHEN @name_pattern <> N'%' THEN
' AND name LIKE N''%' + REPLACE(@name_pattern, '''', '''''') + '%'''
ELSE '' END
+ CASE WHEN @dblist IS NOT NULL THEN
' AND name IN (' + @dblist + ')'
ELSE '' END
+ CASE WHEN @recovery_model_desc IS NOT NULL THEN
' AND recovery_model_desc = N''' + @recovery_model_desc + ''''
ELSE '' END
+ CASE WHEN @compatibility_level IS NOT NULL THEN
' AND compatibility_level = ' + RTRIM(@compatibility_level)
ELSE '' END
+ CASE WHEN @state_desc IS NOT NULL THEN
' AND state_desc = N''' + @state_desc + ''''
ELSE '' END
+ CASE WHEN @is_read_only IS NOT NULL THEN
' AND is_read_only = ' + RTRIM(@is_read_only)
ELSE '' END
+ CASE WHEN @is_auto_close_on IS NOT NULL THEN
' AND is_auto_close_on = ' + RTRIM(@is_auto_close_on)
ELSE '' END
+ CASE WHEN @is_auto_shrink_on IS NOT NULL THEN
' AND is_auto_shrink_on = ' + RTRIM(@is_auto_shrink_on)
ELSE '' END
+ CASE WHEN @is_broker_enabled IS NOT NULL THEN
' AND is_broker_enabled = ' + RTRIM(@is_broker_enabled)
ELSE '' END;
INSERT #x EXEC sp_executesql @sql;
DECLARE c CURSOR
LOCAL FORWARD_ONLY STATIC READ_ONLY
FOR SELECT CASE WHEN @suppress_quotename = 1 THEN
db
ELSE
QUOTENAME(db)
END
FROM #x ORDER BY db;
OPEN c;
FETCH NEXT FROM c INTO @db;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @sql = REPLACE(@command, @replace_character, @db);
IF @print_command_only = 1
BEGIN
PRINT '/* For ' + @db + ': */'
+ CHAR(13) + CHAR(10) + CHAR(13) + CHAR(10)
+ @sql
+ CHAR(13) + CHAR(10) + CHAR(13) + CHAR(10);
END
ELSE
BEGIN
IF @print_dbname = 1
BEGIN
PRINT '/* ' + @db + ' */';
END
EXEC sp_executesql @sql;
END
FETCH NEXT FROM c INTO @db;
END
CLOSE c;
DEALLOCATE c;
END
GO Caveats
The procedure doesn't cope well with databases with a single quote ( ' ) in their name or with leading / trailing spaces, but it gladly handles databases that violate other best practices, such as beginning with a number or containing special characters like ., ", [, or ]. Here is a quick list of databases that it has been tested against:
(Try creating those databases on your system and running EXEC sp_MSforeachdb 'SELECT * FROM ?.sys.objects;'; - you'll get a variety of errors.)
Also, you do not need to QUOTENAME parameter values... you should pass in 'master, model' to @database_list, not '[master], [model]', and you should use 'USE ?;' and not 'USE [?];' for the command and replace_character values - this escaping is handled for you. However, if you have a command where you want to be able to selectively choose whether or not to apply QUOTENAME to the replace_character (for example, @command = 'SELECT ''[?]'', * FROM sys.databases WHERE name = ''?'';'), you can use the override parameter @suppress_quotename.
While there are parsing solutions for all of these problems, they quickly explode the code and become more maintenance trouble than they're worth. At least, in this author's opinion.
Finally, the procedure does not currently include any logging or error handling, which you may want to add if you are going to use this type of procedure in any automated processes.
Examples
To perform a full backup to the same folder of all user databases that are in simple mode. This is one case where you'll want to use the @suppress_quotename parameter, otherwise you end up with files named [database_name].bak.
To search all databases matching the name pattern 'Company%' for objects matching the name pattern '%foo%'. Place into a #temp table so the result is a single result set instead of the number of databases that match the naming pattern.
To turn auto_shrink off for all databases where it is enabled:
To find the last created object date/time for each database in a defined set (in this case, three databases that I know exist).
To reset service broker for every database - after testing an application, for example:
Next Steps
| Wednesday, December 29, 2010 - 10:12:22 AM - Tim Plas | Read The Tip |
|
Very useful; thanks for creating it & posting it. If you're considering any enhancements to it, one feature I occasionally wish for, is the capability to select "all-except" certain databases. Perhaps implemented like Ola Hallengren does, by preceeding DB names with a dash if you want them to be skipped. Anyway, thanks again.
|
|
| Wednesday, December 29, 2010 - 10:13:49 AM - johns | Read The Tip |
|
Aaron, Thanks for rewriting a very useful procedure. I feel free to use it in my production jobs now that it is documentated and maintainable. Note - I will change the name to sp_dba_foreachdb. This naming convention keeps all the procs that I have put into master grouped together and helps avoid naming conflicts. Thanks again.
|
|
| Tuesday, January 18, 2011 - 1:13:50 AM - Oliver Holloway | Read The Tip |
|
Have recently been seeing this in my production systems, thanks for this article! |
|
| Tuesday, January 18, 2011 - 12:24:09 PM - Howard | Read The Tip |
|
Thanks for posting. I've seen numerous times where sp_MSforeachdb skips seemingly random databases. Looking forward to trying this solution. |
|
| Friday, January 28, 2011 - 12:02:18 AM - Adam | Read The Tip |
|
This was a very nice re-write. Great new added functionality. |
|
| Wednesday, February 09, 2011 - 10:32:44 AM - Oliver Holloway | Read The Tip |
|
Thank you for this replacement proc. I added a variable @affects_tempdb defaulted to 0 to allow proc to be used for backups for all databases without having to work around the tempdb. + CASE WHEN @affects_tempdb = 0 THEN ' AND name <> ''tempdb''' ELSE '' END |
|
| Wednesday, February 09, 2011 - 10:52:00 AM - Oliver Holloway | Read The Tip |
|
@Tim Plas, this was not too difficult, if you are willing to feed a prepped string. I used a variable named @exclude_list, defaulted to NULL, and added the following in the SET @sql statement: + CASE WHEN @exclude_list IS NOT NULL THEN ' AND name NOT IN (' + @exclude_list + ')' ELSE '' END I was then able to add this line to the proc call: @exclude_list = '''master'', ''model''' and received the expected result set.
|
|
| Wednesday, February 09, 2011 - 5:08:59 PM - Oliver Holloway | Read The Tip |
|
@Aaron Bertrand, there seems to be a bit of confusion between the auto_close and auto_shrink lines in the SET @sql statement. |
|
| Wednesday, February 09, 2011 - 5:30:26 PM - Adam Gojdas | Read The Tip |
|
I had a bad feeling about these stored procedures (sp_MSforeachtable, sp_MSforeachDB, sp_MSforeach_Worker) a good while back also when I saw them using a global cursor. I also took a stab at re-writing them into a single stored proc. I mostly followed along the logic of the original fixing what was bad practice and encapsulated everything in a try/catch. However I replace what scared me the most about them, the global cursor, with a dynamic local cursor. I set it up the following way: /* Create the SELECT */ |
|
| Thursday, February 10, 2011 - 3:50:20 PM - Aaron Bertrand | Read The Tip |
|
Thanks Oliver, errant copy & paste. I'll submit a revision. |
|
| Tuesday, April 26, 2011 - 3:05:36 PM - Eddy White | Read The Tip |
|
To address some of the Dynamic T-SQL you can also create a user defined table-valued function that returns a table. We use this a lot! In our case, we use semi-colon as the delimiter, however you could change the function to use a different delimeter if you need to. CODE: CREATE FUNCTION [dbo].[udfParsedString]
(
@List varchar(8000))
RETURNS
@ParsedString table (RetStr varchar(80 )) AS BEGIN
-- Assumes the returned parced value is 80 characters DECLARE @column varchar(80), @Pos int
SET @List = LTRIM(RTRIM(@List))+ ';'
SET @Pos = CHARINDEX(';',@List,1)
IF REPLACE(@List, ';', '') <> ''
-- If there are values in the list, go and process parameters
BEGIN
WHILE @Pos > 0
BEGIN
SET @column = LTRIM(RTRIM(LEFT(@List, @Pos - 1)))
IF @column <> ''
BEGIN
INSERT INTO @ParsedString (RetStr) VALUES (@column)
END
SET @List = RIGHT(@List, LEN(@List) - @Pos)
SET @Pos = CHARINDEX(';', @List, 1)
END
END
RETURN
END
Example call:
DECLARE @DBExclusionList varchar(2000) SET @DBExclusionList = 'column1;column2;column3' SELECT RetStr FROM dbo.udfMT_ParsedString(@DBExclusionList) ; Results look as such:
RetStr column1 With it returning a table, you can even use it in inner joins or in statement as such:
SELECT name FROM master.dbo.sysdatabases (NOLOCK) WHERE LOWER(name) IN (SELECT LOWER(RetStr) FROM dbo.udfMT_ParsedString(@DatabaseList) ) |
|
| Friday, July 06, 2012 - 4:09:30 PM - Larry Silverman | Read The Tip |
|
When database replication is enabled, SQL Server 2008 R2 adds a new system database named 'distribution'. Your script does not account for this new system database. In order to support the distribution system database, I changed the code like so: SET @sql = N'SELECT name FROM sys.databases WHERE 1=1' + CASE WHEN @system_only = 1 THEN ' AND (database_id IN (1,2,3,4) OR is_distributor = 1) ' ELSE '' END + CASE WHEN @user_only = 1 THEN ' AND (database_id NOT IN (1,2,3,4) AND is_distributor = 0)' ELSE '' END |
|
| Wednesday, October 17, 2012 - 12:09:42 AM - div | Read The Tip |
|
*** NOTE *** - If you want to include code from SQL Server Management Studio (SSMS) in your post, please copy the code from SSMS and past DECLARE @SQL AS VARCHAR(1000), @command varchar(1000), @tablename1 VARCHAR(5000), @temp2 varchar(8000), @rptdirMaster varchar(1000); SET @rptdirMaster = 'C:\TEMP$\' SET @tablename1 = @rptdirMaster + 'QueryTable.csv' SELECT @command = 'IF ''?'' IN(''brisbane_tsql'', ''sydney'',''perth'',''goldcoast'',''sunshinecoast'') BEGIN USE ? EXEC('' SELECT TOP 3 * FROM [brisbane_tsql].[DBO].[DOICLAIMDATA]; '') END' EXEC sp_MSforeachdb @command
how can export it to csv |
|
| Wednesday, October 17, 2012 - 9:13:44 AM - Aaron Bertrand | Read The Tip |
|
@div, SQL Server doesn't really have an export to CSV functionality. Regardless of the query you're using (sp_MSforeachdb, my replacement, or something else entirely), you'll need to combine it with something else to export to CSV. For example you can use PowerShell to run your query and then save it to a file using export-csv. |
|
| Thursday, October 18, 2012 - 12:40:34 AM - div | Read The Tip |
|
Thanks Aaron ... i got its solution
DECLARE @SQL AS VARCHAR(1000), @command varchar(1000), @tablename1 VARCHAR(5000), @temp2 varchar(8000), @dbname varchar(8000), @rptdirMaster varchar(1000); SET @rptdirMaster = 'C:\TEMP$\' SET @tablename1 = @rptdirMaster + 'QueryTable.csv' SELECT @command = 'IF ''?'' IN(''brisbane_tsql'', ''sydney'',''perth'',''goldcoast'',''sunshinecoast'') BEGIN USE ? EXEC('' SELECT TOP 3 * FROM [?].[DBO].[DOICLAIMDATA]; '') END' EXEC sp_MSforeachdb @command
SET @dbname = 'Brisbane_tsql'
SET @SQL='SELECT TOP 3 * FROM [databasename].[DBO].[DOICLAIMDATA]' SET @temp2 = 'bcp "'+ REPLACE(REPLACE(replace(@SQL, 'databaseName', @dbname),char(13),' '), CHAR(10), ' ') +'" queryout "D:\DO IT\TEMP\claims_"'+ @dbname +'".csv" -t, -c -T -SMULTICARD2\SQLEXPRESS' exec master..xp_cmdshell @temp2 |
|
|
privacy | disclaimer | copyright | advertise | about authors | contribute | feedback | giveaways | user groups Some names and products listed are the registered trademarks of their respective owners. Edgewood Solutions LLC | MSSharePointTips.com | MSSQLTips.com |