sp_msforeachdb

       有时我们想在SQL服务器上的所有数据库上运行查询或一组查询。

 

       已经有太多次,涉及到这样的需求了. sp_msforeachdb就是这个没有被记录的存储过程.(MSDN,以及别的官方文档都没有记录)

但是它可以帮助你在所有数据库上执行查询.

       没有被记录的存储过程 ,这点本身就是一个谜一样的存在. 难道这个存储过程有什么问题吗? (兼容性…)

 

sp_helptext sp_msforeachdb   得到它的代码如下:

/*  
* The following table definition will be created by SQLDMO at start of each connection.  
* We don't create it here temporarily because we need it in Exec() or upgrade won't work.  
*/  
  
create proc sys.sp_MSforeachdb  
@command1 nvarchar(2000), @replacechar nchar(1) = N'?', @command2 nvarchar(2000) = null, @command3 nvarchar(2000) = null,  
@precommand nvarchar(2000) = null, @postcommand nvarchar(2000) = null  
as  
    set deadlock_priority low  
      
/* This proc returns one or more rows for each accessible db, with each db defaulting to its own result set */  
/* @precommand and @postcommand may be used to force a single result set via a temp table. */  
  
/* Preprocessor won't replace within quotes so have to use str(). */  
declare @inaccessible nvarchar(12), @invalidlogin nvarchar(12), @dbinaccessible nvarchar(12)  
select @inaccessible = ltrim(str(convert(int, 0x03e0), 11))  
select @invalidlogin = ltrim(str(convert(int, 0x40000000), 11))  
select @dbinaccessible = N'0x80000000'  /* SQLDMODbUserProf_InaccessibleDb; the negative number doesn't work in convert() */  
  
if (@precommand is not null)  
  exec(@precommand)  
  
declare @origdb nvarchar(128)  
select @origdb = db_name()  
  
/* If it's a single user db and there's an entry for it in sysprocesses who isn't us, we can't use it. */  
   /* Create the select */  
exec(N'declare hCForEachDatabase cursor global for select name from master.dbo.sysdatabases d ' +  
   N' where (d.status & ' + @inaccessible + N' = 0)' +  
   N' and (DATABASEPROPERTYEX(d.name, ''UserAccess'') <> ''SINGLE_USER'' and (has_dbaccess(d.name) = 1))' )  
  
declare @retval int  
select @retval = @@error  
if (@retval = 0)  
  exec @retval = sys.sp_MSforeach_worker @command1, @replacechar, @command2, @command3, 1  
  
if (@retval = 0 and @postcommand is not null)  
  exec(@postcommand)  
  
   declare @tempdb nvarchar(258)  
   SELECT @tempdb = REPLACE(@origdb, N']', N']]')  
   exec (N'use ' + N'[' + @tempdb + N']')  
  
return @retval 


 

 

内部涉及的参数:

  • @command1: 执行的sql语句
  • @replacechar: 替换成对应的数据库名
  • @command2: 执行的第二个sql语句
  • @command3: 执行的第三个sql语句
  • @precommand: 在@command1 之前单次执行的sql语句
  • @postcommand: 在@command3 之前单次执行的sql语句.

 

注意在代码中还调用了一个 sys.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, 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 


 

 

这个存储过程在很多的sys存储过程中都被用到. 它涉及了很多更复杂的逻辑.这在分析sp_msforeachdb是用不到的.

 

exec(N'declare hCForEachDatabase cursor global for select name from master.dbo.sysdatabases d ' +
                     N' where (d.status & ' + @inaccessible + N' = 0)' +
                     N' and (DATABASEPROPERTYEX(d.name, ''UserAccess'') <> ''SINGLE_USER'' and (has_dbaccess(d.name) = 1))' )
注意代码中的这句, 它检查数据库是否具有可访问的状态.

@inaccessible

1自动关闭
4大容量载入
8登录检查点
16分页检测
32读取
64还原前
128还原中
256未还原
512离线
1024只读
2048限制模式
4096单用户模式
32768紧急模式
4194304自动收缩
1073741824关闭

 

代码中还用到了一个老的系统视图sysdatabases,这个已经是不推荐使用的了.


总结:

sp_msforeachdb 代码中涉及很多老的视图, 非常繁复的字符串操作. 这在生产环境中,会产生不可预知的影响. 





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值