Within While Loop how to execute "Use [database name] Go" - sql-server

I need to shrink the log file for every database weekly.
I am writing a while loop query to loop each database. but i don't think it is allowed to do the following:
declare #database_id int
declare #database varchar(255)
declare #log varchar(255)
declare #cmd varchar(500)
while (select count(*) from #LogFiles where processed = 0) > 0
begin
set #database_id = (select min(database_id) from #LogFiles where processed = 0)
set #database = (select name from #LogFiles where database_id = #database_id and [type] = 0)
set #log = (select name from #LogFiles where database_id = #database_id and [type] = 1)
select #database, #log
set #cmd = 'Use ' + #Database
exec(#cmd)
set #cmd = 'DBCC SHRINKFILE (' + #log + ');'
exec(#cmd)
update #LogFiles
set processed = 1
where database_id = #database_id
end
Or is there another way to do so?
Thanks

As was mentioned in multiple comments, this really, really, REALLY isn't a good idea. Shrinking these files just so they can grow again next week is wasted effort and, since log file autogrow events can't take advantage of instant file initialization (since, unlike data file allocations, log file allocations really have to be zeroed out before use), this can really impact end user performance in ways that you can't predict or control.
That said, ditch the #temp table and cursor while loop. Technically this still uses a loop to concatenate but it's so much easier to set up and look at.
DECLARE #sql NVARCHAR(MAX); SET #sql = N'';
SELECT #sql = #sql + N'
USE ' + QUOTENAME(db) + ';
PRINT DB_NAME();
CHECKPOINT; -- since we now know it is simple recovery
DBCC SHRINKFILE(' + QUOTENAME(f) + ') WITH NO_INFOMSGS;'
FROM
(
SELECT DB_NAME(database_id), name
FROM sys.master_files
WHERE database_id > 4 AND [type] = 1
-- AND LOWER(name) NOT IN (N'reportserver', N'reportservertempdb')
) AS x(db, f);
PRINT #sql;
--EXEC sp_executesql #sql;
Check out the PRINT output. When you're satisfied it's going to do what you think, uncomment the EXEC and run it again.

Related

Dynamic Database Stored Procedure on SQL Server 2016

I'm trying to build a stored procedure that will query multiple database depending on the databases required.
For example:
SP_Users takes a list of #DATABASES as parameters.
For each database it needs to run the same query and union the results together.
I believe a CTE could be my best bet so I have something like this at the moment.
SET #DATABASES = 'DB_1, DB_2' -- Two databases in a string listed
-- I have a split string function that will extract each database
SET #CURRENT_DB = 'DB_1'
WITH UsersCTE (Name, Email)
AS (SELECT Name, Email
FROM [#CURRENT_DB].[dbo].Users),
SELECT #DATABASE as DB, Name, Email
FROM UsersCTE
What I don't want to do is hard code the databases in the query. The steps I image are:
Split the parameter #DATABASES to extract and set the #CURRENT_DB Variable
Iterate through the query with a Recursive CTE until all the #DATABASES have been processed
Union all results together and return the data.
Not sure if this is the right approach to tackling this problem.
Using #databases:
As mentioned in the comments to your question, variables cant be used to dynamically select a database. Dynamic sql is indicated. You can start by building your template sql statement:
declare #sql nvarchar(max) =
'union all ' +
'select ''#db'' as db, name, email ' +
'from [#db].dbo.users ';
Since you have sql server 2016, you can split using the string_split function, with your #databases variable as input. This will result in a table with 'value' as the column name, which holds the database names.
Use the replace function to replace #db in the template with value. This will result in one sql statement for each database you passed into #databases. Then, concatenate the statements back together. Unfortunately, in version 2016, there's no built in function to do that. So we have to use the famous for xml trick to join the statements, then we use .value to convert it to a string, and finally we use stuff to get rid of the leading union all statement.
Take the results of the concatenated output, and overwrite the #sql variable. It is ready to go at this point, so execute it.
I do all that is described in this code:
declare #databases nvarchar(max) = 'db_1,db_2';
set #sql = stuff(
(
select replace(#sql, '#db', value)
from string_split(#databases, ',')
for xml path(''), type
).value('.[1]', 'nvarchar(max)')
, 1, 9, '');
exec(#sql);
Untested, of course, but if you print instead of execute, it seems to give the proper sql statement for your needs.
Using msForEachDB:
Now, if you didn't want to have to know which databases had 'users', such as if you're in an environment where you have a different database for every client, you can use sp_msForEachDb and check the structure first to make sure it has a 'users' table with 'name' and 'email' columns. If so, execute the appropriate statement. If not, execute a dummy statement. I won't describe this one, I'll just give the code:
declare #aggregator table (
db sysname,
name int,
email nvarchar(255)
);
insert #aggregator
exec sp_msforeachdb '
declare #sql nvarchar(max) = ''select db = '''''''', name = '''''''', email = '''''''' where 1 = 2'';
select #sql = ''select db = ''''?'''', name, email from ['' + table_catalog + ''].dbo.users''
from [?].information_schema.columns
where table_schema = ''dbo''
and table_name = ''users''
and column_name in (''name'', ''email'')
group by table_catalog
having count(*) = 2
exec (#sql);
';
select *
from #aggregator
I took the valid advice from others here and went with this which works great for what I need:
I decided to use a loop to build the query up. Hope this helps someone else looking to do something similar.
CREATE PROCEDURE [dbo].[SP_Users](
#DATABASES VARCHAR(MAX) = NULL,
#PARAM1 VARCHAR(250),
#PARAM2 VARCHAR(250)
)
BEGIN
SET NOCOUNT ON;
--Local variables
DECLARE
#COUNTER INT = 0,
#SQL NVARCHAR(MAX) = '',
#CURRENTDB VARCHAR(50) = NULL,
#MAX INT = 0,
#ERRORMSG VARCHAR(MAX)
--Check we have databases entered
IF #DATABASES IS NULL
BEGIN
RAISERROR('ERROR: No Databases Provided,
Please Provide a list of databases to execute procedure. See stored procedure:
[SP_Users]', 16, 1)
RETURN
END
-- SET Number of iterations based on number of returned databases
SET #MAX = (SELECT COUNT(*) FROM
(SELECT ROW_NUMBER() OVER (ORDER BY i.value) AS RowNumber, i.value
FROM dbo.udf_SplitVariable(#DATABASES, ',') AS i)X)
-- Build SQL Statement
WHILE #COUNTER < #MAX
BEGIN
--Set the current database
SET #CURRENTDB = (SELECT X.Value FROM
(SELECT ROW_NUMBER() OVER (ORDER BY i.value) AS RowNumber, i.value
FROM dbo.udf_SplitVariable(#DATABASES, ',') AS i
ORDER BY RowNumber OFFSET #COUNTER
ROWS FETCH NEXT 1 ROWS ONLY) X);
SET #SQL = #SQL + N'
(
SELECT Name, Email
FROM [' + #CURRENTDB + '].[dbo].Users
WHERE
(Name = #PARAM1 OR #PARAM1 IS NULL)
(Email = #PARAM2 OR #PARAM2 IS NULL)
) '
+ N' UNION ALL '
END
PRINT #CURRENTDB
PRINT #SQL
SET #COUNTER = #COUNTER + 1
END
-- remove last N' UNION ALL '
IF LEN(#SQL) > 11
SET #SQL = LEFT(#SQL, LEN(#SQL) - 11)
EXEC sp_executesql #SQL, N'#CURRENTDB VARCHAR(50),
#PARAM1 VARCHAR(250),
#PARAM2 VARCHAR(250)',
#CURRENTDB,
#PARAM1 ,
#PARAM2
END
Split Variable Function
CREATE FUNCTION [dbo].[udf_SplitVariable]
(
#List varchar(8000),
#SplitOn varchar(5) = ','
)
RETURNS #RtnValue TABLE
(
Id INT IDENTITY(1,1),
Value VARCHAR(8000)
)
AS
BEGIN
--Account for ticks
SET #List = (REPLACE(#List, '''', ''))
--Account for 'emptynull'
IF LTRIM(RTRIM(#List)) = 'emptynull'
BEGIN
SET #List = ''
END
--Loop through all of the items in the string and add records for each item
WHILE (CHARINDEX(#SplitOn,#List)>0)
BEGIN
INSERT INTO #RtnValue (value)
SELECT Value = LTRIM(RTRIM(SUBSTRING(#List, 1, CHARINDEX(#SplitOn, #List)-1)))
SET #List = SUBSTRING(#List, CHARINDEX(#SplitOn,#List) + LEN(#SplitOn), LEN(#List))
END
INSERT INTO #RtnValue (Value)
SELECT Value = LTRIM(RTRIM(#List))
RETURN
END

SQL Server cursor - loop through multiple servers and execute query

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.

Can I apply an update statement to multiple databases at the same time?

Say I want to run the following:
update users set age = 10
on databases:
db1, db2, db3
All on the same server, I want to loop through and perform the same action.
Currently I am doing this manually using management studio via the dropdown.
Hoping there is a better way.
You could probably do it with dynamic SQL. Something like so:
create table #dbs (db_name sysname not null)
insert into #dbs values ('db1'),('db2'),('db3')
declare curs cursor for
select db_name from #dbs
declare #db sysname, #sql nvarchar(max)
open curs
while(1=1)
begin
fetch next from curs into #db
if (##fetch_status <> 0)
break
set #sql = 'update ' + quotename(#db) + '.dbo.users set age = 10'
exec(#sql)
end
close curs
deallocate curs
drop table #dbs
Not sure about doing it 'dynamically', i.e. a FOR-EACH style loop on all the databases in a server, but this should work:
USE db1
update users set age = 10
GO
USE db2
update users set age = 10
GO
USE db3
update users set age = 10
Designate a server as a central management server and then add the other servers to the server group. Then you can run the update on all databases within the group. http://msdn.microsoft.com/en-us/library/bb934126.aspx
use [WWAUTHxxx__] -- a db containing active databases.
set nocount on
declare #Catalog as nvarchar(32)
declare #LibraryName as varchar(255)
declare #dbtable as varchar(50)
declare #retval as nvarchar(50)
declare #sSQL as nvarchar(max)
declare #parmdef as nvarchar(500)
declare #retvalout as nvarchar(50)
Declare Library_Cursor Cursor for
select top(1000) xCatalog, xLibraryName
from Active_DBs
order by xcatalog
Open Library_Cursor;
Fetch Next from Library_Cursor into #Catalog, #LibraryName
while ##Fetch_status = 0
begin
set #dbTable = #Catalog + '.dbo.las_circperiods'
set #ParmDef = N'#retvalOUT int OUTPUT';
set #sSQL = N'Select #retvalout = count(*) from ' + #dbtable
+ ' where xlastcircdate is null'
exec sp_executesql #ssql,#parmdef,#retvalout=#retval output
if #retval > 0 -- check/print Sql and then activate.
-- I like checking to see the potentially affected databases.
begin
print #Catalog + ',' + #LibraryName + ',' + #retval
set #ssql = N'update ' + #dbTable
+ ' set xlastcircdate = '''' '
+ ' where xlastcircdate is null'
-- print #ssql -- View what you might will do
exec sp_executesql #ssql -- Do it.
end
Fetch Next from Library_Cursor into #Catalog, #LibraryName
end;
close Library_cursor
Deallocate Library_cursor

SQL Server multiple shrink operations

I'm trying to shrink all databases (files and logs) in SQL Server 2008 R2.
I have finished the script, but the problem is that when I loop over all databases and execute the query to do shrink file the first 3 or 4 shrinks work but them I have this error :
Msg 0, Level 11, State 0, Line 0
A severe error occurred on the current command. The results, if any,
should be discarded.
The script :
declare #db_name as varchar(30)
declare #db_recorvery_model as varchar(30)
declare #db_files_name as varchar(250)
declare #db_files_physical_name as varchar(250)
declare get_files cursor for
select b.name, a.name
from sys.master_files as a,
sys.databases as b
where a.database_id = b.database_id
order by b.name
open get_files
fetch next from get_files into #db_files_name, #db_files_physical_name
set #db_files_name = (select #db_files_name)
set #db_files_physical_name = (select #db_files_physical_name)
DECLARE #Command as nvarchar(max)
set #Command=''
while(##FETCH_STATUS=0)
BEGIN
if (#db_files_name='master' or #db_files_name='msdb' or #db_files_name='tempdb' or #db_files_name='model')
BEGIN
print 'Bases de dados do sql server: '+#db_files_name
END
ELSE
BEGIN
set #Command = 'USE ' + '[' + #db_files_name + '] DBCC SHRINKFILE ("'+#db_files_physical_name+'", 1 )'
EXEC sp_executesql #Command
print #Command
END
fetch next from get_files into #db_files_name, #db_files_physical_name
set #db_files_name = (select #db_files_name)
set #db_files_physical_name = (select #db_files_physical_name)
END
close get_files
deallocate get_files
Does anyone have any ideas ?
PS: I know that I shouldn't shrink but is a very special environment and not productive.
Can you determine which database flags the error? Can you try running your script on the single database that has the problem and see if it is consistently the same database that triggers the error? Perhaps it's a special database that you've missed that cannot be shrunk that way.
I have a similar environment containing temp databases that are not for long term storage, and I use the following script which has worked perfectly for hundreds of databases:
CREATE procedure [dbo].[ShrinkLog]
#DB varchar(200)
as
declare #LogFile varchar(200)
declare #Sql varchar(500)
SELECT #LogFile = name
FROM sys.master_files
where type_desc = 'LOG'
and db_name(database_id) = #DB
set #Sql = '
Use [' + #DB + ']
DBCC SHRINKFILE([' + #LogFile + '], 1)
'
print(#sql)
exec(#sql)
Keep in mind also that you don't want to run this command unless your server has plenty of hard drive/memory space as well.
Best regards,
If you want to do a log shrink, this will be the best code. I am using it for a while and it never crash to me.
declare #SQL nvarchar(max)
select #SQL = coalesce(#SQL + char(13) + char(10),'') + N'
Use ' + QUOTENAME(d.[name]) + ';' + CHAR(13) + '
ALTER DATABASE ' + QUOTENAME(d.[name]) + ' SET RECOVERY SIMPLE;
DBCC SHRINKFILE (' + quotename(mf.[name],'''') + ', 1);
ALTER DATABASE ' + QUOTENAME(d.[name]) + ' SET RECOVERY FULL;'
FROM sys.databases d
INNER JOIN sys.master_files mf ON [d].[database_id] = [mf].[database_id]
WHERE
d.[database_id] > 4 --no sys dbs
AND d.recovery_model = 1
AND d.is_read_only = 0
AND mf.[type] = 1 --log files
ORDER BY d.name
--print #SQL
execute (#SQL)

How many tables are on an instance of SQL Server

How do I find out how many tables are on my instance of SQL Server? I can get it for a single schema using select count(*) from sysobjects where type = 'U'
(from how to count number of tables/views/index in my database)
You're using the word "schema", but I think you're really asking to count tables across all "databases".
declare #t table (
DBName sysname,
NumTables int
)
insert into #t
exec sp_MSforeachdb N'select ''?'', count(*)
from [?].dbo.sysobjects
where type = ''U'''
select DBName, NumTables
from #t
where DBName not in ('distribution','master','model','msdb','tempdb')
order by DBName
select SUM(NumTables) as TotalTables
from #t
where DBName not in ('distribution','master','model','msdb','tempdb')
An option without using the hidden, undocumented sp_MSforeachdb
declare #sql nvarchar(max)
select #sql = coalesce(#sql + ' + ', '') + REPLACE('
(select count(*)
from ::DB::.sys.objects
where is_ms_shipped = 0
and type_desc = ''USER_TABLE'')', '::DB::', QUOTENAME(name))
from master.sys.databases
where owner_sid != 0x01
select #sql = 'select ' + #sql
exec (#sql) -- returns a single count of all [user] tables in the instance
>
A note on performance. It is insignificant in the greater scheme of things, but with all things interesting, someone is bound to time it. Here is a comparison of the ms_foreachdb approach passing through a temp table (it internally uses a cursor) against the string-concat method.
-- all the variables that we will use
declare #i int -- loop variable
declare #sql nvarchar(max) -- statement var used for 1st approach
declare #t table (DBName sysname, NumTables int) -- table used for 2nd approach
-- init plan cache and buffers
dbcc freeproccache dbcc dropcleanbuffers
print convert(varchar(30), getdate(), 121)
set #i = 0 while #i < 5 begin
set #sql = null
select #sql = coalesce(#sql, '') + REPLACE('
select #c = #c + count(*)
from ::DB::.sys.objects
where is_ms_shipped = 0
and type_desc = ''USER_TABLE''', '::DB::', QUOTENAME(name))
from master.sys.databases
where owner_sid != 0x01
select #sql = 'set nocount on declare #c int set #c = 0 ' + #sql + ' select #c'
exec (#sql)
-- clear plan cache and buffers after each run
dbcc freeproccache dbcc dropcleanbuffers set #i = #i + 1
end
print convert(varchar(30), getdate(), 121)
set #i = 0 while #i < 5 begin
insert into #t
exec sp_MSforeachdb N'select ''?'', count(*)
from [?].dbo.sysobjects
where type = ''U'''
select SUM(NumTables) as TotalTables
from #t
where DBName not in ('distribution','master','model','msdb','tempdb')
-- unfortunately this is required
delete from #t
-- clear plan cache and buffers after each run
dbcc freeproccache dbcc dropcleanbuffers set #i = #i + 1
end
print convert(varchar(30), getdate(), 121)
The result obtained for only 5 invocations (loop iterations) of each. YMMV
start : 2011-01-21 14:21:45.180
end of string-concat : 2011-01-21 14:21:57.497 (12.317)
end of sp_msforeachdb : 2011-01-21 14:22:13.937 (16.440)
It has to be noted that the temp table has to be emptied between each iteration of the 2nd approach, so that could contribute to the total time. It should have been insignificant though
Here is an answer that does not use undocumented functions and works in SQL Server 2005, 2008 and 2008R2. This answer can be used with minor modifications to run any statement across databases.
DECLARE #sql varchar(200), #dbname sysname, #dbid smallint;
CREATE table #alltables
(dbname sysname,
[number of tables] int);
SELECT top 1 #dbname = name, #dbid = database_id
FROM sys.databases
where database_id > 4;
WHILE (#dbname is not null)
begin
-- the statement below could contain any valid select statement
set #sql = 'use ' + #dbname + '; insert into #alltables select ''' + #dbname + ''', count(*) from sys.tables';
EXEC (#sql)
set #dbname = null;
SELECT top 1 #dbname = name, #dbid = database_id
FROM sys.databases
where database_id > #dbid;
end;
select * FROM #alltables;
SELECT sum([number of tables]) "Total Number of Tables in all user databases" from #alltables;
drop table #alltables;
Select Count(*)
From INFORMATION_SCHEMA.TABLES
Where TABLE_TYPE = 'BASE TABLE'
If what you are seeking is a way to determine how many tables exist across all databases on a given SQL Server instance, then you need to cycle through each database. One way would be:
Declare #Databases Cursor
Declare #DbName as nvarchar(64)
Declare #SQL nvarchar(max)
Declare #BaseSQL nvarchar(max)
Declare #Count int
Declare #TotalCount int
Set #Databases = Cursor Fast_Forward For
select [name]
from master..sysdatabases
where [name] Not In('master','model','msdb','tempdb')
Open #Databases
Fetch Next From #Databases Into #DbName
Set #BaseSQL = 'Select #Count = Count(*)
From DatabaseName.INFORMATION_SCHEMA.TABLES
Where TABLE_TYPE = ''BASE TABLE'''
Set #TotalCount = 0
While ##Fetch_Status = 0
Begin
Set #Count = 0
Set #SQL = Replace(#BaseSQL, 'DatabaseName', QuoteName(#DbName))
exec sp_executesql #SQL, N'#Count int OUTPUT', #Count OUTPUT
Set #TotalCount = #TotalCount + #Count
Fetch Next From #Databases Into #DbName
End
Close #Databases
Deallocate #Databases
Select #TotalCount
This solution has the advantage of not using any undocumented features such as sp_MSforeachdb however it is obviously more verbose.

Resources