sp_MSforeachdb: only include results from databases with results - sql

I'm running the below stored procedure sp_MSforeachdb with a simple command. My question is how to limit the result to show only the databases that have at least 1 record satisfying the command:
Here's my stored procedure:
EXECUTE master.sys.sp_MSforeachdb 'USE [?];
IF (EXISTS (SELECT *
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = ''Tabs''))
BEGIN
SELECT ''?'' as dbname,T.TabName, T.TabPath
FROM Tabs T
WHERE T.TabID IN (
SELECT Distinct TM.TabID
FROM TabModules TM
WHERE mID IN (
...
)
)
ORDER BY T.TabName
END
'
Any ideas how I can modify the sp so that it doesn't display the databases that have empty results (see image)?

Well, first, stop using sp_MSforEachDb. Oh, the problems (if you want proof, see here).
How about:
DECLARE #cmd NVARCHAR(MAX) = N'', #sql NVARCHAR(MAX) = N'';
SELECT #cmd += N'IF EXISTS (SELECT 1 FROM '
+ QUOTENAME(name) + '.sys.tables WHERE name = N''Tabs'')
SET #sql += N''UNION ALL
SELECT ''''' + name + ''''',T.TabName
FROM ' + QUOTENAME(name) + '.dbo.Tabs AS T
WHERE EXISTS
(
SELECT 1 FROM ' + QUOTENAME(name) + '.dbo.TabModules AS TM
WHERE TM.TabID = T.TabID
AND TM.mID IN -- this should probably be exists too
(
...
)
)
'''
FROM sys.databases
WHERE state = 0 -- assume you only want online databases
AND database_id > 4; -- assume you don't want system dbs
EXEC sp_executesql #cmd, N'#sql NVARCHAR(MAX) OUTPUT', #sql OUTPUT;
SET #sql = STUFF(#sql, 1, 10, '') + N' ORDER BY TabName;';
PRINT #sql; -- this will appear truncated, but trust me, it is not truncated
-- EXEC sp_executesql #sql;
If you really want some unknown, arbitrary number of separate resultsets, the change is simple.
DECLARE #cmd NVARCHAR(MAX) = N'', #sql NVARCHAR(MAX) = N'';
SELECT #cmd += N'IF EXISTS (SELECT 1 FROM '
+ QUOTENAME(name) + '.sys.tables WHERE name = N''Tabs'')
SET #sql += N''SELECT ''''' + name + ''''',T.TabName
FROM ' + QUOTENAME(name) + '.dbo.Tabs AS T
WHERE EXISTS
(
SELECT 1 FROM ' + QUOTENAME(name) + '.dbo.TabModules AS TM
WHERE TM.TabID = T.TabID
AND TM.mID IN -- this should probably be exists too
(
...
)
)
ORDER BY T.TabName;
'';'
FROM sys.databases
WHERE state = 0 -- assume you only want online databases
--AND database_id > 4; -- assume you don't want system dbs
EXEC sp_executesql #cmd, N'#sql NVARCHAR(MAX) OUTPUT', #sql OUTPUT;
PRINT #sql; -- this will appear truncated, but trust me, it is not truncated
-- EXEC sp_executesql #sql;

You basically need another IF to only run the select if data exists.
Here's what i did to test.
On DB1:
create table mytesttable(a int)
insert mytesttable values(1)
On DB2:
create table mytesttable(a int)
So you want DB1 to return results, but DB2 not to. you can use the following sql:
EXECUTE master.sys.sp_MSforeachdb 'USE [?];
IF (EXISTS (SELECT *
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = ''mytesttable''))
BEGIN
IF EXISTS (SELECT 1 FROM mytesttable) BEGIN
SELECT ''?'' as dbname,T.A
FROM mytesttable AS T
END
END
'
This only returns:
db1, 1

Related

Dynamic FROM source based on query results

I have a SQL Server instance that has several databases. Each database has a users table. I'm attempting to write a query that will count user record counts for each database. I've gotten to a point where I can select all database_name, schema_name, and table_name's into a temp table. I need to be able to select as from the results of that query though but I don't know how to go about doing that.
DECLARE #sql nvarchar(max);
SELECT #sql =
(SELECT ' UNION ALL
SELECT ' + + quotename(name,'''') + ' as database_name,
s.name COLLATE DATABASE_DEFAULT
AS schema_name,
t.name COLLATE DATABASE_DEFAULT as table_name
FROM '+ quotename(name) + '.sys.tables t
JOIN '+ quotename(name) + '.sys.schemas s
ON s.schema_id = t.schema_id'
FROM sys.databases
WHERE state = 0
ORDER BY [name] FOR XML PATH(''), type).value('.', 'nvarchar(max)');
SET #sql = stuff(#sql, 1, 12, '') + ' order by database_name,
schema_name,
table_name';
DECLARE #allTables TABLE
(
database_name NVARCHAR(MAX),
schema_name NVARCHAR(MAX),
table_name NVARCHAR(MAX)
)
INSERT INTO #allTables
EXEC (#sql)
SELECT
*
-- ,(select count(*) from [database_name].[schema_name].[table_name] where isadmin = 0) as 'Users'
-- ,(select count(*) from [database_name].[schema_name].[table_name] where isadmin = 1) as 'Admins'
FROM
#allTables
WHERE
table_name = 'C2_SEC_USERS'
This results in the following result set:
I have commented out the part of the select that I need to actually count the users because it's not valid SQL. Is it possible to select FROM based on the result set? If so, how?
You are almost there. Now you need to do 2 things -
Firstly Add an identity field in your Table variable #allTables like following
DECLARE #allTables TABLE
(
id int identity(1,1),
database_name NVARCHAR(MAX),
schema_name NVARCHAR(MAX),
table_name NVARCHAR(MAX)
)
Secondly build dynamic query and execute
DECLARE
#CurrentId AS INT = 0,
#CurrentDb AS NVARCHAR(MAX),
#CurrentSchema AS NVARCHAR(MAX);
SET #sql = '';
WHILE (EXISTS(SELECT id FROM #allTables WHERE table_name = 'C2_SEC_USERS' AND id > #CurrentId))
BEGIN
SELECT TOP 1 #CurrentId = id, #CurrentDb = [database_name], #CurrentSchema = [schema_name] FROM #allTables WHERE table_name = 'C2_SEC_USERS' AND id > #CurrentId
SET #sql += CONCAT('SELECT ''', #CurrentDb, ''' ''database'', COUNT(*) ''User Count'' FROM ', #CurrentDb,'.',#CurrentSchema,'.C2_SEC_USERS');
SET #sql += '
';
END;
EXEC (#sql)
I ended up figuring it out by using a cursor
declare #sql nvarchar(max);
declare #allTables TABLE (database_name NVARCHAR(MAX), schema_name NVARCHAR(MAX), table_name NVARCHAR(MAX))
select #sql =
(select ' UNION ALL
SELECT ' + quotename(name,'''') + ' as database_name,
s.name COLLATE DATABASE_DEFAULT
AS schema_name,
t.name COLLATE DATABASE_DEFAULT as table_name
FROM '+ quotename(name) + '.sys.tables t
JOIN '+ quotename(name) + '.sys.schemas s
on s.schema_id = t.schema_id'
from sys.databases
where state=0
order by [name]
for xml path(''), type).value('.', 'nvarchar(max)');
set #sql = stuff(#sql, 1, 12, '') + ' order by database_name, schema_name, table_name';
insert #allTables
exec (#sql)
declare #userCounts TABLE (database_name NVARCHAR(MAX),
users int,
admins int)
DECLARE #dbName VARCHAR(MAX)
DECLARE #schemaName VARCHAR(MAX)
DECLARE #tableName VARCHAR(MAX)
DECLARE #countSql NVARCHAR(MAX)
DECLARE tables_cursor CURSOR FOR SELECT database_name, schema_name, table_name
FROM #allTables
where table_name = 'C2_SEC_USERS' and database_name != 'master'
OPEN tables_cursor
FETCH NEXT FROM tables_cursor INTO #dbName, #schemaName, #tableName
WHILE ##FETCH_STATUS = 0 BEGIN
set #countSql = 'Select ''' + #dbName + ''' as database_name, (select count(*) from [' + #dbName + '].dbo.C2_SEC_USERS where isadmin = 0 and isinactive = 0) as users, (select count(*) from [' + #dbName + '].dbo.C2_SEC_USERS where isadmin = 1 and isinactive = 0) as admins'
INSERT INTO #userCounts
exec (#countSql)
FETCH NEXT FROM tables_cursor INTO #dbName, #schemaName, #tableName;
END;
CLOSE tables_cursor;
DEALLOCATE tables_cursor;
select * from #userCounts;

Select all databases that have a certain table and a certain column

I need to create a query that is executed on all databases of my SQL server instance. An additional constraint is, that the query should only be executed on databases that contain a special table with a special column. Background is that in some databases the special table does (not) have the special column.
Based on this solution, what I have until now is a query that executes only on databases that contain a certain table.
SELECT *
FROM sys.databases
WHERE DATABASEPROPERTY(name, 'IsSingleUser') = 0
AND HAS_DBACCESS(name) = 1
AND state_desc = 'ONLINE'
AND CASE WHEN state_desc = 'ONLINE'
THEN OBJECT_ID(QUOTENAME(name) + '.[dbo].[CERTAIN_TABLE]', 'U')
END IS NOT NULL
However, what is still missing is a constraint that the query should only select databases where the table CERTAIN_TABLE has a specific column. How can this be achieved?
When i want to loop through all databases, i do a loop like the following. Its easy to follow:
DECLARE #dbs TABLE ( dbName NVARCHAR(100) )
DECLARE #results TABLE ( resultName NVARCHAR(100) )
INSERT INTO #dbs
SELECT name FROM sys.databases
DECLARE #current NVARCHAR(100)
WHILE (SELECT COUNT(*) FROM #dbs) > 0
BEGIN
SET #current = (SELECT TOP 1 dbName FROM #dbs)
INSERT INTO #results
EXEC
(
'IF EXISTS(SELECT 1 FROM "' + #current + '".INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = ''Target_Table_Name'' AND COLUMN_NAME = ''Target_Column_Name'')
BEGIN
--table and column exists, execute query here
SELECT ''' + #current + '''
END'
)
DELETE FROM #dbs
WHERE dbName = #current
END
SELECT * FROM #results
You are going to need either some looping or dynamic sql for this. I really dislike loops so here is how you could do this with dynamic sql.
declare #TableName sysname = 'CERTAIN_TABLE'
, #ColumnName sysname = 'CERTAIN_COLUMN'
declare #SQL nvarchar(max) = ''
select #SQL = #SQL + 'select DatabaseName = ''' + db.name + ''' from ' + QUOTENAME(db.name) + '.sys.tables t join ' + QUOTENAME(db.name) + '.sys.columns c on c.object_id = t.object_id where t.name = ''' + QUOTENAME(#TableName) + ''' and c.name = ''' + QUOTENAME(#ColumnName) + '''' + char(10) + 'UNION ALL '
from sys.databases db
where db.state_desc = 'ONLINE'
order by db.name
select #SQL = substring(#SQL, 0, len(#SQL) - 9)
select #SQL
--uncomment the line below when you are comfortable the query generated is correct
--exec sp_executesql #SQL

How can I return a distinct count of a variable for all tables in my database?

I have a SQL database with 60+ tables, almost all of which are populated with a CLIENTID field. I want to count the number of unique client IDs in each table.
The results I'm looking for are:
TABLE_NAME; CLIENTID_COUNT
dbo.HISTORY; 650
dbo.VISITS; 596
dbo.SALES; 1053
...; ...
This seems like it should be so simple but I've been playing around with cursors for hours and can't figure this one out. Please help!
IF OBJECT_ID('tempdb..#temp_RESULTS') IS NOT NULL DROP TABLE #temp_RESULTS
CREATE TABLE #TEMP_RESULTS
(
TABLENAME VARCHAR(MAX),
CLIENTCNT BIGINT
)
DECLARE #TABLENAME VARCHAR(MAX)
DECLARE #command VARCHAR(MAX)
IF OBJECT_ID('tempdb..#temp_PROCESS') IS NOT NULL DROP TABLE #temp_PROCESS
SELECT * INTO #TEMP_PROCESS FROM sys.tables
WHILE EXISTS(SELECT * FROM [#TEMP_PROCESS])
BEGIN
SET #TABLENAME = (SELECT TOP 1 [NAME] FROM [#TEMP_PROCESS])
SET #command = ('SELECT ''' + #TABLENAME + ''', COUNT(DISTINCT CLIENTID) AS CLIENTCNT FROM ' + #TABLENAME)
SELECT #command
INSERT INTO #TEMP_RESULTS
EXEC(#command)
DELETE FROM [#TEMP_PROCESS] WHERE [NAME] = #TABLENAME
END
SELECT * FROM [#TEMP_RESULTS]
Assuming the column is exactly ClientId in every table, you should be able to use this as is:
DROP TABLE IF EXISTS #clientId
CREATE TABLE #clientId
(
TableName nvarchar(1000),
ClientIdCount bigint
)
DECLARE #TableName nvarchar(1000);
DECLARE #CurrentQuery nvarchar(2000);
DECLARE result_cursor CURSOR local fast_forward FOR
SELECT DISTINCT
'['+TABLE_SCHEMA + '].[' + TABLE_NAME + ']'
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
COLUMN_NAME = 'ClientId'
OPEN result_cursor
FETCH NEXT FROM result_cursor into #TableName
WHILE ##FETCH_STATUS = 0
BEGIN
SET #CurrentQuery = 'SELECT ''' + #TableName + ''', COUNT(DISTINCT ClientId) FROM ' + #TableName
--print #CurrentQuery
INSERT INTO
#clientId
(
TableName,
ClientIdCount
)
EXEC(#CurrentQuery)
FETCH NEXT FROM result_cursor into #TableName
END
--end loop
--clean up
CLOSE result_cursor
DEALLOCATE result_cursor
GO
SELECT
*
FROM
#clientId
You could use dynamic sql.
This will read through your system tables, find those that have a ClientID column, and build the text of a query that's in the general shape of 'Select Count(DISTINCT ClientID)' from each table.
DECLARE #SQLQuery as nvarchar(max) = ''
------------------------------------
-- GET THE TABLES THAT HAVE A CLIENTID FROM SCHEMA
SELECT #SQLQuery = #SQLQuery + qryTxt FROM (
SELECT DISTINCT 'SELECT ''' + tables.name + ''', COUNT(DISTINCT CLIENTID) FROM ' + tables.name + ' UNION ' AS qryTxt
FROM sys.columns left join sys.tables on columns.object_id = tables.object_id where columns.name = CLIENTID AND isnull(tables.name, '') <> '') subquery
------------------------------------
-- REMOVE THE LAST 'UNION' KEYWORD FROM SQLQUERY
SET #SQLQuery = left(#sqlQuery, len(#sqlQuery) - 5)
------------------------------------
-- EXECUTE
execute sp_executesql #SQLQuery
I really dislike cursors and loops. Even though this is not going to be much difference for a performance perspective I like to share how you can leverage the system tables and dynamic sql to avoid using a cursor, while loop or temp tables for something like this.
This code is literally all you need to to do this.
declare #SQL nvarchar(max) = ''
select #SQL = #SQL + 'select TableName = ''' + t.name + ''', ClientID_Count = count(distinct clientID)
from ' + QUOTENAME(t.name) + ' UNION ALL '
from sys.tables t
join sys.columns c on c.object_id = t.object_id
where c.name = 'clientID'
select #SQL = left(#SQL, len(#SQL) - 10) --removes the last UNION ALL
select #SQL
--once your comfortable the dynamic sql is correct just uncomment the line below.
--exec sp_executesql #SQL
A similar pattern to other answers here, but this is how I would tackle it:
IF OBJECT_ID('#Tables', 'U') IS NOT NULL
DROP TABLE #Tables;
SELECT ID = IDENTITY(INT, 1, 1),
SchemaName = OBJECT_SCHEMA_NAME([object_id]),
TableName = OBJECT_NAME([object_id]),
ColumnName = name,
DistinctCount = 0
INTO #Tables
FROM sys.columns
WHERE name = 'CLIENTID';
DECLARE #ID INT = 1,
#MaxID INT = (SELECT MAX(ID) FROM #Tables);
WHILE #ID < #MaxID
BEGIN;
DECLARE #SQLCommand VARCHAR(MAX);
SELECT #SQLCommand = FORMATMESSAGE('
UPDATE #Tables SET DistinctCount = (
SELECT COUNT(DISTINCT %s) FROM %s.%s
)
WHERE ID = %i;',
QUOTENAME(ColumnName), QUOTENAME(SchemaName), QUOTENAME(TableName), ID)
FROM #Tables
WHERE ID = #ID;
EXEC (#SQLCommand);
SET #ID += 1;
END;
SELECT *
FROM #Tables;

Union of multiple sp_MSforeachdb result sets

I can successfully query the same table in multiple databases as follows:
DECLARE #command varchar(1000)
SELECT #command = 'select * from table'
EXEC sp_MSforeachdb #command
However, all of these results are, as expected, returned in different result windows. What's the easiest way to perform a union of all of these results?
Please stop using sp_MSforeachdb. For anything. Seriously. It's undocumented, unsupported, and spectacularly broken:
Making a more reliable and flexible sp_MSforeachdb
Execute a Command in the Context of Each Database in SQL Server
If you know that all databases have the same table (and that they all have the same structure!), you can do this:
DECLARE #sql NVARCHAR(MAX);
SET #sql = N'';
SELECT #sql = #sql + N'UNION ALL SELECT col1,col2 /*, etc. */
FROM ' + QUOTENAME(name) + '.dbo.tablename'
FROM sys.databases WHERE database_id > 4 AND state = 0;
SET #sql = STUFF(#sql, 1, 10, '');
EXEC sp_executesql #sql;
This ignores system databases and doesn't attempt to access any databases that are currently not ONLINE.
Now, you may want to filter this further, e.g. not include any databases that don't have a table called tablename. You'll need to nest dynamic SQL in this case, e.g.:
DECLARE #sql NVARCHAR(MAX);
SET #sql = N'DECLARE #cmd NVARCHAR(MAX);
SET #cmd = N'''';';
SELECT #sql = #sql + N'
SELECT #cmd = #cmd + N''UNION ALL
SELECT col1,col2 /*, etc. */ FROM '
+ QUOTENAME(name) + '.dbo.tablename ''
WHERE EXISTS (SELECT 1 FROM ' + QUOTENAME(name)
+ '.sys.tables AS t
INNER JOIN ' + QUOTENAME(name) + '.sys.schemas AS s
ON t.[schema_id] = s.[schema_id]
WHERE t.name = N''tablename''
AND s.name = N''dbo'');'
FROM sys.databases WHERE database_id > 4 AND state = 0;
SET #sql = #sql + N';
SET #cmd = STUFF(#cmd, 1, 10, '''');
PRINT #cmd;
--EXEC sp_executesql #cmd;';
PRINT #sql;
EXEC sp_executesql #sql;
This doesn't validate the column structure is compatible, but you'll find that out pretty quickly.
Another way to skin this cat is to use dynamic SQL:
DECLARE #sql varchar(max);
SELECT #sql = Coalesce(#sql + ' UNION ALL ', '') + 'SELECT list, of, columns FROM ' + QuoteName(name) + '.schema.table'
FROM sys.databases
;
PRINT #sql
--EXEC (#sql);
Had some collation issues and had to use
AND COLLATION_NAME = 'SQL_Latin1_General_CP1_CI_AS'

Drop tables that starts with specific characters

I have multiple table names like g_str_a , g_str_ab , g_str_abc . I would like to drop all those tables that start with g_str on SQL Server 2008.
Will DROP Tables like 'g_str' help?
Please help me with the script.
SELECT
'DROP TABLE ' + QUOTENAME(SCHEMA_NAME(schema_id)) + '.' + QUOTENAME(name) + ';'
FROM sys.tables
WHERE name LIKE 'g\_str%' ESCAPE '\'
Then review the script and run it.
You can also concatenate the result into a single string and execute with EXEC if you need an entirely automated solution.
You could use dynamic SQL:
DECLARE #SQL NVARCHAR(MAX) = '';
SELECT #SQL = #SQL + 'DROP TABLE ' + QUOTENAME(SCHEMA_NAME([Schema_ID])) + '.' + QUOTENAME([name]) + ';'
FROM sys.tables
WHERE Name LIKE 'g\_str%' ESCAPE('\');
EXECUTE SP_EXECUTESQL #SQL;
Following query will delete tables automatically:
BEGIN TRANSACTION
DECLARE #tmpTablesToDelete TABLE (
RowNumber INT PRIMARY KEY
,Query NVARCHAR(MAX)
)
INSERT INTO
#tmpTablesToDelete
SELECT
RowNumber = ROW_NUMBER() OVER (ORDER BY (SELECT (0)))
,'DROP TABLE '+schemas.name+'.'+objects.name AS Query
FROM
sys.objects
INNER JOIN
sys.schemas
ON
schemas.schema_id = objects.schema_id
WHERE
type = 'U' AND objects.name like 'g_str%'
DECLARE #Counter INT
SELECT #Counter = MAX(RowNumber) FROM #tmpTablesToDelete
WHILE(#Counter > 0) BEGIN
DECLARE #Query NVARCHAR(MAX)
SELECT #Query = Query FROM #tmpTablesToDelete WHERE RowNumber = #Counter
PRINT #Query
EXEC sp_executesql #statement = #Query
SET #Counter = #Counter - 1
END
COMMIT TRANSACTION