SQL Server - How to PIVOT multi value of same key - sql

How to PIVOT a column which have multi values with same Key.
For instance, we have table
ID FieldName FieldValue
1 Name Jack
1 Country Auz
1 PostCode 1234
1 Name Jhon
2 Name Leo
2 Country USA
2 PostCode 2345
I want to get the result table after pivot:
ID Name Country PostCode
1 Jack Auz 1234
1 Jhon Auz 1234
2 Leo USA 2345
I have used code with pivot function:
SELECT *
FROM (
SELECT
ID, FieldName, FieldValue
FROM Table
) AS p
PIVOT
(
MAX(FieldValue)
FOR FieldName IN ([Name], [Country], [PostCode])
) AS pvt
From the above code, The result table will only get one record for ID '1', I think this may due to the function of 'MAX' used. Is there any way to show two records by using any other function to solve it?

You have an unusual situation in your data because ID 1 has 2 names associated with it, and you want both to share the other attributes such as postcode etc. For this you can use an apply operator to return 1 or more names for each ID, and then group by the combination of id and name.
SELECT
id
, Name
, MAX(CASE WHEN fieldname = 'Country' THEN fieldvalue END) AS Country
, MAX(CASE WHEN fieldname = 'Postcode' THEN fieldvalue END) AS Postcode
FROM (
SELECT
t.*, ca.Name
FROM mytable AS t
CROSS APPLY (
SELECT
FieldValue AS Name
FROM mytable AS t2
WHERE t.id = t2.id
AND t2.FieldName = 'Name'
) AS ca
WHERE FieldName IN ('Country','PostCode')
) t
GROUP BY
id
, name
;
also see: https://rextester.com/GKOK78960
result:
+----+----+------+---------+----------+
| | id | Name | Country | Postcode |
+----+----+------+---------+----------+
| 1 | 1 | Jack | Auz | 1234 |
| 2 | 1 | Jhon | Auz | 1234 |
| 3 | 2 | Leo | USA | 2345 |
+----+----+------+---------+----------+
note that using cross apply is a bit like an inner join; if there is no name associated to an id, that id will not be returned by the query above. If there is a need for an id without name use outer apply instead (as this is similar to left join).
As an alternative, you could use pivot like so:
SELECT id, name, [Country], [PostCode]
FROM (
SELECT
t.*, ca.Name
FROM mytable AS t
CROSS APPLY (
SELECT
FieldValue AS Name
FROM mytable AS t2
WHERE t.id = t2.id
AND t2.FieldName = 'Name'
) AS ca
WHERE FieldName IN ('Country','PostCode')
) AS p
PIVOT
(
MAX(FieldValue)
FOR FieldName IN ([Country], [PostCode])
) AS pvt

Related

Reverse col and rows in SQL

