sp_MSforeachview и sp_MSforeachproc

by Alexey Knyazev 12. мая 2011 01:44

Хотелось бы поговорить о недокументированных процедурах, которые могут быть полезны в повседневной работе, а именно: sp_MSforeachdb и sp_MSforeachtable. Эти процедуры появились ещё в SQL Server версии 6.5 и, несмотря на то, что они продолжают присутствовать из версии к версии, их так и не описали в BOL (Books Online).

Эти процедуры позволяют выполнять операции над базами данных (sp_MSforeachdb) и таблицами (sp_MSforeachtable) перебирая их в курсоре. Немного расcкажу о каждой из этих процедур.

sp_MSforeachdb - Выполняет до 3-х команд для всех баз сервера. Символ, заданный параметром @replacechar в исполняемых командах будет заменяться на имя базы. Параметры @precommand и @postcomand могут быть использованы для перенаправления результатов выполнения команд в один выходной набор.

Все входные параметры:
@command1 nvarchar(2000)
,@replacechar nchar(1) = N'?'
,@command2 nvarchar(2000) = null
,@command3 nvarchar(2000) = null
,@precommand nvarchar(2000) = null
,@postcommand nvarchar(2000) = null


Для перебора всех баз данных используется курсор. Не выбираются базы данных, недоступные на данный момент, находящиеся в режиме single user(кроме той, к которой присоединен пользователь) и базы, к которым у пользователя запустившего процедуру нет прав доступа.

Примеров для использования достаточно много, например вывести размеры баз данных, выполнив в контексте каждой процедуру sp_spaceused

exec sp_msforeachdb 'use ?; exec sp_spaceused'
или проверить логическую и физическую целостность всех объектов в каждой базе данных (DBCC CHECKDB)
exec sp_msforeachdb 'dbcc checkdb(?)'

6 распространённых способов использования sp_msforeachdb можно прочитать тут: http://www.sqlservercurry.com/2009/04/6-common-uses-of-undocumented-stored.html, но вы сами можете вспомнить сценарий, который периодически необходимо выполнять в контексте нескольких баз данных.

sp_MSforeachtable - Выполняет до 3-х команд для всех таблиц базы(дополнительный фильтр можно задать с помощью параметра @whereand). Символ, заданный параметром @replacechar в исполняемых командах будет заменяться на имя таблицы. Параметры @precommand и @postcomand могут быть использованы для перенаправления результатов выполнения команд в один выходной набор.

Все входные параметры:
@command1 nvarchar(2000)
,@replacechar nchar(1) = N'?'
,@command2 nvarchar(2000) = null
,@command3 nvarchar(2000) = null
,@whereand nvarchar(2000) = null
,@precommand nvarchar(2000) = null
,@postcommand nvarchar(2000) = null


Для перебора всех таблиц базы используется курсор. Параметр @whereand должен содержать условия пригодные для использования в выражении WHERE запроса и начинаться с AND.
Примеров для использования этой процедуры достаточно много, например вывести название таблицы и кол-во строк в ней через count(*).
--Вывожу информацию по 10 таблицам БД ReportServer
use ReportServer
go
create table #rowcount ( tablename varchar(128), rowcnt int )
exec sp_MSforeachtable 'insert into #rowcount select ''?'', count(*) from ?'
select top 10 * from #rowcount
  order by tablename
Результат:
tablename                                rowcnt
---------------------------------------- -----------
[dbo].[ActiveSubscriptions]              0
[dbo].[Batch]                            0
[dbo].[CachePolicy]                      0
[dbo].[Catalog]                          1
[dbo].[ChunkData]                        0
[dbo].[ChunkSegmentMapping]              0
[dbo].[ConfigurationInfo]                23
[dbo].[DataSets]                         0
[dbo].[DataSource]                       0
[dbo].[DBUpgradeHistory]                 31
А теперь выведу информацию только по таблицам, которые начинаются на букву P%
use ReportServer
go
create table #rowcount ( tablename varchar(128), rowcnt int )
exec sp_MSforeachtable @command1 = 'insert into #rowcount select ''?'', count(*) from ?'
                    ,  @whereand = 'and o.name like ''p%'''
