SQL - How to select rows which have the same multiple values - sql

I'll start with a simplified example of my table:
+-----+----------+
|Name |Teaches |
+-----+----------+
|Dave |Science |
+-----+----------+
|Dave |History |
+-----+----------+
|Alice|History |
+-----+----------+
|Alice|Literature|
+-----+----------+
|Alice|Science |
+-----+----------+
|John |History |
+-----+----------+
I'm trying to select those people who also teach the same classes as Dave. (In this case, Alice). I'm thinking of using a cursor to go through Dave's courses and selecting those people who teach the same course and intersecting the results, but I'd like to know if there is a better (simpler) way.

Here is one method:
select t.name
from t join
t td
on td.teaches = t.teaches
where td.name = 'Dave'
group by t.name
having count(*) = (select count(*) from t where t.name = 'Dave');

You need to use Self join, something like this
SELECT a.NAME
FROM Table1 a
INNER JOIN (SELECT Teaches,
Count(*)OVER() AS cnt
FROM Table1
WHERE NAME = 'Dave') b
ON a.Teaches = b.Teaches
WHERE a.NAME <> 'Dave'
GROUP BY a.NAME,
b.cnt
HAVING Count(*) = b.cnt

One method, used CTE here.
;WITH CTE_Parent
AS (
SELECT Name,Teaches,COUNT(*) OVER() AS Parent_Count
FROM #Teachers
WHERE Name = 'Dave'
)
SELECT T.Name
FROM #Teachers AS T
INNER JOIN CTE_Parent AS C ON C.Name <> T.Name
AND C.Teaches= T.Teaches
GROUP BY T.Name,C.Parent_Count
HAVING COUNT(*) = C.Parent_Count

Related

How to avoid joining the same table multiple times

In the simplified example:
The idea is to get all (player, coach, and ref) names into the final query, but the only way I can think to do that is to join 3 times on the respective id. What is a better way?
Team
...|Coachid | Playerid | Refid|
--------------------------
...| 98 | 23 | 77 |
Name
Id | Name |
--------------------
98 | Andy |
23 | Charlie |
SELECT [t].[Id],
[t].[TeamName],
[c].[Name] AS CoachName,
[p].[Name] AS PlayeName,
[r].[Name] as RefName
FROM Team [t]
JOIN Name [c]
ON c.id = t.Coachid
JOIN Name [p]
ON p.id = t.PlayerId
JOIN Name [r]
ON r.id = t.RefId
As it has been commented, your approach is the best way to adress your case and should have good performance.
Alternatives would include:
1) A series of correlated subqueries - this is OK because there is just one value to return per relation:
SELECT
t.Id,
t.TeamName,
(SELECT n.Name FROM AS Name n WHERE n.id = t.CoachId) CoachName,
(SELECT n.Name FROM AS Name n WHERE n.id = t.PlayerId) PlayerName,
(SELECT n.Name FROM AS Name n WHERE n.id = t.RefId) RefName
FROM Team t
2) Conditional aggregation - makes the query more cumbersome:
SELECT
t.Id,
t.TeamName,
MAX(CASE WHEN n.id = t.CoachId THEN n.Name END) CoachName,
MAX(CASE WHEN n.id = t.PlayerId THEN n.Name END) PlayerName,
MAX(CASE WHEN n.id = t.RefId THEN n.Name END) RefName
FROM Team t
INNER JOIN Name n ON n.id IN (t.CoachId, t.PlayerId, t.RefId)
GROUP BY t.Id, t.TeamName
With the way your table is structured, the way you provided is the best way to do it.
It is odd though, the way you have structured the Team table.
You can simplify the whole query just to one join, if you had made your Team table just have the id's of each person, and in your Name table, you would have the id and the role / position of that id.
I guess this is a good example of how important it is to structure your tables correctly.

Is it possible to replace content from multiple foreign keys when doing a query?