I have to create query, to reverse rows and cols correct. I am using MS SQL SERVER 2016.
This is what I have:
Row_ID | Group_ID | Group_Status | MemberRole | name
2807 | 10568 | accept | chairman | Rajah
2808 | 10568 | accept | member | Vaughan
2812 | 10568 | accept | secretary | Susan
This is what I need:
Group_ID | Status | Chairman | Secretary | Member1 | Member2 | Member3 | ... | Member20
10568 | Accept | Rajah | Susan | Vaughan | Kane | Oprah | ... | Imelda
(users with member role can be between 0-20)
Probably I should use pivot, but I have no idea how.
Ok, I have this code:
SELECT *
FROM
(
SELECT group_id,
group_status,
memberRole,
name
FROM DataGroup
) dataSource PIVOT(MAX(name) FOR memberRole IN([chairman],
[secretary],
[member])) pivotTab;
But I losing rows with members (get only one member), how to extract them to columns?
You can try this with a unioned query:
Some mockup (please provide such a dummy table with your sample data yourself in your next question):
DECLARE #mockup TABLE(Row_ID INT,Group_ID INT,Group_Status VARCHAR(100),MemberRole VARCHAR(100),[name] VARCHAR(100));
INSERT INTO #mockup VALUES
(2807,10568,'accept','chairman','Rajah')
,(2808,10568,'accept','member','Vaughan')
,(2812,10568,'accept','secretary','Susan')
,(2899,10568,'accept','member','Onemore');
--The query
SELECT p.*
FROM
(
SELECT Group_ID
,Group_Status
,[name]
,MemberRole
FROM #mockup
WHERE MemberRole IN('chairman','secretary')
UNION ALL
SELECT Group_ID
,Group_Status
,[name]
,CONCAT('Member',ROW_NUMBER() OVER(PARTITION BY Group_ID ORDER BY Row_ID))
FROM #mockup
WHERE MemberRole='member'
) t
PIVOT
(
MAX([name]) FOR MemberRole IN(Chairman,Secretary,Member1,Member2,Member3 /*add as many as you need*/)
) p;
The result
Group_ID Group_Status Chairman Secretary Member1 Member2 Member3
10568 accept Rajah Susan Vaughan Onemore NULL
In short:
The first part of the query will Just pick the two fix names.
The second part will pick the members and number them sorted by their Row_ID.
The PIVOT will then transform this to a single row, using the column MemberRole for the new column names.
You will have to think about some more things:
What if not all the lines are accepted?
What of there are many groups?
If you need help, you can comeback with a new question. Happy Coding!
I would simply use conditional aggregation:
select group_id, group_status,
max(case when member_role = 'chairman' then name end) as chairman,
max(case when member_role = 'secretary' then name end) as secretary,
max(case when member_role = 'member' and seqnum = 1 then name end) as member_01,
max(case when member_role = 'member' and seqnum = 2 then name end) as member_02,
. . .
from (select m.*,
row_number() over (partition by group_id, member_role order by row_id) as seqnum
from #mockup m
) m
group by group_id, group_status;
I find conditional aggregation to be much more flexible than pivot. This is an example of the situation where the query is simpler.

How to create a condition for this case?

Sample Table:
Id |Acc_Code|Description |Balance | Acclevel| Acctype| Exttype|
--- -------- ----------------- |-------- |-------- | -------| -------|
1 |SA |Sales | 0.00 | 1 | SA | |
2 |CS |Cost of Sales | 0.00 | 1 | CS | |
3 |5000/001|Revenue | 94.34 | 2 | SA | |
4 |5000/090|Sales(Local) | 62.83 | 2 | SA | |
5 |7000/000|Manufacturing Acc |-250.80 | 2 | CS | MA |
6 |7000/200|Manufacturing Acc | 178.00 | 2 | CS | |
This is a sample data of a temporary table which would be used to be inserted into another temporary table that would calculate the data for Profit and Loss Statement (For Manufacturing related Accounts only).
In this case, the acc_code for Manufacturing accounts start from 7000/000 and separated/partitioned for each following Exttype.
Eg: We start from the exttype of MA and based on its acclevel (could be 2 or more) until the next exttype.
The idea is we get the manufacturing accounts by SELECT FROM tmp_acc_list WHERE acc_code BETWEEN #start_acc_code (7000/000 in this case) AND #end_acc_code (the data before the next exttype)
I don't know what the exttype is, I'm still learning the tables.
How do we create the #end_acc_code part out from this sample table?
So here is a all in one script.
I created Your table for test:
create table #tmp_acc_list(
Id numeric,
Acc_Code nvarchar(100),
Acclevel numeric,
Acctype nvarchar(100),
Exttype nvarchar(100));
GO
insert into #tmp_acc_list(Id, Acc_Code, Acclevel, Acctype, Exttype)
select 1 , 'SA', 1,'SA', null union all
select 2 , 'CS', 1,'CS', null union all
select 3 , '5000/001', 2,'SA', null union all
select 4 , '5000/090', 2,'SA', null union all
select 5 , '7000/000', 2,'CS', 'MA' union all
select 6 , '7000/200', 2,'CS', null
;
Then comes the query:
with OrderedTable as -- to order the table is Id is not an order
(
select
t.*, ROW_NUMBER() over (
order by id asc --use any ordering You need here
)
as RowNum
from
#tmp_acc_list as t
),
MarkedTable as -- mark with common number
(
select
t.*,
Max(case when t.Exttype is null then null else t.RowNum end)
over (order by t.RowNum) as GroupRownum
from OrderedTable as t
),
GroupedTable as -- add group Exttype
(
select
t.Id, t.Acc_Code, t.Acclevel, t.Acctype, t.Exttype,
max(t.Exttype) over (partition by t.GroupRownum) as GroupExttype
from MarkedTable as t
)
select * from GroupedTable where GroupExttype = 'MA'
Is this what You need?
select *
from
(
select Id, Acc_Code
from tmp_acc_list
where Acc_Code = '7000/000'
) s
cross join tmp_acc_list a
cross apply
(
select top 1 x.Id, x.Acc_Code
from tmp_acc_list x
where x.Id >= a.Id
and x.AccLevel = a.AccLevel
and x.Acctype = a.Acctype
and x.Exttype = ''
order by Id desc
) e
where a.Id between s.Id and e.Id

