Assign values to variables in stored procedure - sql

select #numOfColumns = count(distinct Col) from #b
SET #sql2=
'SELECT'+ #columns +'+= QUOTENAME(Col) + '',''
from (SELECT DISTINCT top #numOfColumns Col FROM #b ORDER BY Col) A';
EXECUTE sp_executesql #sql2;
I am trying to get this stored procedure to work. Trying to pass #numOfColumns to the statement then assign the values from QUOTENAME(Col) to #columns and Then Exec the statement.
Script
DECLARE
#columns NVARCHAR(MAX) = '',
#sql NVARCHAR(MAX) = '',
#sql2 NVARCHAR(MAX) = '',
#numOfColumns int = 0;
SELECT customerid, curbal, Col = CAST (ROW_NUMBER() OVER (PARTITION BY
convert(int,customerid) ORDER BY convert(float,curbal) desc) as int)
into #b
FROM [sav acc]
select #numOfColumns = count(distinct Col) from #b
SET #sql2= 'SELECT '+ #columns+' += QUOTENAME(Col) + '','' from (SELECT
DISTINCT top (#numOfColumns) Col FROM #b ORDER BY Col) A';
EXECUTE sp_executesql #sql2, N'#numOfColumns', #numOfColumns int;
This is origin of post. ORIGINAL POST . The solution did work, But i tested it with the Source data it would positions column at [3], [2], 1 and and I wanted the columns as 1,[2],[3]

Firstly, the way you are using TOP is pointless. If you have 5 distinct values for Col in #b then SELECT DISTINCT Col FROM #b will only return 5 records, regardless of if you apply TOP 5 or not. It seems you are only using it to allow sorting, but you could just put the sort in the outer query.
Secondly, you should not use variable assignment to concatenate strings, the behaviour is unreliable and undocumented you should use STRING_AGG, or for earlier versions of SQL Server you can leverage the XML extensions, either:
CREATE TABLE #B (Col CHAR(1));
INSERT #B (Col) VALUES ('A'), ('A'), ('B'), ('C');
DECLARE #Columns NVARCHAR(MAX) =
( SELECT STRING_AGG(QUOTENAME(Col), ',') WITHIN GROUP (ORDER BY Col)
FROM (SELECT DISTINCT Col FROM #B) AS b
);
Or
DECLARE #Columns NVARCHAR(MAX) =
STUFF(( SELECT CONCAT(',', QUOTENAME(Col))
FROM #B
GROUP BY Col
ORDER BY Col
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'), 1, 1, '');
While the above shows that dynamic sql is unnecessary, it would also be unnecessary if you were using #top, you could simply use:
DECLARE #numOfColumns INT = (SELECT count(distinct Col) from #b);
DECLARE #Columns NVARCHAR(MAX) = '';
SELECT #Columns += QUOTENAME(Col) + ','
FROM (SELECT DISTINCT TOP (#numOfColumns) Col FROM #B) AS b
ORDER BY Col;
SELECT #Columns;

Related

How do I use loop to generate column names dynamically?

I have table sdata and it has 35 columns (id, name, TRx1, TRx2, TRx3, TRx4,..., TRx30, city, score, total)
I want to fetch data from the TRx1,...TRx30 columns.
Can I use loop here?
I did following code:
DECLARE #flag INT
DECLARE #sel varchar(255)
DECLARE #frm varchar(255)
SET #flag = 1;
SET #sel = 'select TRx';
SET #frm = ' from sdata';
exec(#sel +
(WHILE #flag <=5
#flag
SET #flag = #flag + 1)
+ #frm)
What wrong am I doing? And how can I resolve this?
If your table name is sdata, this code should work for you:
-- Grab the names of all the remaining columns
DECLARE #sql nvarchar(MAX);
DECLARE #columns nvarchar(MAX);
SELECT #columns = STUFF ( ( SELECT N'], [' + name
FROM sys.columns
WHERE object_id = (select top 1 object_id FROM sys.objects where name = 'sdata')
AND name LIKE 'TRx%' -- To limit which columns
ORDER BY column_id
FOR XML PATH('')), 1, 2, '') + ']';
PRINT #columns
SELECT #sql = 'SELECT ' + #columns + ' FROM sdata';
PRINT #sql;
EXEC (#sql);
Note I included PRINT statements so you could see what's going on. You might want to comment out the EXEC while testing.
This would be much easier to do by just copy/pasting the column names and changing them to be the correct one. However if you must do it this way, I do not advise using a loop at all. This method uses a tally table to generate the columns you want to select (in this example, columns 1 through 30, but that can be changed), then generates a dynamic SQL statement to execute against the SData table:
Declare #From Int = 1,
#To Int = 30,
#Sql NVarchar (Max)
Declare #Columns Table (Col Varchar (255))
;With Nums As
(
Select *
From (Values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) As V(N)
), Tally As
(
Select Row_Number() Over (Order By (Select Null)) As N
From Nums A --10
Cross Join Nums B --100
Cross Join Nums C --1000
)
Insert #Columns
Select 'TRx' + Cast(N As Varchar)
From Tally
Where N Between #From And #To
;With Cols As
(
Select (
Select QuoteName(Col) + ',' As [text()]
From #Columns
For Xml Path ('')
) As Cols
)
Select #Sql = 'Select ' + Left(Cols, Len(Cols) - 1) + ' From SData'
From Cols
--Select #Sql
Execute (#Sql)
Note: The --Select #Sql section is there to preview the generated query before executing it.
You can select the column names like this:
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'my name here'

Is it possible to remove the cursor from this dynamic query?

Simplified example of what I have:
Two tables (table_1 and table_2) which have one similar column (Id) but also several "payload" columns with different names (col_1_1, col_2_1, col_2_2). Amount of "payload" columns is different for different tables.
I am interested in extracting the IDs from both tables into another table for rows which have all "payload" columns empty.
There is a list of all "payload" columns for all tables which can be used (#temp)
This is how it is done with cursor:
CREATE TABLE #temp (tab nvarchar(20) not null, col nvarchar(20) not null)
INSERT INTO #temp SELECT 'table_1','col_1_1' UNION SELECT 'table_2','col_2_1' UNION SELECT 'table_2','col_2_2'
DECLARE #table_name nvarchar(20)
DECLARE #sql nvarchar(max)
DECLARE curs CURSOR FOR (SELECT DISTINCT tab FROM #temp)
OPEN curs
FETCH NEXT FROM curs INTO #table_name
WHILE ##FETCH_STATUS = 0
BEGIN
SELECT #sql = ISNULL(#sql,'')+col+' IS NULL AND ' FROM #temp WHERE tab = #table_name
SET #sql += 'Id IS NOT NULL'
SET #sql = 'INSERT INTO #temp_master SELECT ID FROM '+#table_name+' WHERE '+#sql
print #sql
SET #sql = ''
FETCH NEXT FROM curs INTO #table_name
END
CLOSE curs
DEALLOCATE curs
This is the result:
INSERT INTO #temp_master SELECT ID FROM table_1 WHERE col_1_1 IS NULL AND Id IS NOT NULL
INSERT INTO #temp_master SELECT ID FROM table_2 WHERE col_2_1 IS NULL AND col_2_2 IS NULL AND Id IS NOT NULL
Is it possible to remove the cursor to get the same resulting dynamic query? The problem is that I am unable to have dynamic "IS NULL AND" part for different tables when I remove the cursor.
It's possible to get rid of that cursor. This is probably what you need:
CREATE TABLE #temp (tab nvarchar(20) not null, col nvarchar(20) not null)
INSERT INTO #temp SELECT 'table_1','col_1_1' UNION SELECT 'table_2','col_2_1' UNION SELECT 'table_2','col_2_2'
DECLARE #table_name nvarchar(20)
DECLARE #sql nvarchar(max) = ''
select #sql = 'INSERT INTO #temp_master SELECT ID FROM ' + t.tab + ' WHERE Id IS NOT NULL AND ' + substring(t.cols, 0, len(t.cols)-3) + '
' + #sql from
(
SELECT
distinct
t2.tab,
stuff(
(
select t1.col + cast(' IS NULL AND ' as varchar(max))
from #temp t1
WHERE t1.tab = t2.tab
order by t1.tab
for xml path('')
), 1, 0, '') AS cols
FROM
#temp t2
) as t
order by t.tab desc
print #sql
drop table #temp
That is a regular CONCAT question, you can find many approaches to accomplish it without cursor. One of approaches is a cursor by the way and it's not that bad for such a task.
Another and more popular - FOR XML which can guarantee row order if any defined:
DECLARE #sql VARCHAR(MAX)
CREATE TABLE #temp (tab nvarchar(20) not null, col nvarchar(20) not null)
INSERT INTO #temp SELECT 'table_1','col_1_1' UNION SELECT 'table_2','col_2_1' UNION SELECT 'table_2','col_2_2'
SET #sql = (SELECT (
SELECT '
INSERT INTO #temp_master (ID) SELECT t.ID FROM '+t.tab +' t WHERE t.Id IS NOT NULL'
+ (select ' AND t.' + tt.col + ' is NULL' from #temp tt WHERE tt.tab = t.tab FOR XML PATH(''), TYPE).value('.', 'varchar(max)')
FROM #temp t
GROUP BY t.tab
FOR XML PATH(''), TYPE).value('.', 'varchar(max)'))
PRINT #sql
DROP TABLE #temp
A little "tricky" thing is that you have two things to collaps:
many queries (separate for specific table)
many columns per table
So you there is one inner FOR XML to collapse columns per table and another - to combine all queries into one big script.

Select Into #Temp not working with a PIVOT

I've got this dynamically created mess that essentially takes all fields in a table and compares two records against each other:
DECLARE #ID1 AS VarChar(3)
DECLARE #ID2 AS VarChar(3)
Set #ID1 = '42'
Set #ID2 = '600'
-- Where clause params
DECLARE #whereClauseParam VARCHAR(MAX) = '['+#ID1+'] <> ['+#ID2+']'
--***************************************--
--******** tblSQLAdminInventory ********--
--***************************************--
--Get the Fields required for the initial pivot
DECLARE #AIFields VARCHAR(MAX)= '';
DECLARE #AIFields2 VARCHAR(MAX)= '';
SELECT #AIFields+=QUOTENAME(t.name)+', '
FROM sys.columns AS t
WHERE t.object_id = OBJECT_ID('tblSQLAdminInventory')
AND t.name <> 'TransID'
--AND t.system_type_id = '56';
SELECT #AIFields2+='Convert(VarChar(250), '+QUOTENAME(t.name)+') AS '+ QUOTENAME(t.name) +', '
FROM sys.columns AS t
WHERE t.object_id = OBJECT_ID('tblSQLAdminInventory')
AND t.name <> 'TransID'
--AND t.system_type_id = '56';
--56 (Int)
--61 (DateTime)
--104 (Bit)
--167 (VarChar)
--231 (NVarChar)
-- Get the KeyId's with alias added
DECLARE #AIkeyIDs VARCHAR(MAX),
#AIkeyIDs1 VARCHAR(MAX);
SELECT #AIkeyIDs = COALESCE(#AIkeyIDs + ',','') + QUOTENAME(t.TransID) + ' AS [KeyID_' + CAST(t.TransID AS VARCHAR(10)) + ']',
#AIkeyIDs1 = COALESCE(#AIkeyIDs1 + ',','') + QUOTENAME(t.TransID)
FROM tblSQLAdminInventory AS t
WHERE TransID IN (#ID1, #ID2);
--Generate Dynamic SQL
DECLARE #AISQL2 VARCHAR(MAX)= 'SELECT Value AS FieldName, ';
SELECT #AISQL2+=#AIkeyIDs+'
FROM
(SELECT TransID, Value, FieldName
FROM
(SELECT TransID, '+SUBSTRING(#AIFields2, 1, LEN(#AIFields2)-1)+'
FROM tblSQLAdminInventory) p
UNPIVOT
(FieldName FOR Value IN
('+SUBSTRING(#AIFields, 1, LEN(#AIFields)-1)+')
)AS unpvt) AS SourceTable
PIVOT
(
MAX(FieldName)
FOR TransID IN ('+#AIkeyIDs1+')
) AS PivotTable
WHERE '+#whereClauseParam
EXECUTE(#AISQL2);
The problem is, it won't seem to let me put the results in a temp table. I tried using this code but it keeps telling me the #Temp1 object doesn't exist:
SELECT #AISQL2+=#AIkeyIDs+'
INTO #Temp1
FROM
(SELECT TransID, Value, FieldName
FROM
(SELECT TransID, '+SUBSTRING(#AIFields2, 1, LEN(#AIFields2)-1)+'
FROM tblSQLAdminInventory) p
UNPIVOT
(FieldName FOR Value IN
('+SUBSTRING(#AIFields, 1, LEN(#AIFields)-1)+')
)AS unpvt) AS SourceTable
PIVOT
(
MAX(FieldName)
FOR TransID IN ('+#AIkeyIDs1+')
) AS PivotTable
WHERE '+#whereClauseParam
What am I doing wrong?
You're using dynamic SQL. The EXECUTE statement starts a whole new scope and that temporary table isn't available in that scope.
There are several work-arounds, like using a permanent table that you clear out or using a global temporary table, but they all have their own pitfalls.

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

Flattening of a 1 row table into a key-value pair table

What's the best way to get a key-value pair result set that represents column-value in a row?
Given the following table A with only 1 row
Column1 Column2 Column3 ...
Value1 Value2 Value3
I want to query it and insert into another table B:
Key Value
Column1 Value1
Column2 Value2
Column3 Value3
A set of columns in table A is not known in advance.
NOTE: I was looking at FOR XML and PIVOT features as well as dynamic SQL to do something like this:
DECLARE #sql nvarchar(max)
SET #sql = (SELECT STUFF((SELECT ',' + column_name
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name='TableA'
ORDER BY column_name FOR XML PATH('')), 1, 1, ''))
SET #sql = 'SELECT ' + #sql + ' FROM TableA'
EXEC(#sql)
A version where there is no dynamic involved. If you have column names that is invalid to use as element names in XML this will fail.
select T2.N.value('local-name(.)', 'nvarchar(128)') as [Key],
T2.N.value('text()[1]', 'nvarchar(max)') as Value
from (select *
from TableA
for xml path(''), type) as T1(X)
cross apply T1.X.nodes('/*') as T2(N)
A working sample:
declare #T table
(
Column1 varchar(10),
Column2 varchar(10),
Column3 varchar(10)
)
insert into #T values('V1','V2','V3')
select T2.N.value('local-name(.)', 'nvarchar(128)') as [Key],
T2.N.value('text()[1]', 'nvarchar(max)') as Value
from (select *
from #T
for xml path(''), type) as T1(X)
cross apply T1.X.nodes('/*') as T2(N)
Result:
Key Value
-------------------- -----
Column1 V1
Column2 V2
Column3 V3
Update
For a query with more than one table you could use for xml auto to get the table names in the XML. Note, if you use alias for table names in the query you will get the alias instead.
select X2.N.value('local-name(..)', 'nvarchar(128)') as TableName,
X2.N.value('local-name(.)', 'nvarchar(128)') as [Key],
X2.N.value('text()[1]', 'nvarchar(max)') as Value
from (
-- Your query starts here
select T1.T1ID,
T1.T1Col,
T2.T2ID,
T2.T2Col
from T1
inner join T2
on T1.T1ID = T2.T1ID
-- Your query ends here
for xml auto, elements, type
) as X1(X)
cross apply X1.X.nodes('//*[text()]') as X2(N)
SQL Fiddle
I think you're halfway there. Just use UNPIVOT and dynamic SQL as Martin recommended:
CREATE TABLE TableA (
Code VARCHAR(10),
Name VARCHAR(10),
Details VARCHAR(10)
)
INSERT TableA VALUES ('Foo', 'Bar', 'Baz')
GO
DECLARE #sql nvarchar(max)
SET #sql = (SELECT STUFF((SELECT ',' + column_name
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name='TableA'
ORDER BY ordinal_position FOR XML PATH('')), 1, 1, ''))
SET #sql = N'SELECT [Key], Val FROM (SELECT ' + #sql + ' FROM TableA) x '
+ 'UNPIVOT ( Val FOR [Key] IN (' + #sql + ')) AS unpiv'
EXEC (#sql)
Results:
Key Val
------------ ------------
Code Foo
Name Bar
Details Baz
There is a caveat, of course. All your columns will need to be the same data type for the above code to work. If they are not, you will get this error:
Msg 8167, Level 16, State 1, Line 1
The type of column "Col" conflicts with the type of
other columns specified in the UNPIVOT list.
In order to get around this, you'll need to create two column string statements. One to get the columns and one to cast them all as the data type for your Val column.
For multiple column types:
CREATE TABLE TableA (
Code INT,
Name VARCHAR(10),
Details VARCHAR(10)
)
INSERT TableA VALUES (1, 'Foo', 'Baf')
GO
DECLARE
#sql nvarchar(max),
#cols nvarchar(max),
#conv nvarchar(max)
SET #cols = (SELECT STUFF((SELECT ',' + column_name
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name='TableA'
ORDER BY ordinal_position FOR XML PATH('')), 1, 1, ''))
SET #conv = (SELECT STUFF((SELECT ', CONVERT(VARCHAR(50), '
+ column_name + ') AS ' + column_name
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name='TableA'
ORDER BY ordinal_position FOR XML PATH('')), 1, 1, ''))
SET #sql = N'SELECT [Key], Val FROM (SELECT ' + #conv + ' FROM TableA) x '
+ 'UNPIVOT ( Val FOR [Key] IN (' + #cols + ')) AS unpiv'
EXEC (#sql)
Perhaps you're making this more complicated than it needs to be. Partly because I couldn't wrap my little brain around the number of PIVOT/UNPIVOT/whatever combinations and a dynamic SQL "sea of red" would be necessary to pull this off. Since you know the table has exactly one row, pulling the value for each column can just be a subquery as part of a set of UNIONed queries.
DECLARE #sql NVARCHAR(MAX) = N'INSERT dbo.B([Key], Value) '
SELECT #sql += CHAR(13) + CHAR(10)
+ ' SELECT [Key] = ''' + REPLACE(name, '''', '''''') + ''',
Value = (SELECT ' + QUOTENAME(name) + ' FROM dbo.A) UNION ALL'
FROM sys.columns
WHERE [object_id] = OBJECT_ID('dbo.A');
SET #sql = LEFT(#sql, LEN(#sql)-9) + ';';
PRINT #sql;
-- EXEC sp_executesql #sql;
Result (I only created 4 columns, but this would work for any number):
INSERT dbo.B([Key], Value)
SELECT [Key] = 'Column1',
Value = (SELECT [Column1] FROM dbo.A) UNION ALL
SELECT [Key] = 'Column2',
Value = (SELECT [Column2] FROM dbo.A) UNION ALL
SELECT [Key] = 'Column3',
Value = (SELECT [Column3] FROM dbo.A) UNION ALL
SELECT [Key] = 'Column4',
Value = (SELECT [Column4] FROM dbo.A);
The most efficient thing in the world? Likely not. But again, for a one-row table, and hopefully a one-off task, I think it will work just fine. Just watch out for column names that contain apostrophes, if you allow those things in your shop...
EDIT sorry, couldn't leave it that way. Now it will handle apostrophes in column names and other sub-optimal naming choices.