How to use dynamic SQL to add value of 2 columns - sql-server

I have small table which contains students marks. Table data is shown in below image.
It is look like below in excel
I want to calculate the total using dynamic SQL. I don't want to update it. However, I just want to select all the data with calculated total using dynamic SQL.
Please refer below code:
DECLARE #SQL NVARCHAR(MAX)=''
DECLARE #SNumberList NVARCHAR(MAX)=''
DECLARE #CalculatedLineNumbers NVARCHAR(MAX)=''
SELECT #CalculatedLineNumbers = #CalculatedLineNumbers+ ', '+
CASE WHEN SNo = 7 THEN '[1] + [4] [7]'
WHEN SNo = 8 THEN '[2] + [5] [8]'
WHEN SNo = 9 THEN '[3] + [6] [7]'
ELSE QUOTENAME(SNo)
END
FROM Student
SELECT #SNumberList = #SNumberList+ ', '+QUOTENAME(SNo)
FROM Student
SELECT #SNumberList=STUFF(#SNumberList, 1,1, ''),
#CalculatedLineNumbers=STUFF(#CalculatedLineNumbers,1,1,'')
SET #SQL= '
SELECT Year,'+#CalculatedLineNumbers+'
FROM
(
SELECT *
from Student s) AS J
PIVOT
(
MAX([Marks]) FOR Marks IN ('+#SNumberList+')
) AS P'
EXEC SP_EXECUTESQL #SQL

