Select one row from multiple rows based on availability - sql

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)

Related

SQL get record when finding in 2 records

Hi I tried to build sql query to find id when is in 2 records (can be more). Let me explained by example
I have 2 tables
C
id
type_id
1
499
1
599
D
type_id
type_name
499
AN
599
DE
And I want to get id which has AN and DE
SELECT *
FROM C
INNER JOIN D
ON D.type_id = C.type_id
WHERE
EXISTS (SELECT 1 FROM D D1 WHERE D1.type_id = C.type_id AND D1.type_name = 'AN') AND
EXISTS (SELECT 1 FROM D D2 WHERE D2.type_id = C.type_id AND D2.type_name = 'DE');
But did not work .Than you for help
If you want all the data from the join then you can use analytic functions:
SELECT id,
type_id,
type_name
FROM (
SELECT c.id,
c.type_id,
d.type_name,
COUNT(CASE d.type_name WHEN 'AN' THEN 1 END) OVER (PARTITION BY c.id)
AS num_an,
COUNT(CASE d.type_name WHEN 'DE' THEN 1 END) OVER (PARTITION BY c.id)
AS num_de
FROM C
INNER JOIN D
ON D.type_id = C.type_id
WHERE d.type_name IN ('AN', 'DE')
)
WHERE num_an > 0
AND num_de > 0;
Which outputs:
ID
TYPE_ID
TYPE_NAME
1
599
DE
1
499
AN
If you just want the id then you can aggregate and use a HAVING clause:
SELECT c.id
FROM C
INNER JOIN D
ON D.type_id = C.type_id
WHERE d.type_name IN ('AN', 'DE')
GROUP BY c.id
HAVING COUNT(CASE d.type_name WHEN 'AN' THEN 1 END) > 0
AND COUNT(CASE d.type_name WHEN 'DE' THEN 1 END) > 0
Which outputs:
ID
1
fiddle
Get the distinct counts of type_name for each ID ensure count = two and limit type_name to 'AN' or 'DE'
SELECT C.ID
FROM C
INNER JOIN D
on C.type_id=D.type_id -- standard join on Type_ID
WHERE D.Type_name in ('AN','DE') -- limit records to only AN/DE since we need both.
GROUP BY C.ID -- group so we get just 1 ID
HAVING Count(Distinct Type_name) = 2 -- ensure distinct count is 2 for each C.ID.
We join the two tables
We limit to ID having either an 'AN' or DE type name
We group by ID's
We count the distinct types for each ID and if it's 2, we know we have an AN and DE type for that ID.
Count distinct is used since I'm unsure if a type_name could be duplicated for a C.ID. It looks possible given table structure. but unsure without known Pk/FK relations. distinct "Might" be able to be removed if we KNOW it's not possible.

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;

Using Sql server with In

I have two tables Tbl_Event (primary key:-EventID) and tbl_items.
tbl_items table contains multiple items for same EventID with status 0 or 1.
Sample Data:
Tbl_event:
EventId Name
5 Test
6 Seminar
7 Meet
tbl_items
ItemId EventId status
1 5 0
2 6 1
2 6 0
3 7 1
3 7 1
I just need to get Event "Meet" having both status value 1.
This is what I tried:
SELECT *
FROM Tbl_items L
INNER JOIN Tbl_Events E ON L.EventId = E.EventId
WHERE L.Eventid NOT IN (SELECT Eventid FROM TBL_VMS_LENT_ITEMS WHERE Status = 0)
One approach is to use a subquery which aggregates over events and checks that all statuses are 1:
WITH cte AS (
SELECT EventId
FROM tbl_items
GROUP BY EventId
HAVING SUM(CASE WHEN status <> 1 THEN 1 ELSE 0 END) = 0
)
SELECT t1.*
FROM Tbl_event t1
INNER JOIN cte t2
ON t1.EventId = t2.EventId;
If you just want the event IDs, use SELECT * FROM cte, otherwise use the full query I gave immediately above. This approach will generalize to more complex logic, should you need it later.
This will give you all events that have all items with status 1.
SELECT
E.EventID
FROM
Tbl_event AS E
INNER JOIN tbl_items AS I ON E.EventID = I.EventID
GROUP BY
E.EventID
HAVING
MIN(I.status) = 1
If you want the full Event record:
;WITH EventsWithItemsStatus1 AS
(
SELECT
E.EventID
FROM
Tbl_event AS E
INNER JOIN tbl_items AS I ON E.EventID = I.EventID
GROUP BY
E.EventID
HAVING
MIN(I.status) = 1
)
SELECT
E.*
FROM
Tbl_event AS E
INNER JOIN EventsWithItemsStatus1 AS N ON E.EventID = N.EventID
Use group by clause with subquery
select * from Tbl_event e
inner join (
select EventId
from tbl_items
where status = 1
group by EventId
having count(*) > 1
) i on i.EventId = e.EventId
Question is not very clear
SELECT min(L.ItemId), L.EventId
FROM Tbl_items L
where L.Status = 1
group by L.EventId
having count(*) > 1

Optimizing SQL query having DISTINCT keyword and functions

