SQL "chained" queries? - mysql

I have two tables.
I am a total newbie to SQL. Using mysql at the moment.
I have the following setup for a school-related db:
Table A contains students records.
Student's id, password,name, lastname and so on.
Table B contains class attendancy records.
Each record goes like this: date, student id, grade
I need to gather all the student info of students that attended classes in a certain date range.
Now, the stupid way would be
first I SELECT all classes from Table B with DATE IN BETWEEN the range
then for each class, I get the student id and SELECT * FROM students WHERE id = student id
What I can't wrap my mind around is the smart way.
How to do this in one query only.
I am failing at understanding the concepts of JOIN, UNION and so on...
my best guess so far is
SELECT students.id, students.name, students.lastname
FROM students, classes
WHERE classes.date BETWEEN 20140101 AND 20150101
AND
classes.studentid = students.id
but is this the appropriate way for this case?

Dont add the join statement in the where clause. Do it like this:
SELECT s.id, s.name, s.lastname,c.date,c.grade
FROM classes c
inner join students s
on c.studentid=s.id
WHERE c.date BETWEEN '01/01/2014' AND '01/01/2015'

This sounds like an assignment so I will attempt to describe the problem and give a hint to the solution.
An example of a union would be;
SELECT students.name, students.lastname
FROM students
WHERE students.lastname IS NOT NULL
UNION
SELECT students.name, 'N/A'
FROM students
WHERE students.lastname IS NULL;
+--------------+--------------+
| name | lastname |
+--------------+--------------+
| John | Doe | <- First two rows came from first query
| Jill | Smith |
| Bill | N/A | <- This came from the second query
+--------------+--------------+
The usual use case for a union is to display the same columns, but munge the data in a different way - otherwise you can usually achieve similar results through a WHERE clause.
An example of a join would be;
SELECT authors.id, authors.name, books.title
FROM authors LEFT JOIN books ON authors.id = books.authors_id
+--------------+--------------+------------------+
| id | name | title |
+--------------+--------------+------------------+
| 1 | Mark Twain | Huckleberry Fin. |
| 2 | Terry Prat.. | Good Omens |
+--------------+--------------+------------------+
^ First two columns from ^ Third column appended
from authors table from books table linked
by "author id"
Think of a join as appending columns to your results, a union is appending rows with the same columns.
In your situation we can rule out a union as you don't want to append more student rows, you want class and student information side by side.

Related

Return value for MySQL NULL result

I am working in MySQL. I have the following query:
SELECT tar.ID, COUNT(tsr.StudentID) AS Students
FROM Teacher_ApplicationRecord tar
LEFT JOIN Teacher_StudentRecord tsr ON sar.ID = tsr.TeacherID AND tsr.Session = 1 AND tsr.Year = 2017
WHERE tar.ApplicationYear = 2017
AND tar.Session1 = 1
This query returns no results. However if I take the COUNT(tsr.StudentID) out of the SELECT statement, it returns all the teacher ID's with NULL for the tsr table.
What I want is a query that returns all the teacher ID's and a count of the students assigned to that teacher, with 0 if the result is NULL.
I have tried COALESCE(COUNT(tsr.StudentID), 0) AND IFNULL(COUNT(tsr.StudentID), 0) with no success so far. Any other thoughts?
UPDATE:
The tsr table has 4 columns: TeacherID, StudentID, Year, Session. It has no records yet. It will be populated next year when students are assigned to teachers.
The tar table has a list of TeacherID's in it with some other data, such as year and faculty.
I want my results to look like below:
+-----------+-----------------+
| TeacherID | COUNT(StudentID)|
+-----------+-----------------+
| 1 | 0 |
+-----------+-----------------+
| 2 | 0 |
+-----------+-----------------+
etc.
As students are assigned to teachers, the COUNT(StudentID) numbers will go up. Hope this helps.
UPDATE 2
The tar table looks like this:
+---------+---------------+
|TeacherID|ApplicationYear|
+---------+---------------+
| 1 | 2017 |
+---------+---------------+
| 2 | 2017 |
+---------+---------------+
| 3 | 2017 |
+---------+---------------+
It has other columns but they are not relevant to the question.
The tsr table looks like this:
+---------+---------+----+-------+
|TeacherID|StudentID|Year|Session|
+---------+---------+----+-------+
| 1 | 10 |2017| 1 |
+---------+---------+----+-------+
| 1 | 11 |2017| 1 |
+---------+---------+----+-------+
| 2 | 12 |2017| 1 |
You can try joining the teacher table to the student table (in that order), and then using GROUP BY to count the number of students per teacher:
SELECT tsr.ID,
COUNT(sar.ID) AS numOfStudents -- count no matching students as zero
FROM Teacher_StudentRecord tsr
LEFT JOIN Student_ApplicationRecord sar
ON tsr.TeacherID = sar.ID AND
tsr.Session = 1 AND
tsr.Year = 2017
GROUP BY tsr.ID
The usefulness of a LEFT JOIN here is the edge case where a teacher has no matching students. In this case, the result set, before aggregation happens, would have a single record for that teacher, and the student ID value would be NULL, which be ignored by COUNT, resulting in a correct zero count.
Note that I removed the WHERE clause, whose intended logic is already contained in the ON clause. This WHERE clause was throwing off your results by removing teacher records before they could even be aggregated.
Update:
Please try the following query to see if you get results:
SELECT tsr.ID,
COUNT(sar.ID) AS numOfStudents
FROM Teacher_StudentRecord tsr
LEFT JOIN Student_ApplicationRecord sar
ON tsr.TeacherID = sar.ID
GROUP BY tsr.ID
If this gives you no results, then no teachers are actually connected to any students, and your data has a problem.
Here is a demo using your sample data. It works as expected:
SQLFiddle

