How can I rewrite this to remove select statement joins? - mysql

As it is now each time I am selecting from Bill in my actual database I am searching over 1 million rows. There has to be a better way to write this query, but I haven't been able to figure it out. There has to be some way I can write this that doesn't join using the select statements.
If you notice I have some form of this join three seperate times. What should I do to remove this join?
join
(
select b.year
, sum(bli.amount) amt
from Bill b
join BillLine bli
on bli.bill_id = b.id
where bli.payment = 0
and bli.refund = 0
and bli.type = 3
group
by b.year
) org
on org.year = b.year
select b.Year,
Round(case when b.year = (select collectoryear -1 from Entity) then 0 else beg.amt end,2) Beginning,
Round(case when b.year >= (select collectoryear -1 from Entity) then ifnull(org.amt,0) else 0 end,2) Additions,
Round(case when b.year = (select collectoryear -1 from Entity) then 0 else beg.amt end + case when b.year >= (select collectoryear -1 from Entity) then ifnull(org.amt,0) else 0 end - ending.amt ,2) Collections,
Round(ending.amt,2) Ending
from Bill b
left join Levy l on l.year = b.year
join Entity e on b.year = e.year
join( select b.year, sum(bli.amount) amt from Bill b join BillLine bli on bli.bill_id = b.id where bli.payment = 0 and bli.refund = 0 and bli.type = 3 group by b.year) org on org.year = b.year
join( select b.year, sum(bli.amount) amt from Bill b join BillLine bli on bli.bill_id = b.id where bli.type = 2 group by b.year ) beg on beg.year = b.year
join( select b.year, sum(bli.amount) amt from Bill b join BillLine bli on bli.bill_id = b.id where bli.type = 2 group by b.year ) ending on ending.year = b.year
where b.year > (select collectoryear -11 from Entity)
and b.year < (select collectoryear from Entity)
group by b.year
order by Year desc ;
Here is the db-fiddle databases
create table Bill
( id varchar(20) PRIMARY KEY
, year varchar(20) FOREIGN KEY
);
Insert into Bill (id, year) values
(1, 2020),
(2, 2020),
(3, 2020),
(4, 2019),
(5, 2019),
(6, 2018),
(7, 2017),
(8, 2016),
(9, 2015),
(10, 2013);
create table BillLine
( id varchar(20) PRIMARY KEY
, bill_id varchar(20) FOREIGN KEY
, amount varchar(20)
, refund varchar(20)
, payment varchar(20)
, type varchar(20)
);
Insert into BillLine (id, bill_id, amount, refund, payment, type) values
(1, 10, 100.00, 0, 0, 3),
(2, 9, 250.00, 1, 1, 5),
(3, 8, 102.00, 0, 0, 3),
(4, 7, 85.00, 1, 1, 5),
(5, 6, 20.00, 0, 0, 3),
(6, 5, 43.75, 0, 1, 2),
(7, 4, 22.22, 0, 0, 3),
(8, 3, 125.25, 0, 1, 2),
(9, 2, 77.70, 0, 0, 3),
(10, 1, 100.75, 1, 1, 5);
create table Entity
( id varchar(20) PRIMARY KEY
, collectoryear varchar(20)
, year varchar(20)
);
Insert into Entity (id, collectoryear, year) values (1, 2021, 2020);
create table Levy
( id varchar(20) PRIMARY KEY, county varchar(20), year varchar(20)
);
Insert into Levy (id, county, year) values (1, null, null);

Related

MYSQL 5.6 get latest data of each user