Rotate columns to rows for joined tables

I have two tables similar to shown below (just leaving out fields for simplicity).
Table lead :
id | fname | lname | email
---------------------------------------------
1 | John | Doe | jd#test.com
2 | Mike | Johnson | mj#test.com
Table leadcustom :
id | leadid | name | value
-------------------------------------------------
1 | 1 | utm_medium | cpc
2 | 1 | utm_term | fall
3 | 1 | subject | business
4 | 2 | utm_medium | display
5 | 2 | utm_term | summer
6 | 2 | month | may
7 | 2 | color | red
I have a database that captures leads for a wide variety of forms that often have many different form fields. The first table gets the basic info that I know is on each form. The second table captures all other forms fields that were sent over so it can really contain a lot of different fields.
What I am trying to do is to do a join where I can grab all fields from lead table along with utm_medium and utm_term from leadcustom table. I don't need any additional fields even if they were sent over.
Desired results :
id | fname | lname | email | utm_medium | utm_term
---------------------------------------------------------------------------
1 | John | Doe | jd#test.com | cpc | fall
2 | Mike | Johnson | mj#test.com | display | summer
Only way I know I could do this is to grab all lead data and then for each record make more calls to get leadcustom data I am looking for but I know there has to me a more efficient way of getting this data.
I appreciate any help with this and it is not something I can change the way I capture that data and table formats.
If your columns are fixed, you can do this with group by + case + max like this:
select
fname,
lname,
email,
max(case when name = 'utm_medium' then value end) as utm_medium,
max(case when name = 'utm_term' then value end) as utm_term
from
lead l
join leadcustom c
on l.id = c.leadid
group by
fname,
lname,
email
The case will assign value from the leadcustom table when it matches the given name, otherwise it will return null, and max will pick take the assigned value if it exists over the null.
You can test this in SQL Fiddle
The other way to do this is to use pivot operator, but that syntax is slightly more complex -- or at least this is more easy for me.
Unless I interpret your question incorrectly - in which case I'm happy to be corrected - you could achieve your goal with a simple left join where you join on ID of the first table:
select ld.*, ldcust.utm_medium, ldcust.utm_term
from lead ld
left join leadcustom ldcust
on ld.id = ldcust.leadid
You can use a cte or a derived table to solve this:
cte:
;with cte as
(
select leadid, [name], [value]
from leadcustom
where name in('utm_medium', 'display')
)
select id, fname, lname, email, [name], [value]
from lead
inner join cte on(id = leadid)
Derived table:
select id, fname, lname, email, [name], [value]
from lead
inner join
(
select leadid, [name], [value]
from leadcustom
where name in('utm_medium', 'display')
) derived on(id = leadid)
and since suslov used JamesZ's fiddle, I will use it too...
declare #t table (Id int,fname varchar(10),lname varchar(10),email varchar(20))
insert into #t(Id,fname,lname,email)values (1,'john','doe','jd#test.com'),(2,'mike','johnson','mj#test.com')
declare #tt table (id int,leadid int,name varchar(10),value varchar(10))
insert into #tt(id,leadid,name,value)values
(1,1,'utm_medium','cpc'),
(2,1,'utm_term','fall'),
(3,1,'subject','business'),
(4,2,'utm_medium','display'),
(5,2,'utm_term','summer'),
(6,2,'month','may'),(7,2,'color','red')
select Id,fname,lname,
email,
[utm_medium],
[utm_term]
from (
select t.Id,
t.fname,
t.lname,
t.email,
tt.name,
tt.value
from #t t JOIN #tt tt
ON t.Id = tt.leadid)R
PIVOT(MAX(value) for name IN([utm_medium],[utm_term]))P
You can try with pivot and join:
select [id]
, [fname]
, [lname]
, [email]
, [utm_medium]
, [utm_term]
from ( select t2.*
, t1.[name]
, t1.[value]
from [leadcustom] t1
join [lead] t2 on t2.[id] = t1.[leadid]
) t
pivot (
max([value])
for [name] in ([utm_medium], [utm_term])
) pt
pivot rotates the joined table-valued expression, by turning the unique values from [value] column in the expression into [utm_medium] and [utm_term] columns in the output, and performs fake aggregation with max function (it works so because a corresponding column can have multiple values for one unique pivoted column, in this case, [name] for [value]).
SQLFiddle

