SQL Unpivot, Cross Apply, Dynamic Query? - sql-server

I'm at crossroads. Can somebody please help me... send me down the right path.
I want to compare / present data from 2 database tables as follows:
Application Database: Many tables have triggers that copy update/delete changes (auditing) to another database.
Audit Database: The information copied from the triggers in the application database
What I want to do should be fairly straightforward. Visually below, is what I want to do to compare data for what changes were made.
I have a working version with CROSS APPLY and UNIONS (it's long and manually typed out for the columns, tables, etc. sucks). The columns are NOT dynamic which makes hundreds of lines of code gross and unmanageable. There has to be a more elegant design. Please any ideas.
I only need to return ONE specific row (ID) from both tables, for comparison.
APP DB
colA colB colC colD
1 hello foo date
APP Audit DB
colA colB colC colD
1 hi bar date
THIS IS WHAT IS WISH TO OUTPUT:
colA_data ColumnName oldData newData
1 colB hi hello
1 colC bar foo
1 colD date date
I hope I have made sense of what I want to accomplish.
I would like to read column names dynamic (not hard), and then put the results side by side like about for reporting reasons. Obviously matching the columns and putting them into rows.
Sample code would be so much appreciated.

Probably the easiest thing to do is using UNPIVOT:
1. Static version
Just to introduce the use of UNPIVOT here is a simple static version that should solve your problem:
declare #appDB table( [colA] int, [colB] nvarchar(50),[colC] nvarchar(50),[colD] nvarchar(50))
declare #auditDB table( [colA] int, [colB] nvarchar(50),[colC] nvarchar(50),[colD] nvarchar(50))
insert into #appDB select 1,'hello', 'foo', 'date'
insert into #auditDB select 1,'hi', 'bar', 'date'
select old.ColA_data, old.ColumnName, old.OldData, new.NewData
from(
select o.colA as ColA_data, o.ColumnName, o.OldData
from #auditDB s
unpivot ([OldData] for [ColumnName] in ([colB], [colC], [colD])) o
) OLD
inner join
(
select n.colA as ColA_data, n.ColumnName, n.NewData
from #appDB t
unpivot ([NewData] for [ColumnName] in ([colB], [colC], [colD])) n
) NEW
on new.ColA_data = old.ColA_data and new.ColumnName = old.ColumnName
Results:
2. Dynamic version
Now the complete version. You can use dynamic SQL to change the columns retrieving them from SQL Server INFORMATION_SCHEMA metadata.
Please note that in this example I added a new column (ColE)
if OBJECT_ID('appDB') is not null drop table appDB
if OBJECT_ID('auditDB') is not null drop table auditDB
create table appDB (colA int, colB nvarchar(50),colC nvarchar(50),colD nvarchar(50),colE nvarchar(50))
create table auditDB(colA int, colB nvarchar(50),colC nvarchar(50),colD nvarchar(50),colE nvarchar(50))
insert into appDB select 1,'hello', 'foo', 'date','time'
insert into auditDB select 1,'hi', 'bar', 'date','time'
declare #cols nvarchar(max)='' --this variable holds all the dates that will become column names
declare #sql nvarchar(max)='' --this variable contains the TSQL dinamically generated
select #cols = #cols + ', [' +COLUMN_NAME + ']'
from INFORMATION_SCHEMA.COLUMNS
where TABLE_NAME='appDB'
and COLUMN_NAME <>'colA'
set #cols = RIGHT(#cols, len(#cols)-2)
set #sql= #sql + ' select old.ColA_data, old.ColumnName, old.OldData, new.NewData '
set #sql= #sql + ' from('
set #sql= #sql + ' select o.colA as ColA_data, o.ColumnName, o.OldData'
set #sql= #sql + ' from auditDB s '
set #sql= #sql + ' unpivot ([OldData] for [ColumnName] in ('+#cols+')) o'
set #sql= #sql + ' ) OLD'
set #sql= #sql + ' inner join'
set #sql= #sql + ' ('
set #sql= #sql + ' select n.colA as ColA_data, n.ColumnName, n.NewData'
set #sql= #sql + ' from appDB t '
set #sql= #sql + ' unpivot ([NewData] for [ColumnName] in ('+#cols+')) n'
set #sql= #sql + ' ) NEW'
set #sql= #sql + ' on new.ColA_data = old.ColA_data and new.ColumnName = old.ColumnName'
exec(#sql)
Results:

Related

SQL Server - SSRS - Display the content of a Table/View directly in the report (and not using table/matrix)

We have a View in SQL Server that constantly evolving.
We want to show it in a report as it is (If we add/remove a field in the View, we don't have to modify the report and add/remove manuelly the field).
A sort of a table/matrix that is refreshing by itself.
Thank you by advance for your help.
Without using a table or matrix? Why not? You can't really do this without using one of these controls...
Ignoring that for a moment though, the problem you will face is that the dataset query has to always return the same structure every time it is run so you can't point it directly at a query that is constantly changing.
The only way you might be able to do this is to write a query that unpivots your table/view into another structure and then report on that. By using a matrix, you could reconstruct the table in the report.
There are drawbacks to this approach. All value data needs to be cast to a constant datatype so if each row has a mix of text and numeric values, they would all have to be converted to text.
This approach also assumes there is a key column on the table/view.
Below is a simple example of the kind of thing I mean. This is based on the sample 'AdventureWorksDW2016' database in case you want to test it.
DECLARE #Schema sysname = 'dbo' -- Schema where table/view resides
DECLARE #Table sysname = 'DimGeography' -- name of table or view to read from
DECLARE #KeyColumn sysname = 'GeographyKey' -- name of keycolumn, assumed to be INT in this exmaple
SELECT
COLUMN_NAME, ORDINAL_POSITION, DATA_TYPE
into #t
FROM INFORMATION_SCHEMA.COLUMNS t
WHERE TABLE_SCHEMA = #Schema AND TABLE_NAME = #Table
AND COLUMN_NAME != #KeyColumn
DECLARE #OrdPos int
DECLARE #ColName sysname
DECLARE #sql varchar(max) = ''
CREATE TABLE #result (KeyID int, ColumnName sysname, ColumnPosition int, ColumnValue varchar(75)) -- <= Update 75 to suit maximum column length
WHILE EXISTS(SELECT * FROM #t)
BEGIN
SELECT TOP 1 #OrdPos = ORDINAL_POSITION, #ColName = COLUMN_NAME FROM #t
SET #SQL = 'INSERT INTO #result SELECT ' + #KeyColumn + ', ''' + #ColName + ''', ' + CAST(#OrdPos as varchar(10)) + ', CAST(' + #ColName + ' as varchar(200)) FROM ' + QUOTENAME(#Schema) + '.' + QUOTENAME(#Table)
EXEC (#sql)
DELETE FROM #t WHERE ORDINAL_POSITION = #OrdPos
END
SELECT * FROM #result
If we take a look at the results (just for 2 keyid vales for simplicity) we can see we have a consistent structure.
SELECT * FROM #result where keyid in (207,208) order by KeyID, ColumnPosition
Now, you can build a simple report using a Matrix, have a row group that groups by KeyID and have a column group that groups by ColumnName. The column group sorting can be set to ColumnPosition and the matrix 'data' cell set to ColumnValue.
This whole process will effectively recreate the table/view and be dynamic.

SQL - Get Databasename from merged query filter

Good afternoon,
I have a working query, where I loop through all my databases, and filter down to check which report is being used where. This works (see below)
I have this working query:
SET NOCOUNT ON;
IF OBJECT_ID (N'tempdb.dbo.#temp') IS NOT NULL
DROP TABLE #temp
CREATE TABLE #temp
(
ReportPath VARCHAR(500)
)
declare #SQL nvarchar(max)
set #SQL = STUFF((SELECT '
UNION ALL
' + 'SELECT path FROM ' + quotename(name) + '.dbo.ReportConfig where path like ''%/Standard Reports/Booking/Booked Out by Location%'' and Active = 1'
from sys.Databases
WHERE name LIKE 'SFB-%'
FOR XML PATH(''), type).value('.','varchar(max)'),1,15,'')
INSERT #temp
execute(#SQL)
SELECT ReportPath FROM #temp
And this is giving me the following output:
So I know that out of my 90 databases, the report is being used 6 times, but I don't know where.
So I want to include the database name of where this report is being used.
I googled around and tried a bunch of things, but I can't get it to work.
Any ideas?
Just a small tweak to what you already have will get you there
SET NOCOUNT ON;
IF OBJECT_ID (N'tempdb.dbo.#temp') IS NOT NULL
DROP TABLE #temp
--Add a column to your temp table
CREATE TABLE #temp
(
DatabaseName varchar(100)
,ReportPath VARCHAR(500)
)
declare #SQL nvarchar(max)
--adjust your dynamic query and add the [name] column as shown below
set #SQL = STUFF((SELECT '
UNION ALL
' + 'SELECT ''' + [name] + ''' as DatabaseName,path FROM ' + quotename(name) + '.dbo.ReportConfig where path like ''%/Standard Reports/Booking/Booked Out by Location%'' and Active = 1'
from sys.Databases
WHERE name LIKE 'SFB-%'
FOR XML PATH(''), type).value('.','varchar(max)'),1,15,'')
INSERT #temp
execute(#SQL)
SELECT DatabaseName, ReportPath FROM #temp

Dynamic sql generation

I need to generate a dynamic sql in below specified format where my table is a parameter i.e, Number of columns is not static
For example, below may be the table schema
ID Name
1 asd
2 xyz
I need a query which generates the select statement as below
select 'ID :' + ID + ',Name :'+Name from table
The output from generated above sql will be like this
ID : 1, Name:asd
ID : 2, Name:xyz
If the table has more number of columns, select statement that needs to be changes varies as below
select 'ID :' + ID + ',Name :'+Name + ',Col3 :' + Col3 ...from table
Could someone help me regarding this
Thanks,
Sree
Here is one option which uses a little XML and string manipulation
I should add, NULL values will be excluded.
Example
Declare #YourTable Table ([ID] varchar(50),[Name] varchar(50))
Insert Into #YourTable Values
(1,'asd')
,(2,'xyz')
Select stuff(
replace(
replace(
replace(
replace(
(Select * from #YourTable for XML RAW)
,'<row ',',')
,'="',':')
,'" ',',')
,'"/>','')
,1,1,'')
Returns
(No column name)
ID:1,Name:asd,ID:2,Name:xyz
Use the information schema views. They contain all the information you need to generate your dynamic sql. The rest is just simple SQL and patience.
I am able to achieve this using below sql
DECLARE #TableName VARCHAR(MAX) = 'tableName'
DECLARE #SQL VARCHAR(MAX) = 'SELECT ''{''+'''
SELECT #SQL = #SQL + '
"'+COLUMN_NAME+'":"''' + '+coalesce(CAST('+COLUMN_NAME+' AS VARCHAR(MAX)),'''')+''",' FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = #TableName
SET #SQL = LEFT(#SQL,LEN(#SQL)-1) + '
}'' FROM ' + #TableName
PRINT #SQL
Thanks,
Sree

SQL Server - select columns from given range

I have a table where I have columns like below
[Index], [Length],[N1],[N2]....[N99]
Now, is possible to select only [N2]] ... [N29] columns without writing all names.
No, it's not possible. You need to explicitly list the subset of columns you want to return.
This is not possible without writing all names.
You can of course drag and drop all the columns from the object browser and then delete the ones you don't want. At least that way you don;t have any typos.
I would be concerned about the design of a table with that many columns. Espceially if they really are N1-N99. You may need a redesign to a related table. Also wide tables can cause performance issues.
How about this:
DECLARE #columns VARCHAR(MAX),
#tablename VARCHAR(255),
#from VARCHAR(255),
#select VARCHAR(100)
SET #tablename = 'orderheader'
SELECT #columns = STUFF(
(
SELECT ',[' + column_name + ']'
FROM information_schema.columns
WHERE table_name = #tablename
AND NOT column_name IN ('N2', 'Index', 'Length')
FOR XML PATH('')
),1, 1, '')
SELECT #select = 'SELECT ', #from = ' from ' + #tablename
EXEC(#select + #columns + #from)
Using dynamic sql is the closest you can get to not writing the columns. Here is an example:
declare #sql varchar(max)
select #sql = coalesce(#sql+',', 'select ') + 'n' + cast(number as varchar(2))
from master..spt_values as N
where type = 'P' and
number between 2 and 29
set #sql = #sql + ' from <yourtable>'
--select #sql
exec (#sql)

MSSQL Find Matching Columns in tables with no defined relationships

Is there a script out there that will let MSSQL find columns with records that have the same data in multiple tables.
What I want to do is find the primary keys to data tables that we imported from excel spread sheets that were made from another database.
Thanks,
Chris
You're going to want to look up the SysObjects and SysColumn system tables, very handy for this sort of thing.
Here's an example that looks through all tables for the integer value 500. Note that if you want to look for a different type of column you'll need to change the xtype. It's not a full blown "Compare every column in my database against every other column" example however it should give you the basic idea and hopefully get you started.
Additionally I'm using a memory table for this example. If your database is large you will want to use a temporary table and a cursor likely.
This returns a single column recordset with the value of "Table - ColumnName = Search Value"
-- declare my search table
DECLARE #Columns TABLE (TableName varchar(50), ColumnName varchar(50))
DECLARE #Results TABLE (Results VARCHAR(255))
DECLARE #SearchData INT
SET #SearchData = 500
DECLARE #TableName VARCHAR(50)
DECLARE #ColumnName VARCHAR(50)
DECLARE #Command VARCHAR(1024)
-- Find all tables with an integer column
Insert INTO #Columns
Select sysobjects.[Name] as TableName, syscolumns.[Name] as ColumnName
from dbo.sysobjects INNER Join dbo.syscolumns ON dbo.sysobjects.id = dbo.syscolumns.id
Where sysobjects.xtype = 'U' and syscolumns.xtype = 56 Order By TableName, ColumnName
--Loop!
WHILE NOT (Select TOP 1 TableName from #Columns) IS NULL
BEGIN
Select TOP 1 #TableName = TableName, #ColumnName = ColumnName from #Columns
SET #Command = 'Select ''' + #TableName + ' - ' + #ColumnName + ' = ' + CAST(#SearchData as varchar(32)) + ''' FROM ' + #TableName + ' WHERE ' + #ColumnName + ' = ' + CAST(#SearchData as VARCHAR(32))
Insert INTO #Results
exec(#Command)
Delete from #Columns where TableName = #TableName AND ColumnName = #ColumnName
END
-- Export all results
Select * from #Results

Resources