How to Pivot on caption? - tsql

I am trying to pivot rows into columns with Tsql and also eliminate Nulls. How do I do this? My current query:
IF OBJECT_ID(N'tempdb..#test_data') IS NOT NULL drop table #test_data
create table #test_data (
question_caption varchar(max),
[0] varchar(max),
[1] varchar(max),
[2] varchar(max),
[3] varchar(max))
insert #test_data values('q1','abc',Null,Null,Null)
insert #test_data values('q2',Null,'def',Null,Null)
insert #test_data values('q3',Null,Null,'ghi',Null)
insert #test_data values('q4',Null,Null,Null,'jkl')
select * from #test_data
pivot (
Max([0])
For question_caption in ([0],[1],[2],[3])
) as PivotTable
Output:
question_caption 0 1 2 3
q1 abc NULL NULL NULL
q2 NULL def NULL NULL
q3 NULL NULL ghi NULL
q4 NULL NULL NULL jkl
What I want:
q1 q2 q3 q4
abc def ghi jkl
How can I achieve this? The above query has the error:
Msg 265, Level 16, State 1, Line 4
The column name "0" specified in the PIVOT operator conflicts with the existing column name in the PIVOT argument.
I have tried multiple Pivot examples, but all of them have resulted in one error or another.

You can do with a simple max case:
select [q1]=max(case when question_caption = 'q1' then [0] else null end),
[q2]=max(case when question_caption = 'q2' then [1] else null end),
[q3]=max(case when question_caption = 'q3' then [2] else null end),
[q4]=max(case when question_caption = 'q4' then [3] else null end)
from #test_data
or the pivot:
select [q1], [q2], [q3], [q4]
from ( select question_caption,
coalesce([0],[1],[2],[3])
from #test_data
) s (c, v)
pivot (max(v) for c in ([q1], [q2], [q3], [q4])) p

Related

How to eliminate Nulls, and insert with split delimter