I have this query that generates about 40,000 records and the execution time of this query is about 1 minute 30 seconds.
SELECT DISTINCT
a.ID,
a.NAME,
a.DIV,
a.UID,
(select NAME from EMPLOYEE where UID= a.UID and UID<>'') as boss_id,
(select DATE(MAX(create_time)) from XYZ where XYZ_ID= 1 and id = a.ID) as TERM1,
(select DATE(MAX(create_time)) from XYZ where XYZ_ID= 2 and id = a.ID) as TERM2,
(select DATE(MAX(create_time)) from XYZ where XYZ_ID= 3 and id = a.ID) as TERM3,
(select DATE(MAX(create_time)) from XYZ where XYZ_ID= 4 and id = a.ID) as TERM4,
(select DATE(MAX(create_time)) from XYZ where XYZ_ID= 5 and id = a.ID) as TERM5,
(select DATE(MAX(create_time)) from XYZ where XYZ_ID= 6 and id = a.ID) as TERM6,
(select DATE(MAX(create_time)) from XYZ where XYZ_ID= 7 and id = a.ID) as TERM7,
(select DATE(MAX(create_time)) from XYZ where XYZ_ID= 8 and id = a.ID) as TERM8
FROM EMPLOYEE a
WHERE ID LIKE 'D%'
I tried using group by, different kinds of join to improve the execution time but couldn't succeed.Both the tables ABC and XYZ are indexed.
Also, I think that the root cause of this problem is either the DISTINCT keyword or the MAX function.
How can I optimize the above query to bring down the execution time to at least less than a minute?
Any help is appreciated.
Query is not tested, this is just an idea on how you could get this done in two different ways.
(SQL Server solutions here)
Using LEFT JOIN for each ID should look something like this:
SELECT a.ID,
a.NAME,
a.DIV,
a.UID,
b.Name as boss_id,
MAX(xyz1.create_time) as TERM1,
MAX(xyz2.create_time) as TERM2,
MAX(xyz3.create_time) as TERM3,
MAX(xyz4.create_time) as TERM4,
MAX(xyz5.create_time) as TERM5,
MAX(xyz6.create_time) as TERM6,
MAX(xyz7.create_time) as TERM7,
MAX(xyz8.create_time) as TERM8
FROM EMPLOYEE a
JOIN EMPLOYEE b on a.UID = b.UID and b.UID <> ''
LEFT JOIN XYZ xyz1 on a.ID = xyz1.ID and xyz1.XYZ_ID = 1
LEFT JOIN XYZ xyz2 on a.ID = xyz2.ID and xyz1.XYZ_ID = 2
LEFT JOIN XYZ xyz3 on a.ID = xyz3.ID and xyz1.XYZ_ID = 3
LEFT JOIN XYZ xyz4 on a.ID = xyz4.ID and xyz1.XYZ_ID = 4
LEFT JOIN XYZ xyz5 on a.ID = xyz5.ID and xyz1.XYZ_ID = 5
LEFT JOIN XYZ xyz6 on a.ID = xyz6.ID and xyz1.XYZ_ID = 6
LEFT JOIN XYZ xyz7 on a.ID = xyz7.ID and xyz1.XYZ_ID = 7
LEFT JOIN XYZ xyz8 on a.ID = xyz8.ID and xyz1.XYZ_ID = 8
WHERE a.ID LIKE 'D%'
GROUP BY a.ID, a.NAME, a.DIV, a.UID, b.Name
Using PIVOT would look something like this:
select * from (
SELECT DISTINCT
a.ID,
a.NAME,
a.DIV,
a.UID,
b.NAME as boss_id,
xyz.xyz_id,
xyz.create_time
FROM EMPLOYEE a
JOIN EMPLOYEE b on a.UID = b.UID and b.UID <> ''
LEFT JOIN (SELECT DATE(MAX(create_time)) create_time, XYZ_ID, ID
from XYZ
where XYZ_ID between 1 and 8
group by XYZ_ID, ID) xyz on a.ID = xyz1.ID
WHERE a.ID LIKE 'D%') src
PIVOT (
max(create_time) for xyz_id IN (['1'], ['2'], ['3'], ['4'],
['5'], ['6'], ['7'], ['8'])
) PIV
Give it a shot
I would recommend group by and conditional aggregation:
SELECT e.ID, e.NAME, e.DIV, e.UID,
DATE(MAX(CASE WHEN XYZ_ID = 1 THEN create_time END)) as term1,
DATE(MAX(CASE WHEN XYZ_ID = 2 THEN create_time END)) as term2,
DATE(MAX(CASE WHEN XYZ_ID = 3 THEN create_time END)) as term3,
DATE(MAX(CASE WHEN XYZ_ID = 4 THEN create_time END)) as term4,
DATE(MAX(CASE WHEN XYZ_ID = 5 THEN create_time END)) as term5,
DATE(MAX(CASE WHEN XYZ_ID = 6 THEN create_time END)) as term6,
DATE(MAX(CASE WHEN XYZ_ID = 7 THEN create_time END)) as term7,
DATE(MAX(CASE WHEN XYZ_ID = 8 THEN create_time END)) as term8
FROM EMPLOYEE e LEFT JOIN
XYZ
ON xyz.ID = e.id
WHERE e.ID LIKE 'D%'
GROUP BY e.ID, e.NAME, e.DIV, e.UID;
I don't understand the logic for boss_id, so I left that out. This should improve the performance significantly.

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