SQL on Self Table Join - sql

I am trying to do a simple self-join SQL and a join to a 2nd table and for the life of me I can't figure it out. I've done some research and can't seem to glean the answer from similar questions. This query is for MS-Access running in VB.NET.
I have 2 tables:
TodaysTeams
-----------
TeamNum PlayerName PlayerID
------- ---------- --------
1 Mark 100
1 Brian 101
2 Mike 102
2 Mike 102
(Note the last 2 rows above are not a typo. In this case a player can be paired with themselves to form a team)
TodaysTeamsPoints
-----------------
TeamNum Points
------- ------
1 90
2 85
The result I want is (2 rows, 1 for each team):
TeamNum PlayerName1 PlayerName2 Points
------- ----------- ----------- ------
1 Mark Brian 90
2 Mike Mike 85
Here is my SQL:
SELECT DISTINCT A.TeamNum, A.PlayerName as PlayerName1, B.PlayerName AS PlayerName2, C.Points
FROM ((TodaysTeams A INNER JOIN
TodaysTeamsPoints C ON A.TeamNum = C.TeamNum) INNER JOIN
TodaysTeams B ON A.TeamNum = B.TeamNum)
ORDER BY C.Points DESC
I know I am missing another join as I'm returning a cartesian produce (i.e. too many rows).
I would appreciate help as to what I am missing here.
Thank you.

Whilst Gordon's suggested method will work well providing that there are at most two players per team, the method breaks down if ever you add another team member and wish to display them in a separate column.
The difficulty in displaying the data in a manner that you can describe logically but cannot easily produce using a query usually implies that the database structure is sub optimal.
For your particular setup, I would personally recommend the following structure:
+---------------+ +----------+------------+
| Players | | PlayerID | PlayerName |
+---------------+ +----------+------------+
| PlayerID (PK) | | 100 | Mark |
| PlayerName | | 101 | Brian |
+---------------+ | 102 | Mike |
+----------+------------+
+-------------+ +--------+----------+
| Teams | | TeamID | TeamName |
+-------------+ +--------+----------+
| TeamID (PK) | | 1 | Team1 |
| TeamName | | 2 | Team2 |
+-------------+ +--------+----------+
+-------------------+ +--------+--------------+----------+
| TeamPlayers | | TeamID | TeamPlayerID | PlayerID |
+-------------------+ +--------+--------------+----------+
| TeamID (PK) | | 1 | 1 | 100 |
| TeamPlayerID (PK) | | 1 | 2 | 101 |
| PlayerID (FK) | | 2 | 1 | 102 |
+-------------------+ | 2 | 2 | 102 |
+--------+--------------+----------+
Using this method, you can use condition aggregation or a crosstab query pivoting on the TeamPlayerID to produce each of your columns, and you would not be limited to two columns.

You can use aggregation:
SELECT ttp.TeamNum, MIN(tt.PlayerName) as PlayerName1,
MAX(tt.PlayerName) as PlayerName2,
ttp.Points
FROM TodaysTeamsPoints as ttp INNER JOIN
TodaysTeams as tt
ON tt.TeamNum = ttp.TeamNum
GROUP BY ttp.TeamNum, ttp.Points
ORDER BY ttp.Points DESC;

Related

How can I query pairwise event attendance in MS Access given its subquery restrictions?