drop table #temp
Create Table #Temp
(
col1 Varchar(20),
col2 Varchar(20),
Col3 Varchar(50),
col4 Varchar(20)
)
Select * From #Temp
Insert Into #Temp(col1)
Select * From SplitDelimiterString('123,456', ',')
Insert Into #Temp(col2)
Select * From SplitDelimiterString('abc,def', ',')
Insert Into #Temp(Col3)
Select * From SplitDelimiterString('fff,ggg', ',')
Insert Into #Temp(col4)
Select * From SplitDelimiterString('520002,520003', ',')
Select * From #Temp
FYI, SplitDelimiterString is a function.
-- Code for SplitDelimiterString
Create FUNCTION [dbo].[SplitDelimiterString] (#StringWithDelimiter VARCHAR(8000), #Delimiter VARCHAR(8))
RETURNS #ItemTable TABLE (Item VARCHAR(8000))
AS
BEGIN
DECLARE #StartingPosition INT;
DECLARE #ItemInString VARCHAR(8000);
SELECT #StartingPosition = 1;
--Return if string is null or empty
IF LEN(#StringWithDelimiter) = 0 OR #StringWithDelimiter IS NULL RETURN;
WHILE #StartingPosition > 0
BEGIN
--Get starting index of delimiter .. If string
--doesn't contain any delimiter than it will returl 0
SET #StartingPosition = CHARINDEX(#Delimiter,#StringWithDelimiter);
--Get item from string
IF #StartingPosition > 0
SET #ItemInString = SUBSTRING(#StringWithDelimiter,0,#StartingPosition)
ELSE
SET #ItemInString = #StringWithDelimiter;
--If item isn't empty than add to return table
IF( LEN(#ItemInString) > 0)
INSERT INTO #ItemTable(Item) VALUES (#ItemInString);
--Remove inserted item from string
SET #StringWithDelimiter = SUBSTRING(#StringWithDelimiter,#StartingPosition +
LEN(#Delimiter),LEN(#StringWithDelimiter) - #StartingPosition)
--Break loop if string is empty
IF LEN(#StringWithDelimiter) = 0 BREAK;
END
RETURN
END
-- The result set is
Col1 Col2 Col3 Col4
123 NULL NULL NULL
456 NULL NULL NULL
NULL abc NULL NULL
NULL def NULL NULL
NULL NULL fff NULL
NULL NULL ggg NULL
NULL NULL NULL 520002
NULL NULL NULL 520003
-- I need a result set like
-- The result set is
col1 col2 col3 col4
123 abc fff 520002
456 def ggg 520003
Please help.
--- Figured out my self. Thanks to #liebs19 for logic
BEGIN TRAN
Create Table #Temp1
(
RowID int not null identity(1,1) primary key,
col1 Varchar(20),
)
Create Table #Temp2
(
RowID int not null identity(1,1) primary key,
col2 Varchar(20),
)
Create Table #Temp3
(
RowID int not null identity(1,1) primary key,
col3 Varchar(20),
)
Create Table #Temp4
(
RowID int not null identity(1,1) primary key,
col4 Varchar(20),
)
Insert Into #Temp1(col1)
Select * From SplitDelimiterString('123,456', ',')
Insert Into #Temp2(col2)
Select * From SplitDelimiterString('abc,def', ',')
Insert Into #Temp3(Col3)
Select * From SplitDelimiterString('fff,ggg', ',')
Insert Into #Temp4(col4)
Select * From SplitDelimiterString('520002,520003', ',')
Select #Temp1.Col1, #Temp2.col2, #Temp3.Col3, #Temp4.Col4
From #Temp1
Inner Join #Temp2 ON #Temp1.RowID = #Temp2.RowID
Inner Join #Temp3 ON #Temp1.RowID = #Temp3.RowID
Inner Join #Temp4 ON #Temp1.RowID = #Temp4.RowID
ROLLBACK TRAN
-- This is the output finally I am looking for.
col1 col2 col3 col4
123 abc fff 520002
456 def ggg 520003
.

Efficiently select the most specific result from a table

I have a table roughly as follows:
CREATE TABLE t_table (
f_userid BIGINT NOT NULL
,f_groupaid BIGINT
,f_groupbid BIGINT
,f_groupcid BIGINT
,f_itemid BIGINT
,f_value TEXT
);
The groups are orthogonal, so no hierarchy can be implied beyond the fact that every entry in the table will have a user ID. There is no uniqueness in any of the columns.
So for example a simple setup might be:
INSERT INTO t_table VALUES (1, NULL, NULL, NULL, NULL, 'Value for anything by user 1');
INSERT INTO t_table VALUES (1, 5, 2, NULL, NULL, 'Value for anything by user 1 in groupA 5 groupB 2');
INSERT INTO t_table VALUES (1, 4, NULL, 1, NULL, 'Value for anything by user 1 in groupA 5 and groupC 1');
INSERT INTO t_table VALUES (2, NULL, NULL, NULL, NULL, 'Value for anything by user 2');
INSERT INTO t_table VALUES (2, 1, NULL, NULL, NULL, 'Value for anything by user 2 in groupA 1');
INSERT INTO t_table VALUES (2, 1, 3, 4, 5, 'Value for item 5 by user 2 in groupA 1 and groupB 3 and groupC 4');
For any given set of user/groupA/groupB/groupC/item I want to be able to obtain the most specific item in the table that applies. If any of the given set are NULL then it can only match relevant columns in the table which contain NULL. For example:
// Exact match
SELECT MostSpecific(1, NULL, NULL, NULL, NULL) => "Value for anything by user 1"
// Match the second entry because groupC and item were not specified in the table and the other items matched
SELECT MostSpecific(1, 5, 2, 3, NULL) => "Value for anything by user 1 in groupA 5 groupB 2"
// Does not match the second entry because groupA is NULL in the query and set in the table
SELECT MostSpecific(1, NULL, 2, 3, 4) => "Value for anything by user 1"
The obvious approach here is for the stored procedure to work through the parameters and find out which are NULL and not, and then call the appropriate SELECT statement. But this seems very inefficient. IS there a better way of doing this?
This should do it, just filter out any non matching rows using a WHERE, then rank the remaining rows by how well they match. If any column doesn't match, the whole bop expression will result in NULL, so we filter that out in an outer query where we also order by match and limit the result to only the single best match.
CREATE FUNCTION MostSpecific(BIGINT, BIGINT, BIGINT, BIGINT, BIGINT)
RETURNS TABLE(f_userid BIGINT, f_groupaid BIGINT, f_groupbid BIGINT, f_groupcid BIGINT, f_itemid BIGINT, f_value TEXT) AS
'WITH cte AS (
SELECT *,
CASE WHEN f_groupaid IS NULL THEN 0 WHEN f_groupaid = $2 THEN 1 END +
CASE WHEN f_groupbid IS NULL THEN 0 WHEN f_groupbid = $3 THEN 1 END +
CASE WHEN f_groupcid IS NULL THEN 0 WHEN f_groupcid = $4 THEN 1 END +
CASE WHEN f_itemid IS NULL THEN 0 WHEN f_itemid = $5 THEN 1 END bop
FROM t_table
WHERE f_userid = $1
AND (f_groupaid IS NULL OR f_groupaid = $2)
AND (f_groupbid IS NULL OR f_groupbid = $3)
AND (f_groupcid IS NULL OR f_groupcid = $4)
AND (f_itemid IS NULL OR f_itemid = $5)
)
SELECT f_userid, f_groupaid, f_groupbid, f_groupcid, f_itemid, f_value FROM cte
WHERE bop IS NOT NULL
ORDER BY bop DESC
LIMIT 1'
LANGUAGE SQL
//
An SQLfiddle to test with.
Try something like:
select *
from t_table t
where f_userid = $p_userid
and (t.f_groupaid is not distinct from $p_groupaid or t.f_groupaid is null) --null in f_groupaid matches both null and not null values
and (t.f_groupbid is not distinct from $p_groupbid or t.f_groupbid is null)
and (t.f_groupcid is not distinct from $p_groupcid or t.f_groupcid is null)
order by (t.f_groupaid is not distinct from $p_groupaid)::int -- order by count of matches
+(t.f_groupbid is not distinct from $p_groupbid)::int
+(t.f_groupcid is not distinct from $p_groupcid)::int desc
limit 1;
It will give you the best match on groups.
A is not distinct from B fill return true if A and B are equal or both null.
::int means cast ( as int). Casting boolean true to int will give 1 (You can not add boolean values directly).
SQL Fiddle
create or replace function mostSpecific(
p_userid bigint,
p_groupaid bigint,
p_groupbid bigint,
p_groupcid bigint,
p_itemid bigint
) returns t_table as $body$
select *
from t_table
order by
(p_userid is not distinct from f_userid or f_userid is null)::integer
+
(p_groupaid is not distinct from f_groupaid or f_userid is null)::integer
+
(p_groupbid is not distinct from f_groupbid or f_userid is null)::integer
+
(p_groupcid is not distinct from f_groupcid or f_userid is null)::integer
+
(p_itemid is not distinct from f_itemid or f_userid is null)::integer
desc
limit 1
;
$body$ language sql;

Using CTE instead of Cursor

I have the following table structure.
I just want to update SubId to all the rows where it is null and where the RawLineNumber is ascending by 1 and also the SeqNumber ascending by 1.
RawlineNumber Claimid SubId SeqNumber
1 6000 A100 1
2 6000 NULL 2
3 6000 NULL 3
10 6000 A200 1
11 6000 NULL 2
25 6000 A300 1
26 6000 NULL 2
27 6000 NULL 3
I want to update
SubId of RawLineNumber 2 and 3 with A100,
SubId of RawLineNumber 11 with A200,
SubId of RawLineNumber 26 and 27 with A300.
I have a cursor which does the job but can I have a CTE to take care of it ?
UPDATE m
SET subid = q.subid
FROM mytable m
CROSS APPLY
(
SELECT TOP 1 subid
FROM mytable mi
WHERE mi.rawLineNumber < m.rawLineNumber
AND mi.subid IS NOT NULL
ORDER BY
rawLineNumber DESC
) q
WHERE m.subid IS NULL
Since a recusive solution was requested, I decided to write one. Also it works for gaps in Seqnumbers and RawlineNumber
declare #t table (RawlineNumber int, Claimid int, SubId varchar(5), SeqNumber int)
insert #t values(1, 6000, 'A100', 1)
insert #t values(2, 6000, NULL, 2)
insert #t values(3, 6000, NULL, 3)
insert #t values(10, 6000, 'A200', 1)
insert #t values(11, 6000, NULL, 2)
insert #t values(25, 6000, 'A300', 1)
insert #t values(26, 6000, NULL, 2)
insert #t values(27, 6000, NULL, 3)
;with cte as
(
select Rawlinenumber, SeqNumber, SubId
from #t where SubId is not null and SeqNumber = 1
union all
select t.Rawlinenumber, t.SeqNumber, c.SubId
from cte c
join
#t t
on c.Rawlinenumber + 1 = t.Rawlinenumber
and c.SeqNumber + 1 = t.SeqNumber
where t.SubId is null and t.SeqNumber > 1
)
update t
set SubId = c.SubId
from #t t join cte c
on c.Rawlinenumber = t.Rawlinenumber
where t.SeqNumber > 1
select * from #t
A not-so simple SQL script should achieve what you want:
update my_table t1 set t1.subid =
(select t2.subid from my_table t2
where t2.rawlinenumber < t1.rawlinenumber
and t2.seqnumber = 1
and t2.rawlinenumber = (
select max(t3.rawlinenumber)
from my_table t3
where t3.seq_number = 1
and t3.rawlinenumber <= t2.rawlinenumber)
where t1.subid is null;
The inner subselect (T3) gives us the last row having seqnumber = 1 before the current line,
the outer subselect gives us the SubID for this row (using windowing functions would be more efficient, but since you didn't mention a specific RDBMS, I stick with this :-) )

T-SQL -- convert comma-delimited column into multiple columns

From the table below, how can I convert the Values column into multiple columns, populated with individual values that are currently separated by commas? Before the conversion:
Name Values
---- ------
John val,val2,val3
Peter val5,val7,val9,val14
Lesli val8,val34,val36,val65,val71,val
Amy val3,val5,val99
The result of the conversion should look like:
Name Col1 Col2 Col3 Col4 Col5 Col6
---- ---- ---- ---- ---- ---- ----
John val val2 val3
Peter val5 val7 val9 val14
Lesli val8 val34 val36 val65 val71 val
Amy val3 val5 val99
First, what database product and version are you using? If you are using SQL Server 2005 and later, you can write a Split user-defined function like so:
CREATE FUNCTION [dbo].[Split]
(
#DelimitedList nvarchar(max)
, #Delimiter varchar(2) = ','
)
RETURNS TABLE
AS
RETURN
(
With CorrectedList As
(
Select Case When Left(#DelimitedList, DataLength(#Delimiter)) <> #Delimiter Then #Delimiter Else '' End
+ #DelimitedList
+ Case When Right(#DelimitedList, DataLength(#Delimiter)) <> #Delimiter Then #Delimiter Else '' End
As List
, DataLength(#Delimiter) As DelimiterLen
)
, Numbers As
(
Select TOP (Coalesce(Len(#DelimitedList),1)) Row_Number() Over ( Order By c1.object_id ) As Value
From sys.objects As c1
Cross Join sys.columns As c2
)
Select CharIndex(#Delimiter, CL.list, N.Value) + CL.DelimiterLen As Position
, Substring (
CL.List
, CharIndex(#Delimiter, CL.list, N.Value) + CL.DelimiterLen
, CharIndex(#Delimiter, CL.list, N.Value + 1)
- ( CharIndex(#Delimiter, CL.list, N.Value) + CL.DelimiterLen )
) As Value
From CorrectedList As CL
Cross Join Numbers As N
Where N.Value < Len(CL.List)
And Substring(CL.List, N.Value, CL.DelimiterLen) = #Delimiter
)
You can then split out the values in you want using something akin to:
Select Name, Values
From Table1 As T1
Where Exists (
Select 1
From Table2 As T2
Cross Apply dbo.Split (T1.Values, ',') As T1Values
Cross Apply dbo.Split (T2.Values, ',') As T2Values
Where T2.Values.Value = T1Values.Value
And T1.Name = T2.Name
)
Here is a solution that uses a recursive cte to generate a "table of numbers" (courtesy of Itzik Ben-Gan), which is useful for all manner of problems including string splitting, and PIVOT. SQL Server 2005 onwards. Full table create, insert and select script included.
CREATE TABLE dbo.Table1
(
Name VARCHAR(30),
[Values] VARCHAR(128)
)
GO
INSERT INTO dbo.Table1 VALUES ('John', 'val,val2,val3')
INSERT INTO dbo.Table1 VALUES ('Peter', 'val5,val7,val9,val14')
INSERT INTO dbo.Table1 VALUES ('Lesli', 'val8,val34,val36,val65,val71,val')
INSERT INTO dbo.Table1 VALUES ('Amy', 'val3,val5,val99')
GO
SELECT * FROM dbo.Table1;
GO
WITH
L0 AS(SELECT 1 AS c UNION ALL SELECT 1),
L1 AS(SELECT 1 AS c FROM L0 AS A, L0 AS B),
L2 AS(SELECT 1 AS c FROM L1 AS A, L1 AS B),
L3 AS(SELECT 1 AS c FROM L2 AS A, L2 AS B),
Numbers AS(SELECT ROW_NUMBER() OVER(ORDER BY c) AS n FROM L3)
SELECT Name, [1] AS Column1, [2] AS Column2, [3] AS Column3, [4] AS Column4, [5] AS Column5, [6] AS Column6, [7] AS Column7
FROM
(SELECT Name,
ROW_NUMBER() OVER (PARTITION BY Name ORDER BY nums.n) AS PositionInList,
LTRIM(RTRIM(SUBSTRING(valueTable.[Values], nums.n, charindex(N',', valueTable.[Values] + N',', nums.n) - nums.n))) AS [Value]
FROM Numbers AS nums INNER JOIN dbo.Table1 AS valueTable ON nums.n <= CONVERT(int, LEN(valueTable.[Values])) AND SUBSTRING(N',' + valueTable.[Values], n, 1) = N',') AS SourceTable
PIVOT
(
MAX([VALUE]) FOR PositionInList IN ([1], [2], [3], [4], [5], [6], [7])
) AS Table2
GO
--DROP TABLE dbo.Table1
Which converts this output
Name Values
John val,val2,val3
Peter val5,val7,val9,val14
Lesli val8,val34,val36,val65,val71,val
Amy val3,val5,val99
to
Name Column1 Column2 Column3 Column4 Column5 Column6 Column7
Amy val3 val5 val99 NULL NULL NULL NULL
John val val2 val3 NULL NULL NULL NULL
Lesli val8 val34 val36 val65 val71 val NULL
Peter val5 val7 val9 val14 NULL NULL NULL

Would this be an appropriate situations for a cursor?

I have the following script:
SELECT left(SHI.FSOKEY, 6) AS [SoNo]
, substring(SHI.FSOKEY, 7, 3) AS [So Item]
, right(SHI.FSOKEY, 3) AS [So Rels]
, QAL.FCLOT AS [LotSerial]
FROM shmast SHM
INNER JOIN shitem SHI
ON SHM.FSHIPNO = SHI.FSHIPNO
INNER JOIN qalotc QAL
ON SHM.FSHIPNO = Left(QAL.FCUSEINDOC, 6)
AND substring(QAL.FCUSEINDOC, 7, 6) = SHI.FITEMNO
This produces output that looks like this:
SoNo So Item SoRels LotSerial
123456 1 001 ABCD
123456 1 001 AMOH
123456 1 001 POWK
123456 1 001 IUIL
123456 1 002 ABCE
I want to group by SoNo, SoItem, SoRels and get a list of LotSerials for each. So, my output would look like this:
SoNo So Item SoRels LotSerial
123456 1 001 ABCD, AMOH, POWK, IUIL
123456 1 002 ABCE
I need to do so that I can pull this information back into a main query based on the SoNo, SoItem, SoRels.
Any help would be greatly appreciated.
As always, avoid cursors whenever possible. Your scenario would be a good fit for a user defined function. I simplified your schema a little for this example. Essentially we are concatenating the serials that match with commas to a variable within a user defined function, and then retuning the result. If Null values are possible, you might want to add usage of Coalesce
create table SO (SONO int)
insert into SO values (1)
insert into SO values (2)
insert into SO values (3)
create table SOCHILD
(SONO int, SerialNo varchar(10))
insert into SOCHILD values (1, 'ABCD')
insert into SOCHILD values (1, 'EFGH')
insert into SOCHILD values (1, 'IJKL')
GO
create function fx_GetSerials(#SONO int)
returns varchar(1000) as
Begin
Declare #ret varchar(1000)
set #ret = ''
Select #ret = #ret + SerialNo + ','
from SOCHILD where SONO = #SONO
if (len(#ret) > 0)
set #Ret = left(#ret, len(#ret) -1)
return #ret
End
GO
select dbo.Fx_GetSerials(1)
drop function fx_GetSerials
Drop table SO
Drop table SOCHILD
Results
ABCD,EFGH,IJKL
#cmsjr beat me to it with his answer which is just bout the same. I do build the string differently, and have a complete working example.
try this:
CREATE TABLE YourTable (SoNo int, SoItem int, SoRels char(3), LotSerial char(4))
go
INSERT INTO YourTable VALUES (123456,1,'001','ABCD')
INSERT INTO YourTable VALUES (123456,1,'001','AMOH')
INSERT INTO YourTable VALUES (123456,1,'001','POWK')
INSERT INTO YourTable VALUES (123456,1,'001','IUIL')
INSERT INTO YourTable VALUES (123456,1,'002','ABCE')
go
CREATE FUNCTION LotSerial_to_CVS(#SoNo int, #SoItem int, #SoRels char(3))
RETURNS varchar(2000) AS
BEGIN
DECLARE #cvs varchar(2000)
SELECT #cvs=ISNULL(#cvs+', ','')+LotSerial
FROM YourTable
WHERE SoNo=#SoNo AND SoItem=#SoItem AND SoRels=#SoRels
RETURN #cvs
END
go
SELECT
SoNo, SoItem, SoRels, dbo.LotSerial_to_CVS(SoNo, SoItem, SoRels)
FROM YourTable
GROUP BY SoNo, SoItem, SoRels
OUTPUT:
SoNo SoItem SoRels
----------- ----------- ------ -----------------------
123456 1 001 ABCD, AMOH, POWK, IUIL
123456 1 002 ABCE
(2 row(s) affected)
There is no need for a cursor in almost ANY case any more.
You can do the same thing using XML PATH as well. Here is a working sample:
SET NOCOUNT ON
Declare #MyTable Table
(
SoNo VarChar (100),
SoItem VarChar (100),
SoRels VarChar (100),
LotSerial VarChar (100)
)
INSERT INTO #MyTable Values ('123456', '1', '001', 'ABCD')
INSERT INTO #MyTable Values ('123456', '1', '001', 'AMOH')
INSERT INTO #MyTable Values ('123456', '1', '001', 'POWK')
INSERT INTO #MyTable Values ('123456', '1', '001', 'IUIL')
INSERT INTO #MyTable Values ('123456', '1', '002', 'ABCE')
SELECT
SoNo,
SoItem,
SoRels,
STUFF ((
SELECT ', ' + LotSerial
FROM #MyTable T1
WHERE 1=1
AND T1.SoNo = T2.SoNo
AND T1.SoItem = T2.SoItem
And T1.SoRels = T2.SoRels
FOR XML PATH ('')
), 1, 2, '') AS LotSerial
FROM #MyTable T2
GROUP BY SoNo, SoItem, SoRels