How to avoid joining the same table multiple times - sql

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.

Related

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;

How to use all records from another table as counting columns?

I have 4 tables:
location:
location_id name
------------------------
1 France
device:
device_id location_id model_id
-------------------------------------
1 1 1
2 1 2
3 1 3
model:
model_id family_id name
-------------------------------------
1 1 C-max
2 1 S-max
3 2 Vectra
and family:
family_id name
---------------------
1 Ford
2 Opel
I need to build a complicated SQL query now. As the result, I would like to receive this:
location_id name Ford Opel
------------------------------------------
1 France 2 1
Is it possible to do it in SQL at all? I see there there problems:
About using other table records as columns in the query
About nested tables
About counting the elements (count function?)
Any comments/reference materials will be for me helpful. I do not await the final code.
In SQL queries the columns are fix. You get more or less rows depending on data, not columns. But that doesn't matter, because SQL is about to get data not to display it. The latter is a task for the GUI layer.
So get the desired data, which is the number of models per location and family mainly.
select l.location_id, l.name as location_name, f.name as family_name, count(*) as models
from location l
join device d on d.location_id = l.location_id
join model m on m.model_id = d.model_id
join family f on f.family_id = m.family_id
group by l.location_id, l.name, f.name
order by l.location_id, l.name, f.name;
This is all you need from the database. How to show the data is a task for your programm, a Delphi app in your case. So use Delphi to read the data with above query and fill your grid in a simple loop.
Thank you all for your helpful tips.
I solved my problem using the static method and the code published by #Matt. Because somebody else may looking for the solution, I paste here my working query for PostgreSQL:
SELECT DISTINCT t.location_id, t.name, SUM(t.ford) AS ford, SUM(t.opel) as opel
FROM(
SELECT l.location_id, l.name,
(SELECT COUNT(m.family_id) WHERE m.family_id = '1') AS ford,
(SELECT COUNT(m.family_id) WHERE m.family_id = '2') AS opel
FROM location l
INNER JOIN device d ON l.location_id = d.location_id
INNER JOIN model m ON d.model_id = m.model_id
INNER JOIN family f ON m.family_id = f.family_id
GROUP BY l.location_id, l.name, m.family_id
) t
GROUP BY t.location_id;

SQL Self-join, Not Exists, Or Something Else?

many-to-many name and role table --
create table t (name varchar, role varchar) ;
insert into t (name, role) values ('joe', 'husband'), ('joe', 'father'),
('tom', 'husband'), ('neo', 'bachelor') ;
> select * from t;
name | role
------+----------
joe | husband
joe | father
tom | husband
neo | bachelor
need to convert into mapping of name and the role(s) he does not have --
not_a | name
---------+-----------
husband | neo
father | tom
father | neo
bachelor | joe
bachelor | tom
How to achieve that in true SQL without iterating through each role/name?
To get roles that someone doesn't have is a little complicated. You have to generate all pairs of names and roles and then pick out the ones that don't exist. This uses a left outer join.
The following is standard SQL for doing this:
select r.role as not_a, n.name
from (select distinct name from t) n cross join
(select distinct role from t) r left outer join
t
on t.name = n.name and t.role = r.role
where t.name is null;
As a note: never use varchar() without a length when defining variables and columns. The default values may not do what you expect.
Assuming you only have this table you can use:
SELECT r.role AS not_a, n.Name
FROM (SELECT DISTINCT Name FROM T) AS n
CROSS JOIN (SELECT DISTINCT Role FROM T) AS r
WHERE NOT EXISTS
( SELECT 1
FROM t
WHERE t.Name = n.Name
AND t.Role = r.Role
);
Example on SQL Fiddle
The main query will generate all pairs of names/roles, then the not exists will exlcude all the pairs that already exist.
If you actually have a name and role table, then you can replace the subqueries with the actual tables:
SELECT r.role AS not_a, n.Name
FROM Names AS n
CROSS JOIN Roles AS r
WHERE NOT EXISTS
( SELECT 1
FROM t
WHERE t.Name = n.Name
AND t.Role = r.Role
);
You haven't specified a DBMS, so if you are using MySQL, using LEFT JOIN\IS NULL will perform better than NOT EXISTS
SELECT r.role AS not_a, n.Name
FROM (SELECT DISTINCT Name FROM T) AS n
CROSS JOIN (SELECT DISTINCT Role FROM T) AS r
LEFT JOIN t
ON t.Name = n.Name
AND t.Role = r.Role
WHERE t.Name IS NULL;
I am also assuming it was just a demo, but in your table DDL you have used VARCHAR without a length which is not a good idea at all

Query on table joined with itself

