Postgres - rows to columns - sql

What i have
Query:
SELECT u.firstname,u.lastname, u.institution as department, u.department as company, c.shortname,
to_char(to_timestamp(p.timecompleted)::date,'YYYY-MM-DD') AS completed
FROM mdl_course_completions AS p
JOIN mdl_course AS c ON p.course = c.id
JOIN mdl_user AS u ON p.userid = u.id
WHERE c.enablecompletion = 1 AND u.firstname is NOT NULL AND p.timecompleted is NOT NULL
ORDER BY u.firstname
Results:
firstname lastname department company course completed
u1 u1 x x c1 date
u1 u1 x x c2 date
What i need
I need to be able to transport course to columns. Resulting in something similar to this:
firstname lastname department company c1 c2
u1 u1 x x date date
u2 u2 x x date date
I have tried using crosstab, but I am not skilled enough on SQL. Could someone please help?
EDIT: the number of courses are in the hundreds, so it needs to be dynamic.
(Also: English is not my first language, so please excuse any unclarities).

This is not what you request, but maybe be useful for you. Last column is an array with the structure: {'course_name: course_completed', ...}.
SELECT u.firstname,u.lastname, u.institution as department, u.department as company
array_agg(c.shortname || ': ' || to_char(to_timestamp(p.timecompleted)::date,'YYYY-MM-DD')) AS courses_completed
FROM mdl_course_completions AS p
JOIN mdl_course AS c ON p.course = c.id
JOIN mdl_user AS u ON p.userid = u.id
WHERE c.enablecompletion = 1 AND u.firstname is NOT NULL AND p.timecompleted is NOT NULL
GROUP BY u.firstname,u.lastname, u.institution, u.department
ORDER BY u.firstname
output will be:
firstname lastname department company courses_completed
u1 u1 x x {"c1: date", "c2: date"}
u2 u2 x x {"c1: date", "c2: date"}
Other option is to use crosstab function (like you said): Postgresql crosstab documentation. You have to install tablefunc module to use crosstab. Read the documentation and if you think it could be useful for you I could help you to write the query (but I will need to know the tables structure and some example data).

You can use conditional aggregate, for example by using the FILTER clause:
SELECT
firstname, lastname, department, company,
MAX(completed) FILTER (WHERE course = 'c1') as c1,
MAX(completed) FILTER (WHERE course = 'c2') as c2
FROM
-- <joins>
GROUP BY firstname, lastname, department, company
You need to group by the columns which are part of the same group. Then you can execute any aggregation function you like on the columns you want to pivot. With the appended FILTER clause you can filter all records which should be recognized for the aggregate.

Related

How to sum up max values from another table with some filtering

