SQL Server find first gap between ID key fields - sql

Is there any other better way to perform this operation?
-- USE EXAMPLE: EXEC GetFirstIdInGap #tableName ='Employees',#column='IdEmployee'
CREATE PROCEDURE GetFirstIdInGap
(#tableName sysname,
#column sysname)
AS
IF #tableName IS NOT NULL and #column IS NOT NULL
BEGIN
DECLARE #col varchar(50), #col2 varchar(50)
SET #col = 'A.' + #column;
SET #col2 = 'A2.' + #column;
EXEC ('SELECT ISNULL((MIN('+#col+') - 1),(SELECT ISNULL(MAX('+#column+')+1,1) FROM '+#tableName+'))
AS '+#column+'
FROM '+#tableName+' AS a
LEFT JOIN '+#tableName+' AS a2
ON '+#col2+' = '+#col+' - 1
WHERE '+#col2+' IS NULL AND '+#col+' > 1');
END
GO
It gets the first free ID (if there are gaps) or the last one + 1 given a #tableName and #column. If there are no rows, it returns as the first ID = 1.
UPDATE:
For those who have asked about why do I need gaps of ID's, I am gonna explain my problem (although I didn't want to dig into it). I work with C# Winforms applications against other firmware applications which have serious memory restrictions. One of those restrictions is that I can only use a maximum code value of 65536. Those codes are equivalent of database ID's, and in some cases the firmware code had reached the value of 65536. That's why gap reusing would be wonderful for me.

t is your table
select
coalesce((select min(id)+1 from t mt where not exists(select 1 from t where id+1 = mt.id )), 1) firstgap

Here is an approach that doesn't require a numbers table (even one with more than 1,000 rows):
CREATE PROCEDURE dbo.GetFirstIdInGap_2
#table SYSNAME,
#column SYSNAME
AS
BEGIN
SET NOCOUNT ON;
DECLARE #sql NVARCHAR(MAX) = N';WITH c AS
(
SELECT n = ' + #column + ',
rn = ROW_NUMBER() OVER (ORDER BY ' + #column + ')
FROM ' + #table + '
)
SELECT ' + #column + ' = 1 + COALESCE(
(SELECT MIN(c.n) FROM c INNER JOIN c AS n
ON n.rn = c.rn + 1 WHERE n.n - c.n > 1),
(SELECT MAX(c.n) FROM c),
0);';
EXEC sp_executesql #sql;
END
GO

t is your table
select min(isnull(id,0)+1) from t where isnull(id,0) + 1 not in (select isnull(id,0) from t)

Related

SQL - How do I get 1 empty column per row in a related table?

I am trying to write a SQL query that adds a certain amount of empty columns, based on the number of rows in a related table (t1) for a Crystal Report. These columns should have the header of the name of the dataset.
So it should look something like this:
However I would need to change the script each time a new row gets added (e.g. opening a store - not very often, but it does happen).
I thought about using the pivot function, but I believe the number of rows must be defined - plus, there is no calculation / aggregation happening.
Does anybody have an idea on how to solve this?
As Larnu already mentioned, dynamic SQL would be one way to go. I would suggest using a combination of XML PATH and dynamic SQL. Following an example:
DECLARE #colList VARCHAR(MAX) = (SELECT STUFF((SELECT ',NULL as t1_row' + cast(col1 AS varchar(3))
FROM MyTable
FOR XML PATH('')) ,1,1,'') AS Txt
)
DECLARE #stmt VARCHAR(MAX) = 'SELECT Col1, Col2, Col3, ' + #colList + ' FROM MyTable'
EXEC (#stmt)
I was able to achieve the result using dynamic SQL.
The Script looks something like this:
DECLARE #STRSQL NVARCHAR(MAX) = 'WITH a AS (SELECT ';
DECLARE #Kst nvarchar(6);
DECLARE #Markt NVARCHAR(30);
DECLARE #SCHEMA_NAME VARCHAR(50) = 'XTRADE';
DECLARE C1 CURSOR FOR
SELECT NUMMER, BEZEICHNUNG
from XTRADE.KUNDE
where NUMMER > 99 and NUMMER not in (194, 196, 198)
and (DATUM_SCHLIESSUNG > GETDATE() or DATUM_SCHLIESSUNG is null)
order by BEZEICHNUNG
OPEN C1
PRINT #Kst + ' ' + #Markt
FETCH NEXT
FROM C1 into #Kst, #Markt
while ##FETCH_STATUS = 0
BEGIN
SET #STRSQL = #STRSQL + 'null as [' + #Markt + '], '
FETCH NEXT
FROM C1 into #Kst, #Markt
END
CLOSE C1
DEALLOCATE C1;
SET #STRSQL = left(#STRSQL, len(#Strsql) - 1) + ')'
DECLARE #Statement nvarchar(max) = ', b as (select 1 as Col1, 1 as Col2, 5 as Col3 union all select 2,2,12 union all select 3, 3, 42)';
DECLARE #Exec nvarchar(max) = #STRSQL + #Statement + 'select * from b cross join a';
print #Exec;
exec sp_executesql #Exec

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;

How to increment column ordinal position in a while function?

Abstract: I have a table with multiple columns and I am trying to implement a while (loop) function to all the columns to pull out the average of each column.
I coded this to attribute a number to each column (to ease the possibility of an incremental method)
Declare #tablename as varchar(128)
Declare #column1 as varchar(128)
Delect #tablename = 'MOMENTUM_Results'
Select #column1 = sc.name
from sysobjects as so inner join syscolumns as sc on so.id = sc.id
where so.name = #tablename and sc.colid = 2
When I print this code it prints the name of the column based on the number I put from 1 to 122.
Code for the loop function:
WHILE ....
BEGIN
SET #SQL = 'SELECT AVG(MR.[' + #column1 + ']) From MOMENTUM_Quintile MQ
Left Join MOMENTUM_Returns MR on MQ.Mnemonic = MR.Mnemonic WHERE MQ.[' + #column1 + '] = 1'
END
But I definitely do not have a clue how to integrate this function into the loop so the function goes through all the columns one by one, therefore looking for some advice..
Thank you!
R.H.
Can you not just move your SELECT from sys.Columns into the body of your WHILE loop to get the one you're after? Something like this...
DECLARE #TABLENAME AS VARCHAR(128);
DECLARE #COLUMN1 AS VARCHAR(128);
DECLARE #COUNTER INT;
SET #TABLENAME = 'MOMENTUM_Results';
SET #COUNTER = 1;
WHILE ....
BEGIN
SELECT #COLUMN1 = C.Name
FROM sys.Columns C
WHERE OBJECT_NAME(C.object_id) = #TABLENAME
AND C.column_id = #COUNTER
;
SET #SQL = 'SELECT AVG(MR.[' + #COLUMN1 + ']) From MOMENTUM_Quintile MQ
Left Join MOMENTUM_Returns MR on MQ.Mnemonic = MR.Mnemonic WHERE MQ.[' + #COLUMN1 + '] = 1';
... Other Stuff ...
SET #COUNTER = #COUNTER + 1;
END
;
Maybe I didn't properly understand your question, but if you already have everything written out you could just change the placement to select the column names you want.
Of course you can also add additional conditions to make sure you're getting matched columns, or columns by name, or whatever...
EDIT:
You asked about inserting the resultant query into a table. If you already have a table with a column ready to receive the query, then the answer is simple... In the body of your WHILE loop, after retrieving the desired query, simply add an INSERT command:
WHILE ....
BEGIN
SELECT #COLUMN1 = C.Name
FROM sys.Columns C
WHERE OBJECT_NAME(C.object_id) = #TABLENAME
AND C.column_id = #COUNTER
;
SET #SQL = 'SELECT AVG(MR.[' + #COLUMN1 + ']) From MOMENTUM_Quintile MQ
Left Join MOMENTUM_Returns MR on MQ.Mnemonic = MR.Mnemonic WHERE MQ.[' + #COLUMN1 + '] = 1';
INSERT INTO <mytable> (<querycolumn>) VALUES (#SQL);
... Other Stuff ...
SET #COUNTER = #COUNTER + 1;
If you need to create the table, you can do that in the head of the script, before declaring anything else, and do the same thing during the WHILE:
DECLARE #TABLENAME AS VARCHAR(128);
DECLARE #COLUMN1 AS VARCHAR(128);
DECLARE #COUNTER INT;
SET #TABLENAME = 'MOMENTUM_Results';
SET #COUNTER = 1;
CREATE TABLE <mytable> (<optional_column1>,<optional_column2>,<querycolumn>);
....
WHILE ....
BEGIN
SELECT #COLUMN1 = C.Name
FROM sys.Columns C
WHERE OBJECT_NAME(C.object_id) = #TABLENAME
AND C.column_id = #COUNTER
;
SET #SQL = 'SELECT AVG(MR.[' + #COLUMN1 + ']) From MOMENTUM_Quintile MQ
Left Join MOMENTUM_Returns MR on MQ.Mnemonic = MR.Mnemonic WHERE MQ.[' + #COLUMN1 + '] = 1';
INSERT INTO <mytable> (<querycolumn>) VALUES (#SQL);
... Other Stuff ...
SET #COUNTER = #COUNTER + 1;
First get all the column names in that table into a temporary table(#temp)
Get the count of all columns(#count_of_columns) in that temptable.
declare a variable #count=1
Declare while loop as While(#count<=#count_of_columns)
--> fetch the column name one after another inside this while loop using the following query
SET #column1=(select top 1 column1 from
(select Row_number()over (order by column1) as r_n_n, column1
from #temp) aa
where r_n_n >=#Count)
--> use your actual code here
--> increment the #count variable
set #count = #count + 1
Don't use loops or cursors, if you can avoid them. The devil of procedural thinking is pulling you away from the light of set based approaches deep into pain and misery!
I know the column names in advance, I just did it to ease the way I would loop through each column
NO! This is not easing the way but punishing the poor engine :-)
Is it this you are looking for?
CREATE TABLE dbo.Test(ID INT, VAl1 DECIMAL(14,4), Val2 DECIMAL(14,4), Val3 DECIMAL(14,4));
GO
INSERT INTO dbo.Test VALUES(1,1,10,100),(2,2,20,200),(3,3,30,300);
GO
SELECT AVG(Val1) AS Avg1,AVG(Val2) AS Avg2,AVG(Val3) AS Avg3
FROM dbo.Test;
GO
DROP TABLE dbo.Test;
The result
Avg1 Avg2 Avg3
2.000000 20.000000 200.000000
UPDATE
If you just want to be lazy and avoid typing, you might try something like this:
SELECT ',AVG(' + COLUMN_NAME + ') AS ' + QUOTENAME('AVG_' + COLUMN_NAME)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA='dbo' AND TABLE_NAME='Test'
FOR XML PATH('')
The result
,AVG(ID) AS [AVG_ID],AVG(VAl1) AS [AVG_VAl1],AVG(Val2) AS [AVG_Val2],AVG(Val3) AS [AVG_Val3]
With this trick you can build your hard coded statement quite easily :-)

Get top three most common values from every column in a table

I'm trying to write a query that will produce a very small sample of data from each column of a table, in which the sample is made up of the top 3 most common values. This particular problem is part of a bigger task, which is to write scripts that can characterize a database and its tables, its data integrity, and also quickly survey common values in the table on a per-column basis. Think of this as an automated "analysis" of a table.
On a single column basis, I do this already by simply calculating the frequency of values and then sorting by frequency. If I had a column called "color" and all colors were in it, and it just so happened that the color "blue" was in most rows, then the top 1 most frequently occurring value would be "blue". In SQL that is easy to calculate.
However, I'm not sure how I would do this over multiple columns.
Currently, when I do a calculation over all columns of a table, I perform the following type of query:
USE database;
DECLARE #t nvarchar(max)
SET #t = N'SELECT '
SELECT #t = #t + 'count(DISTINCT CAST(' + c.name + ' as varchar(max))) "' + c.name + '",'
FROM sys.columns c
WHERE c.object_id = object_id('table');
SET #t = SUBSTRING(#t, 1, LEN(#t) - 1) + ' FROM table;'
EXEC sp_executesql #t
However, its not entirely clear to me how I would do that here.
(Sidenote:columns that are of type text, ntext, and image, since those would cause errors while counting distinct values, but i'm less concerned about solving that)
But the problem of getting top three most frequent values per column has got me absolutely stumped.
Ideally, I'd like to end up with something like this:
Col1 Col2 Col3 Col4 Col5
---------------------------------------------------------------------
1,2,3 red,blue,green 29,17,0 c,d,j nevada,california,utah
I hacked this together, but it seems to work:
I cant help but think I should be using RANK().
USE <DB>;
DECLARE #query nvarchar(max)
DECLARE #column nvarchar(max)
DECLARE #table nvarchar(max)
DECLARE #i INT = 1
DECLARE #maxi INT = 10
DECLARE #target NVARCHAR(MAX) = <table>
declare #stage TABLE (i int IDENTITY(1,1), col nvarchar(max), tbl nvarchar(max))
declare #results table (ColumnName nvarchar(max), ColumnValue nvarchar(max), ColumnCount int, TableName NVARCHAR(MAX))
insert into #stage
select c.name, o.name
from sys.columns c
join sys.objects o on o.object_id=c.object_id and o.type = 'u'
and c.system_type_id IN (select system_type_id from sys.types where [name] not in ('text','ntext','image'))
and o.name like #target
SET #maxi = (select max(i) from #stage)
while #i <= #maxi
BEGIN
set #column = (select col from #stage where i = #i)
set #table = (select tbl from #stage where i = #i)
SET #query = N'SELECT ' +''''+#column+''''+' , '+ #column
SELECT #query = #query + ', COUNT( ' + #column + ' ) as count' + #column + ' , ''' + #table + ''' as tablename'
select #query = #query + ' from ' + #table + ' group by ' + #column
--Select #query
insert into #results
EXEC sp_executesql #query
SET #i = #i + 1
END
select * from #results
; with cte as (
select *, ROW_NUMBER() over (partition by Columnname order by ColumnCount desc) as rn from #results
)
select * from cte where rn <=3
Start with this SQL Statement builder, and modify it to suit your liking:
EDIT Added Order by Desc
With ColumnSet As
(
Select TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME
From INFORMATION_SCHEMA.COLUMNS
Where 1=1
And TABLE_NAME IN ('Table1')
And COLUMN_NAME IN ('Column1', 'Column2')
)
Select 'Select Top 3 ' + COLUMN_NAME + ', Count (*) NumInstances From ' + TABLE_SCHEMA + '.'+ TABLE_NAME + ' Group By ' + COLUMN_NAME + ' Order by Count (*) Desc'
From ColumnSet

Getting SQL Server Cross database Dependencies

SQL Server Version - 2008 R2
I am working on evaluating a DMS solution, with an objective of taking over maintenance. The original solution has one central database, that has data pertaining to the manufacturer. It also has one database for each dealer, which means there are a lot of cross database dependencies.
The problems:
No DB documentation
No code comments
Lots of heaps
No standard object naming conventions
The central DB has 460+ tables and 900+ SProcs, in addition to other
objects
Each dealer DB has 370+ tables and 2350+ SProcs, in addition to other
objects
As a first step, I am recommending a complete clean-up of the DB, for which it is critical to understand object dependencies, including cross database dependencies. I tried using Red Gate's solution, but the output is way too voluminous. All I want is a list of objects in the databases that do not have any dependencies - they neither depend on other objects, nor are there any objects that depend on them.
Here is the script I have used to get a list of dependencies:
SELECT
DB_NAME() referencing_database_name,
OBJECT_NAME (referencing_id) referencing_entity_name,
ISNULL(referenced_schema_name,'dbo') referenced_schema_name,
referenced_entity_name,
ao.type_desc referenced_entity_type,
ISNULL(referenced_database_name,DB_NAME()) referenced_database_name
FROM sys.sql_expression_dependencies sed
JOIN sys.all_objects ao
ON sed.referenced_entity_name = ao.name
I will be creating a table - Dependencies - into which I will be inserting this result set from each DB. As a next step, I will also be creating another table - AllObjects- which will contain a list of all objects in the Databases. Here is the script to do this:
SELECT
DB_NAME() DBName,
name,
type_desc
FROM sys.all_objects
WHERE type_desc IN
(
'VIEW',
'SQL_TABLE_VALUED_FUNCTION',
'SQL_STORED_PROCEDURE',
'SQL_INLINE_TABLE_VALUED_FUNCTION',
'USER_TABLE',
'SQL_SCALAR_FUNCTION'
)
Now, a list of name from this table, that do not appear in the referenced_entity_name column in the dependencies table should give a list of objects that I am looking for.
SELECT
AO.DBName,
AO.name,
AO.type_desc
FROM AllObjects AO
LEFT OUTER JOIN Dependencies D ON
D.referenced_database_name = AO.DBName AND
D.referenced_entity_name = AO.name AND
D.referenced_entity_type = AO.type_desc
WHERE
D.referenced_database_name IS NULL AND
D.referenced_entity_name IS NULL AND
D.referenced_entity_type IS NULL
Now the questions:
Some object dependencies seem to be missing in the output. What am I
missing?
How do I validate that my findings are correct?
I mean is there a different way to do this, so I can compare the
results and double check?
Thanks in advance,
Raj
You can compare your results to the ones that the following script finds.
Here is the full article
CREATE PROCEDURE [dbo].[get_crossdatabase_dependencies] AS
SET NOCOUNT ON;
CREATE TABLE #databases(
database_id int,
database_name sysname
);
INSERT INTO #databases(database_id, database_name)
SELECT database_id, [name]
FROM sys.databases
WHERE 1 = 1
AND [state] <> 6 /* ignore offline DBs */
AND database_id > 4; /* ignore system DBs */
DECLARE
#database_id int,
#database_name sysname,
#sql varchar(max);
CREATE TABLE #dependencies(
referencing_database varchar(max),
referencing_schema varchar(max),
referencing_object_name varchar(max),
referenced_server varchar(max),
referenced_database varchar(max),
referenced_schema varchar(max),
referenced_object_name varchar(max)
);
WHILE (SELECT COUNT(*) FROM #databases) > 0 BEGIN
SELECT TOP 1 #database_id = database_id,
#database_name = database_name
FROM #databases;
SET #sql = 'INSERT INTO #dependencies select
DB_NAME(' + convert(varchar,#database_id) + '),
OBJECT_SCHEMA_NAME(referencing_id,'
+ convert(varchar,#database_id) +'),
OBJECT_NAME(referencing_id,' + convert(varchar,#database_id) + '),
referenced_server_name,
ISNULL(referenced_database_name, db_name('
+ convert(varchar,#database_id) + ')),
referenced_schema_name,
referenced_entity_name
FROM ' + quotename(#database_name) + '.sys.sql_expression_dependencies';
EXEC(#sql);
DELETE FROM #databases WHERE database_id = #database_id;
END;
SET NOCOUNT OFF;
SELECT * FROM #dependencies;
Oh, MS made a good effort at detecting cross-database dependencies with sys.sql_expression_dependencies, but I've seen it miss things before. In your case, I'd find an example of a missing dependency, and start backtracking: have you dropped it from your query some how? If so, fix your query. Does sys.sql_expression_dependencies omit a certain class of dependencies? Under what conditions? Is dynamic SQL to blame? etc.
You should also run sp_refreshsqlmodule for each object in sys.sql_modules, and then rerun your code. It forces SQL Server to refresh the dependency info (to the best of its ability).
Now, for validation, set up a trace, and listen for event 114, "Audit Schema Object Access Event", plus the starting and completed events for stored procedure and/or RPC calls. Include columns DatabaseName, ParentName, ObjectName, ServerName, SPID and RequestID (for MARS-enabled connections). Maybe some others too. "Audit Schema Object Access Event" happens anytime an object is accessed, so exercise the app while this trace is running, then collate the data using SPID + RequestId and compare it to your results using sys.sql_expression_dependencies. If anything is in the trace data that doesn't appear in your dependencies data, then you've missed something.
If you have to deal with linked servers, I adapted #MilicaMedic's answer to work for cross-server dependencies. I also output column names where available in a dependency.
You can use it like this:
create table #dependencies (
referencing_server nvarchar(128),
referencing_database nvarchar(128),
referencing_schema nvarchar(128),
referencing_object_name nvarchar(128),
referencing_column nvarchar(128),
referenced_server nvarchar(128),
referenced_database nvarchar(128),
referenced_schema nvarchar(128),
referenced_object_name nvarchar(128),
referenced_column nvarchar(128)
);
insert #dependencies
exec crossServerDependencies
'ThisServerName, LinkedServerName, LinkedServerName2, etc'
From there you join it to your AllObjects table as you described in your answer.
My code requires two external functions: "splitString", and "AddBracketsWhenNecessary". You can simplify the former and completely eliminate the latter, as you desire. But I use them for other things so they make it into my implementation. The code for both is at the bottom.
Here is the main procedure:
create procedure crossServerDependencies
#server_names_csv nvarchar(500) = null -- csv list of server names you want to pull dependencies for
as
-- Create output table
if object_id('tempdb..#dependencies') is not null
drop table #dependencies;
create table #dependencies (
referencing_server nvarchar(128),
referencing_database nvarchar(128),
referencing_schema nvarchar(128),
referencing_object_name nvarchar(128),
referencing_column nvarchar(128),
referenced_server nvarchar(128),
referenced_database nvarchar(128),
referenced_schema nvarchar(128),
referenced_object_name nvarchar(128),
referenced_column nvarchar(128)
);
-- Split server csv into table
set #server_names_csv = isnull(#server_names_csv, ##servername);
declare #server_names table (
server_row int,
server_name nvarchar(128),
actuallyExists bit
);
insert #server_names
select server_row = id,
server_name,
actuallyExists = case when sv.name is not null then 1 else 0 end
from dbo.splitString(#server_names_csv, ',') sp
cross apply (select server_name = dbo.AddBracketsWhenNecessary(val)) ap
left join sys.servers sv on sp.val = dbo.AddBracketsWhenNecessary(sv.name);
-- Loop servers
declare
#server_row int = 0,
#server_name nvarchar(50),
#server_exists bit = 0,
#server_is_local bit = 0,
#server_had_some_inserts bit = 0;
while #server_row <= (select max(server_row) from #server_names)
begin
-- Server loop initializations
set #server_row += 1;
set #server_had_some_inserts = 0;
select #server_name = server_name,
#server_exists = actuallyExists
from #server_names
where server_row = #server_row;
set #server_is_local =
case when #server_name = dbo.AddBracketsWhenNecessary(##servername) then 1 else 0 end;
-- Handle non-existent server (and prevent sql injection)
if #server_exists = 0
begin
print
'"' + #server_name + '" does not exist. ' +
'Please check your spelling and/or access to view the linked server ' +
'(running under ' + user_name() + ').';
continue;
end
-- Get database list
if object_id('tempdb..#databases') is not null
drop table #databases;
create table #databases (
rownum int identity(1,1),
database_id int,
database_name nvarchar(128)
);
declare #sql nvarchar(max) = '
select database_id, [name]
from master.sys.databases
where state <> 6 -- ignore offline dbs
and database_id > 4 -- ignore system dbs
and has_dbaccess([name]) = 1
and [name] not in (''ReportServer'', ''ReportServerTempDB'')
';
if #server_is_local = 0
begin
set #sql = replace(#sql, '''', '''''');
set #sql = 'select * from openquery( #server_name, ''' + #sql + ''')';
end
set #sql = 'insert #databases (database_id, database_name)' + #sql;
set #sql = replace(#sql, '#server_name', #server_name);
exec (#sql);
delete #databases
where database_name = 'ReportServer';
-- Loop databases
declare #rowNum int = 0;
while #rowNum <= (select max(rownum) from #databases)
begin
-- Database loop initializations
set #rowNum += 1;
declare
#database_id nvarchar(max),
#database_name nvarchar(max);
select #database_id = database_id,
#database_name = dbo.AddBracketsWhenNecessary(database_name)
from #databases
where rownum = #rowNum;
-- Get object dependency info
set #sql = '
with
getTableColumnIds as (
select table_id = o.object_id,
table_name = o.name,
column_id = c.column_id,
column_name = c.name
from #database_name.sys.objects o
join #database_name.sys.all_columns c on o.object_id = c.object_id
)
#insertStatement
select ''#server_name'',
db_name(#database_id),
object_schema_name(referencing_id, #database_id),
object_name(referencing_id, #database_id),
referencing_column = ringTCs.column_name,
isnull(referenced_server_name, ''#server_name''),
isnull(referenced_database_name, db_name(#database_id)),
isnull(referenced_schema_name, ''dbo''),
referenced_entity_name,
referenced_column = redTCs.column_name
from #database_name.sys.sql_expression_dependencies d
left join getTableColumnIds ringTCs
on d.referencing_id = ringTCs.table_id
and d.referencing_minor_id = ringTCs.column_id
left join getTableColumnIds redTCs
on d.referenced_id = redTCs.table_id
and d.referenced_minor_id = redTCs.column_id
';
set #sql = replace(#sql, '#database_id', #database_id);
set #sql = replace(#sql, '#database_name', #database_name);
if #server_is_local = 0
begin
set #sql = replace(#sql, '''', '''''');
set #sql = replace(#sql, '#insertStatement', '');
set #sql = 'select * from openquery(#server_name, ''' + #sql + ''')';
end
set #sql = replace(#sql, '#insertStatement', 'insert #dependencies ');
set #sql = replace(#sql, '#server_name', #server_name);
exec (#sql);
-- Database loop terminations
if ##rowcount > 0
set #server_had_some_inserts = 1;
end -- database loop
-- server loop terminations
if #server_had_some_inserts = 0
begin
declare #remote_user_name nvarchar(255);
select #remote_user_name = remote_name
from sys.linked_logins li
join sys.servers s on li.server_id = s.server_id
where remote_name is not null
and s.name = 'sisag'
print (
'No dependencies found for ' + #server_name + '. ' +
'If this is unexpected, you may need to run "grant view any definition to ' +
'[' + isnull(#remote_user_name, '?') + ']" ' +
'on the remote server.'
);
end
end -- server loop
-- Terminate
select * from #dependencies
The code for AddBracketsWhenNecessary:
create function AddBracketsWhenNecessary (
#objectName nvarchar(250)
)
returns nvarchar(250) as
begin
if left(#objectName, 1) = '[' and right(#objectName, 1) = ']'
return #objectName;
declare #hasInvalidCharacter bit;
select #hasInvalidCharacter = max(isInvalid)
from dbo.splitString(#objectName, null) chars
cross apply (select
isLetter = patindex('[a-z,_]', val),
isNumber = PATINDEX('[0-9]', val)
) getCharType
cross apply (select
isInvalid =
case
when isLetter = 1 then 0
when isNumber = 1 and not chars.id = 1 then 0
else 1
end
) getValidity
return
case when #hasInvalidCharacter = 1 then '[' else '' end
+ #objectName
+ case when #hasInvalidCharacter = 1 then ']' else '' end;
end
Any finally, my splitter function (but see Arnold Fribble here if you want a simpler version, or use the built in function if you have SqlServer 2016 or above):
create function splitString (
#stringToSplit nvarchar(max),
#delimiter nvarchar(50)
)
returns table as
return
with
split_by_delimiter as (
select id = 1,
start = 1,
stop = convert(int,
charindex(#delimiter, #stringToSplit)
)
union all
select id = id + 1,
start = newStart,
stop = convert(int,
charindex(#delimiter, #stringToSplit, newStart)
)
from split_by_delimiter
cross apply (select newStart = stop + len(#delimiter)) ap
where Stop > 0
),
split_into_characters as (
select id = 1,
chr = left(#stringToSplit,1)
union all
select id = id + 1,
chr = substring(#stringToSplit, ID + 1, 1)
from split_into_characters
where id < len(#stringToSplit)
)
select id,
val =
ltrim(rtrim(substring(
#stringToSplit,
start,
case
when stop > 0 then stop - start
else len(#stringtosplit)
end
)))
from split_by_delimiter
where len(#delimiter) > 0
union all
select id,
val = chr
from split_into_characters
where #delimiter = ''
or #delimiter is null
I had to make some small changes from the real code I use, so if there are any reference errors, please let me know in the comments and I'll edit.
A Query I often use to find the tables used from other databases is the following:
SELECT OBJECT_NAME (referencing_id) AS referencing_object, referenced_database_name,
referenced_schema_name, referenced_entity_name
FROM sys.sql_expression_dependencies
WHERE referenced_database_name IS NOT NULL
AND is_ambiguous = 0;
This gives you all tables used in the stored procedures / views that origin from this database, but also other databases.
source
I upgraded one of the answer adding referencing/referenced id and referencing/referenced type like table, view, etc.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[get_crossdatabase_dependencies] AS
SET NOCOUNT ON;
CREATE TABLE #databases(
database_id int,
database_name sysname
);
INSERT INTO #databases(database_id, database_name)
SELECT database_id, [name]
FROM sys.databases
WHERE 1 = 1
AND [state] <> 6 /* ignore offline DBs */
AND database_id NOT IN (4, 5, 6, 7, 11, 13, 14); /* ignore system DBs */
DECLARE
#database_id int,
#database_name sysname,
#sql varchar(max);
CREATE TABLE #dependencies(
referencing_id int,
referencing_database varchar(max),
referencing_schema varchar(max),
referencing_object_name varchar(max),
referencing_type varchar(max),
referenced_id int,
referenced_server varchar(max),
referenced_database varchar(max),
referenced_schema varchar(max),
referenced_object_name varchar(max),
referenced_type varchar(max),
);
WHILE (SELECT COUNT(*) FROM #databases) > 0 BEGIN
SELECT TOP 1 #database_id = database_id,
#database_name = database_name
FROM #databases;
SET #sql = 'INSERT INTO #dependencies select
referencing_id,
DB_NAME(' + convert(varchar,#database_id) + '),
OBJECT_SCHEMA_NAME(referencing_id,'
+ convert(varchar,#database_id) +'),
OBJECT_NAME(referencing_id,' + convert(varchar,#database_id) + '),
CASE
WHEN OBJECTPROPERTY(referencing_id, ''IsTable'') = 1 THEN ''Table''
WHEN OBJECTPROPERTY(referencing_id, ''IsView'') = 1 THEN ''View''
WHEN OBJECTPROPERTY(referencing_id, ''IsProcedure'') = 1 THEN ''Procedure''
WHEN OBJECTPROPERTY(referencing_id, ''IsTableFunction'') = 1 THEN ''Table-valued Function''
ELSE ''Unknown''
END AS referencing_type,
referenced_id,
referenced_server_name,
ISNULL(referenced_database_name, db_name('
+ convert(varchar,#database_id) + ')),
referenced_schema_name,
referenced_entity_name,
CASE
WHEN OBJECTPROPERTY(referenced_id, ''IsTable'') = 1 THEN ''Table''
WHEN OBJECTPROPERTY(referenced_id, ''IsView'') = 1 THEN ''View''
WHEN OBJECTPROPERTY(referenced_id, ''IsProcedure'') = 1 THEN ''Procedure''
WHEN OBJECTPROPERTY(referenced_id, ''IsTableFunction'') = 1 THEN ''Table-valued Function''
ELSE ''Unknown''
END AS referenced_type
FROM ' + quotename(#database_name) + '.sys.sql_expression_dependencies
ORDER BY
referencing_type';
EXEC(#sql);
DELETE FROM #databases WHERE database_id = #database_id;
END;
SET NOCOUNT OFF;
SELECT * FROM #dependencies;