My colleague and I have been wracking our heads on this for days and have come to the conclusion that we just don't know SQL well enough to solve this problem. Please help us!
We have a table in MS Access called EventAttendance with 2 fields: MemberID (the person who attended the event) and EventID (the event they attended). Unique Member and Event IDs are stored in their own tables (Member and Event, respectively.) EventAttendance contains entries for hundreds of members and events, but a simple collection of records for 3 events might look like this:
|---------------------|------------------|
| MemberID | EventID |
|---------------------|------------------|
| 1 | 1 |
|---------------------|------------------|
| 2 | 1 |
|---------------------|------------------|
| 4 | 1 |
|---------------------|------------------|
| 1 | 2 |
|---------------------|------------------|
| 2 | 2 |
|---------------------|------------------|
| 3 | 2 |
|---------------------|------------------|
| 1 | 3 |
|---------------------|------------------|
| 3 | 3 |
|---------------------|------------------|
| 4 | 3 |
|---------------------|------------------|
In this example, Member 1 attended Events 1, 2, & 3; Member 2 attended Events 1 & 2; Member 3 attended Events 2 & 3; and Member 4 attended Events 1 & 3.
We are now trying to create a table that documents how often each pair of members attended an event together. Ideally, we would want the table to look like the one below (for the example records above), where CoAttendance is the number of events each pair attended together:
|---------------------|------------------|------------------|
| Member1 | Member2 | CoAttendance |
|---------------------|------------------|------------------|
| 1 | 2 | 2 |
|---------------------|------------------|------------------|
| 1 | 3 | 2 |
|---------------------|------------------|------------------|
| 1 | 4 | 2 |
|---------------------|------------------|------------------|
| 2 | 3 | 1 |
|---------------------|------------------|------------------|
| 2 | 4 | 1 |
|---------------------|------------------|------------------|
| 3 | 4 | 1 |
|---------------------|------------------|------------------|
This has proven a lot more challenging than we assumed. First, we haven't been able to figure out to get pairs in such a way that combinations don't repeat, but we have been able to list all permutations by querying a perfect copy of the Member Table (MemberClone) and using the following query:
SELECT Member.MemberID AS Member1, MemberClone.MemberID AS Member2
FROM Member, MemberClone;
This query resulted in a table like so:
|---------------------|------------------|
| Member1 | Member2 |
|---------------------|------------------|
| 1 | 1 |
|---------------------|------------------|
| 1 | 2 |
|---------------------|------------------|
| 1 | 3 |
|---------------------|------------------|
| 1 | 4 |
|---------------------|------------------|
| 2 | 1 |
|---------------------|------------------|
| 2 | 2 |
|---------------------|------------------|
| 2 | 3 |
|---------------------|------------------|
| 2 | 4 |
|---------------------|------------------|
| 3 | 1 |
|---------------------|------------------|
| 3 | 2 |
|---------------------|------------------|
| 3 | 3 |
|---------------------|------------------|
| 3 | 4 |
|---------------------|------------------|
| 4 | 1 |
|---------------------|------------------|
| 4 | 2 |
|---------------------|------------------|
| 4 | 3 |
|---------------------|------------------|
| 4 | 4 |
|---------------------|------------------|
Not perfect, but good enough (this is a problem we'd like solved eventually, but not the main one.)
The bigger problem is getting the third column (CoAttendance) to work. The closest we've gotten to a solution is through the use of subqueries:
SELECT Member.MemberID AS Member1, MemberClone.MemberID AS Member2,
(SELECT Count(*)
FROM (SELECT EventID FROM EventAttendance WHERE EventAttendance.MemberID = Member1) AS Member1Attendance
INNER JOIN (SELECT EventID FROM EventAttendance WHERE EventAttendance.MemberID = Member2) AS Member2Attendance
ON Member1Attendance.EventID = Member2Attendance.EventID) AS CoAttendance
FROM MemberClone, Member;
This should theoretically generate a list of only events that Member1 and Member2 attended together for each pair, and the count(*) operation would count those events.
The problem is that Access subqueries can only see one level above themselves, so Member1 and Member2 are undefined in the nested subqueries (they are defined 2 levels above.) I've tried finding solutions, but find that I just don't understand SQL enough to process similar solutions posted elsewhere (e.g., Nested subquery in Access alias causing "enter parameter value") while also making the inner join work.
Any help you can offer would be super appreciated!
I think you just want a self-join with aggregation:
select ea1.memberid, ea2.memberid, count(*) as num_events
from EventAttendance as ea1 inner join
EventAttendance as ea2
on ea1.eventid = ea2.eventid and ea1.memberid < ea2.memberid
group by ea1.memberid, ea2.memberid;
MS Access might be finicky about the < in the on clause. This is an inner join, so you can do:
select ea1.memberid, ea2.memberid, count(*) as num_events
from EventAttendance as ea1 inner join
EventAttendance as ea2
on ea1.eventid = ea2.eventid
where ea1.memberid < ea2.memberid
group by ea1.memberid, ea2.memberid;
This requires doing a match finding all people from events (so a self-join on EventAttendance)
SQL for this is below
SELECT Member.ID AS Member1, Member_1.ID AS Member2, Count(EventAttendance_1.Event_ID) AS CountOfEvent_ID
FROM (Member INNER JOIN (EventAttendance INNER JOIN EventAttendance AS EventAttendance_1 ON EventAttendance.Event_ID = EventAttendance_1.Event_ID) ON Member.ID = EventAttendance.Member_ID) INNER JOIN Member AS Member_1 ON EventAttendance_1.Member_ID = Member_1.ID
GROUP BY Member.ID, Member_1.ID
HAVING (((Member_1.ID)>[Member].[ID]));
That is the SQL provided by Access for the following setup:
Note that you do not necessarily need links to Member or Member_1 - just that you may want to get info about them other than their ID.
---- Update
Gah this is basically the same answer as #Gordon's above (so I've upvoted his). I've not deleted this one in case you find the picture useful (when using Access, I much prefer the GUI query designer).

Pivot with unknown row result

to make my problem simple, let's say I have two tables: users and cars, that have a many-to-many relation (one user can have many cars, and one car can be of many users):
tbl_users tbl_user_car tbl_cars
------------- ---------------------------- ------
id_usr | name id_usr_car | fk_usr | fk_car id_car | name
1 | usr1 1 | 1 | 1 1 | car1
2 | usr2 2 | 1 | 2 2 | car2
3 | usr3 3 | 2 | 1 3 | car3
I have a web CRUD that admin could associate cars to users, so I don't know how many cars has an user. So I want to make a query that shows the user and all its associated cars but like a pivot, something like this:
SELECT * FROM pivot
------
name_usr | id_car | name_car | id_car2 | name_car2 | ... n number of cars
usr1 | 1 | car1 | 2 | car2 | ...
usr2 | 1 | car1 | | | ...
usr3 | | | | | ...
I tried to use crosstab() function but I need to know how many columns I want to return, and I need to make 5 inner join to see more data. Any help would be very appreciated.

Need PIVOT or CASE solution for converting columns to rows (NON-Dynamic if possible)

So I have a table that has data such as this:
SCHD_ID | INST_ID |
|---------|---------|
| 1001 | Mike |
| 1001 | Ted |
| 1001 | Chris |
| 1002 | Jill |
| 1002 | Jamie |
| 1003 | Brad |
| 1003 | Carl |
| 1003 | Drew |
| 1003 | Nick |
I need to come up with a query to display the data like below:
|SCHD_ID | INST 1 | INST 2 | INST 3 |
|---------|--------|--------|--------|
| 1001 | Mike | Ted | Chris |
| 1002 | Jill | Jamie | Null |
| 1003 | Brad | Carl | Drew |
I have tried looking into all the pivot descriptions and some case examples but everything seems to use a common repeated value to pivot around. This is one of those cases where the columns need to be dynamic, but only to a point. I can drop off any data after the third instructor. In the example above I did not put in a column for INST 4 for SCHD_ID 1003 even though in my data set example it existed. Can adding in a restraint like this make it possible to come up with a non dynamic solution for the pivot/case statement?
Thanks for the help,
Dwayne
You can do this using row_number() and conditional aggregation. However, your data doesn't have an ordering column, so you cannot guarantee which three instructors you will get:
select schd_id,
max(case when seqnum = 1 then inst_id end) as inst1,
max(case when seqnum = 2 then inst_id end) as inst2,
max(case when seqnum = 3 then inst_id end) as inst3
from (select t.*,
row_number() over (partition by schd_id order by sched_id) as seqnum
from table t
) t
group by SCHD_ID ;
If you have a priority ordering for choosing the instructors, then put the logic in the order by clause.

Create a pivot table from two tables based on dates

I have two MS Access tables sharing a one to many relationship. Their structures are like the following:
tbl_Persons
+----------+------------+-----------+
| PersonID | PersonName | OtherData |
+----------+------------+-----------+
| 1 | PersonA | etc. |
| 2 | PersonB | |
| 3 | PersonC | |
tbl_Visits
+----------+------------+------------+-----------------------
| VisitID | PersonID | VisitDate | dozens of other fields
+----------+------------+------------+-----------
| 1 | 1 | 09/01/13 |
| 2 | 1 | 09/02/13 |
| 3 | 2 | 09/03/13 |
| 4 | 2 | 09/04/13 | etc...
I wish to create a new table based on the VisitDate field, the column headings of which are Visit-n where n is 1 to the number of visits, Visit-n-Data1, Visit-n-Data2, Visit-n-Data3 etc.
MergedTable
+----------+----------+---------------+-----------------+----------+----------------+
| PersonID | Visit1 | Visit1Data1 | Visit1Data2... | Visit2 | Visit2Data1... |
+----------+----------+---------------+-----------
| 1 | 09/01/13 | | | 09/02/13 |
| 2 | 09/03/13 | | | 09/04/13 |
| 3 | etc. | |
I am really not sure how to do this. Whether SQL query or using DAO then looping through records and columns. It is essential that there is only 1 PersonID per row and all his data appears chronologically into columns.
Start of by ranking the visits with something like
SELECT PersonID, VisitID,
(SELECT COUNT(VisitID) FROM tbl_Visits AS C
WHERE C.PersonID = tbl_Visits.PersonID
AND C.VisitDate < tbl_Visits.VisitDate) AS RankNumber
FROM tbl_Visits
Use this query as a base for the 'pivot'
Since you seem to have some visits of persons on the same day (visit 1 and 2) the WHERE clause needs to be a bit more sophisticated. But I hope you get the basic concept.
Pivoting can be done with multiple LEFT JOINs.
I question if my solution will have a high performance, since I did not test it. It is easier in SQL Server than in MS Access to accomplish.

MySQL: How to select and display ALL rows from one table, and calculate the sum of a where clause on another table?

I'm trying to display all rows from one table and also SUM/AVG the results in one column, which is the result of a where clause. That probably doesn't make much sense, so let me explain.
I need to display a report of all employees...
SELECT Employees.Name, Employees.Extension
FROM Employees;
--------------
| Name | Ext |
--------------
| Joe | 123 |
| Jane | 124 |
| John | 125 |
--------------
...and join some information from the PhoneCalls table...
--------------------------------------------------------------
| PhoneCalls Table |
--------------------------------------------------------------
| Ext | StartTime | EndTime | Duration |
--------------------------------------------------------------
| 123 | 2010-09-05 10:54:22 | 2010-09-05 10:58:22 | 240 |
--------------------------------------------------------------
SELECT Employees.Name,
Employees.Extension,
Count(PhoneCalls.*) AS CallCount,
AVG(PhoneCalls.Duration) AS AverageCallTime,
SUM(PhoneCalls.Duration) AS TotalCallTime
FROM Employees
LEFT JOIN PhoneCalls ON Employees.Extension = PhoneCalls.Extension
GROUP BY Employees.Extension;
------------------------------------------------------------
| Name | Ext | CallCount | AverageCallTime | TotalCallTime |
------------------------------------------------------------
| Joe | 123 | 10 | 200 | 2000 |
| Jane | 124 | 20 | 250 | 5000 |
| John | 125 | 3 | 100 | 300 |
------------------------------------------------------------
Now I want to filter out some of the rows that are included in the SUM and AVG calculations...
WHERE PhoneCalls.StartTime BETWEEN "2010-09-12 09:30:00" AND NOW()
...which will ideally result in a table looking something like this:
------------------------------------------------------------
| Name | Ext | CallCount | AverageCallTime | TotalCallTime |
------------------------------------------------------------
| Joe | 123 | 5 | 200 | 1000 |
| Jane | 124 | 10 | 250 | 2500 |
| John | 125 | 0 | 0 | 0 |
------------------------------------------------------------
Note that John has not made any calls in this date range, so his total CallCount is zero, but he is still in the list of results. I can't seem to figure out how to keep records like John's in the list. When I add the WHERE clause, those records are filtered out.
How can I create a select statement that displays all of the Employees and only SUMs/AVGs the values returned from the WHERE clause?
Use:
SELECT e.Name,
e.Extension,
Count(pc.*) AS CallCount,
AVG(pc.Duration) AS AverageCallTime,
SUM(pc.Duration) AS TotalCallTime
FROM Employees e
LEFT JOIN PhoneCalls pc ON pc.extension = e.extension
AND pc.StartTime BETWEEN "2010-09-12 09:30:00" AND NOW()
GROUP BY e.Name, e.Extension
The issue is when using an OUTER JOIN, specifying criteria in the JOIN section is applied before the JOIN takes place--like a derived table or inline view. The WHERE clause is applied after the OUTER JOIN, which is why when you specified the WHERE clause on the table being LEFT OUTER JOIN'd to that the rows you still wanted to see are being filtered out.