Taking the excel screenshot to be the expected output, you could accomplish this with just specifying the Year of interest.
Sample Data:
create table #sample_data
(
SNo int
, [LineNo] int
, ColumnNo int
, LineName varchar(15)
, ColumnName varchar(25)
, Marks int
, [Year] int
)
insert into #sample_data
values (1, 1, 1, 'Math', 'Jay', 97, 2018)
, (2, 1, 2, 'Math', 'Sam', 95, 2018)
, (3, 1, 3, 'Math', 'Jack', 90, 2018)
, (4, 2, 1, 'Science', 'Jay', 87, 2018)
, (5, 2, 2, 'Science', 'Sam', 88, 2018)
, (6, 2, 3, 'Science', 'Jack', 86, 2018)
, (7, 3, 1, 'Total', 'Jay', null, 2018)
, (8, 3, 2, 'Total', 'Sam', null, 2018)
, (9, 3, 3, 'Total', 'Jack', null, 2018)
Answer:
The script below, determines the relevant ColumnName values based on setting the Year, and forces the columns to show up in the expected order based on the ColumnNo values. After pivoting the appropriate records, the query makes use of the group by grouping sets to generate the Total record.
declare #ColumnNameList nvarchar(max)
, #ColumnNameListSums nvarchar(max)
, #DynamicQuery nvarchar(max)
, #Year int = 2018 --set by OP in question
--get the full list of ColumnNames in a delimeter ("|") seperated string
set #ColumnNameList =
(
select stuff((
select '| ' + a.ColumnName
from (
select t.ColumnName
, min(t.ColumnNo) as ColumnNo
from #sample_data as t
where t.[Year] = #Year
group by t.ColumnName
) as a
order by a.ColumnNo
for xml path ('')
),1,1,'')
);
--its possible to use the previous variable as well, but easier to create another one
set #ColumnNameListSums =
(
select stuff((
select ', sum(a.' + a.ColumnName + ') as ' + a.ColumnName
from (
select t.ColumnName
, min(t.ColumnNo) as ColumnNo
from #sample_data as t
where t.[Year] = #Year
group by t.ColumnName
) as a
order by a.ColumnNo
for xml path ('')
),1,1,'')
);
set #DynamicQuery =
'
select isnull(b.LineName, ''Total'') as LineName
, b.' + ltrim(replace(#ColumnNameList, '| ', ', b.')) + '
from (
select a.LineName
, ' + #ColumnNameListSums + '
from (
select t.LineName
, t.ColumnName
, t.Marks
, t.[Year]
from #sample_data as t
where t.LineName <> (''Total'') --don''t need it, will generate totals later
and t.[Year] = ' + cast(#Year as char(4)) + '
) as a
pivot (max(a.Marks) for a.ColumnName in ([' + ltrim(replace(#ColumnNameList, '| ', '], [')) + '])) as a
group by grouping sets
(
(
a.LineName
)
,
(
--purposefully left empty
)
)
) as b
'
print #DynamicQuery --in order to see query being executed
exec(#DynamicQuery);
Output:
Given the sample data, the following output is generated.
+----------+-----+-----+------+
| LineName | Jay | Sam | Jack |
+----------+-----+-----+------+
| Math | 97 | 95 | 90 |
| Science | 87 | 88 | 86 |
| Total | 184 | 183 | 176 |
+----------+-----+-----+------+
SQL Server does not do "double headers", so you can't get the 2018 in the output of a query. You could manually add the top header of "2018" in row 1 in excel.

Related

Create varchar column with mixed values from other columns in SQL

I have this scenario:
CREATE TABLE tbl(templateId INT, id INT, name NVARCHAR(50), value NVARCHAR(50), row INT);
INSERT INTO tbl(templateId, id, name, value, row)
VALUES
(1, 12, 'question1', '5 To 10', 1),
(2, 12, 'question2', 'washing machine', 1),
(3, 12, 'question3', 'yes', 1),
(4, 12, 'question2', 'tv', 2),
(5, 12, 'question1', '11 To 15', 2),
(6, 12, 'question1', '16 To 20', 2),
(7, 12, 'question4', 'employed' 2);
The data must be grouped by id and row
and what I would need would be another column with data like this:
-If we have different questions on the same row (grouped by id = 12 and row = 1):
(question1: (5 To 10) [AND] question2: (washing machine) [AND] question3: (yes))
-If we have different questions on the same row and one of them has many answers it should look like this (id = 12 and row = 2):
(question2: (tv) [AND] question1: (11 To 15, 16 To 20) [AND] question4: (employed))
I managed to create the first case, but I’m having problems with the second. For the second I created something like
(question2: (tv) [AND] question1: (11 To 15) OR question1:(16 To 20) OR question4:(employed))
but it's not good, the answers for question1 have to be separated by comma and the name shouldn't be displayed everytime. Moreover, it puts [AND] only between the first two names, it should be between question1 [AND] question4 as well, I just don't know how to replace that OR...
I’ve created a function like this :
declare #result varchar(1000), #name1 varchar(250), #name2 varchar(250),
#duplicates int;
set #result = '';
set #duplicates = 0;
set #name1 = '';
set #name2 = '';
SELECT #result = #result + ' [AND] ' + t.name + ': (' + t.value + ')',
#duplicates = (len(#result) - len(replace(#result,t.name,''))) /
LEN(t.name)
FROM tbl t
WHERE t.id = #table_id and t.row = #row
if(len(#result)>0)
if (#duplicates > 1)
begin
SET #result =replace(substring(#result, 7, LEN(#result) - 4), '
[AND] ', ' OR ');
SET #name1 = LEFT(#result,CHARINDEX(': ',#result)-1);
SET #name2 = SUBSTRING(SUBSTRING(#result,CHARINDEX('OR ', #result)
+ 2,LEN(#result)), 0,CHARINDEX(':', #result) + 0)
if (#name1 <> #name2)
begin
SET #result=STUFF(#result, CHARINDEX('OR', #result), LEN('OR'),
'[AND]')
end
end
else
begin
SET #result=substring(#result, 7, LEN(#result) - 4);
end
return #result;
I hope I managed to make clear what I want to accomplish. Every suggestion will be highly appreciated. Thanks !
Give this a shot
Example
;with cte as (
Select top 1 with ties
[id]
,[row]
,[name]
,TempValue = Stuff((Select ', ' + value From tbl Where [Row]=A.[Row] and [Name]=A.[Name] For XML Path ('')),1,2,'')
,RN = Row_Number() over (Partition By [id],[row] Order by templateId)
From tbl A
Order by Row_Number() over (Partition By ID,[name],row order by templateid)
)
Select [Row]
,NewValue = '('+Stuff((Select ' [AND] ' +concat(Name,': (',TempValue,')') From cte Where [Row]=A.[Row] Order by RN For XML Path ('')),1,7,'')+')'
From cte A
Group By [Row]
Returns
Row NewValue
1 (question1: (5 To 10) [AND] question2: (washing machine) [AND] question3: (yes))
2 (question2: (tv) [AND] question1: (11 To 15, 16 To 20) [AND] question4: (employed))

TSQL Pivot Table [duplicate]

I have small table which contains students marks. Table data is shown in below image.
It is look like below in excel
I want to calculate the total using dynamic SQL. I don't want to update it. However, I just want to select all the data with calculated total using dynamic SQL.
Please refer below code:
DECLARE #SQL NVARCHAR(MAX)=''
DECLARE #SNumberList NVARCHAR(MAX)=''
DECLARE #CalculatedLineNumbers NVARCHAR(MAX)=''
SELECT #CalculatedLineNumbers = #CalculatedLineNumbers+ ', '+
CASE WHEN SNo = 7 THEN '[1] + [4] [7]'
WHEN SNo = 8 THEN '[2] + [5] [8]'
WHEN SNo = 9 THEN '[3] + [6] [7]'
ELSE QUOTENAME(SNo)
END
FROM Student
SELECT #SNumberList = #SNumberList+ ', '+QUOTENAME(SNo)
FROM Student
SELECT #SNumberList=STUFF(#SNumberList, 1,1, ''),
#CalculatedLineNumbers=STUFF(#CalculatedLineNumbers,1,1,'')
SET #SQL= '
SELECT Year,'+#CalculatedLineNumbers+'
FROM
(
SELECT *
from Student s) AS J
PIVOT
(
MAX([Marks]) FOR Marks IN ('+#SNumberList+')
) AS P'
EXEC SP_EXECUTESQL #SQL
Taking the excel screenshot to be the expected output, you could accomplish this with just specifying the Year of interest.
Sample Data:
create table #sample_data
(
SNo int
, [LineNo] int
, ColumnNo int
, LineName varchar(15)
, ColumnName varchar(25)
, Marks int
, [Year] int
)
insert into #sample_data
values (1, 1, 1, 'Math', 'Jay', 97, 2018)
, (2, 1, 2, 'Math', 'Sam', 95, 2018)
, (3, 1, 3, 'Math', 'Jack', 90, 2018)
, (4, 2, 1, 'Science', 'Jay', 87, 2018)
, (5, 2, 2, 'Science', 'Sam', 88, 2018)
, (6, 2, 3, 'Science', 'Jack', 86, 2018)
, (7, 3, 1, 'Total', 'Jay', null, 2018)
, (8, 3, 2, 'Total', 'Sam', null, 2018)
, (9, 3, 3, 'Total', 'Jack', null, 2018)
Answer:
The script below, determines the relevant ColumnName values based on setting the Year, and forces the columns to show up in the expected order based on the ColumnNo values. After pivoting the appropriate records, the query makes use of the group by grouping sets to generate the Total record.
declare #ColumnNameList nvarchar(max)
, #ColumnNameListSums nvarchar(max)
, #DynamicQuery nvarchar(max)
, #Year int = 2018 --set by OP in question
--get the full list of ColumnNames in a delimeter ("|") seperated string
set #ColumnNameList =
(
select stuff((
select '| ' + a.ColumnName
from (
select t.ColumnName
, min(t.ColumnNo) as ColumnNo
from #sample_data as t
where t.[Year] = #Year
group by t.ColumnName
) as a
order by a.ColumnNo
for xml path ('')
),1,1,'')
);
--its possible to use the previous variable as well, but easier to create another one
set #ColumnNameListSums =
(
select stuff((
select ', sum(a.' + a.ColumnName + ') as ' + a.ColumnName
from (
select t.ColumnName
, min(t.ColumnNo) as ColumnNo
from #sample_data as t
where t.[Year] = #Year
group by t.ColumnName
) as a
order by a.ColumnNo
for xml path ('')
),1,1,'')
);
set #DynamicQuery =
'
select isnull(b.LineName, ''Total'') as LineName
, b.' + ltrim(replace(#ColumnNameList, '| ', ', b.')) + '
from (
select a.LineName
, ' + #ColumnNameListSums + '
from (
select t.LineName
, t.ColumnName
, t.Marks
, t.[Year]
from #sample_data as t
where t.LineName <> (''Total'') --don''t need it, will generate totals later
and t.[Year] = ' + cast(#Year as char(4)) + '
) as a
pivot (max(a.Marks) for a.ColumnName in ([' + ltrim(replace(#ColumnNameList, '| ', '], [')) + '])) as a
group by grouping sets
(
(
a.LineName
)
,
(
--purposefully left empty
)
)
) as b
'
print #DynamicQuery --in order to see query being executed
exec(#DynamicQuery);
Output:
Given the sample data, the following output is generated.
+----------+-----+-----+------+
| LineName | Jay | Sam | Jack |
+----------+-----+-----+------+
| Math | 97 | 95 | 90 |
| Science | 87 | 88 | 86 |
| Total | 184 | 183 | 176 |
+----------+-----+-----+------+
SQL Server does not do "double headers", so you can't get the 2018 in the output of a query. You could manually add the top header of "2018" in row 1 in excel.

How to convert column values to comma separated with alias

How to convert column values to comma separated with alias for using this output in dynamic query? MyTable table has two columns DayNo & CR_Date with data:
Day1 01/01/2000
Day2 01/02/2002
Day3 05/01/2003
Day4 01/01/1999
Day5 08/01/1998
Day6 01/19/2010
Day7 01/01/2000
Day8 01/01/2011
Day9 12/05/2000
Day10 01/01/2017
My attempt:
declare #AllRowsInOneRow nvarchar (2000)
set #AllRowsInOneRow = ''
select #AllRowsInOneRow =
case when #AllRowsInOneRow = ''
then CR_Date
else #AllRowsInOneRow + coalesce(''' as '+Day_No+',''' + CR_Date, '')
end
from MyTable
select #AllRowsInOneRow = 'select ''' + #AllRowsInOneRow + ''''
select #AllRowsInOneRow
Output:
select '01/01/2000' as Day2,'01/02/2002' as Day3,'05/01/2003' as Day4,
'01/01/1999' as Day5,'08/01/1998' as Day6,'01/19/2010' as Day7,
'01/01/2000' as Day8,'01/01/2011' as Day9,'12/05/2000' as Day10, '01/01/2017'
Desired output:
select '01/01/2000' as Day1,'01/02/2002' as Day2,'05/01/2003' as Day3,
'01/01/1999' as Day4,'08/01/1998' as Day5,'01/19/2010' as Day6,
'01/01/2000' as Day7,'01/01/2011' as Day8,'12/05/2000' as Day9,
'01/01/2017' as Day10
DDL:
Create table MyTable (Day_No varchar(5), CR_Date varchar(20))
go
insert into MyTable values ( 'Day1' , '01/01/2000')
insert into MyTable values ( 'Day2' , '01/02/2002')
insert into MyTable values ( 'Day3' , '05/01/2003')
insert into MyTable values ( 'Day4' , '01/01/1999')
insert into MyTable values ( 'Day5' , '08/01/1998')
insert into MyTable values ( 'Day6' , '01/19/2010')
insert into MyTable values ( 'Day7' , '01/01/2000')
insert into MyTable values ( 'Day8' , '01/01/2011')
insert into MyTable values ( 'Day9' , '12/05/2000')
insert into MyTable values ( 'Day10' , '01/01/2017')
try this this will give you the desired output .
declare #tmp varchar(250)
SET #tmp = ''
select #tmp = #tmp + ''''+ CR_Date + '''' + ' as ' +Day_No + ', ' from MyTable
select + 'select ' + SUBSTRING(#tmp, 0, LEN(#tmp))
Output:-
select '01/01/2000' as Day1, '01/02/2002' as Day2,
'05/01/2003' as Day3, '01/01/1999' as Day4,
'08/01/1998' as Day5, '01/19/2010' as Day6,
'01/01/2000' as Day7, '01/01/2011' as Day8,
'12/05/2000' as Day9, '01/01/2017' as Day10

Pivot and concatenate values from column in SQL Server

I have table with these columns:
ID | Name | Value
------------------
1 | Test1 | 0
2 | Test2 | 1
3 | Test3 | 0
4 | Test4 | 0
5 | Test5 | 1
And I want to have pivoted and concatenated value column as string
01001
The below code will give the expected result:
SELECT #Result = #Result + CAST(VALUE AS VARCHAR)
FROM #TmpTestingTable
Or you can use the STUFF:
SELECT STUFF(
( SELECT CAST(VALUE AS VARCHAR)
FROM #TmpTestingTable
FOR XML PATH ('')
), 1, 0, '')
For sample, I inserted the columns into the temporary table and execute the code.
CREATE TABLE #TmpTestingTable (ID INT, Name VARCHAR (20), Value INT)
INSERT INTO #TmpTestingTable (ID, Name, Value) VALUES
(1 , 'Test1' , 0),
(2 , 'Test2' , 1),
(3 , 'Test3' , 0),
(4 , 'Test4' , 0),
(5 , 'Test5' , 1)
DECLARE #Result AS VARCHAR (100) = '';
-- using variable approach
SELECT #Result = #Result + CAST(VALUE AS VARCHAR)
FROM #TmpTestingTable
SELECT #Result
-- using STUFF approach
SELECT STUFF(
( SELECT CAST(VALUE AS VARCHAR)
FROM #TmpTestingTable
FOR XML PATH ('')
), 1, 0, '')
DROP TABLE #TmpTestingTable
Use FOR XML to concatinate. It is important that you also include an ORDER BY. Otherwise you have no control of the order of the values and you risk an arbitrary order.
SELECT
(SELECT CAST([VALUE] AS CHAR(1))
FROM yourtable
ORDER BY ID
FOR XML PATH ('')
)
SELECT GROUP_CONCAT(Value SEPARATOR '') FROM Table
EDIT:
Not working on SQL Server. Have a look at Simulating group_concat MySQL function in Microsoft SQL Server 2005? to try to make it work

Aggregate bitfield values with binary OR

I have a table with int values being used as bitfields (where each bit is a flag).
Now I would like to aggregate them with a binary operation (in my case OR) so that:
SELECT 1 AS bitfield
INTO #TABLE
UNION ALL SELECT 1 + 2 + 8 + 32
UNION ALL SELECT 2 + 128
UNION ALL SELECT 2 + 32
SELECT AND_AGGR(bitfield) -- Invalid because AND_AGGR doesn't exist
FROM #TABLE
DROP #TABLE
would result in the value 171
What would be a good way to do this that hopefully doesn't require a lot of | and MAX (but if you must, you must)?
I am using MS SQL Server 2008 myself, but solutions on other servers are also of interest.
On MySQL and PostgreSQL you can use BIT_OR.
I don't think SQL Server has this aggregate function.
You could do it with lots of MAX and & as you said:
MAX(x & 1) + MAX(x & 2) + ... + MAX(x & 128)
If you're expecting the result 171, surely you mean binary OR not AND?
In any case, this solution aggregates the values into a variable:
SELECT 1 AS bitfield
INTO #TABLE
UNION ALL SELECT 1 + 2 + 8 + 32
UNION ALL SELECT 2 + 128
UNION ALL SELECT 2 + 32
DECLARE #i int = 0
SELECT #i = #i | bitfield
FROM #TABLE
SELECT #i
DROP TABLE #table
This might not meet your requirements if you want to group the aggregation by another field.
It is also unlikely to perform well on a large table.
In MS SQL Server
DECLARE #agg VARCHAR(MAX) = '0001,0011,0101,0101,0101'
SELECT CONVERT(binary(4), VALUE, 2) , VALUE FROM STRING_SPLIT( #agg , ',')
DECLARE #sum AS BIGINT = 0
DECLARE #mul AS BIGINT = 0xffffffff
SELECT #sum |= v
, #mul &= v
FROM STRING_SPLIT( #agg , ',')
CROSS APPLY (VALUES (CONVERT(binary(4), VALUE, 2))) _(v)
PRINT FORMAT(#sum,'X8')
PRINT FORMAT(#mul,'X8')
Prints
VALUE
---------- ------------
0x00010000 0001
0x00110000 0011
0x01010000 0101
0x01010000 0101
0x01010000 0101
01110000
00010000
In more complex word you need:
CREATE OR ALTER FUNCTION dbo.BOR( #agg VARCHAR(MAX))
RETURNS BIGINT
AS
BEGIN
DECLARE #sum AS BIGINT = 0
SELECT #sum |= CONVERT(BIGINT, VALUE)
FROM STRING_SPLIT( #agg , ',')
RETURN #sum
END
GO
CREATE OR ALTER FUNCTION dbo.BAND( #agg VARCHAR(MAX))
RETURNS BIGINT
AS
BEGIN
DECLARE #mul AS BIGINT = 0xffffffffffffffff
SELECT #mul &= CONVERT(BIGINT, VALUE)
FROM STRING_SPLIT( #agg , ',')
RETURN #mul
END
GO
when using bitmap of payment periods
;WITH delayedPayment AS
(SELECT * FROM ( VALUES
( 123, 67, '2020-2-1')
,( 123, 67, '2020-4-1')
,( 123, 67, '2020-5-1')
,( 123, 67, '2020-6-1')
,( 123, 68, '2020-6-1') -- another agreement
,( 123, 67, '2020-12-1')
,( 456, 69, '2020-4-1')
,( 456, 69, '2020-8-1')
,( 456, 69, '2020-10-1')
,( 456, 69, '2020-11-1')) _(cuno, loan, missedDuedate)
)
, delayPattern AS
(SELECT cuno
, sum_months
, bor_months
, IIF( FORMAT( CAST(bor_months AS BIGINT), 'X16') LIKE '%111%', 'dalyad 3+ month in row', NULL) delayState
FROM (SELECT cuno
, SUM(POWER( 16.0, CONVERT( BIGINT, DATEDIFF( month, missedDuedate, '2020-12-1')))) sum_months
, dbo.BOR( STRING_AGG( CONVERT( BIGINT, POWER( 16.0, DATEDIFF( month, missedDuedate, '2020-12-1'))),',')) bor_months
FROM delayedPayment
GROUP BY cuno
) s
)
SELECT cuno
, FORMAT( CAST(sum_months AS BIGINT), 'X16') sum_months
, FORMAT( CAST(bor_months AS BIGINT), 'X16') bor_months
, delayState
FROM delayPattern
cuno sum_months bor_months delayState
123 00000*10112*000001 00000*10111*000001 dalyad 3+ month in row
456 0000000100010110 0000000100010110 NULL
But sometimes just one need to think and you can do it with SUM
, delayPattern AS -- optimal
(SELECT cuno
, bor_months
, IIF( FORMAT( CAST(bor_months AS BIGINT), 'X16') LIKE '%111%', 'dalyad 3+ month in row', NULL) delayState
FROM (SELECT cuno
, SUM(POWER( 16.0, missedmonth)) bor_months
FROM ( SELECT DISTINCT cuno
, missedmonth
FROM delayedPayment
CROSS APPLY (VALUES ( DATEDIFF( month, missedDuedate, '2020-12-1'))) _(missedmonth)
GROUP BY cuno, missedmonth
) ss
GROUP BY cuno
) s
)
SELECT cuno
, FORMAT( CAST(bor_months AS BIGINT), 'X16') bor_months
, delayState
FROM delayPattern
Will print
cuno bor_months delayState
123 0000010111000001 dalyad 3+ month in row
456 0000000100010110 NULL
NOTE: I am using HEX format and POWER(16.0, X) , just to be lazy, POWER(2.0, X) will be correct, but then you need bin->string formatter. Something like this:
CREATE OR ALTER FUNCTION dbo.toBinaryString(#p INT)
RETURNS VARCHAR(24)
AS
BEGIN
RETURN REVERSE(REPLACE( REPLACE(
REPLACE( REPLACE( REPLACE( REPLACE(
REPLACE( REPLACE( REPLACE( REPLACE(
REPLACE( REPLACE( REPLACE( REPLACE(
REPLACE( REPLACE( REPLACE( REPLACE( FORMAT(#p,'X8'),
'0', '....'), '1', '...x'),'2', '..x.'),'3', '..xx'),
'4', '.x..'), '5', '.x.x'),'6', '.xx.'),'7', '.xxx'),
'8', 'x...'), '9', 'x..x'),'A', 'x.x.'),'B', 'x.xx'),
'C', 'xx..'), 'D', 'xx.x'),'E', 'xxx.'),'F', 'xxxx'),
'.','0'),'x','1'))
END

Resources