Best way to get max value when joining mysql tables

I've got THREE MYSQL TABLES (innoDB) :
NAMES
id nid version fname lname birth
RELATIONS
id rid version idname idperson roleid
ROLES
id role
I want to select the last version of each RELATIONS joined to the last version of their related NAMES for a particular idperson (and the name of the ROLE)
Of course, idperson will have 0, 1 or more relations and there will be one or more versions of RELATIONS and NAMES
I wrote something like :
SELECT A.id,A.nid,MAX(A.version),A.idname,A.idperson,A.roleid,B.id,B.role
FROM RELATIONS A
INNER JOIN
ROLES
ON A.roleid = B.id
INNER JOIN
(SELECT id,nid,MAX(version),fname,lname,birth FROM NAMES) C
ON A.idname = C.id
WHERE A.idperson = xx
It doesn't work maybe because MAX() seems to return only one line...
How to get the maximum value for more than one line in this joining context?
PS: how do you generate this kind of nice data set?
i.e. :
id home datetime player resource
---|-----|------------|--------|---------
1 | 10 | 04/03/2009 | john | 399
2 | 11 | 04/03/2009 | juliet | 244
5 | 12 | 04/03/2009 | borat | 555
8 | 13 | 01/01/2009 | borat | 700
Adding a GROUP BY statement, both in the subquery you have, as well as in the outer query should allow the MAX function to generate the result that you're looking for.
This is untested code, but should give you the result that you're looking for:
SELECT A.id,A.nid,MAX(A.version),A.idname,A.idperson,A.roleid,B.id,B.role
FROM RELATIONS A
INNER JOIN
ROLES
ON A.roleid = B.id
INNER JOIN
(SELECT id,nid,MAX(version),fname,lname,birth FROM NAMES GROUP BY fname,lname) C
ON A.idname = C.id
WHERE A.idperson = xx
GROUP BY fname,lname
Alternatively, if it works better for your database architecture, you can use any unique identifier for the employees you'd like (possibly nid?).
As to the question that you've posed in your PS, I'm unsure as to what you're asking. I don't seem a home, datetime, player, or resource field in the examples of your tables that you've provided. If you could clarify, I'd be happy to try and help you with that as well.

Is there a better way than using 2 SELECT?

Say that I have 2 tables, one with entries of films that an user likes and an other with events that an user has gone. Each table has a column for knowing the user. Something like:
Table Films:
id | iduser | film | number of watches | note ....
Table events:
id | iduser | event | date | ....
both iduser are connected with a relation to a table with other information of the user.
If I want to select some columns from table films and others from table events with the same iduser, is there a better way than 2 SELECT? I say this because each select has diferent number of rows so UNION gives me an error and join gives me like:
EDIT
FILM | NOTE | EVENT | DATE
-----------------------------------------
tlor | 9 | going to park | 20/7/12
tlor | 9 | eat a sandwich | 5/9/10
B film | 7 | going to park | 20/7/12
B film | 7 | eat a sandwich | 5/9/10
EDIT 2
I say only a select because I think is the faster way but if there's a faster way, please let me know it.
Doing two SELECTs is the correct solution here. You're loading two different sets of data.
IF the two tables had similar schemas (e.g, same column names and types), you could combine the two using:
SELECT * FROM table1 WHERE userid = ? UNION SELECT * FROM table2 WHERE userid = ?
However, this will not work sensibly with two tables with different schemas.
If you for some reason need to fetch your data in using exactly one SELECT you can unify your resultsets for UNION ALL like this
SELECT 'film' type, iduser, film name, watches, note, NULL date
FROM films
WHERE iduser = ?
UNION ALL
SELECT 'event' type, iduser, event name, NULL, NULL, date
FROM events
WHERE iduser = ?
Another approach to grab all data in one go is to pack column values specific to particular table with GROUP_CONCAT into a details column and then explode it in client code
SELECT 'film' type, iduser, film name, GROUP_CONCAT(CONCAT_WS('|', watches, note)) details
FROM films
WHERE iduser = 1
GROUP BY iduser, film
UNION ALL
SELECT 'event' type, iduser, event name, GROUP_CONCAT(CONCAT_WS('|', date))
FROM events
WHERE iduser = 1
GROUP BY iduser, event
Here is SQLFiddle demo
In addition to UNION as others have mentioned, you can do this with multiple LEFT JOINS.
Something like
SELECT U.UID, F.FIELD1, E.FIELD1
FROM USERS U
LEFT JOIN EVENTS E ON U.USERID=E.USERID
LEFT JOIN FILMS F ON U.USERID=F.USERID
WHERE F.USERID IS NOT NULL OR F.USERID IS NOT NULL
Something like that could work based on your requirement. This way if the row was an EVENT row it would have fields for your event and if it was a film record the film fields would be populated.

