都内で働くSEの技術的なひとりごと

都内でサラリーマンやってます。マイクロソフト系(たまに、OSS系などマイクロソフト以外の技術も...)の技術的なことについて書いています。日々の仕事の中で、気になったことを技術要素関係なく気まぐれに選んでいるので記事内容は開発言語、インフラ等ばらばらです。なお、当ブログで発信、発言は私個人のものであり、所属する組織、企業、団体等とは何のかかわりもございません。ブログの内容もきちんと検証して使用してください。よろしくお願いします♪

undocumented なものを見つけたので、調べてみる - その2 ( sp_MSforeachtable と sp_MSforeach_worker ) -

 undocumented なものを見つけたので、調べてみる - その1 - - 都内で働くSEの技術的なひとりごと では、sp_MSforeachtable について触れました。今回は、sp_MSforeachtable で使用されている、sp_MSforeach_worker について調べてみたいと思います。早速下記のステートメントを実行して、内容を調べていきましょう。

exec sp_helptext sp_MSforeach_worker

f:id:koogucc11:20140614181952p:plain

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, 16, 1); -- 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
  • @command1
    指定されたステートメントを実行します。
  • @replacechar
    デフォルトは ? がテーブル名に置き換えられますが、他の文字にしたい場合replaccechar に変換したい文字を指定します。
  • @command2
    指定されたステートメントを実行します。
  • @command3
    指定されたステートメントを実行します。
  • @worker_type
    worker_type によって、どのタイプのカーソルを使用するか判断します。sp_MSforeach_worker は、呼び出し元でカーソルがオープンされている前提で動作します。デフォルトの 0 では、hCForEachDatabase が作成され、オープンされている状態を前提とします。1では、hCForEachTable が作成され、オープンされている状態を前提とします。もし、sp_MSforeach_worker を直接使用したい場合は、この部分に注意する必要があります。

 例えば、以下のようなステートメントを実行したら、テーブル単位に、統計情報とCHECKTABLE をやってくれたりします。テーブル変換の文字は、! を指定したので、? → ! に変更しています。

exec sp_MSforeachtable @command1="print '!' select count(*) from !",
@whereand = "and o.name like '%State%'",
@precommand="print 'IroIroyaruyo' print ''",
@postcommand="print '' print 'IroIroOwattayo!'",
@replacechar = "!",
@command2 = "UPDATE STATISTICS !;",
@command3 = "DBCC CHECKTABLE ([!]);"

f:id:koogucc11:20140614203355p:plain

 まだまだ、undocumented なストアドプロシージャはたくさんあります。undocumented なストアドプロシージャの内容見ているだけでも色々勉強になりますね。