Today I have a final Exam. I approved, happily :D but one of the problems is really blowing my mind.
I need help, so I can rest in peace.
THE PROBLEM
We have a table "People"
(PK)id | name | fatherID
---------------------
1 | gon | 2
2 | cesar| 6
3 | luz | 2
4 | maria| 5
5 | diego| 6
6 | john | -
this is only an example of data.
This table has a relation with itself, on table fatherId(FK) with table id(PK)
I need to do a query that show me 2 columns, in one the name of a person, and in the another one, his/her cousin.
Pretty simple until here, right?
The problem is that I have some restrictions
ONLY ANSI allowed. NO T-sql, or another one. Also, ANSI 99 standard, not 2003 or higher
subquerys are not allowed. And the worst:
NO relations repeated.
For example, considering in this example, gon and maria are cousins.
If I show, gon | maria in the results, I can't show maria | gon.
SO, how I can do this?
Is really burning my head.
What I tried?
Well, the big problem was in the last requisite, the repetition of data. Ignoring that, I put this on my exam (knowing is wrong..)
select p3.name as OnePerson, p4.name as Cousin
from
people p1
inner join people p2 on p1.fatherid = p2.fatherid and p1.id != p2.id
inner join people p3 on p1.id = p3.fatherid
inner join people p4 on p1.id = p4.fatherid
of course, this is not solving the last requeriment, and I have a 4 in the test(we pass with 4) but anyway, my head is burning. So please, help me!
Another options explored
one of my friends, that also had the same exam said me
"Well, considering every relation is duplicated, I can use top
count(*) and an order by and get the half correct"
but.. Top is not ANSI!
You can add to your query WHERE p3.id < p4.id. This will eliminate duplicate results like gon | maria and maria | gon.
SELECT T1.id , T2.id FROM
(
SELECT A.id,A.fid FROM family A
WHERE a.fid IN
(
SELECT id FROM family
WHERE fid IN (SELECT id FROM family WHERE fid IS NULL)
)
)T1
JOIN
(
SELECT A.id,A.fid FROM family A
WHERE a.fid IN
(
SELECT id FROM family
WHERE fid IN (SELECT id FROM family WHERE fid IS NULL)
)
)T2
ON t1.fid<>t2.fid
AND t1.id<t2.id
This will give you the results in format you want.
SELECT TAB1.ID,TAB2.ID
FROM
(
SELECT * FROM people T1
WHERE fatherID IN ( SEL T1.ID FROM people T1 INNER JOIN people T2
ON( T1.id=T2.fatherID) WHERE T1.fatherID IS NOT NULL GROUP BY 1) ) TAB1
INNER JOIN
(
SELECT * FROM people T1
WHERE fatherID IN ( SEL T1.ID FROM people T1 INNER JOIN people T2
ON( T1.id=T2.fatherID)WHERE T1.fatherID IS NOT NULL GROUP BY 1) ) TAB2
ON( TAB1.fatherID<>TAB2.fatherID)
GROUP BY 1,2
WHERE TAB1.ID <TAB2.ID;

Need help with SQL Query

Say I have 2 tables:
Person
- Id
- Name
PersonAttribute
- Id
- PersonId
- Name
- Value
Further, let's say that each person had 2 attributes (say, gender and age). A sample record would be like this:
Person->Id = 1
Person->Name = 'John Doe'
PersonAttribute->Id = 1
PersonAttribute->PersonId = 1
PersonAttribute->Name = 'Gender'
PersonAttribute->Value = 'Male'
PersonAttribute->Id = 2
PersonAttribute->PersonId = 1
PersonAttribute->Name = 'Age'
PersonAttribute->Value = '30'
Question: how do I query this such that I get a result like this:
'John Doe', 'Male', '30'
SELECT p.name, p1.Value, p2.Value
FROM Person p, PersonAttribute p1, PersonAttribute p2
WHERE p.Id = p1.PersonId AND p.Id = p2.PersonId
AND p1.Name = 'Gender' AND p2.Name = 'Age'
I think you need redesign your schema.
Why not?
Person
- Id
- Name
- Gender
- Birthday
...
SELECT p.Name, g.Value, a.Value
FROM Person p INNER JOIN PersonAttribute g ON p.Id = g.Id AND g.Name = "Gender" INNER JOIN PersonAttribute a ON p.Id = a.Id AND a.Name = "Age"
Storing Name Value pairs does give flexibility but is very cumbersome to query.
Take a look at http://www.simple-talk.com/community/blogs/philfactor/archive/2008/05/29/56525.aspx
Leaving the design aside, you can always PIVOT the result but you need to know how many attributes you are selecting out in advance.
There's no easy way to do this.
The concept of a pivot table (already mentioned by another answer) is basically what you are looking for, except that pivot tables require you to know the names of the columns you wish to use. Clearly this is a problem when you want to exploit the power of such a table design!
In my previous life, I just settled on X number of columns, like 20-30, and if they didn't exist, then the row set included a bunch of null values. No big deal.
select piv.name,
max(case piv.a_name when 'Gender' then piv.a_value else null end) as Gender,
max(case piv.a_name when 'Age' then piv.a_value else null end) as Age,
max(case piv.a_name when 'Hobby' then piv.a_value else null end) as Hobby
from
(select p.name as name, pa.name as a_name, pa.value as a_value
from person p, personattribute pa
where p.id = pa.personid) piv
group by piv.name
This would generate output like so:
name | gender | age | hobby
-----------+--------+-----+---------
Bob Swift | Male | | Reading
John Doe | Male | 30 |
(2 rows)
Which is pretty damned close to what you are looking for. I would leave the rest of it up to your application-layer.
I also highly recommend that you include the attribute NAME as part of the return value, to provide context to the VALUEs.
These types of so-called Entity-Attribute designs often end up having to rely on a combination of server-specific functions, stored procedures, and hard-coded queries.
You need to JOIN the two tables. Wikipedia provides a pretty good explanation of JOIN: http://en.wikipedia.org/wiki/Join_%28SQL%29
SELECT Name, g.Value, a.Value
FROM Person,
PersonAttribute g INNER JOIN ON g.Name = "Gender",
PersonAttribute a INNER JOIN ON a.Name = "Age"