一.本文所涉及的内容(Contents)
二.背景(Contexts)
在我的数据库服务器上,同一个实例下面挂载着许多相同结构的数据库,他们为不同公司提供着服务,在许多时候我需要同时创建、修改、删除一些对象,存储过程就是其中一个,但是想要批量创建存储,这有些特殊,下面就教你如何实现在多个数据库中创建同一个存储过程(Create Same Stored Procedure in All Databases)。
三.遇到的问题(Problems)
在之前的文章中多次谈到使用游标的方式处理的各种问题:
- SQL Server 游标运用:查看所有数据库所有表大小信息(Sizes of All Tables in All Database)
- SQL Server 游标运用:查看一个数据库所有表大小信息(Sizes of All Tables in a Database)
如果使用游标来批量创建存储过程,可能你会遇到下面的一些问题,假设我们需要在多个数据库(当然可以过滤掉部分数据库)中创建同样一个存储过程sp_GetId,存储过程的脚本如下Script1所示:
-- Script1: -- 需要被批量创建的存储过程 USE [ master ] GO Create PROCEDURE [ dbo ] . [ sp_GetId ] AS BEGIN DECLARE @database_id INT SET @database_id = 0 SELECT TOP 1 @database_id = [ database_id ] FROM sys. [ databases ] END
根据前面提到使用游标方式,我们可能会写出类似下面的代码,错误代码Script2示例:
-- Script2: -- ============================================= -- Author: <听风吹雨> -- Blog: <http://gaizai.cnblogs.com/> -- Create date: <2014/05/03> -- Description: <批量创建存储过程,错误代码示例> -- ============================================= DECLARE @databaseName VARCHAR ( 100 ) DECLARE @SQL NVARCHAR ( MAX ) DECLARE @itemCur CURSOR SET @itemCur = CURSOR FOR SELECT ' [ ' + [ name ] + ' ] ' FROM sys.databases WHERE database_id > 4 OPEN @itemCur FETCH NEXT FROM @itemCur INTO @databaseName WHILE @@FETCH_STATUS = 0 BEGIN -- 逻辑处理 PRINT @databaseName SET @SQL = ' USE ' + @databaseName + ' GO CREATE PROCEDURE [dbo].[sp_GetId] AS BEGIN DECLARE @database_id INT SET @database_id = 0 SELECT TOP 1 @database_id = [database_id] FROM sys.[databases] END ' PRINT ( @SQL ); EXEC ( @SQL ); FETCH NEXT FROM @itemCur INTO @databaseName END CLOSE @itemCur DEALLOCATE @itemCur
执行上面的代码你会遇到这样的错误信息:
(Figure1:错误信息1)
根据错误信息修改上面的SQL代码,把”GO”改成“;”但还是会出现下图Figure2的错误信息:
(Figure2:错误信息2)
既然这样行不通,也许你还会尝试在[dbo].[sp_GetId]前面加上数据库名的变量,但是却出现下图Figure3的错误信息:
(Figure3:错误信息3)
四.实现代码(SQL Codes)
上面的3个错误让我们陷入了困境,也许你想过放弃了,但是经过努力,我通过2种方式实现了在多个数据库中创建同一个存储过程(大家可认为是批量创建存储过程),下面是实现的2种方式概述:
1. 通过修改过的系统存储过程[dbo].[sp_MSforeachdb_Filter]和拼接SQL;
2. 通过创建一个模板存储过程,由系统存储过程sp_MSForEachDB循环调用另外一个创建存储过程的存储来创建模板存储过程(这也许听起来很拗口,看后面的实现脚本Script7,你就会理解了)。
(一) 实现方式1:通过修改过的系统存储过程[dbo].[sp_MSforeachdb_Filter]和拼接SQL;
1) 首先我们需要在master数据库中创建一个存储过程[dbo].[sp_MSforeachdb_Filter],这是通过修改系统存储过程sp_MSforeachdb得来的,做的改进主要是可以过滤数据库,创建的SQL代码如下Script3所示:
-- Script3: -- ============================================= -- Author: <听风吹雨> -- Blog: <http://gaizai.cnblogs.com/> -- Create date: <2013.05.06> -- Description: <扩展sp_MSforeachdb,增加@whereand参数> -- ============================================= USE [ master ] GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER OFF GO create proc [ dbo ] . [ sp_MSforeachdb_Filter ] @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 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 (DATABASEPROPERTY(d.name, '' issingleuser '' ) = 0 and (has_dbaccess(d.name) = 1)) ' + @whereand ) 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
2) 接着在master数据库中执行下面的SQL在多个数据库中创建同一个存储过程,其实是把需要创建的存储过程通过拼接保存在@SQL变量中,使用[sp_MSforeachdb_Filter]来过滤数据库,并在符合条件的每个数据库中执行@SQL中的语句,SQL代码如下Script4所示:
-- Script4: -- 批量创建存储过程 USE [ master ] GO DECLARE @SQL NVARCHAR ( MAX ) SELECT @SQL = COALESCE ( @SQL , '' ) + ' USE [?]; EXEC( '' CREATE PROCEDURE [dbo].[sp_GetId] AS BEGIN DECLARE @database_id INT SET @database_id = 0 SELECT TOP 1 @database_id = [database_id] FROM sys.[databases] END '' ) ' PRINT @SQL -- 过滤数据库 EXEC [ sp_MSforeachdb_Filter ] @command1 = @SQL , @whereand = " and [ name ] not in ( ' tempdb ' , ' master ' , ' model ' , ' msdb ' ) "
3) 执行上面的SQL脚本之后,除了('tempdb','master','model','msdb')4个数据库之外的数据库都会创建了存储过程sp_GetId,为了快速验证,可以使用下面的SQL脚本进行验证:
-- Script5: -- 返回所有数据库sp_GetId存储过程列表 IF NOT EXISTS ( SELECT * FROM [ master ] .sys.objects WHERE object_id = OBJECT_ID (N ' [master].[dbo].[SPList] ' ) AND type in (N ' U ' )) BEGIN CREATE TABLE [ master ] . [ dbo ] . [ SPList ] ( [ db_name ] [ sysname ] NULL , [ sp_name ] [ sysname ] NULL , ) ON [ PRIMARY ] END ELSE TRUNCATE TABLE [ master ] . [ dbo ] . [ SPList ] EXEC [ sp_MSforeachdb_Filter ] @command1 = ' INSERT INTO [master].[dbo].[SPList]([sp_name]) SELECT [name] FROM [?].sys.[sysobjects] WHERE TYPE = '' P '' AND [name] = '' sp_GetId '' UPDATE [master].[dbo].[SPList] SET [db_name] = '' ? '' WHERE [db_name] IS NULL ' SELECT * FROM [ master ] . [ dbo ] . [ SPList ]
执行上面的SQL脚本的结果如下图Figure4所示:
(Figure4:创建了sp_GetId存储过程的数据库列表)
(二) 实现方式2:通过创建一个模板存储过程,由系统存储过程sp_MSForEachDB循环调用另外一个创建存储过程的存储来创建模板存储过程;
1) 为了能看到方式2的实际效果,我们需要把存在sp_GetId存储过程的数据库中批量删除这个存储过程,通过下面的脚本Script6来实现:
-- Script6: -- 批量删除存储过程 USE [ master ] GO DECLARE @SQL NVARCHAR ( MAX ) SELECT @SQL = COALESCE ( @SQL , '' ) + ' USE [?]; EXEC( '' IF EXISTS (SELECT * FROM sys.[objects] WHERE object_id = OBJECT_ID(N '''' [dbo].[sp_GetId] '''' ) AND type in (N '''' P '''' , N '''' PC '''' )) DROP PROCEDURE [dbo].[sp_GetId] '' ) ' PRINT @SQL -- 过滤数据库 EXEC [ sp_MSforeachdb_Filter ] @command1 = @SQL , @whereand = " and [ name ] not in ( ' tempdb ' , ' master ' , ' model ' , ' msdb ' ) "
2) 通过Script5确认所有数据库都不存在sp_GetId存储过程;
3) 接着在master数据库中创建模板存储过程 sp_GetId,创建脚本如Script1所示,这里不做重复;
4) 再接着创建一个存储过程CreateProcedure,这个存储过程的作用就是创建存储过程,在这个存储过程CreateProcedure利用系统表返回sp_GetId存储过程的内容,保存在变量@proc_text中,查询出如下所示:
-- Script7: -- 创建存储过程的存储过程 USE [ master ] GO -- ============================================= -- Author: <听风吹雨> -- Blog: <http://gaizai.cnblogs.com/> -- Create date: <2014.05.06> -- Description: <创建存储过程的存储过程> -- ============================================= CREATE PROC CreateProcedure ( @dbname SYSNAME, @spname SYSNAME ) AS BEGIN SELECT @dbname = REPLACE ( REPLACE ( @dbname , ' [ ' , '' ), ' ] ' , '' ) IF @dbname <> ' master ' BEGIN DECLARE @proc_text NVARCHAR ( MAX ) SELECT @proc_text = REPLACE ( [ text ] , '''' , '''''' ) FROM [ sysobjects ] o INNER JOIN [ syscomments ] c ON c.id = o.id WHERE o.type = ' P ' AND o.name = @spname DECLARE @sql NVARCHAR ( MAX ) SET @sql = ' USE [ ' + @dbname + ' ]; EXEC ( '' ' + @proc_text + ''' ); ' EXEC SP_EXECUTESQL @sql END END GO
5) 准备完上面的步骤,只需要下面的一条SQL语句就能批量创建存储过程sp_GetId:
-- Script8: -- 批量创建存储过程 USE [ master ] GO -- 过滤数据库 EXEC [ sp_MSforeachdb_Filter ] @command1 = ' CreateProcedure '' [?] '' , '' sp_GetId ''' , @whereand = " and [ name ] not in ( ' tempdb ' , ' master ' , ' model ' , ' msdb ' ) "
执行上面的SQL脚本的结果如下图Figure5所示,与Figure4的区别就是在master数据库中多了一个模板存储过程sp_GetId。
(Figure5:创建了 sp_GetId存储过程的数据库列表)
上面已经通过两种方式实现了在多个数据库中创建同一个存储过程,如果存储过程sp_GetId属于比较简单的,使用方式1实现会比较快捷,如果sp_GetId比较复杂了,比如存储过程里面还包含单引号或者代码比较多的情况下,建议使用方式2,虽然方式2的步骤会多一点,但是只要创建好模板存储过程,其它的根本不会因为存储过程sp_GetId而变得复杂;
在实际运用中,很多时候你需要的并不单单是在多个数据库中创建同一个存储过程,可能还需要修改同一个存储过程,通过上面的阅读你也许猜到修改存储过程,可以先删除,再创建,对的,这是没有问题的,不过也可以直接修改,下面提供SQL代码:
1) 首先修改下master数据库的模板存储过程sp_GetId,在存储过程里面中加入一个变量@id:
-- Script10: -- 需要被批量创建的存储过程 USE [ master ] GO Create PROCEDURE [ dbo ] . [ sp_GetId ] AS BEGIN DECLARE @database_id INT -- 修改部分,增加了一个变量 DECLARE @id INT SET @database_id = 0 SELECT TOP 1 @database_id = [ database_id ] FROM sys. [ databases ] END
2) 接着创建一个修改存储过程的存储过程AlterProcedure,只需要把变量@proc_text里面的“CREATE PROC”替换成“ALTER PROC”就可以了:
-- Script10: -- 修改存储过程的存储过程 USE [ master ] GO -- ============================================= -- Author: <听风吹雨> -- Blog: <http://gaizai.cnblogs.com/> -- Create date: <2014.05.06> -- Description: <修改存储过程的存储过程> -- ============================================= CREATE PROC AlterProcedure ( @dbname SYSNAME, @spname SYSNAME ) AS BEGIN SELECT @dbname = REPLACE ( REPLACE ( @dbname , ' [ ' , '' ), ' ] ' , '' ) IF @dbname <> ' master ' BEGIN DECLARE @proc_text NVARCHAR ( MAX ) SELECT @proc_text = REPLACE ( [ text ] , '''' , '''''' ) FROM [ sysobjects ] o INNER JOIN [ syscomments ] c ON c.id = o.id WHERE o.type = ' P ' AND o.name = @spname DECLARE @sql NVARCHAR ( MAX ) SET @proc_text = REPLACE ( @proc_text , ' CREATE PROC ' , ' ALTER PROC ' ) SET @sql = ' USE [ ' + @dbname + ' ]; EXEC ( '' ' + @proc_text + ''' ); ' EXEC SP_EXECUTESQL @sql END END GO
3) 准备完上面的步骤,再把Script8的脚本中调用存储过程CreateProcedure改成调用存储过程AlterProcedure,通过下面的一条SQL语句批量修改存储过程sp_GetId:
-- Script11: -- 批量修改存储过程 USE [ master ] GO -- 过滤数据库 EXEC [ sp_MSforeachdb_Filter ] @command1 = ' AlterProcedure '' [?] '' , '' sp_GetId ''' , @whereand = " and [ name ] not in ( ' tempdb ' , ' master ' , ' model ' , ' msdb ' ) "
4) 创建完成后,剩下的就是验证下数据库中存储过程sp_GetId的内容了;
五.参考文献(References)
Create Same Stored Procedure on All Databases using sp_MSForEachDB T-SQL Example
SQL Server 在多个数据库中创建同一个存储过程(Create Same Stored Procedure in All Databases)