I have the following tables:
TABLE PLAYER
id | name
1 | A
2 | B
3 | C
4 | D
TABLE PAIRINGS
id | player_a | player_b
1 | 3 |1
2 | 2 |4
Both columns in table Pairings are foreign keys to column id of table player.
My problem is, I would like to avoid making a query from code every time I want to know which is my player's name (like, Select name From Player Where Id = pairings.player_a). I have thought about adding Name as an extra columnd to Pairings table, but that would denormalize it.
Instead, it would be nice if I could get the names in just one query, like:
RESULT
player_a | player_b
C | A
B | D
Is it this possible? Thanks so much in advance.
You may join the PAIRINGS table to the PLAYER table, twice:
SELECT
p1.name AS player_a,
p2.name AS player_b
FROM PAIRINGS pr
INNER JOIN PLAYER p1
ON pr.player_a = p1.id
INNER JOIN PLAYER p2
ON pr.player_b = p2.id;
Demo
Don't do it! One of the points of using a relational database is that data is stored in only one place. That is a big convenience. Of course, there are exceptions, but these exceptions should have firm reasons.
In this case, just define a view:
CREATE VIEW vw_pairings AS
SELECT p.*, pa.name AS player_a_name,
pb.name AS player_b_name
FROM PAIRINGS p JOIN
PLAYER pa
ON p.player_a = pa.id JOIN
PLAYER pb
ON p.player_b = pb.id;
When you query from the view, you will see the names, along with all the other information in the PAIRINGS table.
Hope can help you
Select * Into #PLAYER From (
Select 1 [ID], 'A' [Name] Union All
Select 2 [ID], 'B' [Name] Union All
Select 3 [ID], 'C' [Name] Union All
Select 4 [ID], 'D' [Name]
) A
Select * Into #PAIRINGS From (
Select 1 [ID], 3 [PLAYER_A], 1 [PLAYER_B] Union All
Select 2 [ID], 2 [PLAYER_A], 4 [PLAYER_B]
) A
Select
P.ID, A.NAME, B.NAME
From #PAIRINGS P
Left Join #PLAYER A On A.ID = P.PLAYER_A
Left Join #PLAYER B On B.ID = P.PLAYER_B
You can create view, for avoid making query
Example
Create View vwPAIRINGS As
Select
P.ID, A.NAME, B.NAME
From #PAIRINGS P
Left Join #PLAYER A On A.ID = P.PLAYER_A
Left Join #PLAYER B On B.ID = P.PLAYER_B
After that, just select usual
Select * from vwPAIRINGS

How to use LISTAGG to concatenate from multiple rows?

I have report query along these lines in APEX 5.0:
WITH inner_table AS
( select distinct
i.ID
,i.name
,i.lastname
,case i.gender
when 'm' then 'Male'
when 'f' then 'Female'
end gender
,i.username
,b.name region
,i.address
,i.city city
,i.EMAIL
,r.name as "ROLE"
,ie.address as "region_location"
,case
when i.gender='m' THEN 'blue'
when i.gender='f' THEN '#F6358A'
END i_color
,b.course as COURSE
,si.city UNIVERSITY
,case
when i.id in (select app_user from scholarship) then 'check'
else 'close'
end as scholarship,
case
when i.id in (select ieur.app_user from ie_user_role ieur where role=4) then 'Admin'
else ''
end admin,
apex_item.checkbox(10, i.id, 'UNCHECKED onclick="highlightRow(this);"') as Del_usr
from app_users i left join regions b on (i.region=b.id)
left join ie_user_role ur on (i.id = ur.app_user)
left join ie_roles r on(ur.role = r.id)
left join user_house uh on (i.id=uh.app_user)
left join reg_location ie on (uh.house=ie.id)
left join study_list sl on i.id = sl.insan
left join study_institute si on sl.institute = si.id
left join course c on sl.course = c.id
where i.is_active='Y'
order by
i.name,i.lastname,i.username,region, city, i.EMAIL)
SELECT * FROM inner_table where (scholarship = :P5_SCHOLARSHIP or :P5_SCHOLARSHIP is null)
I might get results like this:
|---------------------|------------------|-------|------------------|
| Name | Lastname | ... | Course |
|---------------------|------------------|-------|------------------|
| Some | User | ... | Course1 |
|---------------------|------------------|-------|------------------|
| Some | User | ... | Course2 |
|---------------------|------------------|-------|------------------|
But I would like to achieve enlisted courses in same row, that was repeating previously, so:
|---------------------|------------------|-------|------------------|
| Name | Lastname | ... | Course |
|---------------------|------------------|-------|------------------|
| Some | User | ... | Course1, Course2 |
|---------------------|------------------|-------|------------------|
I tried using LISTAGG, and I didn't note down my attempts, so unfortunately I can't post that now. I basically tried:
,LISTAGG(b.course, ', ') within group (order by b.course) as COURSE
Then adding GROUP BY using COURSE, but in that case whole query is affected by GROUP BY and I have to apply other columns correctly, right? Otherwise its resulting in "ORA-00937: not a single-group group function". I got lost a bit there.
Other thing I tried is using a subquery table with same LISTAGG line above, and got wanted output from subquery, but then joining to the rest of the query didn't provide expected results.
I think I could use a bit of SQL help here for LISTAGG when joining multiple tables.
Thanks.
When you use an aggregate function (that collapses multiple rows into one) you need a GROUP BY clause, so you'd need something like this:
SELECT i.username,
LISTAGG( c.course, ', ' ) WITHIN GROUP ORDER BY ( c.course )
FROM app_users i
...
LEFT JOIN course c on sl.course = c.id
GROUP BY i.username
Basically, anything that's not being aggregated, needs to be in the GROUP BY clause. Try it in a much simpler query until you get the hang of it, then make your big one.
What you want is LISTAGG with an analytical window function. Then remove duplicates using distinct. Here is my sample result/ data: http://sqlfiddle.com/#!4/6e8e3f/3
Select DISTINCT name, last_name, other columns,
LISTAGG(course, ', ') WITHIN GROUP (ORDER BY course)
OVER (PARTITION BY name, last_name) as "Course"
FROM inner_table;

Joining tables when row matches multiple rows in other table