My Database table is as shown below. I need to get latest mark of each student. Latest entry is the row with maximum udate and maximum oder. (The oder will be incremented by one on each entry with same date)
In my example, I have two students Mujeeb, Zakariya and two subjects ENGLISH, MATHS. I need to get latest mark of each student for each subject. My expectd result is as follows
My sample data is
DROP TABLE IF EXISTS `students`;
CREATE TABLE IF NOT EXISTS `students` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`udate` date NOT NULL,
`oder` int(11) NOT NULL,
`name` varchar(20) NOT NULL,
`Subject` varchar(20) NOT NULL,
`mark` int(11) NOT NULL,
PRIMARY KEY (`uid`)
) ENGINE=MyISAM AUTO_INCREMENT=13 DEFAULT CHARSET=latin1;
INSERT INTO `students` (`uid`, `udate`, `oder`, `name`, `Subject`, `mark`) VALUES
(1, '2021-08-01', 1, 'Mujeeb', 'ENGLISH', 10),
(2, '2021-08-01', 1, 'Zakariya', 'ENGLISH', 20),
(3, '2021-08-10', 2, 'Mujeeb', 'ENGLISH', 50),
(4, '2021-08-11', 2, 'Zakariya', 'ENGLISH', 60),
(5, '2021-08-02', 1, 'Mujeeb', 'ENGLISH', 100),
(6, '2021-08-03', 1, 'Zakariya', 'ENGLISH', 110),
(7, '2021-08-10', 1, 'Mujeeb', 'ENGLISH', 500),
(8, '2021-08-11', 1, 'Zakariya', 'ENGLISH', 600),
(9, '2021-08-01', 2, 'Mujeeb', 'MATHS', 100),
(10, '2021-08-01', 2, 'Zakariya', 'MATHS', 75),
(11, '2021-08-10', 3, 'Mujeeb', 'MATHS', 50),
(12, '2021-08-11', 3, 'Zakariya', 'MATHS', 60);
Use NOT EXISTS:
SELECT s1.*
FROM students s1
WHERE NOT EXISTS (
SELECT 1
FROM students s2
WHERE s2.name = s1.name AND s2.Subject = s1.Subject
AND (s2.udate > s1.udate OR (s2.udate = s1.udate AND s2.oder > s1.oder))
);
Or with a correlated subquery in the WHERE clause:
SELECT s1.*
FROM students s1
WHERE s1.uid = (
SELECT s2.uid
FROM students s2
WHERE s2.name = s1.name AND s2.Subject = s1.Subject
ORDER BY s2.udate DESC, s2.oder DESC LIMIT 1
);
See the demo.
As ROW_NUMBER() function doesn't work at lower version of MySQL, So alternate way of row_number() is used for this solution.
-- MySQL (v5.6)
SELECT p.uid, p.udate, p.oder, p.name, p.Subject, p.mark
FROM (SELECT #row_no := IF((#prev_val = t.name && #prev_val1 = t.Subject), #row_no + 1, 1) AS row_number
, #prev_val := t.name AS name
, #prev_val1 := t.Subject AS Subject
, t.mark
, t.oder
, t.uid
, t.udate
FROM students t,
(SELECT #row_no := 0) x,
(SELECT #prev_val := '') y,
(SELECT #prev_val1 := '') z
ORDER BY t.name, t.Subject, t.udate DESC, t.oder DESC ) p
WHERE p.row_number = 1
ORDER BY p.name, p.Subject;
Please check the url http://sqlfiddle.com/#!9/b5befe/18

Matching two column values

I have 2 tables. 'user_cities' and 'visits'. I want to check if a user visited a city which he was not supposed to visit.
CREATE TABLE `user_cities` (
`id` INT NOT NULL AUTO_INCREMENT,
`user_id` INT,
`name` varchar(255),
`city_id` INT,
PRIMARY KEY (`id`)
);
CREATE TABLE `visits` (
`id` INT NOT NULL AUTO_INCREMENT,
`user_id` INT,
`visit_id` INT,
`city_id` INT,
PRIMARY KEY (`id`)
);
INSERT INTO `user_cities` VALUES
(1, 1, 'John', 35),
(2, 1, 'John', 36),
(3, 1, 'John', 37),
(4, 2, 'Michael', 38),
(5, 2, 'Michael', 39);
INSERT INTO `visits` VALUES
(1, 1, 1, 35),
(2, 1, 2, 36),
(3, 1, 3, 38),
(4, 2, 4, 38),
(5, 2, 5, 39);
http://sqlfiddle.com/#!9/68c658
Example: John must visit only 35, 36 and 37. Michael must visit 38 and 39. These are defined in 'user_cities'
However, John has visited 38 (visits id 3)
How can i query users that visited the wrong city?
If you're not familiar with SQL this kind of problem is a bit trickier than it appears.
You want to select the information from the visits where that visit is not specified by user_cities. The usual way to do this is to use a LEFT JOIN to the user_cities table for the record that validates the visit, and then to find entries where the LEFT JOIN can't find a match.
So your query would be:
SELECT *
FROM visits
LEFT JOIN user_cities
ON(visits.user_id = user_cities.user_id AND
visits.city_id = user_cities.city_id)
WHERE user_cities.id IS NULL
Of course you would probably also extend the query to give you the name of the user (and the coty, but that's not in your example code)
You can try using left join
DEMO
select a.user_id,a.city_id
FROM visits a left join user_cities b on a.user_id=b.user_id and
a.city_id=b.city_id
where b.user_id is null
OUTPUT:
1 John 37
The only way I can see to do this is via a full outer join (which MySQL doesn't even support, so we have to use a workaround):
SELECT COALESCE(user_id_1, user_id_2) AS user_id_matching
FROM
(
SELECT
uc.user_id AS user_id_1,
uc.city_id AS city_id_1,
v.user_id AS user_id_2,
v.city_id AS city_id_2
FROM user_cities uc
LEFT JOIN visits v
ON uc.user_id = v.user_id AND uc.city_id = v.city_id
UNION ALL
SELECT
uc.user_id AS user_id_1,
uc.city_id AS city_id_1,
v.user_id AS user_id_2,
v.city_id AS city_id_2
FROM user_cities uc
RIGHT JOIN visits v
ON uc.user_id = v.user_id AND uc.city_id = v.city_id
WHERE uc.user_id IS NULL
) t
GROUP BY COALESCE(user_id_1, user_id_2)
HAVING
COUNT(CASE WHEN user_id_1 IS NULL THEN 1 END) = 0 AND
COUNT(CASE WHEN user_id_2 IS NULL THEN 1 END) = 0;
Demo

mySQL - SUM and COUNT and JOIN to display all records [duplicate]

This question already has answers here:
How can I do a FULL OUTER JOIN in MySQL?
(15 answers)
Closed 4 years ago.
This is a following question to this one Join two tables with SUM and COUNT.
What I try to do is to have all values displayed as some are in history table and not in rota table or vice-versa (999 and 777)
So my tables are:
create table history (
code int(10) primary key,
PN varchar(10) not null,
Qty int(10) not null,
LOC_ID int(10));
insert into history values (1, 'T1', 1, 1);
insert into history values (2, 'A1', 2,2);
insert into history values (3, 'J1', 3,3);
insert into history values (4, 'A2', 1,4);
insert into history values (5, 'J2', 2,1);
insert into history values (6, 'A3', 3,2);
insert into history values (7, 'J3', 4,3);
insert into history values (8, 'T1', 5,4);
insert into history values (9, 'A1', 1,1);
insert into history values (10, '999', 3,2);
insert into history values (11, 'J2', 4,3);
insert into history values (12, 'A1', 3,4);
insert into history values (13, 'J2', 5,1);
create table rota (
code int(10) primary key,
PN varchar(10) not null,
SN varchar(10) not null,
LOC_ID int(10));
insert into rota values (1, 'T1', 't1a',1);
insert into rota values (2, 'A1', 'a1a',2);
insert into rota values (3, 'J1', 'j1a',3);
insert into rota values (4, 'A2', 'a2a',4);
insert into rota values (5, 'J2', 'j2a',1);
insert into rota values (6, 'A3', 'a3a',2);
insert into rota values (7, 'J3', 'j3a',3);
insert into rota values (8, '777', 't1b',4);
insert into rota values (9, 'A1', 'a1b',1);
insert into rota values (10, 'J2', 'j2b',2);
insert into rota values (11, 'J2', 'j2c',3);
insert into rota values (12, 'A1', 'a1c',4);
insert into rota values (13, 'J2', 'j2d',1);
insert into rota values (14, 'J2', 'j2e',2);
insert into rota values (15, 'J2', 'j2f',3);
create table loca (
code1 int(10) primary key,
LOC varchar(10) not null);
insert into loca values (1, 'AAA');
insert into loca values (2, 'BBB');
insert into loca values (3, 'CCC');
insert into loca values (4, 'DDD');
The code I have got is
select CASE WHEN a.pn IS NULL THEN b.pn ELSE a.pn END AS PN
, a.q
, b.c
, a.LOC_ID
, b.LOC_ID
from
(select
h.pn
, sum(qty) q
, h.LOC_ID
from
history h
group by h.pn, h.LOC_ID) a
RIGHT JOIN
(select
r.pn
, count(sn) c
, r.LOC_ID
from
rota r
group by r.pn, r.LOC_ID) b
on a.pn = b.pn WHERE a.LOC_ID = b.LOC_ID
order by a.pn;
The above code works great for all PN that are in both tables. The problem is for values that are specific to one of the tables. I can remove the WHERE clause from JOIN but it is not corect. The question is - how to get all PNs from history and rota where some of them are present i just one table. I had some luck with RIGHT JOIN but that did not cover unique values from the other table. Any one came across solution before?
Results shoud look like the following table
PN LOC_ID Count Qty
T1 1 1 1
A1 2 1 2
J1 3 1 3
A2 4 1 1
J2 1 2 2
A3 2 1 3
J3 3 1 4
777 4 1 NULL
A1 1 1 1
J2 2 2 NULL
J2 3 2 4
A1 4 1 3
J2 1 2 2
J2 2 2 NULL
J2 3 2 4
999 2 NULL 3
use another join and that is left and make them union
select t.PN,t.q,t.c,t.LOC_ID,t.LOC_ID_b from
(
select CASE WHEN a.pn IS NULL THEN b.pn ELSE a.pn END AS PN
, a.q
, b.c
, a.LOC_ID
, b.LOC_ID as LOC_ID_b
from
(select
h.pn
, sum(qty) q
, h.LOC_ID
from
history h
group by h.pn, h.LOC_ID) a
RIGHT JOIN
(select
r.pn
, count(sn) c
, r.LOC_ID
from
rota r
group by r.pn, r.LOC_ID) b
on a.pn = b.pn and a.LOC_ID = b.LOC_ID
) as t
union
select t2.PN,t2.q,t2.c,t2.LOC_ID,t2.LOC_ID_b from
(
select CASE WHEN a.pn IS NULL THEN b.pn ELSE a.pn END AS PN
, a.q
, b.c
, a.LOC_ID
, b.LOC_ID as LOC_ID_b
from
(select
h.pn
, sum(qty) q
, h.LOC_ID
from
history h
group by h.pn, h.LOC_ID) a
left JOIN
(select
r.pn
, count(sn) c
, r.LOC_ID
from
rota r
group by r.pn, r.LOC_ID
) b
on a.pn = b.pn and a.LOC_ID = b.LOC_ID
) t2
http://sqlfiddle.com/#!9/c20c81/20

Two Limit Clauses in one statement?

I am trying to build a product review page where users can comment any submitted reviews like Amazon. I want the page to display 3 user reviews as well as 2 responses to each of the reviews if they exist.
Here's the table
CREATE TABLE product_review
(`ID` int, `username` varchar(21), `review_title` varchar(30), `review_or_reply` int)
;
INSERT INTO product_review
(`ID`, `username`, `review_title`, `review_or_reply`)
VALUES
(1, 'Tom', 'Rip-off', 0),
(2, 'Peter', 'Rip-off', 1),
(3, 'May', 'Rip-off', 1),
(4, 'June', 'Rip-off', 1),
(5, 'Tommy', 'Worth the Price', 0),
(6, 'Sammy', 'Worth the Price', 1),
(7, 'Sam', 'Worth the Price',1),
(8, 'Bryan','Worth the Price',1),
(9, 'Sally', 'Average Product', 0)
;
The review_or_reply field is effectively a Yes or No field, where 0 means it's a review and 1 is the review's comments by other users.
Is there a single select statement that can limit 3 reviews and bring up two of their comments? For example:
Select `username`,`review_title`,`reply` from product_review where review_or_reply ='0' Limit 3
Select `username`,`review_title`,`reply` from product_review where review_or_reply = '1' and title = 'Rip-off' Limit 2
Select `username`,`review_title`,`reply` from product_review where review_or_reply = '1' and title = 'Worth the Price' Limit 2
Select `username`,`review_title`,`reply` from product_review where review_or_reply = '1' and title = 'Average Product' Limit 2
I want the output to be like this:
username review_title review_or_reply
Tom Rip-off 0
Peter Rip-off 1
May Rip-off 1
Tommy Worth the Price 0
Sammy Worth the Price 1
Sam Worth the Price 1
Sally Average Product 0
this will return 3 review_titles and then pull out two responses to that
SELECT
pr.*,
IF( #A = t.review_title,
IF(#B = 3, #B := 1, #B := #B +1)
, #B
) AS group_col,
#A := t.review_title
FROM (
SELECT
id,
username,
review_title
FROM product_review
WHERE reply ='0' LIMIT 3
) t
JOIN product_review pr ON pr.review_title=t.review_title
CROSS JOIN (SELECT #A := "", #B := 1) AS temp
GROUP BY group_col, review_title
ORDER BY id;
EDIT:
if there are more than one reply that is 0 in the database like so then this query will check for that. (since you did specify in the other queries that the reply had to be 1).
INSERT INTO product_review
(`ID`, `username`, `review_title`, `reply`)
VALUES
(1, 'Tom', 'Rip-off', 0),
(2, 'Peter', 'Rip-off', 1),
(3, 'May', 'Rip-off', 0),
(4, 'June', 'Rip-off', 1),
(5, 'Tommy', 'Worth the Price', 0),
(6, 'Sammy', 'Worth the Price', 1),
(7, 'Sam', 'Worth the Price',1),
(8, 'Bryan','Worth the Price',1),
(9, 'Sally', 'Average Product', 0),
(10, 'Timothy', 'Rip-off', 1)
notice that at id 3 there is a reply of 0 with id 10 a reply of 1. this query will correctly skip the reply = 0.
SELECT
pr.*,
IF( #A = t.review_title,
IF(pr.reply = 0, 1,
IF(#B = 3, #B := 1, #B := #B +1)
), #B
) AS group_col,
#A := t.review_title
FROM (
SELECT
DISTINCT
id,
username,
review_title
FROM product_review
WHERE reply ='0'
GROUP BY review_title
LIMIT 3
) t
JOIN product_review pr ON pr.review_title=t.review_title
CROSS JOIN (SELECT #A := "", #B := 1) AS temp
GROUP BY group_col, review_title
ORDER BY id;
DEMO
...or slower but simpler...
SELECT x.*
FROM product_review x
JOIN product_review y
ON y.review_title = x.review_title
AND y.id <= x.id
GROUP
BY x.id
HAVING COUNT(*) <= 3
ORDER
BY MIN(y.id)
LIMIT 3;

Multiple INNER JOIN subqueries sql

I have the below query which comes from this post count number of items in a row in mysql which counts how many times in a row a student has been present/absent from a class.
SELECT
classlist.studentid,
student.name,
classStatus.name status,
COUNT(*) presentcnt
FROM
classlist
INNER JOIN student ON classlist.studentid=student.id
INNER JOIN classstatus ON classlist.presentid=classstatus.id
INNER JOIN (
SELECT
studentid,
max(CASE WHEN presentid=0 THEN id END) max_0,
max(CASE WHEN presentid=1 THEN id END) max_1
FROM classlist
GROUP BY studentid
) s
ON coalesce(classlist.id>least(max_0,max_1) AND classlist.id<=greatest(max_0,max_1),1) AND s.studentid=classlist.studentid
GROUP BY classlist.studentid
This works as expected,
STUDENTID NAME STATUS PRESENTCNT
111 John Present 1
222 Kate Absent 2
333 Matt Present 5
I want to extend the query so that I have a column showing if the student particpated in the class.
If I run an independent query I get the results I want
SELECT
classlist.studentid,
student.name,
participatedStatus.name status,
COUNT(*) participatedcnt
FROM
classlist
INNER JOIN student ON classlist.studentid=student.id
INNER JOIN participatedStatus ON classlist.participatedid=participatedStatus.id
INNER JOIN (
SELECT
studentid,
max(CASE WHEN participatedid=0 THEN id END) max_0,
max(CASE WHEN participatedid=1 THEN id END) max_1
FROM classlist
group by studentid
) s
ON coalesce(classlist.id>least(max_0,max_1)
AND classlist.id<=greatest(max_0,max_1),1)
AND s.studentid=classlist.studentid
group by classlist.studentid
STUDENTID NAME STATUS PARTICIPATEDCNT
111 John Yes 1
222 Kate No 2
333 Matt Yes 2
However I want to merge them into the one query so I get
STUDENTID NAME STATUS PRESENTCNT STATUS2 PARTICIPATEDCNT
111 John Present 1 Yes 1
222 Kate Absent 2 No 2
333 Matt Present 5 Yes 2
I am confused about how this can be achieved as I am selecting count *, how can I acheive this?
A sample of the data I am using is in this fiddle and below
CREATE TABLE classlist
(`id` int, `studentid` int, `subjectid` int, `presentid` int, `participatedid` int);
CREATE TABLE student
(`id` int, `name` varchar(4));
CREATE TABLE subject
(`id` int, `name` varchar(4));
CREATE TABLE classStatus
(`id` int, `name` varchar(8));
CREATE TABLE participatedStatus
(`id` int, `name` varchar(8));
INSERT INTO classlist (`id`, `studentid`, `subjectid`, `presentid`, `participatedid`)
VALUES (1, 111, 1, 1, 0), (2, 222, 3, 0, 0), (3, 333, 2, 1, 0), (4, 111, 4, 0, 0), (5, 111, 1, 1, 0), (6, 222, 3, 0, 0), (7, 333, 2, 1, 1), (8, 111, 4, 0, 0), (9, 111, 4, 0, 0), (10, 111, 4, 0, 0), (11, 111, 1, 1, 1), (12, 333, 3, 1, 0), (13, 333, 2, 1, 1), (14, 333, 3, 1, 1);
INSERT INTO student (`id`, `name`)
VALUES (111, 'John'),(222, 'Kate'),(333, 'Matt');
INSERT INTO subject (`id`, `name`)
VALUES (1, 'MATH'),(2, 'ENG'),(3, 'SCI'),(4, 'GEO');
INSERT INTO classStatus (`id`, `name`)
VALUES (0, 'Absent'), (1, 'Present');
INSERT INTO participatedStatus (`id`, `name`)
VALUES (0, 'No'),(1, 'Yes');
SELECT
studid,
studname,
status,
presentcnt,
status1,
participatedcnt FROM
(SELECT
classlist.studentid studid,
student.name studname,
classStatus.name status,
COUNT(*) presentcnt
FROM
classlist
INNER JOIN student ON classlist.studentid=student.id
INNER JOIN classstatus ON classlist.presentid=classstatus.id
INNER JOIN (
SELECT
studentid,
max(CASE WHEN presentid=0 THEN id END) max_0,
max(CASE WHEN presentid=1 THEN id END) max_1
FROM classlist
GROUP BY studentid
) s
ON coalesce(classlist.id>least(max_0,max_1) AND classlist.id<=greatest(max_0,max_1),1) AND s.studentid=classlist.studentid
GROUP BY classlist.studentid)x
JOIN
(SELECT
classlist.studentid,
student.name,
participatedStatus.name status1,
COUNT(*) participatedcnt
FROM
classlist
INNER JOIN student ON classlist.studentid=student.id
INNER JOIN participatedStatus ON classlist.participatedid=participatedStatus.id
INNER JOIN (
SELECT
studentid,
max(CASE WHEN participatedid=0 THEN id END) max_0,
max(CASE WHEN participatedid=1 THEN id END) max_1
FROM classlist
group by studentid
) s
ON coalesce(classlist.id>least(max_0,max_1)
AND classlist.id<=greatest(max_0,max_1),1)
AND s.studentid=classlist.studentid
group by classlist.studentid)y
ON x.studid=y.studentid
Fiddle