SQL Server cursor - loop through multiple servers and execute query - sql-server

I have the following code (cursor):
DECLARE #SN VARCHAR(20);
DECLARE #sql NVARCHAR(MAX);
DECLARE C CURSOR LOCAL FAST_FORWARD
FOR SELECT DISTINCT(SERVERNAME) FROM INSTALLATION
where DATABASETYPE = 'MsSql' AND SERVERNAME IN ('x');
OPEN C;
FETCH NEXT FROM C INTO #SN;
WHILE (##FETCH_STATUS = 0)
BEGIN
PRINT #SN;
-- you could loop here for each database, if you'd define what that is
SELECT name
FROM master.dbo.sysdatabases
WHERE name not in ('master','model','msdb','tempdb');
SET #sql = N'SELECT TOP 1 NAME FROM TABLE ';
EXEC sp_executesql #sql;
FETCH NEXT FROM C INTO #SN;
END
CLOSE C;
DEALLOCATE C;
I would like to be able to loop through every server and execute a select statement on some (not all) of their databases.
The query is something like:
SELECT TOP 1 NAME FROM TABLE
The server from where I am running the cursor has all the others as linked servers.

DECLARE #SN VARCHAR(20);
DECLARE C CURSOR LOCAL FAST_FORWARD
FOR SELECT DISTINCT(SERVERNAME) FROM TABLE
where SERVERNAME NOT IN ('SRV1','SRV2','SRV3');
OPEN C;
FETCH NEXT FROM C INTO #SN;
WHILE (##FETCH_STATUS = 0)
BEGIN
PRINT #SN;
-- you could loop here for each database, if you'd define what that is
SET #sql = N'SELECT * FROM ' + #SN + '.master.dbo.TABLE;';
EXEC sys.sp_executesql #sql;
FETCH NEXT FROM C INTO #SN;
END
CLOSE C;
DEALLOCATE C;
Changes:
There is no reason to use the default cursor options here - global, updatable, dynamic, scrollable, etc. Background.
As a habit / best practice, use sp_executesql and not EXEC(). While it doesn't really matter in this case, it can matter in others, so I'd prefer to always code the same way. Background.
Also, please get in the habit of terminating your statements with semi-colons. You'll have to, eventually. Background.
EDIT
Now that we have a little more information about your actual requirements, I suggest this bit of code. Oh, and look, no cursors (well, no explicit cursor declarations and all the scaffolding that comes with them)!
SET NOCOUNT ON;
DECLARE #dbs TABLE(SERVERNAME SYSNAME, DBNAME SYSNAME);
DECLARE #sql NVARCHAR(MAX) = N'';
-- first, let's get the databases on each server:
SELECT #sql += N'SELECT ''' + SERVERNAME + ''', name FROM '
+ QUOTENAME(SERVERNAME) + '.master.sys.databases
WHERE database_id > 4
AND name NOT IN (N''somedb'',N''someotherdb'');'
FROM dbo.INSTALLATION
WHERE DATABASETYPE = 'MsSql'
AND SERVERNAME IN ('x');
INSERT #dbs EXEC sys.sp_executesql #sql;
SELECT #sql = N'';
-- now, build a command to run in each database context:
SELECT #sql += N'
EXEC ' + QUOTENAME(SERVERNAME) + '.'
+ QUOTENAME(DBNAME) + '.sys.sp_executesql #sql;'
FROM #dbs;
-- feel free to change the 3rd parameter here:
EXEC sys.sp_executesql #sql, N'#sql NVARCHAR(MAX)',
N'SELECT ##SERVERNAME, DB_NAME(), actual_columns FROM dbo.table_name;';
This will fail if table_name doesn't exist, so you may still have some work to do if you want to facilitate error handling. But this should get you started.
Also, please be conscious of, and consistently use, the schema prefix. Background.

Related

Database name with special character 'İ'

Somebody create database with special characters name like 'PRİNCE' long time ago.So we can't change database name anymore.
Because of ' İ ' we can't call database from sys.databases.
select name
from dbo.sysdatabases
where name=cast('PRİNCE' as varchar(100)) COLLATE SQL_Latin1_General_CP1253_CI_AI
it works well
But there is a procedure which we using gives error
'Database 'PRİNCE' does not exist. Make sure that the name is entered correctly.'
DECLARE #dbname VARCHAR(50) -- database name
Declare #strSQL nvarchar(max)
DECLARE c CURSOR FOR
select name from MASTER.dbo.sysdatabases where (name not in('master','model','msdb','tempdb'))
OPEN c;
FETCH NEXT FROM c INTO #dbname;
WHILE ##FETCH_STATUS = 0
begin
select #strSQL='use '+cast(#dbname as varchar(100))COLLATE SQL_Latin1_General_CP1253_CI_AI+' Select Db_name(DB_ID()) as Dbname from sys.database_files ;'
exec sp_executesql #strSQL
fetch next from c into #dbname
end
close c
deallocate c
The problem is your use of varchar throughout and not nvarchar. The specific problem statement is this one:
select #strSQL='use '+cast(#dbname as varchar(100))COLLATE SQL_Latin1_General_CP1253_CI_AI+' Select Db_name(DB_ID()) as Dbname from sys.database_files ;'
I am guessing your database (master I assume) is not in the collation SQL_Latin1_General_CP1253_CI_AI so as soon as the assignment happens, the value is lost.
See, for example, the below:
DECLARE #Database sysname = N'USE ' + 'İ' COLLATE SQL_Latin1_General_CP1253_CI_AI + ';';
SELECT #Database;
This returns USE I; not USE İ;. As object names are a sysname (a synonym of nvarchar(128) NOT NULL) then make sure you use that data type through your code:
USE master;
GO
DECLARE #dbname sysname; -- database name
DECLARE #strSQL nvarchar(max);
DECLARE c CURSOR FOR
SELECT [name]
FROM sys.databases --Don't use the old objects!
WHERE ([name] NOT IN(N'master',N'model',N'msdb',N'tempdb'));
OPEN c;
FETCH NEXT FROM c INTO #dbname;
WHILE ##FETCH_STATUS = 0 BEGIN
SET #strSQL = N'USE ' + QUOTENAME(#dbname) + N'; SELECT DB_NAME(DB_ID()) AS DBName from sys.database_files;'; --Note QUOTENAME as well for proper quoting.
EXEC sys.sp_executesql #strSQL;
FETCH NEXT FROM c INTO #dbname;
END;
CLOSE c;
DEALLOCATE c;
Ideally, I'd get rid of the CURSOR as well, but that's a completely different question.

Facilitating shorthand referencing for linked server with synonyms

I'm using a linked server and finding it very painful to write queries like this:
select * from [10.150.10.109].lhf.[dbo].[TABLE_NAME]
Is it possible to use a synonym for something like this:
CREATE SYNONYM [DataRelay] FOR [10.150.10.109].[lhf].[dbo]
in order to be able to query like this:
select * from DataRelay.TABLE_NAME
Without the capabilities of Intellisense, this is just painful...
No, there is no short hand for linked servers, however, you can alias tables in your queries to make it a bit easier.
select * from [10.150.10.109].lhf.[dbo].[TABLE_NAME] T
WHERE
T.FieldName=1
OR
T.FieldName=2
Now that I had a minute what I was saying in my comment is that you cannot create a synonym for just part of an object path as you desire. But you can dynamically script the drop and creation of synonyms for any object in your remote database pretty easily. here is an example of how to do if for user tables. For other objects you can use the sys.objects instead of sys.table system view.
Technique key words for more learning. Dynamic SQL, cursor, schema views.
DECLARE #ServerAndDB SYSNAME = '[10.150.10.109].[lhf]'
DECLARE #SynonymSchema SYSNAME = '[syn]'
DECLARE #ObjectPath NVARCHAR(1000)
DECLARE #SynonymName NVARCHAR(1000)
DECLARE CursorName CURSOR FOR
SELECT
#ServerAndDB + QUOTENAME(SCHEMA_NAME(t.schema_id)) + '.' + QUOTENAME(t.name) as ObjectPath
,#SynonymSchema + '.' + QUOTENAME(t.name) as SynonymName
FROM
[10.150.10.109].[lhf].sys.tables t
WHERE
t.type = 'U'
OPEN CursorName
FETCH NEXT FROM CursorName
INTO #ObjectPath, #SynonymName
WHILE ##FETCH_STATUS = 0
BEGIN
BEGIN TRY
DECLARE #SQL NVARCHAR(MAX)
IF EXISTS (SELECT * FROM sys.synonyms WHERE object_id = OBJECT_ID(#SynonymName))
BEGIN
SET #SQL = 'DROP SYNONYM ' + #SynonymName
EXECUTE sp_executesql #SQLString
SET #SQL = ''
END
SET #SQL = 'CREATE SYNONYM ' + #SynonymName + ' FOR ' + #ObjectPath
EXECUTE sp_executesql #SQLString
SET #SQL = ''
END TRY
BEGIN CATCH
--Can do error handling here
END CATCH
FETCH NEXT FROM CursorName
INTO #ObjectPath, #SynonymName
END
CLOSE CursorName
DEALLOCATE CursorName

Set variable as schema

I have a query that I want to be able to use across database schemas. Right now it is written so that I need to replace the schema in several places of the query. How do I set that up as a variable so that all I need to do is change it in one place?
You can do this with Dynamic SQL:
DECLARE #sql VARCHAR(MAX)
,#schema VARCHAR(255) = 'dbo'
SET #sql = 'SELECT *
FROM '+#schema+'.yourTable
'
EXEC (#sql)
You could use this in a cursor to loop through schema's:
DECLARE #Iterator varchar(255)
,#strSQL varchar(MAX)
DECLARE xyz CURSOR
FOR
--Select stuff to iterate over
SELECT name
FROM sys.schemas
OPEN xyz
FETCH NEXT FROM xyz
INTO #Iterator
WHILE ##FETCH_STATUS = 0
BEGIN
--Do stuff
SET #strSQL = 'SELECT *
FROM '+#Iterator+'.yourTable
'
Exec (#strSQL)
FETCH NEXT FROM xyz
INTO #Iterator
END
CLOSE xyz
DEALLOCATE xyz
GO
To test your dynamic SQL statements, you can change EXEC to PRINT and ensure that the resulting query is as you intended.
Synonyms might be the answer to your question.
You can use dynamic SQL. Here is an example -
DECLARE #Column varchar(25)
DECLARE #sqlStmt varchar(max)
SET #Column = 'MyColumn'
SET #sqlStmt = N'SELECT ' + #Column + ' FROM MyTable'
EXEC (#sqlStmt)

dynamic tsql in sql 2005

Hi i am using the following dynamic sql:
declare #cmd nvarchar(4000)
set #cmd=
'select ''exec msdb.dbo.sp_update_job #job_id=''''''
+ convert(nvarchar(255), job_id)+ '''''',
#owner_login_name=''''sa'''';''
from msdb..sysjobs'
exec sp_executesql #cmd
but all it is doing is printing as exec .......
I want to execute the results not just print it.
What should i do?
Regards
Manjot
How about:
DECLARE #sql NVARCHAR(MAX);
SELECT #sql = COALESCE(#sql, '') + '
EXEC msdb.dbo.sp_update_job #job_id = '''
+ CONVERT(NVARCHAR(255), job_id)
+ ''', #owner_login_name = ''sa'';'
FROM msdb.dbo.sysjobs;
EXEC sp_sqlexec #sql;
NOTE that sp_sqlexec is undocumented and unsupported. To do this in a supported way, why not just create a cursor, it is much easier to follow and debug (no sea of red and no "invisible" SQL):
DECLARE #j UNIQUEIDENTIFIER;
DECLARE c CURSOR FOR
SELECT job_id FROM msdb.dbo.sysjobs;
OPEN c;
FETCH NEXT FROM c INTO #j;
WHILE ##FETCH_STATUS = 0
BEGIN
EXEC msdb.dbo.sp_update_job
#job_id = #j,
#owner_login_name = 'sa';
FETCH NEXT FROM c INTO #j;
END
DEALLOCATE c;
Or ever easier, since msdb.dbo.sysjobs is not technically a system table, and you can edit it directly:
UPDATE msdb.dbo.sysjobs
SET owner_sid = SUSER_SID('sa');
Remove the 'Select' ! (and also remove some escaped single quotes)
In other words...
set #cmd=
'exec msdb.dbo.sp_update_job #job_id='''
+ convert(nvarchar(255), job_id)
+ ''', #owner_login_name=''sa'';'
Note: not quite sure where this last thing fits (probably at the end of the sp_update_job statement.
-- from msdb..sysjobs'
Essentially what was going on, with the SELECT statement was that the dynamic SQL being executed was a select statement which happen to select a string dynamically crafted. SQL would then print that string, just as if the SELECT statement had been, say:
select 'Once upon a time, a very poor lumberjack..."
(Whatever is in the string after select doesn't affect SQL in any way)
Now, as is more obvious with Remus' nice formatting of the question, the dyanamic SQL statement itself seems to be wrong, but at least SQL will try to execute it, now...

GRANTing permissions across different databases (schemas)

I'm securing the DB by only allowing interaction with the DB through a series of Sprocs; pretty common fare.
I've dug up and modified a script which loops through and assigns the user EXECUTE permission for all non-system SProcs. It works a treat except that I'd ideally like to add it to the Master DB so that I can easily use it for any subsequent projects. Yes, I could save simple as a .sql file but I'd prefer it this way.
The problem is that I don't know how to dynamically refer to objects in another DB. For example, I can easily query on MyDB.dbo.INFORMATION_SCHEMA.ROUTINES, but if the DB name is dynamic (e.g. #MyDBName), how can I query the objects in this DB?
Edit: Thanks to the posters below, I now have a working solution:
USE [master]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[spGrantExec]
#User sysname,
#DB varchar(50),
#Target varchar(50)
AS
/*---------------------------- SQL 2005 + -------------------------------*/
SET NOCOUNT ON
-- 1 - Variable declarations
DECLARE #SQL varchar(8000)
-- 2 - Create temporary table
Set #SQL =
'USE #DB
DECLARE #MAXOID int
DECLARE #OwnerName varchar(128)
DECLARE #ObjectName varchar(128)
DECLARE #CMD1 varchar(8000)
CREATE TABLE #StoredProcedures
(OID int IDENTITY (1,1),
StoredProcOwner varchar(128) NOT NULL,
StoredProcName varchar(128) NOT NULL)
-- 3 - Populate temporary table
INSERT INTO #StoredProcedures (StoredProcOwner, StoredProcName)
SELECT ROUTINE_SCHEMA, ROUTINE_NAME
FROM INFORMATION_SCHEMA.ROUTINES
WHERE ROUTINE_NAME LIKE ''' + #Target + '%''
AND ROUTINE_TYPE = ''PROCEDURE''
-- 4 - Capture the #MAXOID value
SELECT #MAXOID = MAX(OID) FROM #StoredProcedures
-- 5 - WHILE loop
WHILE #MAXOID > 0
BEGIN
-- 6 - Initialize the variables
SELECT #OwnerName = StoredProcOwner,
#ObjectName = StoredProcName
FROM #StoredProcedures
WHERE OID = #MAXOID
-- 7 - Build the string
SELECT #CMD1 = ''GRANT EXEC ON '' + ''['' + #OwnerName + '']'' + ''.'' + ''['' + #ObjectName + '']'' + '' TO #user''
-- 8 - Execute the string
Print #CMD1
EXEC(#CMD1)
-- 9 - Decrement #MAXOID
SET #MAXOID = #MAXOID - 1
END
-- 10 - Drop the temporary table
DROP TABLE #StoredProcedures'
Set #SQL = REPLACE(REPLACE(REPLACE(#SQL, '#DB', #DB), '#User', #User), '#Target', #Target)
--Select #SQL
--Print #SQL
Exec (#SQL)
SET NOCOUNT OFF
Similiar to #Cade's answer, the way to do this is to use dynamic sql. Before each call to a database table, add '#DbName.' Then replace the #DbName with the actual database name (the database name can't be passed as a variable in SQL, so you have to do the replace).
Also Cursors are normally considered evil for performance reasons, however using one in this case makes sense. For one, it would greatly simplify the procedure, plus since you're only going to run this once during application updates, you probably won't notice a performance hit, even if it added an extra second or two (which I doubt it would add anywhere near that much).
ALTER PROCEDURE [dbo].[spGrantExec]
#User SysName,
#DbName VarChar(512)
AS
BEGIN
DECLARE #Sql VarChar(1024)
SET #Sql = 'DECLARE #OwnerName varchar(128)
DECLARE #ObjectName varchar(128)
DECLARE #Cmd1 VarChar(128)
DECLARE ProcCursor CURSOR FOR
SELECT ROUTINE SCHEMA, ROUTINE NAME
FROM #DbName.INFORMATION SCHEMA.ROUTINES
WHERE ROUTINENAME NOT LIKE ''dt %'' AND ROUTINE TYPE = ''PROCEDURE''
OPEN ProcCursor
FETCH NEXT FROM ProcCursor INTO #OwnerName, #ObjectName
WHILE ##FETCH STATUS = 0
BEGIN
SET #CMD1 = ''GRANT EXEC ON '' + ''['' + #OwnerName + '']'' + ''.'' + ''['' + #ObjectName + '']'' + '' TO '' + ''#user''
EXEC (#CMD1)
FETCH NEXT FROM ProcCursor INTO #OwnerName, #ObjectName
END
CLOSE ProcCursor
DEALLOCATE ProcCursor
'
SET #Sql = Replace(Replace(#Sql, '#DbName', #DbName), '#user', #User)
EXEC (#Sql)
END
You can call this using: EXEC [spGrantExec] 'bob', 'Northwind'
Sorry the spacing is a little off in the sp. Developed using Sql 2005.
I found another technique, which I think is cleaner:
SELECT #sql = 'CREATE VIEW ...'
SELECT #sp_executesql = quotename(#dbname) + '..sp_executesql'
EXEC #sp_executesql #sql
This relies on setting the database context by calling sp_executesql in the other database (just like one could call an SP in any database).
In your case it would be equivalent to:
SELECT #sp_executesql = quotename(#dbname) + '..sp_executesql'
EXEC #sp_executesql #CMD1
You can use the double exec technique.
In your case, instead of just:
EXEC(#CMD1)
You would have:
SET #CMD1 =
'USE OtherDatabase;
EXEC (''' + REPLACE(#CMD1, '''', '''''') + ''')'
EXEC(#CMD1)

Resources