Dynamically decide number of joins - sql

I have two tables.
Table 1: Question_Master which contains the questions
id question
1 Q1
2 Q2
3 Q3
Table 2: Option Master Which contains the Options
id option
1 H
2 N
3 S
I want all the combinations of options for all the questions.
Something Like this
Q1 Q2 Q3
H H H
H H N
H H s
H N H
NOTE: There can be any number of records in both table.If it has 4 records in option_master than i want all combination for 4 records.

You can do it dynamically by using some string concatenation queries to build out the Select statement based on the Question_Master table values
DECLARE #SelectSQL VARCHAR(MAX),
#JoinSQL VARCHAR(MAX),
#OrderSQL VARCHAR(MAX)
SELECT #SelectSQL = COALESCE(#SelectSQL + ',', '')
+ QUOTENAME(question) + '.[option] as ' + QUOTENAME(question),
#JoinSQL = COALESCE(#JoinSQL + ' CROSS JOIN ', '')
+ 'Option_Master as ' + QUOTENAME(question),
#OrderSQL = COALESCE(#OrderSql + ',', '')
+ QUOTENAME(question) + '.[option]'
FROM Question_Master
ORDER BY question
DECLARE #Sql AS NVARCHAR(MAX) = N'SELECT ' + #SelectSQL + ' FROM ' + #JoinSQL + ' ORDER BY ' + #OrderSQL
EXECUTE sp_executesql #Sql;
using QUOTENAME will allow you to have questions that have spaces or some other characters in the value.
SQL Fiddle Example