select * from #rowcount
  order by tablename
tablename                                rowcnt
---------------------------------------- -----------
[dbo].[Policies]                         2
[dbo].[PolicyUserRole]                   4
Конечно же получить именно этот результат можно было проще, через системное представление sys.partitions, но в данном случаи хотелось именно показать, как можно с помощью процедуры sp_MSforeachtable перебирать все таблицы какой-то конкретной БД.

Как я уже сказал в самом начале, обе эти процедуры недокументированны, но описание и примеров их работы в сети можно найти достаточно много, например это видео: http://www.sqlservercentral.com/articles/video/66317/. Однако, очень удручает отсутствие подобных процедур для перебора других объектов БД ( view, procedure, udf и т.д. ). Конечно же можно написать подобные процедуры самостоятельно, перебирая объекты в курсоре, но можно пойти более простым и хитрым способом, а именно - взять за основу код процедур sp_MSforeachdb и sp_MSforeachtable.
Для начала взглянем на исходный код процедур, описанных выше:
exec sp_helptext 'sp_MSforeachdb'
exec sp_helptext 'sp_MSforeachtable'
Обе они используют для получения результата процедуру sp_MSforeach_worker, которая так же недокументирована. Вот её исходный код:

/*
 * This is the worker proc for all of the "for each" type procs.  Its function is to read the
 * next replacement name from the cursor (which returns only a single name), plug it into the
 * replacement locations for the commands, and execute them.  It assumes the cursor "hCForEach***"
 * has already been opened by its caller.
 * worker_type is a parameter that indicates whether we call this for a database (1) or for a table (0)
 */
create proc sys.sp_MSforeach_worker
	@command1 nvarchar(2000), @replacechar nchar(1) = N'?', @command2 nvarchar(2000) = null, @command3 nvarchar(2000) = null, @worker_type int =1
