SQL group table by user with custom column on same row - sql

I have 3 tables
PATIENT:
patientid name comment result
--------------------------------
1 davis test p
2 brown test p
3 mike test p
PHONE TYPE:
phone_id patient_id phone_type
-------------------------------
3324 1 1
3325 1 4
5467 2 1
PHONE DETAILS:
phone_id number
-----------------------
3324 8253322
3325 180040204
5467 5674543
The goal is to add a column on the select statement based on the value of phone type (1 = phone, 4 = fax) and to group the column so that the phone and
fax are on the same line, not alternating.
Desired result:
name phone fax comment result
---------------------------------------------
davis 8253322 180040204 test p
brown 5674543 null test p
mike null null test p
I have this so far
select
a.name, a.comment, a.result,
(CASE WHEN (b.phone_type = 1) THEN c.number ELSE NULL END) AS Phone
(CASE WHEN (b.phone_type = 4) THEN c.number ELSE NULL END) AS Fax
The problem with this is that it doesn't group the values by the user.
name phone fax comment result
-----------------------------------------------
davis 8253322 null test p
davis null 180040204 test p
mike null null test p

If we can assume 1 phone/fax per name.
select a.name
, a.comment
, a.result
, max(CASE WHEN (b.phone_type = 1) THEN c.number ELSE NULL END) AS Phone
, max((CASE WHEN (b.phone_type = 4) THEN c.number ELSE NULL END) AS Fax
FROM...
GROUP BY a.name
, a.comment
, a.result
If not... then break phone and fax into two data sets based the type and generate row number for each set. Then use a full outer join on patentId and that row number.
With Phone as (SELECT phone_id, patient_id, phone_type, PD.Number, row_number() over (partition by Patient_ID order by PD.Number) as RN
FROM Phone_type PT
INNER JOIN Phone_Details PD
ON PT.Phone_ID = PD.Phone_ID
WHERE Phone_Type = 1),
Fax as (SELECT phone_id, patient_id, phone_type, PD.Number, row_number() over (partition by Patient_ID order by PD.Number) as RN
FROM Phone_type PT
INNER JOIN Phone_Details PD
ON PT.Phone_ID = PD.Phone_ID
WHERE Phone_Type = 1)
SELECT *
FROM PHONE P
FULL OUTER JOIN FAX F
on P.Patent_Id =F.PatentID
and P.RN=F.RN

You can use an aggregation function if you are sure you will only ever have one value at most for phone or fax.
select a.name, a.comment, a.result,
Min((CASE WHEN (b.phone_type = 1) THEN c.number ELSE NULL END) AS Phone,
Min((CASE WHEN (b.phone_type = 4) THEN c.number ELSE NULL END)) AS Fax
FROM (...)
GROUP BY a.name, a.comment, a.result

You can use the following SQL statement
WITH patient_phone AS (
SELECT a.name,
a.comment,
a.result,
(CASE WHEN (b.phone_type = 1) THEN c.number ELSE NULL END) AS Phone,
(CASE WHEN (b.phone_type = 4) THEN c.number ELSE NULL END) AS Fax
FROM patient a
LEFT OUTER JOIN phone_type b
ON a.patient_id = b.patient_id
LEFT OUTER JOIN phone_details c
ON b.phone_id = c.phone_id
GROUP BY a.name,
a.comment,
a.result,
b.phone_type,
c.number
)
SELECT name,
MAX(phone) phone,
MAX(fax) fax,
comment,
result
FROM patient_phone
GROUP BY name,
comment,
result
Result
name phone fax comment result
brown 5674543 NULL test p
davis 8253322 180040204 test p
mike NULL NULL test p

Related

Oracle merge two rows as single row with more columns

I have two tables Employee, Employeerows. I have to pull the employee records who has role 2 or 3.
I have the query below.
SELECT
E.ID,
E.NAME,
ER.PHONE,
ER.ADDRESS,
ER.ROLE
FROM
EMPLOYEE E LEFT JOIN EMPLOYEEROWS ER ON E.ID = ER.ID WHERE ER.ROLE_ID IN (2,3)
This returns either 1 or 2 records for each employee
ID NAME PHONE ADDRESS ROLE
1 ABC 9898989 ABC NJ 2
1 ABC 7878787 ABC XJ 3
2 DEF 7898765 DEF NJ 2
But I have to merge two records into one for that employee with phone number and address as separate columns if the employee has 2 records. My result should like this.
ID NAME PHONE ALT_PHONE ADDRESS ALT_ADDESS
1 ABC 9898989 7878787 ABC NJ ABC XJ
2 DEF 7898765 DEF NJ
Please help me with this.
You can use conditional aggregation. But the left join seems superfluous. You only want to use the left join if you want "employees" that have no rows.
It appears from your data that "2" corresponds to the primary address and "3" to the alternate:
select e.id, e.name,
max(case when er.role_id = 2 then er.phone end) as phone,
max(case when er.role_id = 3 then er.phone end) as alt_phone,
max(case when er.role_id = 2 then er.address end) as address,
max(case when er.role_id = 3 then er.address end) as alt_address
from employee e join
employeerows er
on e.id = er.id
where er.role_id in (2, 3)
group by e.id, e.name;
However, if "2" could be missing, then you would phrase this as:
select e.id, e.name,
max(case when seqnum = 2 then er.phone end) as phone,
max(case when seqnum = 3 then er.phone end) as alt_phone,
max(case when seqnum = 2 then er.address end) as address,
max(case when seqnum = 3 then er.address end) as alt_address
from employee e join
(select er.*,
row_number() over (partition by er.id order by er.role_id) as seqnum
from employeerows er
where er.role_id in (2, 3)
) er
on e.id = er.id
group by e.id, e.name;
You can pivot with conditional aggregation:
select e.id, e.name,
max(case when er.role_id = 2 then er.phone end) as phone,
max(case when er.role_id = 3 then er.phone end) as alt_phone,
max(case when er.role_id = 2 then er.address end) as address,
max(case when er.role_id = 3 then er.address end) as alt_address
from employee e
left join employeerows er on e.id = er.id where er.role_id in (2,3)
group by e.id, e.name
You can use conditional aggregation. But your query is not doing LEFT OUTER JOIN but it is INNER JOIN as you have used the er.role_id in (2,3) in WHERE clause.
Use the following aggreagation technique to fetch the desired result:
SELECT
E.ID,
E.NAME,
MIN(ER.PHONE),
CASE WHEN MIN(ER.PHONE) <> MAX(ER.PHONE) THEN MAX(ER.PHONE) END AS ALT_PHONE,
MIN(ER.ADDRESS),
CASE WHEN MIN(ER.ADDRESS) <> MAX(ER.ADDRESS) THEN MAX(ER.ADDRESS) END AS ALT_ADDRESS
FROM EMPLOYEE E
LEFT JOIN EMPLOYEEROWS ER ON E.ID = ER.ID
AND ER.ROLE_ID IN (2,3) -- added it in the join condition
GROUP BY E.ID, E.NAME;

Pivot two columns

I have data that looks likes:
stu_id
course_name
staff_name
1
Economics - 3
Kuzma, Brian
1
History
Hulings, Kreg
1
IHS IB Lit of Americ
Duncan, Amy
2
Marine Biology A
Crews, Erin
2
Pre-Calculus
Soderholm, Lodi
2
Environ & Wld Iss
Haberman, Helen
I am trying format it as such using SQL, so that each student's data is all in one row:
stu_id
course1
staff1
course2
staff2
etc ...
1
Economics - 3
Kuzma, Brian
History
Hulings, Kreg
etc ...
2
Marine Biology A
Crews, Erin
Pre-Calculus
Soderholm, Lodi
etc ...
Each student can have up to 6 courses and associated staff names all pivoted to a single row.
The basic query is:
SELECT dtbl_students.student_id stu_id,
course_name,
staff_name
FROM k12intel_dw.ftbl_student_schedules
INNER JOIN k12intel_dw.dtbl_students WITH (nolock)
ON ftbl_student_schedules.student_key = dtbl_students.student_key
INNER JOIN k12intel_dw.dtbl_staff WITH (nolock)
ON ftbl_student_schedules.staff_key = dtbl_staff.staff_key
INNER JOIN k12intel_dw.dtbl_courses WITH (nolock)
ON ftbl_student_schedules.course_key = dtbl_courses.course_key
INNER JOIN k12intel_dw.dtbl_schools WITH (nolock)
ON ftbl_student_schedules.school_key = dtbl_schools.school_key
INNER JOIN k12intel_dw.dtbl_school_dates period_date WITH (nolock)
ON ftbl_student_schedules.school_dates_key =
period_date.school_dates_key
WHERE local_school_year = '2019-2020'
AND local_semester = 3
I am attempting to pivot on course_name and staff_name.
I have managed to UNPIVOT as such:
WITH Courses
AS (SELECT dtbl_students.student_id stu_id,
course_name,
staff_name
FROM K12intel_dw.FTBL_STUDENT_SCHEDULES
INNER JOIN K12intel_dw.DTBL_STUDENTS WITH (Nolock)
ON FTBL_STUDENT_SCHEDULES.STUDENT_KEY =
DTBL_STUDENTS.STUDENT_KEY
INNER JOIN K12intel_dw.DTBL_staff WITH (Nolock)
ON FTBL_STUDENT_SCHEDULES.staff_KEY =
DTBL_staff.staff_KEY
INNER JOIN K12intel_dw.DTBL_COURSES WITH (Nolock)
ON FTBL_STUDENT_SCHEDULES.COURSE_KEY =
DTBL_COURSES.COURSE_KEY
INNER JOIN K12intel_dw.DTBL_SCHOOLS WITH (Nolock)
ON FTBL_STUDENT_SCHEDULES.SCHOOL_KEY =
DTBL_SCHOOLS.SCHOOL_KEY
INNER JOIN K12intel_dw.DTBL_SCHOOL_DATES period_date WITH (
Nolock)
ON FTBL_STUDENT_SCHEDULES.SCHOOL_DATES_KEY =
period_date.SCHOOL_DATES_KEY
WHERE local_school_year = '2019-2020'
AND local_semester = 3)
SELECT *
FROM Courses
UNPIVOT ( Course
FOR Value IN ([course_name],
[staff_name]) ) unpiv
Which produces something like:
stu_id
course
value
1
IHS IB Economics - 3
course_name
1
Kuzma, Brian
staff_name
1
IHS IB History of th
course_name
1
Hulings, Kreg
staff_name
I have been experimenting with an unpivot then pivot or two pivots, using all the examples I can find on stack Overflow and elsewhere, but have had no success.
If you have a know or maximum number of pairs AND you want to try the PIVOT
Example
Select *
From (
Select A.stu_id
,B.*
From (
Select *
,RN = row_number() over (partition by stu_id order by course_name)
from YourTable
) A
Cross Apply ( values (concat('course_',RN),course_name)
,(concat('staff_',RN),staff_name)
) B(Item,Value)
) src
Pivot (max(Value) for Item in ([Course_1],[Staff_1],[Course_2],[Staff_2],[Course_3],[Staff_3] )) pvt
Returns
I would suggest row_number() to enumerate the columns, then conditional aggregation:
select
stu_id,
max(case when rn = 1 then course_name end) course1,
max(case when rn = 1 then staff_name end) staff1,
max(case when rn = 2 then course_name end) course2,
max(case when rn = 2 then staff_name end) staff2,
...
max(case when rn = 6 then course_name end) course6,
max(case when rn = 6 then staff_name end) staff6
from (
select t.*, row_number() over(partition by stu_id order by course_name) rn
from mytable t
) t
group by stu_id
This gives you 6 pairs of columns for each stu_id, with the corresponding course and staff names. Courses are sorted alphabetically over the columns. If a student has less than 6 courses, the final columns will be empty.
I am quite unclear on how your query and your sample data relate. This answer is based on the sample data and expected results.

How to display a ROW based on value in CASE STATEMENT

I have a query as below, and want to display a row only if the value is 1 using CASE. Please can you advice how I can do that
SELECT DISTINCT
a.AccountID,
a.ForeName,
a.Surname,
a.Gender,
CASE
WHEN B.Value = '1145' THEN '1'
WHEN B.Value = '1007' THEN '2' ELSE '0'
END AS Value,
b.Address,
b.Town
FROM
Customer a
LEFT OUTER JOIN
AdditionalDetails b
ON
b.ID = a.AccountID
The result I am getting:
AccountID ForeName Surname Gender NoName Address Town
00012 Eric Manse Male 0 Porto Porto
00013 Peter Mark Male 0 Porto Porto
00014 Tom Jerry Male 0 Porto Porto
00014 Tom Jerry Male 1 Porto Porto
00015 Sarah Parker Female 0 Porto Porto
00015 Sarah Parker Female 1 Porto Porto
If there is a 1 in the CASE statement, it should not display the 0 just the row with the value 1
I speculate that you want either MAX() or MIN():
SELECT c.AccountID, c.ForeName, c.Surname, c.Gender,
MAX(CASE WHEN ad.Value = '1145' THEN '1'
WHEN ad.Value = '1007' THEN '2'
ELSE '0'
END),
ad.Address, ad.Town
FROM Customer c LEFT OUTER JOIN
AdditionalDetails ad
ON c.ID = ad.ID
GROUP BY c.AccountID, c.ForeName, c.Surname, c.Gender, ad.Address, ad.Town;
EDIT:
You seem to want prioritization:
SELECT cad.*
FROM (SELECT c.AccountID, c.ForeName, c.Surname, c.Gender,
ad.Address, ad.Town,
ROW_NUMBER() OVER (PARTITION BY c.ACCOUNTID
ORDER BY (CASE WHEN ad.Value = '1145' THEN 1
WHEN ad.Value = '1007' THEN 2
ELSE 0'
END) DESC
) as seqnum
FROM Customer c LEFT OUTER JOIN
AdditionalDetails ad
ON c.ID = ad.ID
) cad
WHERE seqnum = 1;
You have altered your question. You are not looking for distinct rows, but you want to rank the rows and only display best matches.
Depending on your exact requirements you'd use RANK or ROW_NUMBER with an appropriate ORDER BY and PARTITION BY clause for this.
For instance:
select c.*, ad.address, ad.town
from customer c
left join
(
select
address,
town,
customer_id,
rank() over (partition by customer_id
order by case value when 1145 then 1 when 1007 then 2 else 0 end desc) as rnk
from additionaldetails
) ad on ad.customer_id = c.id and d.rnk = 1;
you could try like below
with cte as (
SELECT a.AccountID, a.ForeName, a.Surname, a.Gender,
b.Address, b.Town,
row_number() over(partition by a.AccountID
order by
(CASE WHEN b.Value = '1145' THEN 1 WHEN b.Value = '1007' THEN 2 ELSE 0
END) desc) as val
FROM Customer a
LEFT OUTER JOIN AdditionalDetails b
ON b.ID = a.AccountID
) select * from cte where val=1

Select one row from multiple rows based on availability

I have 3 tables:
Emp (Id(PK), Name)
Address (AddressId(PK), AddressType)
EmpAddress (EmpId(FK), AddresId(FK))
One employee may have multiple address.
Sample data:
Emp
1 abc
2 pqr
Address
1 a
2 b
3 c
EmpAddress
1 1
1 2
1 3
Here empid 1 has all 3 addresses.
I want the only one address at a time based on availability.
If adresstype a is available then display only a
If adresstype c is available then display only c
If adresstype b is available then display only b
Priority is a->c->b
If only one available then display that without any Priority .
I wrote this query, but it is not working:
select *
from Emp
inner join EmpAddress on Emp.Id = .EmpAddress .Emp
inner join Address on Address.Id = EmpAddress.Address_Id
where AddressType is NOT NULL
and AddressType = case
when AddressType = 'a' then 'a'
when AddressType = 'c' then 'c'
when AddressType = 'b' then 'b'
end
One approach would be to use ROW_NUMBER() to assign a numerical priority to each address type based on the ordering a > c > b. Then, subquery to retain only the highest ranking address for each employee.
SELECT Id, Emp, AddressType
FROM
(
SELECT e.Id, ea.Emp, a.AddressType,
ROW_NUMBER() OVER (PARTITION BY e.Id
ORDER BY CASE WHEN a.AddressType = 'a' THEN 1
WHEN a.AddressType = 'b' THEN 2
WHEN a.AddressType = 'c' THEN 3 END) rn
FROM Emp e
INNER JOIN EmpAddress ea
ON e.Id = ea.Emp
INNER JOIN Address a
ON a.Id = ea.Address_Id
) t
WHERE t.rn = 1;
select *
from Emp e
cross apply
(
select top 1 ea.AddresId, a.AddressType
from EmpAddress ea
inner join Address a on ea.AddresId = a.AddresId
where ea.EmpId = e.Id
order by case a.AddressType
when 'a' then 1
when 'c' then 2
when 'b' then 3
end
) a
You could also retrieve the records based on Priority via TOP(1) with TIES
SELECT
TOP(1) with TIES e.Id, *
FROM Emp e
INNER JOIN EmpAddress ea ON e.Id = ea.Emp
INNER JOIN Address a ON a.Id = ea.Address_Id
ORDER BY ROW_NUMBER() OVER (PARTITION BY e.Id ORDER BY
CASE (a.AddressType) WHEN 'a' THEN 1
WHEN 'c' THEN 2 WHEN 'b' THEN 3 END)

Join one to many get one row, by priority

I have a contacts table:
ID NAME
--- ----
1 KK
2 JKI
3 HU
And I have a phone table:
ID ContactID Phone Type
--- --------- ----- --------
1 1 569 Business
2 1 896 Mobile
3 1 258 Fax
4 2 369 Mobile
5 3 124 Fax
6 2 496 Fax
I want to get all contacts with at least one phone number. The phone number to be displayed should be Business, if there are no Busniess Type available, then Mobile, if there are no Mobile type available then Fax else null
Sample Result:
ID NAME PHONE
--- ------ ------
1 KK 569 -- Business present
2 JKI 369 -- Business not present but mobile present
3 HU 124 -- only fax present
;WITH [prior](i,t) AS
(
SELECT 1, 'Business'
UNION ALL SELECT 2, 'Mobile'
UNION ALL SELECT 3, 'Fax'
),
x AS
(
SELECT c.ID, c.Name, p.Phone,
rn = ROW_NUMBER() OVER (PARTITION BY c.ID ORDER BY r.i)
FROM dbo.Contacts AS c
LEFT OUTER JOIN dbo.Phone AS p
ON p.ContactID = c.ID
LEFT OUTER JOIN [prior] AS r
ON r.t = p.[Type]
)
SELECT ID, Name, Phone FROM x
WHERE rn = 1;
If you want to eliminate contacts with no phones, just change both instances of LEFT OUTER to INNER.
select c.ID
, c.Name
, coalesce(business.Phone, mobile.Phone, fax.Phone) as Phone
from Contacts c
left join
Phone business
on business.ContactID = c.ID
and business.type = 'Business'
left join
Phone mobile
on mobile.ContactID = c.ID
and mobile.type = 'Mobile'
left join
Phone fax
on fax.ContactID = c.ID
and fax.type = 'Fax'
where coalesce(business.Phone, mobile.Phone, fax.Phone) is not null
Your data model is not great for querying this efficiently, but this may do the trick:
SELECT C.ID, C.Name, COALESCE(
(SELECT TOP 1 P.Phone FROM Phones P WHERE P.ContactID = C.ID AND P.[Type] = 'Business'),
(SELECT TOP 1 P.Phone FROM Phones P WHERE P.ContactID = C.ID AND P.[Type] = 'Mobile'),
(SELECT TOP 1 P.Phone FROM Phones P WHERE P.ContactID = C.ID AND P.[Type] = 'Fax')
) Phone
FROM Contacts C
The most compact solution I can think of is,
; WITH CTE AS (
SELECT
c.ID, c.NAME, p.Phone
, r = ROW_NUMBER()OVER(PARTITION BY c.ID ORDER BY CASE p.[TYPE] WHEN 'Business' THEN 1 WHEN 'Mobile' THEN 2 ELSE 3 END)
FROM Contacts c
LEFT JOIN Phone p ON p.ContactID = c.ID AND p.[TYPE] IN ('Business','Mobile','Fax')
WHERE EXISTS(SELECT 1 FROM Phone WHERE ContactID = c.ID)
)
SELECT ID, NAME, Phone
FROM CTE
WHERE r = 1;
This solution returns,
Contacts with the first matching phone in the specified order
Contact with NULL phone # if a phone # exists but none of the specified types
No result for Contacts having no phone at all