SQL Server 在多個數據庫中建立同一個儲存過程(Create Same Stored Procedure in All Databases)
一.本文所涉及的內容(Contents)
二.背景(Contexts)
在我的資料庫伺服器上,同一個例項下面掛載著許多相同結構的資料庫,他們為不同公司提供著服務,在許多時候我需要同時建立、修改、刪除一些物件,儲存過程就是其中一個,但是想要批量建立儲存,這有些特殊,下面就教你如何實現在多個數據庫中建立同一個儲存過程(Create Same Stored Procedure in All Databases)。
三.遇到的問題(Problems)
在之前的文章中多次談到使用遊標的方式處理的各種問題:
如果使用遊標來批量建立儲存過程,可能你會遇到下面的一些問題,假設我們需要在多個數據庫(當然可以過濾掉部分資料庫)中建立同樣一個儲存過程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的內容了;