as

	create table #qtemp (	/* Temp command storage */
		qnum				int				NOT NULL,
		qchar				nvarchar(2000)	COLLATE database_default NULL
	)

	set nocount on
	declare @name nvarchar(517), @namelen int, @q1 nvarchar(2000), @q2 nvarchar(2000)
   declare @q3 nvarchar(2000), @q4 nvarchar(2000), @q5 nvarchar(2000)
	declare @q6 nvarchar(2000), @q7 nvarchar(2000), @q8 nvarchar(2000), @q9 nvarchar(2000), @q10 nvarchar(2000)
	declare @cmd nvarchar(2000), @replacecharindex int, @useq tinyint, @usecmd tinyint, @nextcmd nvarchar(2000)
   declare @namesave nvarchar(517), @nametmp nvarchar(517), @nametmp2 nvarchar(258)

	declare @local_cursor cursor
	if @worker_type=1	
		set @local_cursor = hCForEachDatabase
	else
		set @local_cursor = hCForEachTable
	
	open @local_cursor
	fetch @local_cursor into @name

	/* Loop for each database */
	while (@@fetch_status >= 0) begin
		/* Initialize. */

      /* save the original dbname */
      select @namesave = @name
		select @useq = 1, @usecmd = 1, @cmd = @command1, @namelen = datalength(@name)
		while (@cmd is not null) begin		/* Generate @q* for exec() */
			/*
			 * Parse each @commandX into a single executable batch.
			 * Because the expanded form of a @commandX may be > OSQL_MAXCOLLEN_SET, we'll need to allow overflow.
			 * We also may append @commandX's (signified by '++' as first letters of next @command).
			 */
			select @replacecharindex = charindex(@replacechar, @cmd)
			while (@replacecharindex <> 0) begin

            /* 7.0, if name contains ' character, and the name has been single quoted in command, double all of them in dbname */
            /* if the name has not been single quoted in command, do not doulbe them */
            /* if name contains ] character, and the name has been [] quoted in command, double all of ] in dbname */
            select @name = @namesave
            select @namelen = datalength(@name)
            declare @tempindex int
            if (substring(@cmd, @replacecharindex - 1, 1) = N'''') begin
               /* if ? is inside of '', we need to double all the ' in name */
               select @name = REPLACE(@name, N'''', N'''''')
            end else if (substring(@cmd, @replacecharindex - 1, 1) = N'[') begin
               /* if ? is inside of [], we need to double all the ] in name */
               select @name = REPLACE(@name, N']', N']]')
            end else if ((@name LIKE N'%].%]') and (substring(@name, 1, 1) = N'[')) begin
               /* ? is NOT inside of [] nor '', and the name is in [owner].[name] format, handle it */
               /* !!! work around, when using LIKE to find string pattern, can't use '[', since LIKE operator is treating '[' as a wide char */
               select @tempindex = charindex(N'].[', @name)
               select @nametmp  = substring(@name, 2, @tempindex-2 )
               select @nametmp2 = substring(@name, @tempindex+3, len(@name)-@tempindex-3 )
               select @nametmp  = REPLACE(@nametmp, N']', N']]')
               select @nametmp2 = REPLACE(@nametmp2, N']', N']]')
               select @name = N'[' + @nametmp + N'].[' + @nametmp2 + ']'
            end else if ((@name LIKE N'%]') and (substring(@name, 1, 1) = N'[')) begin
               /* ? is NOT inside of [] nor '', and the name is in [name] format, handle it */
       /* j.i.c., since we should not fall into this case */
               /* !!! work around, when using LIKE to find string pattern, can't use '[', since LIKE operator is treating '[' as a wide char */
               select @nametmp = substring(@name, 2, len(@name)-2 )
               select @nametmp = REPLACE(@nametmp, N']', N']]')
               select @name = N'[' + @nametmp + N']'
            end
            /* Get the new length */
            select @namelen = datalength(@name)

            /* start normal process */
				if (datalength(@cmd) + @namelen - 1 > 2000) begin
					/* Overflow; put preceding stuff into the temp table */
					if (@useq > 9) begin
						close @local_cursor
						if @worker_type=1	
							deallocate hCForEachDatabase
						else
							deallocate hCForEachTable
							
						raiserror 55555 N'sp_MSforeach_worker assert failed:  command too long'
						return 1
					end
					if (@replacecharindex < @namelen) begin
						/* If this happened close to beginning, make sure expansion has enough room. */
						/* In this case no trailing space can occur as the row ends with @name. */
						select @nextcmd = substring(@cmd, 1, @replacecharindex)
						select @cmd = substring(@cmd, @replacecharindex + 1, 2000)
						select @nextcmd = stuff(@nextcmd, @replacecharindex, 1, @name)
						select @replacecharindex = charindex(@replacechar, @cmd)
						insert #qtemp values (@useq, @nextcmd)
						select @useq = @useq + 1
						continue
					end
					/* Move the string down and stuff() in-place. */
					/* Because varchar columns trim trailing spaces, we may need to prepend one to the following string. */
					/* In this case, the char to be replaced is moved over by one. */
					insert #qtemp values (@useq, substring(@cmd, 1, @replacecharindex - 1))
					if (substring(@cmd, @replacecharindex - 1, 1) = N' ') begin
						select @cmd = N' ' + substring(@cmd, @replacecharindex, 2000)
						select @replacecharindex = 2
					end else begin
						select @cmd = substring(@cmd, @replacecharindex, 2000)
						select @replacecharindex = 1
					end
					select @useq = @useq + 1
				end
				select @cmd = stuff(@cmd, @replacecharindex, 1, @name)
				select @replacecharindex = charindex(@replacechar, @cmd)
			end

			/* Done replacing for current @cmd.  Get the next one and see if it's to be appended. */
			select @usecmd = @usecmd + 1
			select @nextcmd = case (@usecmd) when 2 then @command2 when 3 then @command3 else null end
			if (@nextcmd is not null and substring(@nextcmd, 1, 2) = N'++') begin
				insert #qtemp values (@useq, @cmd)
				select @cmd = substring(@nextcmd, 3, 2000), @useq = @useq + 1
				continue
			end

			/* Now exec() the generated @q*, and see if we had more commands to exec().  Continue even if errors. */
			/* Null them first as the no-result-set case won't. */
			select @q1 = null, @q2 = null, @q3 = null, @q4 = null, @q5 = null, @q6 = null, @q7 = null, @q8 = null, @q9 = null, @q10 = null
			select @q1 = qchar from #qtemp where qnum = 1
			select @q2 = qchar from #qtemp where qnum = 2
			select @q3 = qchar from #qtemp where qnum = 3
			select @q4 = qchar from #qtemp where qnum = 4
			select @q5 = qchar from #qtemp where qnum = 5
			select @q6 = qchar from #qtemp where qnum = 6
			select @q7 = qchar from #qtemp where qnum = 7
			select @q8 = qchar from #qtemp where qnum = 8
			select @q9 = qchar from #qtemp where qnum = 9
			select @q10 = qchar from #qtemp where qnum = 10
			truncate table #qtemp
			exec (@q1 + @q2 + @q3 + @q4 + @q5 + @q6 + @q7 + @q8 + @q9 + @q10 + @cmd)
			select @cmd = @nextcmd, @useq = 1
		end /* while @cmd is not null, generating @q* for exec() */

		/* All commands done for this name.  Go to next one. */
		fetch @local_cursor into @name
	end /* while FETCH_SUCCESS */
	close @local_cursor
	if @worker_type=1	
		deallocate hCForEachDatabase
	else
		deallocate hCForEachTable
		
	return 0
И теперь, чтобы не изобретать велосипед, мы в наших новых процедурах, в виде курсора по представлениям (View) и процедурам (Procedure), будем использовать именно sp_MSforeach_worker.
Начнем с процедуры sp_MSforeachview, которая будет "пробегать" по всем представлениям определенной БД и выполнять с (над) ними наши операции. Но сразу же хочу сказать об одной особенности: если мы создадим процедуру с настройками по умолчанию, то при вызове нашей процедуры мы получим ошибку:
Msg 16958, Level 16, State 3, Procedure sp_MSforeach_worker, Line 31
Could not complete cursor operation because the set options have changed since the cursor was declared.
Msg 16917, Level 16, State 2, Procedure sp_MSforeach_worker, Line 32
Cursor is not open.
Msg 16917, Level 16, State 1, Procedure sp_MSforeach_worker, Line 153
Cursor is not open.
Все дело в том, что процедуры sp_MSforeachdb и sp_MSforeachtable созданы с настройкой SET QUOTED_IDENTIFIER OFF (Заставляет SQL Server следовать правилам отличным от ISO относительно разделения кавычками идентификаторов и строк-литералов.)
В этом можно убедиться, выполнив код:
select name, objectproperty( object_id, 'ExecIsQuotedIdentOn' ) quoted_identifier
from sys.system_objects
where name like 'sp_MSforeach%'
name                           quoted_identifier
------------------------------ -----------------
sp_MSforeach_worker            0
sp_MSforeachdb                 0
sp_MSforeachtable              0
Это значение отлично от тех, что установлены на сервере по умолчанию (SET QUOTED_IDENTIFIER ON), поэтому обязательно, при создании необходимо выставить OFF
Код нашей новой процедуры выглядит примерно так:
--Удалим нашу процедуру, если она уже существует
if object_id ( 'dbo.sp_MSforeachview', 'P' ) is not null
drop procedure dbo.sp_MSforeachview
go
SET QUOTED_IDENTIFIER OFF
go
--Скрипт процедуры
create proc dbo.sp_MSforeachview
	@command1 nvarchar(2000), @replacechar nchar(1) = N'?', @command2 nvarchar(2000) = null,
   @command3 nvarchar(2000) = null, @whereand nvarchar(2000) = null,
	@precommand nvarchar(2000) = null, @postcommand nvarchar(2000) = null
as
	declare @mscat nvarchar(12)
	select @mscat = ltrim( str( convert( int, 0x0002 ) ) )

	if ( @precommand is not null )
		exec( @precommand )

   exec( N'declare hCForEachTable cursor global 
              for select ''['' + REPLACE(schema_name(syso.schema_id), N'']'', N'']]'') + '']'' + ''.'' + ''['' 
              + REPLACE(object_name(o.id), N'']'', N'']]'') + '']'' 
                  from dbo.sysobjects o join sys.all_objects syso on o.id = syso.object_id '
         + N' where OBJECTPROPERTY( o.id, N''IsView'' ) = 1 ' + N' and o.category & ' + @mscat + N' = 0 '
         + @whereand )
	declare @retval int
	select @retval = @@error
	if ( @retval = 0 )
		exec @retval = sys.sp_MSforeach_worker @command1, @replacechar, @command2, @command3, 0

	if ( @retval = 0 and @postcommand is not null )
		exec( @postcommand )

	return @retval
go
И сразу же процедура-курсор sp_MSforeachproc
--Удалим нашу процедуру, если она уже существует
if object_id ( 'dbo.sp_MSforeachproc', 'P' ) is not null
drop procedure dbo.sp_MSforeachproc
go
SET QUOTED_IDENTIFIER OFF
go
--Скрипт процедуры
create proc dbo.sp_MSforeachproc
	@command1 nvarchar(2000), @replacechar nchar(1) = N'?', @command2 nvarchar(2000) = null,
   @command3 nvarchar(2000) = null, @whereand nvarchar(2000) = null,
	@precommand nvarchar(2000) = null, @postcommand nvarchar(2000) = null
as
	declare @mscat nvarchar(12)
	select @mscat = ltrim( str( convert( int, 0x0002 ) ) )

	if ( @precommand is not null )
		exec( @precommand )

   exec( N'declare hCForEachTable cursor global 
              for select ''['' + REPLACE(schema_name(syso.schema_id), N'']'', N'']]'') + '']'' + ''.'' + ''['' 
              + REPLACE(object_name(o.id), N'']'', N'']]'') + '']'' 
                  from dbo.sysobjects o join sys.all_objects syso on o.id = syso.object_id '
         + N' where OBJECTPROPERTY( o.id, N''IsProcedure'' ) = 1 ' + N' and o.category & ' + @mscat + N' = 0 '
         + @whereand )
	declare @retval int
	select @retval = @@error
	if ( @retval = 0 )
		exec @retval = sys.sp_MSforeach_worker @command1, @replacechar, @command2, @command3, 0

	if ( @retval = 0 and @postcommand is not null )
		exec( @postcommand )

	return @retval
go
Работают они аналогично процедурам sp_MSforeachtable.
--Вывожу информацию по всем представлениям БД ReportServer
use ReportServer
go
create table #rowcount ( viewname varchar(128), rowcnt int )
exec dbo.sp_MSforeachview 'insert into #rowcount select ''?'', count(*) from ?'
select top 10 * from #rowcount
  order by viewname
Результат:
name                           rowcnt
------------------------------ -----------
[dbo].[ExecutionLog]           0
[dbo].[ExecutionLog2]          0
[dbo].[ExecutionLog3]          0
[dbo].[ExtendedDataSets]       0
[dbo].[ExtendedDataSources]    0
Или вот вариант вызова процедуры sp_MSforeachproc:
--Перекомпилирую все процедуры
exec dbo.sp_MSforeachproc 'exec sp_recompile "?"'
Аналогично можно создать процедуру для работы с другими объектами БД (функции, триггеры и т.д.), либо создать универсальную процедуру sp_MSforeachobject для работы с любыми объектами БД.

Tags:

SQL Server

Комментарии (1) -

Mihail
Mihail Russia
28.06.2012 12:42:20 #

Добавлю для SQL Server 2012:  Выставления SET QUOTED_IDENTIFIER OFF при создании процедуры мало - все равно выдает ошибку    Msg 16958, Level 16, State 3, Procedure sp_MSforeach_worker, Line 31 Could not complete cursor operation because the set options have changed since the cursor was declared.
Приходится создавать пользовательскую процедуру dbo.sp_MSforeach_worker2, выставив SET QUOTED_IDENTIFIER OFF, и изменять ссылку на нее в первой процедуре:
...
exec @retval = dbo.sp_MSforeach_worker2 @command1, @replacechar, @command2, @command3, 0  
...

Reply

Добавить комментарий

  Country flag

biuquote
  • Комментарий
  • Предпросмотр
Loading