Sql select query based on a column value

I have a Table1 like this:
ApplicableTo IdApplicable
---------------------------
Dept 1
Grade 3
section 1
Designation 2
There other tables like:
tblDept:
ID Name
1 dept1
2 baking
3 other
tblGrade:
ID Name
1 Grd1
2 Manager
3 gr3
tblSection:
id Name
1 Sec1
2 sec2
3 sec3
tblDesignation:
id Name
1 Executive
2 Developer
3 desig3
What I need is a query for table1 in such a way that gives me
ApplicableTo (table1)
Name (from the relevant table based on the value in `ApplicableTo` column)
Is this possible?
Desired Result:
eg: ApplicableTo IdApplicable Name
Dept 1 dept1
grade 3 gr3
Section 1 sec1
Designation 2 Developer.
This is the result I desire.
You could do something like the following so the applicable to becomes part of the JOIN predicate:
SELECT t1.ApplicableTo, t1.IdApplicable, n.Name
FROM Table1 AS t1
INNER JOIN
( SELECT ID, Name, 'Dept' AS ApplicableTo
FROM tblDept
UNION ALL
SELECT ID, Name, 'Grade' AS ApplicableTo
FROM tblGrade
UNION ALL
SELECT ID, Name, 'section' AS ApplicableTo
FROM tblSection
UNION ALL
SELECT ID, Name, 'Designation' AS ApplicableTo
FROM tblDesignation
) AS n
ON n.ID = t1.IdApplicable
AND n.ApplicableTo = t1.ApplicableTo
I would generally advise against this approach, although it may seem like a more consice approach, you would be better having 4 separate nullable columns in your table:
ApplicableTo | IdDept | IdGrade | IdSection | IdDesignation
-------------+--------+---------+-----------+---------------
Dept | 1 | NULL | NULL | NULL
Grade | NULL | 3 | NULL | NULL
section | NULL | NULL | 1 | NULL
Designation | NULL | NULL | NULL | 2
This allows you to use foreign keys to manage your referential integrity properly.
You can use CASE here,
SELECT ApplicableTo,
IdApplicable,
CASE
WHEN ApplicableTo = 'Dept' THEN (SELECT Name FROM tblDept WHERE tblDept.ID = IdApplicable)
WHEN ApplicableTo = 'Grade' THEN (SELECT Name FROM tblGrade WHERE tblGrade.ID = IdApplicable)
WHEN ApplicableTo = 'Section' THEN (SELECT Name FROM tblSection WHERE tblSection.ID = IdApplicable)
WHEN ApplicableTo = 'Designation' THEN (SELECT Name FROM tblDesignation WHERE tblDesignation.ID = IdApplicable)
END AS 'Name'
FROM Table1
The easiest way to achieve that is to add an extra column in table1 to keep the table where id is refferred to. Otherwise you can't know in which table the applicable id is reffered to.
Or you can create the applicable id in a way that you can extract the table afterwords from it for example a1 for id 1 in tblDept. And then use [case] (http://dev.mysql.com/doc/refman/5.0/en/case.html) (for mysql) in order to make the correct Join.

Using single query to retrieve multivalued attribute by joining two tables without causing duplication of fields in mysql

Table 1 :
QUERY: Create table client (
applicationNo int primary key,
name varchar(20)
);
Insert statement: Insert into client values (1,'XYZ'),(1,'ABC'),(1,'DEF');
applicationNo | name
1 | XYZ
2 | ABC
3 | DEF
Table 2:
Query : Create table client (
applicationNo int,
phoneNo Bigint,
foreign key (applicationNo) references client (applicationNo),
primary key(applicationNO,phoneNo)
);
Insert into phoneNO values (1,999999),(1,888888),(2,777777),(3,666666),(3,555555);
applicationNo | phoneNo
1 | 999999
1 | 888888
2 | 777777
3 | 666666
3 | 555555
Can I retrieve the tuples by joining both the tables in such a way that get the following output, but using single query, also I'm using mysql 5.1
applicationNo | name | phoneNo1 | phoneNo2
1 | XYZ | 999999 | 88888
2 | ABC | 77777 | Null
3 | DEF | 66666 | 555555
Edited : extra information
I tried using this something called cross tab .But I'm not able to use the totalPhoneNo inside the case statement
SELECT applicationNo,count(phoneNo) as totalPhoneNo,
SUM(CASE WHEN totalPhoneNo= 1 THEN phoneNO ELSE Null END) AS phoneNo1,
SUM(CASE WHEN totalPhoneNO = 2 THEN phoneNo ELSE Null END) AS phoneNo2
FROM phoneNO GROUP BY applicationNo;
Try:
select c.applicationNo,
max(c.name) name,
max(p.phoneNo) phoneNo1,
case
when max(p.phoneNo) = min(p.phoneNo) then NULL
else min(p.phoneNo)
end phoneNo2
from client c
left join phoneNo p on c.applicationNo = p.applicationNo
group by c.applicationNo
Here is is for MSSQL. Does this convert nicely?
With phones (ApplicationNo, PhoneNo, Instance) as
(Select ApplicationNo, PhoneNo,
Row_Number OVER (Partition By ApplicationNo) as RowNum)
Select client.ApplicationNo, client.Name,
p1.PhoneNo as phoneNo1, p2.PhoneNo as phoneNo2
From client
Left Join phones p1 on client.ApplicationNo=p1.ApplicationNo as p1.RowNum=1
Left Join phones p2 on client.ApplicationNo=p2.ApplicationNo as p2.RowNum=2
use subselects with limit clauses. the first subselect picks the first phone the other the second.
select ApplicationNo,
(SELECT phoneno FROM phones where phones.applicationno=app.ApplicationNo order by phoneno LIMIT 0, 1)as phone1,
(SELECT phoneno FROM phones where phones.applicationno=app.ApplicationNo order by phoneno LIMIT 1, 1)as phone2
from application app