I have 3 tables
User Table
id
Name
1
Mike
2
Sam
Score Table
id
UserId
CourseId
Score
1
1
1
5
2
1
1
10
3
1
2
5
Course Table
id
Name
1
Course 1
2
Course 2
What I'm trying to return is rows for each user to display user id and user name along with the sum of the maximum score per course for that user
In the example tables the output I'd like to see is
Result
User_Id
User_Name
Total_Score
1
Mike
15
2
Sam
0
The SQL I've tried so far is:
select TOP(3) u.Id as User_Id, u.UserName as User_Name, SUM(maxScores) as Total_Score
from Users as u,
(select MAX(s.Score) as maxScores
from Scores as s
inner join Courses as c
on s.CourseId = c.Id
group by s.UserId, c.Id
) x
group by u.Id, u.UserName
I want to use a having clause to link the Users to Scores after the group by in the sub query but I get a exception saying:
The multi-part identifier "u.Id" could not be bound
It works if I hard code a user id in the having clause I want to add but it needs to be dynamic and I'm stuck on how to do this
What would be the correct way to structure the query?
You were close, you just needed to return s.UserId from the sub-query and correctly join the sub-query to your Users table (I've joined in reverse order to you because to me its more logical to start with the base data and then join on more details as required). Taking note of the scope of aliases i.e. aliases inside your sub-query are not available in your outer query.
select u.Id as [User_Id], u.UserName as [User_Name]
, sum(maxScore) as Total_Score
from (
select s.UserId, max(s.Score) as maxScore
from Scores as s
inner join Courses as c on s.CourseId = c.Id
group by s.UserId, c.Id
) as x
inner join Users as u on u.Id = x.UserId
group by u.Id, u.UserName;

join table on condition

I have 3 tables user, student_data, teacher_data. A user can be either student or a teacher. If it is the teacher I want to join user and teacher_data. And if it is a student then I want to join user with student_data.
How I can do this join with the condition.
I'd combine the two data tables in a sub-query, and then join the users to that.
SELECT
*
FROM
usr u
LEFT JOIN
(
SELECT user_id, datum, xxx, NULL AS yyy FROM student_data
UNION ALL
SELECT user_id, datum, NULL, yyy FROM teacher_data
)
d
ON d.user_id = u.id
https://dbfiddle.uk/?rdbms=oracle_21&fiddle=9b801ea739d42fe50c00ef4e17eaf143
NOTES:
The columns selected from the two data tables must match
Any unmatched columns must either be skipped or filled with NULL
Please don't call a table user, it's a reserved keyword and Oracle won't allow it.
You can write it like this:
select u.user_id,
s.student_id,
t.teacher_id
from usr u
left join student_data s on u.user_id=s.student_id
left join teacher_data t on u.user_id=t.teacher_id
where s.student_id is not null or t.teacher_id is not null
order by u.user_id
For every user_id check if he is a student or teacher, if he is student get his student column values else null, if he is a teacher get his teacher column values else null.
maybe try a union - something like this
select user_id, user_other_stuff
from user, student_data
where user.user_id = student_data.user_id
UNION
select user_id, user_other_stuff
from user, teacher_data
where user.user_id = teacher_data.user_id

sql select records with matching subsets

There are two sets of employees: managers and grunts.
For each manager, there's a table manager_meetings that holds a list of which meetings each manager attended. A similar table grunt_meetings holds a list of which meetings each grunt attended.
So:
manager_meetings grunt_meetings
managerID meetingID gruntID meetingID
1 a 4 a
1 b 4 b
1 c 4 c
2 a 4 d
2 b 5 a
3 c 5 b
3 d 5 c
3 e 6 a
6 c
7 b
7 a
The owner doesn't like it when a manager and a grunt know exactly the same information. It makes his head hurt. He wants to identify this situation, so he can demote the manager to a grunt, or promote the grunt to a manager, or take them both golfing. The owner likes to golf.
The task is to list every combination of manager and grunt where both attended exactly the same meetings. If the manager attended more meeting than the grunt, no match. If the grunt attended more meetings than the manager, no match.
The expected results here are:
ManagerID GruntID
2 7
1 5
...because manager 2 and grunt 7 both attended (a,b), while manager 1 and grunt 5 both attended (a,b,c).
I can solve it in a clunky way, by pivoting up the subset of meetings in a subquery into XML, and comparing each grunt's XML list to each manager's XML. But that's horrible, and also I have to explain to the owner what XML is. And I don't like golfing.
Is there some better way to do "WHERE {subset1} = {subset2}"? It feels like I'm missing some clever kind of join.
SQL Fiddle
Here is a version that works:
select m.mId, g.gId, count(*) --select m.mid, g.gid, mm.meetingid, gm.meetingid as gmm
from manager m cross join
grunt g left outer join
(select mm.*, count(*) over (partition by mm.mid) as cnt
from manager_meeting mm
) mm
on mm.mid = m.mId full outer join
(select gm.*, count(*) over (partition by gm.gid) as cnt
from grunt_meeting gm
) gm
on gm.gid = g.gid and gm.meetingid = mm.meetingid
group by m.mId, g.gId, mm.cnt, gm.cnt
having count(*) = mm.cnt and mm.cnt = gm.cnt;
The string comparison method is shorter, perhaps easier to understand, and probably faster.
EDIT:
For your particular case of getting exact matches, the query can be simplified:
select mm.mId, gm.gId
from (select mm.*, count(*) over (partition by mm.mid) as cnt
from manager_meeting mm
) mm join
(select gm.*, count(*) over (partition by gm.gid) as cnt
from grunt_meeting gm
) gm
on gm.meetingid = mm.meetingid and
mm.cnt = gm.cnt
group by mm.mId, gm.gId
having count(*) = max(mm.cnt);
This might be more competitive with the string version, both in terms of performance and clarity.
It counts the number of matches between a grunt and a manager. It then checks that this is all the meetings for each.
An attempt at avenging Aaron's defeat – a solution using EXCEPT:
SELECT
m.mID,
g.gID
FROM
manager AS m
INNER JOIN
grunt AS g
ON NOT EXISTS (
SELECT meetingID
FROM manager_meeting
WHERE mID = m.mID
EXCEPT
SELECT meetingID
FROM grunt_meeting
WHERE gID = g.gID
)
AND NOT EXISTS (
SELECT meetingID
FROM grunt_meeting
WHERE gID = g.gID
EXCEPT
SELECT meetingID
FROM manager_meeting
WHERE mID = m.mID
);
Basically, subtract a grunt's set of meetings from a manager's set of meetings, then the other way round. If neither result contains rows, the grunt and the manager attended the same set of meetings.
Please note that this query will match managers and grunts that never attended a single meeting.
An alternative version - but requires another table. Basically, we give each meeting a distinct power of two as it's 'value', then sum every manager's meeting value and each grunt's meeting value. Where they're the same, we have a match.
It should be possible to make the meeting_values table a TVF, but this is a little bit simpler.
SQL Fiddle
Additional table:
CREATE TABLE meeting_values (value INT, meetingID CHAR(1));
INSERT INTO meeting_values VALUES
(1,'a'),(2,'b'),(4,'c'),(8,'d'),(16,'e');
And the query:
SELECT managemeets.mID, gruntmeets.gID
FROM
( SELECT gm.gID, sum(value) AS meeting_totals
FROM grunt_meeting gm
INNER JOIN
meeting_values mv ON gm.meetingID = mv.meetingID
GROUP BY gm.gID
) gruntmeets
INNER JOIN
( SELECT mm.mID, sum(value) AS meeting_totals
FROM manager_meeting mm
INNER JOIN
meeting_values mv ON mm.meetingID = mv.meetingID
GROUP BY mm.mID
) managemeets ON gruntmeets.meeting_totals = managemeets.meeting_totals

SQL select, 3 tables

How can I use select if I have 3 tables?
Tables:
school_subject(ID_of_subject, workplace, name)
student(ID_of_student, firstName, surname, adress)
writing(ID_of_action, ID_of_sbuject, ID_of_student)
I want to write the student's name and surname (alphabetically) who have workplace=home.
Is this possible? And how to alphabetize?
SELECT s.firstName, s.surname
FROM student S, school_subject P, writing Z
WHERE P.workplace = 'home'
AND P.ID_of_subject = Z.ID_of_subject
AND Z.ID_of_student = s.ID_of_student;
SELECT s.firstName, s.surname
FROM student S INNER JOIN writing Z
ON Z.ID_of_student = s.ID_of_student
INNER JOIN school_subject P
ON P.ID_of_subject = Z.ID_of_subject
WHERE P.workplace = 'home'
ORDER BY S.firstName, S.surname // Sort the list
To order alphabetically the result it is possible to use ORDER BY keyword. So your query becomes:
SELECT DISTINCT S.firstName, S.surname
FROM student S, school_subject P, writing Z
WHERE P.workplace = 'home' AND
P.ID_of_subject = Z.ID_of_subject AND
Z.ID_of_student = S.ID_of_student
ORDER BY S.surname, S.firstName
The DISTINCT keyword is necessary, because in writing table there are eventually more tuples given keys ID_of_subject and ID_of_student.
So this is necessary to avoid repeating firstName and surname many times.
Note that each student is identified by ID_of_student, not by firstName and surname, so as #danjok said use DISTINCT if you only want the name and surname.
If you want to select all students that satisfy your requirement (even if two or more students have the same firstName and surname), you should including ID_of_student on SELECT clause:
SELECT S.ID_of_student, S.firstName, S.surname
FROM student S
INNER JOIN writing W ON W.ID_of_student = S.ID_of_student
INNER JOIN school_subject P ON P.ID_of_subject = W.ID_of_subject
WHERE P.workplace = 'home'
ORDER BY S.firstName asc, S.surname asc

SQL Statement that Subs 0 for no Results

I am having an issue getting the results I want from my SQL statement. I know I'm probably missing something simple but I just can't see it.
Here are my tables.
Table: Users (RoleID is linked to ID in Roles Table)
ID, FirstName, LastName, RoleID
1, Matt, Ryan, 1
2, Chipper, Jones, 1
3, Julio, Jones, 2
4, Jason, Bourn, 3
Table: Roles
ID, Name
1, Field Rep
2, Tech
3, Admin
Table: FRrequests (UserID is linked to ID in Users table)
ID, UserID, Status
1, 1, Open
2, 1, Submitted
3, 1, Delayed
4, 1, Complete
What I want is an SQL statement that shows me a count of all the "Submitted" & "Delayed" requests for all the Field Reps. Below is an example of desired results.
Name Count
Chipper Jones 0
Matt Ryan 2
Here is the statement I have so far and the results it gives me.
SELECT Users.FirstName + ' ' + Users.LastName AS Name, COUNT(FRrequests.ID) AS 'Open Requests'
FROM Users INNER JOIN
Roles ON Users.RoleID = Roles.ID LEFT OUTER JOIN
FRrequests ON Users.ID = FRrequests.UserID
WHERE (Roles.Name = N'Field Rep') AND (FRrequests.Status = 'Submitted' OR FRrequests.Status = 'Delayed')
GROUP BY Users.FirstName, Users.LastName
Name Count
Matt Ryan 2
I know that the "AND (FRrequests.Status = 'Submitted' OR FRrequests.Status = 'Delayed')" part is what is breaking it. If I run it without that in the statement I get all the users but it counts all status not just submitted and delayed. I just can't figure out what I'm missing to get this to work. Any help would be greatly appreciated.
You are really close, try the following:
SELECT U.FirstName + ' ' + U.LastName AS Name, COUNT(F.ID) AS 'Open Requests'
FROM Users U
INNER JOIN Roles R
ON U.RoleID = R.ID
LEFT JOIN ( SELECT * FROM FRrequests
WHERE Status IN ('Submitted','Delayed')) F
ON U.ID = F.UserID
WHERE R.Name = N'Field Rep'
GROUP BY U.FirstName, U.LastName
You need to have a row for each Field Rep if you wish to include Field Reps that have no requests. Therefore you need to use a left outer join along the following lines:
SELECT
u.FirstName || ' ' || u.LastName as Name
, COALESCE(frr.Counter,0) as Count
FROM
Users u
LEFT OUTER JOIN (
SELECT
UserID
,count(*) as Counter
FROM
FRrequests
WHERE
Status IN ('Submitted', 'Delayed')
GROUP BY
UserID
) frr ON frr.UserID = u.ID
, Roles r
WHERE
u.ID = f.UserID
AND u.RoleID = r.ID
AND r.Name = 'Field Rep'
;
A left outer join will provide a join only where a match occurs, so it does not remove rows from the naturally joined set. In this case you will get a list of all Users who are Field Reps, together with a count of their FRrequests with a 'Submitted' or 'Delayed' status if any.
COALESCE is used (in Postgresql at least) to check for a null value and then coerce it to the supplied value, in this case 0. So if a NULL value occurs (because, for instance, there was no 'Counter' value returned from the left outer join) it is replaced by 0.