Mysql error: Subquery returns more than 1 row (but I want it to - I want to select a vector on each row)

For the sake of simplicity, let's say I have tables users and interests
users
id | name
---------
1 | amy
2 | brian
3 | carole
interests
uid | interest
--------------
1 | apples
3 | catfish
3 | cobwebs
3 | cryogenics
What I want to get back is output that looks something like
name | interests
----------------
amy | apples
brian |
carole| catfish, cobwebs, cryogenics
Where interests could be a string consisting of the concatenation of all relevant values with some delimiter, or a vector of discrete values. I'm interested in dumping this to a file, rather than putting it in a table or doing any kind of further SQL stuff with it. Doing
SELECT name, (SELECT interest from interests where uid=id) as interests from users;
Is giving me the error I mentioned in the title. Is this just not possible in the SQL paradigm? I know I can dump a join of these tables to a file, and then aggregate the values I need using a python script or something, but this feels inelegant.
try this
SELECT name , group_concat(interest) as interests from interests
LEFT JOIN users on users.id = interests.uid
GROUP BY name
DEMO HERE
update:
if you want spaces do this group_concat(interest SEPARATOR ', ')
SELECT u.name, i.interest
FROM users AS u, interests AS i
WHERE u.uid = i.id
Try a join instead, this might work.
You might be getting errors because of the ordering of your select and from statements, or do you need to have a comma after name. Like:
SELECT name, ( *then your select statement here ...* )

How to filter duplicates within row using Distinct/group by with JOINS

For simplicity, I will give a quick example of what i am trying to achieve:
Table 1 - Members
ID | Name
--------------------
1 | John
2 | Mike
3 | Sam
Table 1 - Member_Selections
ID | planID
--------------------
1 | 1
1 | 2
1 | 1
2 | 2
2 | 3
3 | 2
3 | 1
Table 3 - Selection_Details
planID | Cost
--------------------
1 | 5
2 | 10
3 | 12
When i run my query, I want to return the sum of the all member selections grouped by member. The issue I face however (e.g. table 2 data) is that some members may have duplicate information within the system by mistake. While we do our best to filter this data up front, sometimes it slips through the cracks so when I make the necessary calls to the system to pull information, I also want to filter this data.
the results SHOULD show:
Results Table
ID | Name | Total_Cost
-----------------------------
1 | John | 15
2 | Mike | 22
3 | Sam | 15
but instead have John as $20 because he has plan ID #1 inserted twice by mistake.
My query is currently:
SELECT
sq.ID, sq.name, SUM(sq.premium) AS total_cost
FROM
(
SELECT
m.id, m.name, g.premium
FROM members m
INNER JOIN member_selections s USING(ID)
INNER JOIN selection_details g USING(planid)
) sq group by sq.agent
Adding DISTINCT s.planID filters the results incorrectly as it will only show a single PlanID 1 sold (even though members 1 and 3 bought it).
Any help is appreciated.
EDIT
There is also another table I forgot to mention which is the agent table (the agent who sold the plans to members).
the final group by statement groups ALL items sold by the agent ID (which turns the final results into a single row).
Perhaps the simplest solution is to put a unique composite key on the member_selections table:
alter table member_selections add unique key ms_key (ID, planID);
which would prevent any records from being added where the unique combo of ID/planID already exist elsewhere in the table. That'd allow only a single (1,1)
comment followup:
just saw your comment about the 'alter ignore...'. That's work fine, but you'd still be left with the bad duplicates in the table. I'd suggest doing the unique key, then manually cleaning up the table. The query I put in the comments should find all the duplicates for you, which you can then weed out by hand. once the table's clean, there'll be no need for the duplicate-handling version of the query.
Use UNIQUE keys to prevent accidental duplicate entries. This will eliminate the problem at the source, instead of when it starts to show symptoms. It also makes later queries easier, because you can count on having a consistent database.
What about:
SELECT
sq.ID, sq.name, SUM(sq.premium) AS total_cost
FROM
(
SELECT
m.id, m.name, g.premium
FROM members m
INNER JOIN
(select distinct ID, PlanID from member_selections) s
USING(ID)
INNER JOIN selection_details g USING(planid)
) sq group by sq.agent
By the way, is there a reason you don't have a primary key on member_selections that will prevent these duplicates from happening in the first place?
You can add a group by clause into the inner query, which groups by all three columns, basically returning only unique rows. (I also changed 'premium' to 'cost' to match your example tables, and dropped the agent part)
SELECT
sq.ID,
sq.name,
SUM(sq.Cost) AS total_cost
FROM
(
SELECT
m.id,
m.name,
g.Cost
FROM
members m
INNER JOIN member_selections s USING(ID)
INNER JOIN selection_details g USING(planid)
GROUP BY
m.ID,
m.NAME,
g.Cost
) sq
group by
sq.ID,
sq.NAME