I have three tables, libraryitems, copies and loans.
A libraryitem hasMany copies, and a copy hasMany loans.
I'm trying to get the latest loan entry for a copy only; The query below returns all loans for a given copy.
SELECT
libraryitems.title,
copies.id,
copies.qruuid,
loans.id AS loanid,
loans.status,
loans.byname,
loans.byemail,
loans.createdAt
FROM copies
INNER JOIN libraryitems ON copies.libraryitemid = libraryitems.id AND libraryitems.deletedAt IS NULL
LEFT OUTER JOIN loans ON copies.id = loans.copyid
WHERE copies.libraryitemid = 1
ORDER BY copies.id ASC, loans.createdAt DESC
I know there needs to be a sub select of some description in here, but struggling to get the correct syntax. How do I only return the latest, i.e MAX(loans.createdAt) row for each distinct copy? Just using group by copies.id returns the earliest, rather than latest entry.
Image example below:
in the subquery , getting maximum created time for a loan i.e. latest entry and joining back with loans to get other details.
SELECT
T.title,
T.id,
T.qruuid,
loans.id AS loanid,
loans.status,
loans.byname,
loans.byemail,
loans.createdAt
FROM
(
SELECT C.id, C.qruuid, L.title, MAX(LN.createdAt) as maxCreatedTime
FROM Copies C
INNER JOIN libraryitems L ON C.libraryitemid = L.id
AND L.deletedAt IS NULL
LEFT OUTER JOIN loans LN ON C.id = LN.copyid
GROUP BY C.id, C.qruuid, L.title) T
JOIN loans ON T.id = loans.copyid
AND T.maxCreatedTime = loans.createdAt
A self left join on loans table will give you latest loan of a copy, you may join the query to the other tables to fetch the desired output.
select * from loans A
left outer join loans B
on A.copyid = B.copyid and A.createdAt < B.createdAt
where B.createdAt is null;
This is your query with one simple modification -- table aliases to make it clearer.
SELECT li.title, c.id, c.qruuid,
l.id AS loanid, l.status, l.byname, l.byemail, l.createdAt
FROM copies c INNER JOIN
libraryitems li
ON c.libraryitemid = li.id AND
li.deletedAt IS NULL LEFT JOIN
loans l
ON c.id = l.copyid
WHERE c.libraryitemid = 1
ORDER BY c.id ASC, l.createdAt DESC ;
With this as a beginning let's think about what you need. You want the load with the latest createdAt date for each c.id. You can get this information with a subquery:
select l.copyid, max(createdAt)
from loans
group by l.copyId
Now, you just need to join this information back in:
SELECT li.title, c.id, c.qruuid,
l.id AS loanid, l.status, l.byname, l.byemail, l.createdAt
FROM copies c INNER JOIN
libraryitems li
ON c.libraryitemid = li.id AND
li.deletedAt IS NULL LEFT JOIN
loans l
ON c.id = l.copyid LEFT JOIN
(SELECT l.copyid, max(l.createdAt) as maxca
FROM loans
GROUP BY l.copyid
) lmax
ON l.copyId = lmax.copyId and l.createdAt = lmax.maxca
WHERE c.libraryitemid = 1
ORDER BY c.id ASC, l.createdAt DESC ;
This should give you the most recent record. And, the use of left join should keep all copies, even those that have never been leant.
Related
I am trying to get a count of the number of logins during a given timeframe, currently my SQL query displays only results that had at least one login, I'd like it to display even those which have zero logins.
Query i'm using:
SELECT c.FullName, COUNT(l.Id)
FROM LoginsTable l JOIN UsersTable u ON u.Email = l.Email JOIN Organisations c ON c.Id = u.OrganisationId
WHERE l.AttemptTime > "2019-10-01" AND l.AttemptTime < "2019-11-01" AND l.Success = 1
GROUP BY c.Name
ORDER BY c.Name ASC;
You have a few issues. Firstly, you either need to use a RIGHT JOIN from LoginsTable or reorder the JOINs to put the JOIN to LoginsTable last and use a LEFT JOIN. Given the nature of your query the latter probably makes more sense.
Secondly, you need to put any conditions on fields from a table which has been LEFT JOINed into the join condition, otherwise MySQL converts the LEFT JOIN into an INNER JOIN (see the manual). Finally, you should GROUP BY the same fields as specified in your SELECT. This should work:
SELECT c.FullName, COUNT(l.Id)
FROM Organisations c
JOIN UsersTable u ON u.OrganisationId = c.Id
LEFT JOIN LoginsTable l ON u.Email = l.Email AND l.AttemptTime > "2019-10-01" AND l.AttemptTime < "2019-11-01" AND l.Success = 1
GROUP BY c.FullName
ORDER BY c.FullName
I found 2 issues here:
your group by column is not listed on your column
date condition is using double quotes.
try below query.
SELECT c.FullName, COUNT(l.Id)
FROM LoginsTable l
LEFT JOIN UsersTable u ON u.Email = l.Email
LEFT JOIN Organisations c ON c.Id = u.OrganisationId
WHERE l.AttemptTime between '2019-10-01' AND '2019-11-01' AND l.Success = 1
GROUP BY c.FullName
ORDER BY c.FullName ASC;
As Roman Hocke said you need to use left join as below :
SELECT c.FullName, COUNT(l.Id)
FROM UsersTable u
JOIN Organisations c ON c.Id = u.OrganisationId
LEFT JOIN LoginsTable l ON u.Email = l.Email
WHERE l.AttemptTime > "2019-10-01" AND l.AttemptTime < "2019-11-01" AND l.Success = 1
GROUP BY c.Name
ORDER BY c.Name ASC;
Moreover, you should fix your group by or select using the same field : SELECT c.Name or GROUP BY c.FullName ORDER BY c.FullName
EDIT : Nick's answer is the one. As he said perfectly well, you need to put your conditions in the on clause of your left join.
SELECT c.FullName, COUNT(l.Id)
FROM UsersTable u
JOIN Organisations c ON c.Id = u.OrganisationId
LEFT JOIN LoginsTable l ON (u.Email = l.Email AND l.AttemptTime > "2019-10-01" AND l.AttemptTime < "2019-11-01" AND l.Success = 1)
GROUP BY c.FullName
ORDER BY c.FullName ASC;
When i added a left join for getting count of foreign table, its multiply my sum value of other left join table with the count, also I cant use distinct sum here as two values can be same:
SELECT c.id as company_id, SUM(ct.amount) as total_billed, count(l.id) as load_count
FROM tbl_companies c
LEFT JOIN tbl_company_transactions ct ON c.id = ct.company_id
LEFT JOIN tbl_loads l ON c.id = l.company_id
GROUP BY c.id;
You need to pre-aggregate the data:
SELECT c.id as company_id, ct.total_billed,
l.load_count
FROM tbl_companies c LEFT JOIN
(SELECT ct.company_id, SUM(ct.amount) as total_billed
FROM tbl_company_transactions ct
GROUP BY ct.company_id
) ct
ON c.id = ct.company_id LEFT JOIN
(SELECT l.company_id, COUNT(*) as load_count
FROM tbl_loads l
GROUP BY l.company_id
) l
ON c.id = l.company_id;
As you have observed, the JOIN multiplies the number of rows and affects the aggregations.
You could isolate aggregate statistics and join results afterwards.
WITH
tranStats AS (
SELECT company_id, SUM(amount) AS total_billed
FROM tbl_company_transactions
GROUP BY company_id
),
loadStats AS (
SELECT company_id, COUNT(1) AS load_count
FROM tbl_loads
GROUP BY company_id
)
SELECT id, total_billed, load_count
FROM tbl_companies c
LEFT JOIN tranStats t ON t.company_id = c.id
LEFT JOIN loadStats l ON l.company_id = c.id
Gordon's answer is more scalable but for this specific query you only need one subquery — which may also offer a performance boost since joins on the pre-aggregated data may not be able to use indexes.
SELECT c.id as company_id, SUM(ct.amount) as total_billed, l.load_count
FROM tbl_companies c
LEFT JOIN tbl_company_transactions ct ON c.id = ct.company_id
LEFT JOIN (
SELECT company_id, count(*) as load_count
FROM tbl_loads
GROUP BY company_id
) l ON c.id = l.company_id
GROUP BY c.id;
The important thing to grasp is that if you need results of an aggregate function like SUM() or COUNT(), you need to be careful when you perform more than one join with multiple rows.
I have a database structure with the following setup:
po: id, stockNumber, factoryId, other columns
order: id, stockNumber, factoryId, other columns
stock_number: id, stockNumber, groupId
factory: id, name, groupId
The important part here is the stock_number/factory tables. The groupId column is just an integer and if two or more rows in the table have the same value then their stock numbers/factory are considered the same. Typically this is used for different sizes of the same product.
What I'd like to do is write a query that will join "order" to "po" through the group of stock_number and factory so I can find orders with no matching po. Also the factory has to match the same way.
I have this query if I have a specific stock number/factory in mind but I'd like to update it to query the whole orders table for me:
SELECT id
FROM order
WHERE
styleNumber IN (SELECT a.stockNumber FROM stock_number a INNER JOIN stock_number b ON a.groupId = b.groupId or a.id = b.id WHERE b.stockNumber = '123')
AND factoryId IN (SELECT a.submitter_id FROM submitter a INNER JOIN submitter b ON a.groupId = b.groupId OR a.submitter_id = b.submitter_id WHERE b.SUBMITTER_ID = 'alpha');
EDIT: I came up with this query which I think might be on the right track. It only joins in the stock number so it doesn't do factory yet. Can anyone confirm if I'm going in the correct direction:
SELECT *
FROM order o
LEFT JOIN stock_number s_o ON o.stockNumber = s_o.stockNumber
LEFT JOIN stock_number s_p ON s_o.groupId = s_p.groupId
LEFT JOIN po p ON s_p.stockNumber = p.stockNumber
WHERE p.id IS NULL;
Just join all the tables.
select o.id
FROM order AS o
JOIN stock_number AS sn ON sn.stockNumber = o.stockNumber
JOIN submitter AS su ON ON o.factoryId = su.submitter_id
You could use an anti-join pattern. In this example, it looks complicated because of the two relationship tables. But a query something like this:
SELECT o.id
, o.stockNumber
, o.factoryId
FROM `order` o
LEFT
JOIN `stock_number` s
ON s.stockNumber = o.stockNumber
LEFT
JOIN `factory` f
ON f.id = o.factoryId
AND f.groupId = s.groupId
LEFT
JOIN `po` p
ON p.stockNumber = s.stockNumber
AND p.factoryId = f.id
WHERE p.id IS NULL
The anti-join pattern is easier to visualize with a simpler example. Say you had the order table (as in your example), and an order_line table, with rows related to the order table by the order_id column.
order_line: id, order_id, othercolumns
To get order along with matching order_line rows:
SELECT o.id AS order_id
, l.id AS line_id
FROM `order` o
JOIN `order_line` l
ON l.order_id = o.id
To include rows from order that don't have any matching rows in order_line, we can use an outer join. We add the LEFT keyword:
SELECT o.id AS order_id
, l.id AS line_id
FROM `order` o
LEFT
JOIN `order_line` l
ON l.order_id = o.id
That gets all rows from order, including rows that don't have a matching row in order_line. The trick now is to exclude all the rows that have a matching row. For any rows that didn't have a match, the columns from order_line will be NULL. So we can add a test in the WHERE clause, to exclude rows that had a match.
SELECT o.id AS order_id
, l.id AS line_id
FROM `order` o
LEFT
JOIN `order_line` l
ON l.order_id = o.id
WHERE l.order_id IS NULL
That gets us rows from order that don't have a matching row in order_line.
We can use this same pattern in a more complicated query. We use outer join operations, and rows from order that don't have a matching row in po will have NULL values for the columns from po.
I have a database with the tables:
Student(SID,Name,Surname,Age)
Registration(StudentID,CourseID)
Course(CID,Name,Cost)
I would like to extract only the name of the courses with students younger than 20. Will the query below do just that?
SELECT C.NAME
FROM Course C
INNER JOIN Registration
INNER JOIN Student S
WHERE CID = CourseID
AND SID = StudentID
AND Age < 20
GROUP BY C.NAME
I would also like to extract the number of students in each course having students younger than 20. Is it correct to do it as below?
SELECT count(S.NAME)
,C.NAME
FROM Student S
INNER JOIN Course C
INNER JOIN Registration
WHERE Age < 20
AND CID = CourseID
AND SID = StudentID
GROUP BY C.NAME
You are missing the ON part for the join otherwise it would just be a CROSS JOIN.
Your first query should look like this if you want just a distinct list of student names:
SELECT DISTINCT C.NAME
FROM Course C
INNER JOIN Registration R ON C.CID = R.CourseID
INNER JOIN Student S ON R.StudentID = S.SID
WHERE Age < 20
Your second query shouldn't really have the C.Name in the select if you want to get just a count unless you want a count of how many students have that name.
SELECT count(*)
FROM Student S
INNER JOIN Registration R ON s.SID = R.StudentID
INNER JOIN Course C ON c.CID = R.CourseID
WHERE Age < 20
GROUP BY C.NAME
First join these tables, then group by Course's PK(CID), Add the HAVING condition to filter the course which has students younger than 20.
Then use Course table to join the result to get the course name and count of students in the course.
SELECT
T1.Name,
T2.StudentCount
FROM
Course T1
INNER JOIN (
SELECT
c.CID,
COUNT(s.SID) AS StudentCount
FROM
Course c
LEFT JOIN Registration r ON c.CID = r.CourseID
LEFT JOIN Student s ON s.SID = r.StudentID
GROUP BY c.CID
HAVING COUNT(IF(s.Age < 20, 1, NULL)) > 0
) T2 ON T1.CID = T2.CID
More correctly, you should move the conditions of the join, to the join statements themselves by including them in the on clause instead of the where. While the results may not change in this instance, if you were to start including outer joins you would encounter difficulties.
SELECT count(S.NAME)
,C.NAME
FROM Student S
INNER JOIN Registration R
ON s.SID = R.StudentID
INNER JOIN Course C
ON c.CID = R.CourseID
WHERE Age < 20
GROUP BY C.NAME
There's a fiddle here showing it in action: http://sqlfiddle.com/#!9/c3b8f/1
Your first query will also produce the results you want, but again, you should move the join predicates to the join itself. Also, you don't need to perform the grouping just to get distinct values, mysql has an expression for that called distinct. So rewritten, the first query would look like:
SELECT DISTINCT C.NAME
FROM Student S
INNER JOIN Registration R
ON s.SID = R.StudentID
INNER JOIN Course C
ON c.CID = R.CourseID
WHERE Age < 20.
Again, the results are the same as what you have already but it is easier to 'read' and will put you in good stead when you move on to other queries. As it stands you have mixed implicit and explicit join syntax.
This fiddle demonstrates both queries: http://sqlfiddle.com/#!9/c3b8f/4
edit
I may have misinterpreted your original question - if you want the total number of students enrolled in a course with at least one student under 19, you can use a query like this:
select name, count(*)
from course c
inner join registration r
on c.cid = r.courseid
where exists (
select 1
from course cc
inner join registration r
on cc.cid = r.courseid
inner join student s
on s.sid = r.studentid
where cc.cid = c.cid
group by cc.cid
having min(s.age) < 20
)
group by name;
Again with the updated fiddle here: http://sqlfiddle.com/#!9/c3b8f/17
Facing a problem and not getting the hint for a few hours. Maybe onyone can help me out.
Have the following query which shows the Topsellers. So the status of the product (active or not) is saved in b.Article_Status (0=inactive, 1=active).
How do I get the products of the result list which have no active product in the productfamily at the moment. But the product shall still be shown if an old one was ordered (and so is in table order_items) is now inactive and the active one was not ordered yet.
Actual query looks as follow. Already fund a solution which works when the actual active product has been ordered once, but still the problem with the mentioned case.
SELECT count( a.order_itemid ) AS numOrders, c.Product_ID, c.Product_Name, d.producer_name
FROM order_items a
LEFT OUTER JOIN product_article b ON b.Article_ID = a.order_itemid
LEFT OUTER JOIN product c ON b.Article_Productid = c.Product_ID
LEFT OUTER JOIN producer d ON c.Product_Producer = d.producer_id
GROUP BY c.Product_ID
ORDER BY `numOrders` DESC
Solution was a WHERE EXISTS subquery
SELECT count( a.order_itemid ) AS numOrders, c.Product_ID, c.Product_Name, d.producer_name
FROM order_items a
LEFT OUTER JOIN product_article b ON b.Article_ID = a.order_itemid
LEFT OUTER JOIN product c ON b.Article_Productid = c.Product_ID
LEFT OUTER JOIN producer d ON c.Product_Producer = d.producer_id
WHERE EXISTS (SELECT * FROM product_article x WHERE c.Product_ID = x.Article_Productid AND x.Article_Status = 1)
GROUP BY c.Product_ID
ORDER BY `numOrders` DESC
LIMIT 5