Create varchar column with mixed values from other columns in SQL - sql-server

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))

Related

Unexpected result from CTE on cyclic data

I have this table:
CREATE TABLE [dbo].[hierarchical] (
[id] INT NOT NULL,
[parent_id] INT NULL,
[name] NVARCHAR (40) NULL
);
Containing some data:
INSERT INTO [dbo].[hierarchical] ([id], [parent_id], [name]) VALUES (9, 11, N'43EB7203-3A7F-49A9-8C58-F18738D2BBC4')
INSERT INTO [dbo].[hierarchical] ([id], [parent_id], [name]) VALUES (10, 9, N'E202CAFA-4C0D-4A84-B02A-BF53AC3AFAB1')
INSERT INTO [dbo].[hierarchical] ([id], [parent_id], [name]) VALUES (11, 10, N'371C28E0-8C54-4A23-B28A-273979810E54')
As you can see the hierarchy is cyclical 9 => 11 => 10 => 9.
I'm trying to create a CTE that can handle this cyclic data like this:
alter procedure GetHierarchicalTree
#rootid int
as begin
;with computed as (
select H.id, H.parent_id, 1 lvl, '[' + cast(H.id as nvarchar(max)) + ']' pat
from hierarchical H
where id = #rootid
union all
select H.id, H.parent_id, C.lvl + 1, C.pat + '[' + CAST(H.id as nvarchar(max)) + ']'
from computed C
inner join hierarchical H on C.id = H.parent_id
where C.pat not like '%[' + cast(H.id as nvarchar(max)) + ']%'
)
select * from computed
option (maxrecursion 0)
end
The guard clause is the where C.pat not like '%[' + cast(H.id as nvarchar(max)) + ']%' so if the ID is already included then stop.
But when running the stored procedure on ID number 9:
exec gethierarchicaltree 9
The sp returns 9 and 10, but not 11, which is odd because 11 shouldn't be in computed.pat already.
Running on 11 returns 9 and 11, but not 10.
Even more puzzling is running on 10, because the sp only returns 10.
Why the sp doesn't return 9 and 11?
I think your problem is in the LIKE, the square brackets [] should not be there:
where C.pat not like '%' + cast(H.id as nvarchar(max)) + '%'
UPDATE:
I understand you use the brackets to delimit your path. In that case you have to escape the opening bracket like this:
where C.pat not like '%[[]' + cast(H.id as nvarchar(max)) + ']%'
You can also change the where condition to use charindex:
CHARINDEX('[' + cast(H.id as nvarchar(max)) + ']',C.pat)=0
Your data has circular reference; 11 is the parent of 9, which is the parent of 10, which is the parent of 11, which is the parent of 9, which... That isn't a hierarchy and actually infers there's a design flaw somewhere.
I suspect what you want is:
CREATE PROCEDURE dbo.GetHierarchicalTree #rootid int
AS BEGIN
WITH rCTE AS
(SELECT H.id,
H.parent_id,
CONVERT(varchar(MAX), QUOTENAME(H.id)) AS [name]
FROM dbo.hierarchical H
WHERE H.id = #rootid
UNION ALL
SELECT H.id,
H.parent_id,
r.[name] + CONVERT(varchar(MAX), QUOTENAME(H.id))
FROM rCTE r
JOIN dbo.hierarchical H ON r.parent_id = H.id
AND h.ID != #rootid)
SELECT *
FROM rCTE
--OPTION (MAXRECURSION 0); --As you have a circular reference I strongly recommend this OPTION. You could very overwhelm your server.
END;
GO
EXEC dbo.GetHierarchicalTree 9;

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 use dynamic SQL to add value of 2 columns

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.

SQL Server: String insertion by position

I want to insert a set of values into a given string at specified positions. I couldn't find an existing solution for this so I created the following query which does so. I am wondering if there is a way to simplify it, especially the portion which queries the cte and creates the NewString. Also, is there a way to construct the cte recursion without involving the ID column? The ID (and the rest) seems to work fine, just wondering if it could be tweaked to make it more organic/elegant or if there is a better solution overall. The different groups are really only for testing purposes; each group corresponds to a possible insertion position scenario. Thus, there will not be groups involved when I use it.
declare
#String varchar(200),
#StringLen int,
#GroupID int,
#PositionMax int
declare #Chars table (
ID int identity,
GroupID int,
Position int,
Value varchar(20)
)
select
#String = 'abcde',
#StringLen = len(#String),
#GroupID = 1
--Affix
--[P]refix
--[I]nfix
--[S]uffix
insert #Chars
select
GroupID,
Position,
Value
from (
values
(1, 0, 'X'), --P
(2, 2, 'Y'), --I
(3, 5, 'Z'), --S
(4, 0, 'X'), --P
(4, 2, 'Y'), --I
(5, 2, 'Y'), --I
(5, 5, 'Z'), --S
(6, 0, 'X'), --P
(6, 5, 'Z'), --S
(7, 0, 'X'), --P
(7, 2, 'Y'), --I
(7, 5, 'Z'), --S
(8, 2, 'Y1'), --I
(8, 4, 'Y2'), --I
(9, 0, 'X'), --P
(9, 2, 'Y1'), --I
(9, 4, 'Y2'), --I
(10, 2, 'Y1'), --I
(10, 4, 'Y2'), --I
(10, 5, 'Z'), --S
(11, 0, 'X'), --P
(11, 2, 'Y1'), --I
(11, 4, 'Y2'), --I
(11, 5, 'Z') --S
) as T(GroupID, Position, Value)
order by GroupID, Position
;with cte (
ID,
GroupID,
LeftString,
Value,
RightString,
Position
) as (
select
ID,
GroupID,
LeftString,
Value,
RightString,
Position
from (
select
row_number() over (partition by GroupID order by ID) as RowNumber,
ID,
GroupID,
cast(left(#String, Position) as varchar(200)) as LeftString,
Value,
cast(right(#String, #StringLen - Position) as varchar(200)) as RightString,
Position
from #Chars
) as C
where RowNumber = 1
union all
select
vc.ID,
vc.GroupID,
cast(left(RightString, vc.Position - cte.Position) as varchar(200)) as LeftString,
vc.Value,
cast(right(RightString, #StringLen - vc.Position) as varchar(200)) as RightString,
vc.Position
from #Chars vc
join cte cte
on cte.GroupID = vc.GroupID
and cte.ID + 1 = vc.ID
)
select
GroupID,
case
when LSLenSumMax < #StringLen
then NewString + RightString
else NewString
end as NewString
from (
select
GroupID,
max(LSLenSum) as LSLenSumMax,
RightString,
stuff((
select
LeftString + Value
from cte cte
where cte.GroupID = cteLR.GroupID
for xml path(''), type
).value('(./text())[1]', 'varchar(max)'), 1, 0, '') as NewString
from (
select
GroupID,
(select sum(len(LeftString)) from cte cteL where cteL.groupID = cte.groupID) as LSLenSum,
(select top 1 RightString from cte cteR where cteR.groupID = cte.groupID order by cteR.ID desc) as RightString
from cte cte
) as cteLR
group by
GroupID,
RightString
) as C
order by GroupID
You can implement a custom
aggregate function... Or try this: A recursive scalar function:
CREATE FUNCTION dbo.GetPrefixTo (#GroupID INT, #Position INT, #String CHAR(200))
RETURNS Char(200)
WITH EXECUTE AS CALLER
AS
BEGIN
DECLARE #str CHAR(200) = NULL
DECLARE #beforePosition INT = 0
IF (SELECT COUNT(*) FROM Chars WHERE GroupID = #GroupID AND Position < #Position) > 0
BEGIN
SELECT #beforePosition = MAX(Position) FROM chars WHERE GroupID = #GroupID AND Position < #Position
select #str = RTRIM(dbo.GetPrefixTo(#GroupID, #beforePosition, substring(#String, 1, #Position))) +
+ RTrim(Value) + substring(#String, #Position + 1, len(#string) + 1)
FROM Chars WHERE GroupID = #GroupID AND Position = #Position
END
ELSE
SELECT #str = substring(#String, 1, #Position) + RTrim(Value) + substring(#String, #Position + 1, len(#string) + 1) FROM Chars
WHERE GroupID = #GroupID AND Position = #Position
RETURN #str
END
And group by GroupID and aggregate max(position):
SELECT groupID, max(Position)
, dbo.GetPrefixTo(groupID, max(Position), 'abcde')
FROM Chars
GROUP BY GroupID
This is the result:
1 0 Xabcde
2 2 abYcde
3 5 abcdeZ
4 2 XabYcde
5 5 abYcdeZ
6 5 XabcdeZ
7 5 XabYcdeZ
8 4 abY1cdY2e
9 4 XabY1cdY2e
10 5 abY1cdY2eZ
11 5 XabY1cdY2eZ
========================================================================

How to replace a character in a string using T-SQL

In my table column, i have below sample data
Test1 145, Area 1
Test2 146,
Test3 145, Area 2, Plot 10
What i want to achieve is to replace "," in the string but only if it is the last character. If i have more characters after "," then the replace should leave the string as it is.
In the example above, the replace would only work in line 2.
The expected out put would be like below
Test1 145, Area 1
Test2 146
Test3 145, Area 2, Plot 10
In line 2 above, "," has been replaced with empty space.
I have tried this Replace(column1, ', ', '') AS ColName but this replaces all the "," in Test1 and Test3.
You can try this:
DECLARE #value VARCHAR(1024) = 'Test2 146,';
SELECT IIF(RIGHT(#value,1) = ',', LEFT(#value, LEN(#value) - 1), #value);
For column it looks like below:
DECLARE #DataSource TABLE
(
[value] VARCHAR(1024)
);
INSERT INTO #DataSource ([value])
VALUES ('Test1 145, Area 1')
,('Test2 146,')
,('Test3 145, Area 2, Plot 10');
SELECT IIF(RIGHT([value],1) = ',', LEFT([value], LEN([value]) - 1), [value])
FROM #DataSource;
You can also do this will LIKE and IIF :
SELECT IIF(t.Column LIKE '%,' , LEFT(t.column, LEN(t.column) - 1) , t.column) as new_val
FROM YourTable t
For older versions: You can use CASE EXPRESSION since IIF is only available since 2012+ version(Link by #gotqn)
SELECT CASE WHEN t.Column LIKE '%,'
THEN LEFT(t.column, LEN(t.column) - 1)
ELSE t.column
END as new_val
FROM YourTable t
I am pretty sure IIF isn't available in SQL Server 2005. This is basically the same logic as the previous answer using CASE instead.
declare #MyString varchar(50)
set #MyString = 'Test2 146,'
select
case
when right(rtrim(#MyString), 1) = ',' then
substring(#MyString, 1, len(rtrim(#MyString)) - 1)
else
#MyString
end
Something like this:
SELECT CASE
WHEN Column1 LIKE '%,' THEN STUFF(column1, LEN(column1), 1, '')
ELSE Column1
END
This shows one way to do it.
DECLARE #test VARCHAR(30);
SET #test = 'Test1, 145, Area 1';
SELECT #test;
IF CHARINDEX(',', #test, LEN(RTRIM(#test))) > 0
BEGIN
SET #test = Replace(#test, ',', '');
END
SELECT #test;
SET #test = 'Test2 146,';
SELECT #test;
IF CHARINDEX(',', #test, LEN(RTRIM(#test))) > 0
BEGIN
SET #test = Replace(#test, ',', '');
END
SELECT #test;
SET #test = 'Test3 145, Area 2, Plot 10';
SELECT #test;
IF CHARINDEX(',', #test, LEN(RTRIM(#test))) > 0
BEGIN
SET #test = Replace(#test, ',', '');
END
SELECT #test;
-- How to work into a SELECT statement
SET #test = 'Test2 146,';
SELECT CASE WHEN CHARINDEX(',', #test, LEN(RTRIM(#test))) > 0 THEN SUBSTRING(#test, 1, LEN(#test) - 1) ELSE #test END AS 'Converted Value';

Resources