I want to join 3 tables (as presented below), but I can't figure out how to do it:
Table 1
vendor_id
---------
1
2
3
4
Table 2
cuisine_type | vendor_id
------------------------
a |1
b |1
c |1
Table 3
cuisine_type|cuisine
--------------------
a |pizza
b |rice
c |steak
I would like to join the 3 tables and get this:
vendor_id|cuisine_type|cuisine
------------------------------
1 |a, b, c |pizza, steak, rice
2.......
I hope this makes sense. I'm very new to postgreSQL so maybe I'm missing something very easy/obvious.
This is a simple join and an aggregation based on the vendor_id:
select v.vendor_id,
string_agg(ct.cuisine_type, ', ') as cuisine_type,
string_agg(c.cuisine, ', ') as cuisine
from table_1 as v
join table_2 as ct on v.vendor_id = ct.vendor_id
join table_3 as c on ct.cuisine_type = c.cuisine_type
group by v.vendor_id;
Your table structure looks a bit strange. For instance, why does each cuisine type have only a single cuisine? Normally, I would expect the vendor to be connected to the cuisines, and then each cuisine to have a type.
However, that is not this question. With the data you have provided, you can use string_agg():
select v.vendor_id, string_agg(cuisine_type, ', ') as cuisine_types,
string_agg(cuisine, ', ') as cuisines
from table1 v left join
table2 v2c
on v.vendor_id = v2c.vendor_id left join
table3 c
on c.cuisine_type = v2c.cuisine_type
group by v.vendor_id;

Limiting Results on Join Query

Say I have the following tables:
|RefNumber| Charge| IssueDate|
------------------------------
| 00001| 40.0|2009-01-01|
| 00002| 40.0|2009-06-21|
|ID|RefNumber|Forename| Surname|
---------------------------------
1| 00001| Joe| Blogs|
2| 00001| David| Jones|
3| 00002| John| Smith|
4| 00002| Paul| Walsh|
I would like to select refnumber, charge and issuedate from the first table then join on refnumber to the second table to retrieve forename and surname but only get the row with the highest id.
So results would look like:
|RefNumber| Charge| IssueDate|ID|Forename| Surname|
-----------------------------------------------------
| 00001| 40.0|2009-01-01| 2| David| Jones|
| 00002| 40.0|2009-06-21| 4| Paul| Walsh|
I am unsure who to limit results on the join to only return the record with the highest ID from the second table.
The most flexible way to write this, which doesn't require a correlated subquery, is to use ROW_NUMBER (SQL Server 2005+ only):
;WITH Names_CTE AS
(
SELECT
ID, RefNumber, Forename, Surname,
ROW_NUMBER() OVER (PARTITION BY RefNumber ORDER BY ID) AS RowNum
FROM Names
)
SELECT o.RefNumber, o.Charge, o.IssueDate, n.Forename, n.Surname
FROM Orders o
[INNER|LEFT] JOIN Names_CTE n
ON n.RefNumber = o.RefNumber
WHERE n.RowNum = 1
Note that ROW_NUMBER isn't always the most efficient if you can use MIN/MAX instead, just the easiest to write.
If you're running SQL 2000, or this isn't efficient enough, you can try a MIN or MAX query:
SELECT o.RefNumber, o.Charge, o.IssueDate, n.Forename, n.Surname
FROM Orders o
[INNER|LEFT] JOIN
(
SELECT RefNumber, MIN(ID) AS MinID
FROM Names
GROUP BY RefNumber
) m
ON m.RefNumber = o.RefNumber
[INNER|LEFT] JOIN Names n
ON n.ID = m.MinID
Sometimes this is actually faster, it depends a lot on the indexing strategy used.
(Edit - this gets rows with the lowest ID, which in most cases will be faster than getting the highest ID. If you need the highest, change the first query to ORDER BY ID DESC and the second query to use MAX instead of MIN).
I'd probably join against a subquery that returns only record from the second table with the highest id.
select a.RefNumber, a.Charge, a.IssueDate, b.ID, b.Forename, b.Surname
from References a inner join
(select ID, RefNumber, ForeName, Surname from Names n1
where n1.ID = (select top 1 n2.ID from Names n2 where n1.RefNumber = n2.RefNumber) ) b
on a.RefNumber = b.RefNumber
Actually your subquery would need to select the highest ID for EACH refnumber, so that would look more like this:
select
a.RefNumber, a.Charge, a.IssueDate, b.BiggestID, b.Forename, b.Surname
from References a
inner join
(select
RefNumber,
max(ID) as BiggestID
from Names
group by
RefNumber) b
on a.RefNumber = b.RefNumber
Hope that helps.
-Tom
select nt.RefNumber, ct.Charge, ct.IssueDate, nt.ID, nt.Forename, nt.Surname
from NameTable nt
join ChargeTable ct on (ct.RefNumber = nt.RefNumber)
where nt.ID = (select MAX(nt2.id)
from NameTable nt2
where nt2.RefNumber = nt.RefNumber)
order by nt.ID