You need to CROSS JOIN the Option_Master with itself. And then you need to cross join the result again with Option_Master. This has to be repeated for each question. I think this has to be done by dynamically creating the SQL statement. Try this example to get an idea:
declare #NumberOfQuestions int
set #NumberOfQuestions = (
select count(*)
from question_master
)
declare #sql varchar(max)
set #sql = 'select om1.opt '
declare #counter int
set #counter = 2
while #Counter <= #NumberOfQuestions
begin
set #sql = #sql + '
, om' + cast (#counter as varchar(1)) + '.opt '
set #counter = #counter + 1
end
set #sql = #sql + '
from option_master om1 '
set #counter = 2
while #Counter <= #NumberOfQuestions
begin
set #sql = #sql + '
cross join option_master om' + cast(#counter as varchar(1)) + ' '
set #counter = #counter + 1
end
set #sql = #sql + '
order by om1.opt '
set #counter = 2
while #Counter <= #NumberOfQuestions
begin
set #sql = #sql + '
, om' + cast(#counter as varchar(1)) + '.opt '
set #counter = #counter + 1
end
exec (#sql)
Albert

Related

Join using dynamic column names SQL Server

I have a table which can output any number of different columns (from 'Level1' to 'Level'N).
I need to perform a left join on each of these dynamic columns against a CTE
I have written the following script but keep getting this error:
Msg 102, Level 15, State 1, Line 15 Incorrect syntax near '10'.
To troubleshoot, I have tried removing the each of the variables in the CTE with no luck.
Any help would be much appreciated!
DECLARE #rel varchar(4) = CAST('X112' AS varchar(4))
DECLARE #todaysdate date = CONVERT(date,GETDATE())
--create cte
DECLARE #sqltext varchar(MAX) =
' WITH CTE AS
(
SELECT
ID
,STARTDATE
,ENDDATE
,NEWID
FROM Tbl
WHERE TYPE = ''' + #rel + '''
AND ENDDATE >= ' + CAST(#todaysdate AS varchar(30)) +' AND STARTDATE <= ' + CAST(#todaysdate AS varchar(30)) +'
)
SELECT ID, NEWID, Level';
--find max lvl, convert to str
DECLARE #counter int = (SELECT MAX(lvl) FROM tbl2)
DECLARE #counterstring varchar(3)
SET #counterstring = CAST(#counter AS varchar(3))
WHILE #counter != 0
BEGIN
SET #sqltext = #sqltext + #counterstring + ' INTO tbl3 '
+ ' FROM tbl2 a '
+ ' LEFT JOIN CTE c ON a.Level' + #counterstring + ' = c.NEWID'
SET #counter = #counter - 1
END
EXEC(#sqltext)
--edited version
DECLARE #rel varchar(4) = CAST('X112' AS varchar(4))
DECLARE #todaysdate date = CONVERT(date,GETDATE())
DECLARE #sqltext varchar(MAX) =
' WITH CTE AS
(
SELECT
ID
,STARTDATE
,ENDDATE AS mgmt_ENDDA
,NEWID
FROM tbl
WHERE SUBTY = ''' + #rel + '''
AND ENDDATE >= ' + CAST(#todaysdate AS varchar(30)) +' AND STARTDATE <= ' + CAST(#todaysdate AS varchar(30)) +'
)
INSERT INTO tbl3
SELECT ID, NEWID, Level';
DECLARE #counter int = (SELECT MAX(lvl) FROM tbl2)
DECLARE #counterstring varchar(3)
WHILE #counter != 0
BEGIN
SET #counterstring = CAST(#counter AS varchar(3))
SET #sqltext = #sqltext + #counterstring
+ ' FROM tbl2 a '
+ ' LEFT JOIN CTE c ON a.Level' + #counterstring + ' = c.NEWID'
SET #counter = #counter - 1
END
EXEC(#sqltext)
Since select * into query creates a new table each time, I am assuming that you are trying to create 'n' number of tables for 'n' number of levels, with the result obtained by joining with nth column. I have 2 suggestions for you
Bring the select query within the while loop and append ';' at the end of the loop to split select queries.
instead of INTO tbl3 use INTO tbl + #counterstring as select * into will create new table
Hope this helps you
Can you change it like this and try again?
--edited version
DECLARE #rel varchar(4) = CAST('A012' AS varchar(4))
DECLARE #todaysdate date = CONVERT(date,GETDATE())
DECLARE #sqltext varchar(MAX) =
' WITH CTE AS
(
SELECT
ID
,STARTDATE
,ENDDATE AS mgmt_ENDDA
,NEWID
FROM tbl
WHERE SUBTY = ''' + #rel + '''
AND ENDDATE >= ' + CAST(#todaysdate AS varchar(30)) +' AND STARTDATE <= ' + CAST(#todaysdate AS varchar(30)) +'
)
INSERT INTO tbl3';
DECLARE #counter int = (SELECT MAX(lvl) FROM tbl2)
DECLARE #counterstring varchar(3)
WHILE #counter != 0
BEGIN
SET #counterstring = CAST(#counter AS varchar(3))
SET #sqltext = #sqltext + CHAR(10) +'SELECT ID, NEWID, Level'+#counterstring
SET #sqltext = #sqltext + ' FROM tbl2 a '
+ ' LEFT JOIN CTE c ON a.Level' + #counterstring + ' = c.NEWID'
SET #counter = #counter - 1
IF #counter <> 0
SET #sqltext = #sqltext + CHAR(10) + ' UNION '
END
EXEC(#sqltext)
You can try 'UNION ALL' instead of 'UNION' if you dont want to get rid of duplicate data. Hope this helps

Return Row Count Using Dynamic SQL

I'm trying to run the following Dynamic SQL statement:
#Tbl, #Fld, and #LookupValue have all been set according to Table to search, Field (Or Column) to search and column value to compare.
DECLARE #Sql AS VARCHAR(500)
SET #Sql = 'SELECT COUNT(*)
FROM ' + #Tbl +
' WITH (NOLOCK)
WHERE ' + #Fld + ' = ''' + #LookupValue + ''''
EXEC(#Sql)
I want to store the result into a variable so I can check to see if there are any returned rows. This statement is in the middle of a WHILE construct that is checking several tables and fields.
If records are found, then I want to display:
SET #Sql = 'SELECT ' + #Fld +
' FROM ' + #Tbl +
' WITH (NOLOCK)
WHERE ' + #Fld + ' = ''' + #LookupValue + ''''
EXEC(#Sql)
Yes, you can store it in a typed variable and use sp_executesql like
DECLARE #Sql AS NVARCHAR(500);
DECLARE #cnt INT;
SET #Sql = 'SELECT #cnt = COUNT(*)
FROM ' + #Tbl +
' WITH (NOLOCK)
WHERE ' + #Fld + ' = ''' + #LookupValue + '''';
EXEC sp_executesql #Sql, N'#cnt INT OUTPUT', #cnt OUTPUT;
SELECT #cnt;
you can create a temporary table and store the count value.
if object_id('tempdb.#mycount') is null
create table #mycount ( countVal int);
DECLARE #Sql AS VARCHAR(500)
SET #Sql = 'INSERT INTO #mycount
SELECT COUNT(*)
FROM ' + #Tbl +
' WITH (NOLOCK)
WHERE ' + #Fld + ' = ''' + #LookupValue + ''''
EXEC(#Sql)
select countVal from #mycount
-- once the temp table usage is done, you can delete it
drop table #mycount

Update Statement in a Stored Procedure

I have a stored Procedure called Active and the code is:
CREATE PROCEDURE dbo.Active
(
#ID INT ,
#Source VARCHAR(25)
)
AS
BEGIN
DECLARE #SQL NVARCHAR(MAX)
DECLARE #SchemaName SYSNAME
DECLARE #TableName SYSNAME
DECLARE #DatabaseName SYSNAME
DECLARE #BR CHAR(2)
SET #BR = CHAR(13) + CHAR(10)
SELECT #SchemaName = Source_Schema ,
#TableName = Source_Table ,
#DatabaseName = Source_Database
FROM Source
WHERE ID = #ID
SET #SQL = 'UPDATE Source_Table' + #BR
+ 'SET __ACTIVE = CASE WHEN rn = 1 THEN 1 ELSE 0 END' + #BR
+ 'FROM ( ' + #BR + 'SELECT ROW_NUMBER() OVER (PARTITION BY '
+ #Source + ' ORDER BY __REC_ID DESC) AS rn
, * FROM ' + #DatabaseName + '.' + #SchemaName + '.' + #TableName
+ #BR + ') Source_Table' + #BR
EXEC #SQL
END
The problem is I am using thsi procedure in another procedure so Everytime that Procedure runs this procedure also runs and does the update to whole table.
The main reason for that update is to check for the duplicates on the table and set the duplicates to 0 and remaining to 1.
I dont want to run this update for whole table but I want the update to run only for Active duplicates.
Is there a way to do it?
To reiterate your issue. You are calling the above stored procedure from another stored procedure. I assume that the parent procedure is what is determining what you call "Active duplicates". If that is the case, then you have a few options:
1) Temp table, have the first procedure create a global temporary table and use it in nested procedure. Make sure to clean up after.
--Base procedure creates global temp table with proper values;
SELECT ID
INTO ##ActiveDups
FROM DUPTABLE
WHERE SOMECONDITION = SOMECONDITION
--Join global temp table on query
SET #SQL = 'UPDATE Source_Table' + #BR
+ 'SET __ACTIVE = CASE WHEN rn = 1 THEN 1 ELSE 0 END' + #BR
+ 'FROM ( ' + #BR + 'SELECT ROW_NUMBER() OVER (PARTITION BY '
+ #Source + ' ORDER BY __REC_ID DESC) AS rn
, * FROM ' + #DatabaseName + '.' + #SchemaName + '.' + #TableName
+ #BR + ') Source_Table' + #BR
+ ' INNER JOIN ##ActiveDups ad ON ad.ID = Source_Table.ID'
--Drop global temp table
DROP TABLE ##ActiveDups
2) Parameter, pass a comma separated list to the nested procedure and filter with IN or EXISTS clause. Not very scalable. (See added parameter and last line of query)
CREATE PROCEDURE dbo.Active
(
#ID INT ,
#Source VARCHAR(25),
#List VARCHAR(MAX)
)
--...
SET #SQL = 'UPDATE Source_Table' + #BR
+ 'SET __ACTIVE = CASE WHEN rn = 1 THEN 1 ELSE 0 END' + #BR
+ 'FROM ( ' + #BR + 'SELECT ROW_NUMBER() OVER (PARTITION BY '
+ #Source + ' ORDER BY __REC_ID DESC) AS rn
, * FROM ' + #DatabaseName + '.' + #SchemaName + '.' + #TableName
+ #BR + ') Source_Table' + #BR
+ ' WHERE SOMECONDITION IN ' #List
3) Add logic to your dynamic SQL to fetch the proper results. (See last line, which was appended. I cannot determine for you what that logic may be.)
SET #SQL = 'UPDATE Source_Table' + #BR
+ 'SET __ACTIVE = CASE WHEN rn = 1 THEN 1 ELSE 0 END' + #BR
+ 'FROM ( ' + #BR + 'SELECT ROW_NUMBER() OVER (PARTITION BY '
+ #Source + ' ORDER BY __REC_ID DESC) AS rn
, * FROM ' + #DatabaseName + '.' + #SchemaName + '.' + #TableName
+ #BR + ') Source_Table' + #BR
+ ' WHERE SOMECONDITION = SOMECONDITION'

How can I dynamically create columns in SQL select statement

I have 3 tables. Team, Option, OptionTeam.
The Team holds a TeamId and Name
Option holds OptionId, OptionGroup
OptionTeam holds TeamId, OptionId, OptionGroup
select a.TeamId, a.Name
(select count(*) from OptionTeam ot where ot.TeamId=a.TeamId and ot.OptionGroup=4) as Option1,
(select count(*) from OptionTeam ot where ot.TeamId=a.TeamId and ot.OptionGroup=5) as Option2,
(select count(*) from OptionTeam ot where ot.TeamId=a.TeamId and ot.OptionGroup=6) as Option3,
(select count(*) from OptionTeam ot where ot.TeamId=a.TeamId and ot.OptionGroup=11) as Option4
from Team a
I want to get a list of Teams, and extra columns indicating how many options of each group are connected to each Team. This is done by the above query, but I want to replace the 4,5,6,11 with values of OptionGroup from a table Option.
It has to be dynamic, because there might be a new OptionGroup in the future, and I want the stored procedure to be able to handle it.
Sample data:
Team
TeamId
1
2
3
Option
OptionId | OptionGroup
11 | 4
12 | 5
13 | 4
14 | 4
15 | 5
OptionTeam
TeamId | OptionId | OptionGroup
1 | 11 | 4
1 | 13 | 4
2 | 12 | 5
2 | 14 | 4
3 | 15 | 5
And the list I want to get is
TeamId | Group4 (OptionGroup=4) | Group5 (OptionGroup=5)
1 | 2 | 0
2 | 1 | 1
3 | 0 | 1
You'll need a dynamic pivot to do this. Here's the stored procedure:
CREATE PROC [dbo].[pivotsp]
#query AS NVARCHAR(MAX), -- The query, can also be the name of a table/view.
#on_rows AS NVARCHAR(MAX), -- The columns that will be regular rows.
#on_cols AS NVARCHAR(MAX), -- The columns that are to be pivoted.
#agg_func AS NVARCHAR(257) = N'SUM', -- Aggregate function.
#agg_col AS NVARCHAR(MAX), -- Column to aggregate.
#output AS NVARCHAR(257) = N'', -- Table for results
#debug AS bit = 0 -- 1 for debugging
AS
-- Example usage:
-- exec pivotsp
-- 'select * from vsaleshistory',
-- 'market,marketid,family,familyid,Forecaster,Forecasterid,product,productid',
-- 'month',
-- 'sum',
-- 'ku',
-- '##sales'
-- Input validation
IF #query IS NULL OR #on_rows IS NULL OR #on_cols IS NULL
OR #agg_func IS NULL OR #agg_col IS NULL
BEGIN
RAISERROR('Invalid input parameters.', 16, 1);
RETURN;
END
-- Additional input validation goes here (SQL Injection attempts, etc.)
BEGIN TRY
DECLARE
#sql AS NVARCHAR(MAX),
#cols AS NVARCHAR(MAX),
#newline AS NVARCHAR(2);
SET #newline = NCHAR(13) + NCHAR(10);
-- If input is a valid table or view
-- construct a SELECT statement against it
IF COALESCE(OBJECT_ID(#query, N'U'),
OBJECT_ID(#query, N'V')) IS NOT NULL
SET #query = N'SELECT * FROM ' + #query;
-- Make the query a derived table
SET #query = N'(' + #query + N') AS Query';
-- Handle * input in #agg_col
IF #agg_col = N'*'
SET #agg_col = N'1';
-- Construct column list
SET #sql =
N'SET #result = ' + #newline +
N' STUFF(' + #newline +
N' (SELECT N'','' + quotename( '
+ 'CAST(pivot_col AS sysname)' +
+ ') AS [text()]' + #newline +
N' FROM (SELECT DISTINCT('
+ #on_cols + N') AS pivot_col' + #newline +
N' FROM' + #query + N') AS DistinctCols' + #newline +
N' ORDER BY pivot_col' + #newline +
N' FOR XML PATH(''''))' + #newline +
N' ,1, 1, N'''');'
IF #debug = 1
PRINT #sql
EXEC sp_executesql
#stmt = #sql,
#params = N'#result AS NVARCHAR(MAX) OUTPUT',
#result = #cols OUTPUT;
IF #debug = 1
PRINT #cols
-- Create the PIVOT query
IF #output = N''
begin
SET #sql =
N'SELECT *' + #newline +
N'FROM (SELECT '
+ #on_rows
+ N', ' + #on_cols + N' AS pivot_col'
+ N', ' + #agg_col + N' AS agg_col' + #newline +
N' FROM ' + #query + N')' +
+ N' AS PivotInput' + #newline +
N' PIVOT(' + #agg_func + N'(agg_col)' + #newline +
N' FOR pivot_col IN(' + #cols + N')) AS PivotOutput;'
end
ELSE
begin
set #sql = 'IF EXISTS (SELECT * FROM tempdb.sys.objects WHERE ' +
'name = ''' + #output + ''' AND type = N''U'') DROP TABLE tempdb.' + #output
EXEC sp_executesql #sql;
SET #sql =
N'SELECT * INTO ' + #output + #newline +
N'FROM (SELECT '
+ #on_rows
+ N', ' + #on_cols + N' AS pivot_col'
+ N', ' + #agg_col + N' AS agg_col' + #newline +
N' FROM ' + #query + N')' +
+ N' AS PivotInput' + #newline +
N' PIVOT(' + #agg_func + N'(agg_col)' + #newline +
N' FOR pivot_col IN(' + #cols + N')) AS PivotOutput;'
end
IF #debug = 1
PRINT #sql
EXEC sp_executesql #sql;
END TRY
BEGIN CATCH
DECLARE
#error_message AS NVARCHAR(2047),
#error_severity AS INT,
#error_state AS INT;
SET #error_message = ERROR_MESSAGE();
SET #error_severity = ERROR_SEVERITY();
SET #error_state = ERROR_STATE();
RAISERROR(#error_message, #error_severity, #error_state);
RETURN;
END CATCH
With that, it's easy to pivot on a variable number of columns:
EXEC pivotsp
'SELECT TeamID, OptionGroup, OptionID AS Options FROM OptionTeam',
'Teamid', -- Row headers
'optiongroup', -- item to aggregate
'count', -- aggregation function
'optiongroup', -- Column header
'##temp' -- output table name
SELECT * FROM ##temp
Results:
Teamid 4 5
1 2 0
2 1 1
3 0 1
SELECT a.*, o.optionGroup, COUNT(*)
FROM team a
CROSS JOIN
option o
JOIN OptionTeam ot
ON ot.teamId = a.teamId
AND ot.optionGroup = o.optionGroup
WHERE o.OptionId = #id
GROUP BY
a.teamId, o.optionGroup
select teamID,
sum(case when optionGroup = 4 then 1 else 0 end) as optionGroup4,
sum(case when optionGroup = 5 then 1 else 0 end) as optionGroup5,
from optionteam
group by teamID
to add more optiongroups without changing the code, try grouping by that field:
select teamID,optionGroup,count(optionID) as optionCount
from optionteam
group by teamID,optionGroup

Cross Join 'n' times a table

It is possible to write a generic function/procedure/select/somethingElse to cross-join a table against himself 'n' times? (yes, 'n' is a given parameter : )
How would you do it?
Example
Having this table:
Value
-------
1
2
3
cross join it 2 times, would return:
Value | Value
------------------
1 1
1 2
1 3
2 1
2 2
2 3
3 1
3 2
3 3
Using dynamic SQL, SQL Server 2005+ (#table_name and #numCrossJoins are stored procedure parameters):
DECLARE #upperLimit INT
SET #upperLimit = 1
DECLARE #SQL NVARCHAR(MAX)
SET #SQL = 'SELECT * FROM '+ #table_name +' '
BEGIN
WHILE (upperLimit <= #numCrossJoins)
BEGIN
SET #SQL = #SQL + 'CROSS JOIN '+ QUOTENAME(#table_name) +' '
SET #upperLimit = #upperLimit + 1
END
EXEC sp_executesql #SQL
END
You can generate dynamic sql to output as many cross joins as you need:
create table #t (value int)
insert into #t values (1)
insert into #t values (2)
insert into #t values (3)
declare #n int
set #n = 4
declare #sql varchar(max)
set #sql = 'SELECT * FROM #t t'
declare #i int
set #i = 0
while (#i <= #n)
begin
set #sql = #sql + ' cross join #t t' + CAST(#i as varchar)
set #i = #i + 1
end
print #sql
execute(#sql)
drop table #t
Try this:
SET #SQL = 'SELECT * FROM ' + replicate('[' + #table_name + '],', #N);
set #SQL = LEFT(LEN(#SQL) - 1);
EXEC sp_executesql #SQL;
If you need to come up with all possible permutations, here is an example:
All Permutations For A String