Rename duplicate rows in MySQL, WHERE IN () - dosen't works fine - mysql

I started from here:
Rename duplicate rows in MySQL
SELECT mid(s,instr(s,',')+1) as dubid
FROM (
SELECT group_concat(id ORDER BY id ASC) s, count(*) c
FROM `...`
GROUP BY field
HAVING c > 1) as z
Seems to work fine and returns dubid rows like
1
3,5,6
8
10,15
So I made this:
set #suf:=0;
UPDATE `table_cf_vendlist`
SET `cf_vendlist` = concat(`cf_vendlist`, ' \(', #suf:=#suf+1,'\)')
WHERE `cf_vendlistid` IN ({SELECT mid...})
But it renames only first ID from each row. So how I can make it work? Also how to nullyfy #suf counter for each row?
And no PHP. MySQL only!
set #suf:=0;
set #last := 0;
UPDATE `vtiger_cf_vendlist` t1
INNER JOIN (
SELECT MIN(cf_vendlistid) as id, cf_vendlist
FROM vtiger_cf_vendlist
GROUP BY cf_vendlist) AS unique1
ON (t1.cf_vendlist = unique1.cf_vendlist)
SET t1.`cf_vendlist` = concat(t1.`cf_vendlist`, ' \(',
#suf := if (#last = t1.cf_vendlistid, #suf+1, 1 and #last := t1.cf_vendlist), '\)')
WHERE t1.cf_vendlistid <> unique1.id
But I am not shure where is t1.cf_vendlist, and where is unique1.cf_vendlist. Also result is 0 and 1 in names, no increments and zeroing for each id set. But still, loks like it renamed everyting is needed.
Here is some data to run with sqlfiddle.com:
CREATE TABLE IF NOT EXISTS `table_cf_vendlist` (
`cf_vendlistid` int(11) NOT NULL AUTO_INCREMENT,
`cf_vendlist` varchar(200) NOT NULL,
PRIMARY KEY (`cf_vendlistid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=78 ;
INSERT INTO `table_cf_vendlist` (`cf_vendlistid`, `cf_vendlist`) VALUES
(2, 'Поставщик 2'),
(3, 'Поставщик 3'),
(4, 'Поставщик 4'),
(5, 'del 1'),
(6, 'Поставщик 1 (1)'),
(7, 'Поставщик 1 (2)'),
(8, 'пост 66'),
(9, 'пост 77'),
(10, 'пост 67'),
(11, 'пост 5'),
(12, '123'),
(13, '102'),
(14, 'пост 5 (1)'),
(15, 'пост 70'),
(47, 'Поставщик 1'),
(48, 'Поставщик 2'),
(49, 'Поставщик 3'),
(50, 'Поставщик 4'),
(51, 'del 1'),
(52, 'Поставщик 1'),
(53, 'Поставщик 1'),
(54, 'пост 5'),
(55, '124523452346256456'),
(56, 'пост 66'),
(57, 'пост 77'),
(58, 'пост 5'),
(59, 'пост 5'),
(60, 'пост 5'),
(61, 'пост 67'),
(62, '123'),
(63, '123'),
(64, '123'),
(65, 'пост 5'),
(66, 'пост 68'),
(67, '123'),
(68, '123'),
(69, '123'),
(70, '123'),
(71, '102'),
(72, 'пост 5'),
(73, 'пост 70');
UPD
We got
52 Поставщик 1 (11)
53 Поставщик 1 (12)
Instead of
52 Поставщик 1 (3)
53 Поставщик 1 (4)
and
48 Поставщик 2 (13)
49 Поставщик 3 (14)
50 Поставщик 4 (15)
Instead of
48 Поставщик 2 (2)
49 Поставщик 3 (2)
50 Поставщик 4 (2)
UPD 2:
set #suf:=0;
set #last := 0;
UPDATE table_cf_vendlist t
INNER JOIN (SELECT cf_vendlistid, concat(cf_vendlist, ' (',
#suf := if (STRCMP(#last1, cf_vendlist) = 0,
#suf+1,
1 or #last := cf_vendlist), ')') as new_label
FROM (
SELECT t1.cf_vendlistid, t1.cf_vendlist, unique1.id as reference_id
FROM table_cf_vendlist t1
INNER JOIN (SELECT MIN(cf_vendlistid) as id, cf_vendlist
FROM table_cf_vendlist
GROUP BY cf_vendlist) AS unique1
ON t1.cf_vendlist = unique1.cf_vendlist
ORDER BY cf_vendlist, cf_vendlistid
) AS t1_sorted
WHERE t1_sorted.cf_vendlistid <> t1_sorted.reference_id)
AS t1_sorted_labeled
ON t.cf_vendlistid = t1_sorted_labeled.cf_vendlistid
SET t.cf_vendlist = t1_sorted_labeled.new_label;
works. But 1 or #last := cf_vendlist is a hack that allows to return 1 or 0(and) instead of cf_vendlist string (see my comment below)

I have a solution in sqlfiddle. To follow how it works,
1) look at the subquery t1_sorted
SELECT t1.cf_vendlistid, t1.cf_vendlist, unique1.id as reference_id
FROM table_cf_vendlist t1
INNER JOIN (SELECT MIN(cf_vendlistid) as id, cf_vendlist
FROM table_cf_vendlist
GROUP BY cf_vendlist) AS unique1
ON t1.cf_vendlist = unique1.cf_vendlist
WHERE t1.cf_vendlistid <> unique1.id
ORDER BY cf_vendlist, cf_vendlistid
which returns results like in fig. 1.
2a) In subquery t1_sorted_labeled, the results are projected to remove the first occurrence of the cf_vendlist in each group and
2b) an extra field new_label is created with the cf_vendlist and (copy number)
3) Finally, the original query is joined with this subquery in the UPDATE statement, which merely copies new_label to cf_vendlist
Thus, complete answer:
set #suf:=0;
set #last := 0;
UPDATE table_cf_vendlist t
INNER JOIN (SELECT cf_vendlistid, concat(cf_vendlist, ' (',
#suf := if (STRCMP(#last, cf_vendlist) = 0,
#suf+1,
1 and length(#last := cf_vendlist)), ')') as new_label
FROM (
SELECT t1.cf_vendlistid, t1.cf_vendlist, unique1.id as reference_id
FROM table_cf_vendlist t1
INNER JOIN (SELECT MIN(cf_vendlistid) as id, cf_vendlist
FROM table_cf_vendlist
GROUP BY cf_vendlist) AS unique1
ON t1.cf_vendlist = unique1.cf_vendlist
ORDER BY cf_vendlist, cf_vendlistid
) AS t1_sorted
WHERE t1_sorted.cf_vendlistid <> t1_sorted.reference_id)
AS t1_sorted_labeled
ON t.cf_vendlistid = t1_sorted_labeled.cf_vendlistid
SET t.cf_vendlist = t1_sorted_labeled.new_label;
BTW, you should really have an index on cf_vendlist.

Related

How to get the value of a different ID that is acquired during the SELECT statement?

I have created an entity tblPerson and from this entity I need to get the bGroup of t.adminID and the bGroup from the d.personID. I have tried the below query but it's not returning anything.
`
SELECT t.adminID, p.firstName, p.lastName, t.transID, t.transDate, t.donationID, p.bGroup, b.bankName, d.personID AS 'Donor ID', 'Donor BGroup'
FROM tblTrans t
JOIN tblAdmin a ON t.adminID = a.adminID
JOIN tblPerson p ON a.personID = p.personID
JOIN tblDonation d ON t.donationID = d.donationID
JOIN tblBank b ON d.bankID = b.bankID
WHERE 'Donor BGroup' IN
(SELECT p.bGroup
FROM tblPerson p
JOIN tblDonation d ON p.personID = d.personID
JOIN tblTrans t ON d.donationID = t.donationID);
`
When I execute the subquery, it gives me the bGroup of the d.personID, what do you think is going on, and maybe any alternatives, please?
Sample Data
INSERT INTO tblPerson (personID, firstName, lastName, bGroup)
VALUES ('1A', 'John', 'Doe', 'XY'),
('2A', 'Joe', 'Bishop', 'AB'),
('1B', 'Elly', 'James', 'OP'),
('2B', 'Andre', 'Butch', 'XY'),
('3A', 'Amy', 'Gree', 'AB'),
('3B', 'Alfred', 'Black', 'OP'),
('4C', 'James', 'Brown', 'XY');
INSERT INTO tblAdmin (adminID, personID, description)
VALUES (1, '1A', 'Whatever.'),
(2, '1B', ''),
(3, '4C', 'Anything.'),
(4, '1A', '');
INSERT INTO tblDonation (donationID, bankID, personID, donationDate)
VALUES (1, 1, '3B', '2018-12-27'),
(2, 1, '2A', '2022-12-28'),
(3, 2, '3A', '2022-03-23'),
(4, 2, '4C', '2022-06-19'),
(5, 3, '1B', '2022-08-19'),
(6, 3, '2B', '2022-08-08'),
(7, 3, '3B', '2022-07-20'),
(8, 2, '4C', '2022-11-26'),
(9, 1, '3B', '2022-11-26'),
(10, 2, '2A', '2022-01-16');
INSERT INTO tblBank (bankID, bankName)
VALUES (1, 'Bank 1'),
(2, 'Bank 2'),
(3, 'Bank 3');
INSERT INTO tblTrans (transID, transDate, donationID, adminID)
VALUES (1, '2022-12-31', 1, 1),
(2, '2022-01-01', 2, 1),
(3, '2022-05-23', 3, 2),
(4, '2022-05-23', 4, 2),
(5, '2022-07-09', 5, 3),
(6, '2022-08-20', 6 4),
(7, '2022-12-27', 7,4);
Sample ERD Diagram
Expected Output
Example: 1, John, Doe, 1, 2022-12-31, 1, XY, Bank 1, 3B, OP.
Your WHERE 'Donor BGroup' IN (SELECT...) clause evaluates to WHERE false, because you don't have any rows with that value in tblPerson.bGroup. So, your SELECT statement's result set is empty.
It's hard to puzzle out your requirement from your question.
I managed to find the solution. Since I needed to call the same table twice with different IDs I had to create two aliases and JOIN the table twice, like so:
SELECT t.adminID, person.firstName, person.lastName, t.transID, t.transDate, t.donationID, person.bGroup, b.bankName, donor.personID AS 'Donor ID', donor.bGroup AS 'Donor BGroup'
FROM tblTrans t
JOIN tblAdmin a ON t.adminID = a.adminID
JOIN tblPerson person ON a.personID = person.personID
JOIN tblDonation d ON t.donationID = d.donationID
JOIN tblPerson donor ON
JOIN tblBank b ON d.bankID = b.bankID;

Select rows grouped by a column having max aggregate

Given the following data set, how would I find the email addresses that were references for the most ApplicationIDs that have an "Accepted" decision?
CREATE TABLE IF NOT EXISTS `EmailReferences` (
`ApplicationID` INT NOT NULL,
`Email` VARCHAR(45) NOT NULL,
PRIMARY KEY (`ApplicationID`, `Email`)
);
INSERT INTO EmailReferences (ApplicationID, Email)
VALUES
(1, 'ref10#test.org'), (1, 'ref11#test.org'), (1, 'ref12#test.org'),
(2, 'ref20#test.org'), (2, 'ref21#test.org'), (2, 'ref22#test.org'),
(3, 'ref11#test.org'), (3, 'ref31#test.org'), (3, 'ref32#test.org'),
(4, 'ref40#test.org'), (4, 'ref41#test.org'), (4, 'ref42#test.org'),
(5, 'ref50#test.org'), (5, 'ref51#test.org'), (5, 'ref52#test.org'),
(6, 'ref60#test.org'), (6, 'ref11#test.org'), (6, 'ref62#test.org'),
(7, 'ref70#test.org'), (7, 'ref71#test.org'), (7, 'ref72#test.org'),
(8, 'ref10#test.org'), (8, 'ref81#test.org'), (8, 'ref82#test.org')
;
CREATE TABLE IF NOT EXISTS `FinalDecision` (
`ApplicationID` INT NOT NULL,
`Decision` ENUM('Accepted', 'Denied') NOT NULL,
PRIMARY KEY (`ApplicationID`)
);
INSERT INTO FinalDecision (ApplicationID, Decision)
VALUES
(1, 'Accepted'), (2, 'Denied'),
(3, 'Accepted'), (4, 'Denied'),
(5, 'Denied'), (6, 'Denied'),
(7, 'Denied'), (8, 'Accepted')
;
Fiddle of same:http://sqlfiddle.com/#!9/03bcf2/1
Initially, I was using LIMIT 1 and ORDER BY CountDecision DESC, like so:
SELECT er.email, COUNT(fd.Decision) AS CountDecision
FROM EmailReferences AS er
JOIN FinalDecision AS fd ON er.ApplicationID = fd.ApplicationID
WHERE fd.Decision = 'Accepted'
GROUP BY er.email
ORDER BY CountDecision DESC
LIMIT 1
;
However, it occurred to me that I could have multiple email addresses that referred different "most accepted" decisions (i.e., a tie, so to speak), and those would be filtered out (is that the right phrasing?) with the LIMIT keyword.
I then tried a variation on the above query, replacing the ORDER BY and LIMIT lines with:
HAVING MAX(CountDecision)
But I realized that that's only half a statement: MAX(CountDecision) needs to be compared to something. I just don't know what.
Any pointers would be much appreciated. Thanks!
Note: this is for a homework assignment.
Update: To be clear, I'm trying to find value and count of Emails from EmailReferences. However, I only want rows that have FinalDecision.Decision = 'Accepted' (on matching ApplicantIDs). Based on my data, the result should be:
Email | CountDecision
---------------+--------------
ref10#test.org | 2
ref11#test.org | 2
For example...
SELECT a.*
FROM
( SELECT x.email
, COUNT(*) total
FROM emailreferences x
JOIN finaldecision y
ON y.applicationid = x.applicationid
WHERE y.decision = 'accepted'
GROUP
BY x.email
) a
JOIN
( SELECT COUNT(*) total
FROM emailreferences x
JOIN finaldecision y
ON y.applicationid = x.applicationid
WHERE y.decision = 'accepted'
GROUP
BY x.email
ORDER
BY total DESC
LIMIT 1
) b
ON b.total = a.total;
MySQL still lack window functions, but when version 8 is production ready, this becomes easier. So for fuure reference, or for those databases like Mariadb that already have window functions:
CREATE TABLE IF NOT EXISTS `EmailReferences` (
`ApplicationID` INT NOT NULL,
`Email` VARCHAR(45) NOT NULL,
PRIMARY KEY (`ApplicationID`, `Email`)
);
INSERT INTO EmailReferences (ApplicationID, Email)
VALUES
(1, 'ref10#test.org'), (1, 'ref11#test.org'), (1, 'ref12#test.org'),
(2, 'ref20#test.org'), (2, 'ref21#test.org'), (2, 'ref22#test.org'),
(3, 'ref30#test.org'), (3, 'ref31#test.org'), (3, 'ref32#test.org'),
(4, 'ref40#test.org'), (4, 'ref41#test.org'), (4, 'ref42#test.org'),
(5, 'ref50#test.org'), (5, 'ref51#test.org'), (5, 'ref52#test.org'),
(6, 'ref60#test.org'), (6, 'ref11#test.org'), (6, 'ref62#test.org'),
(7, 'ref70#test.org'), (7, 'ref71#test.org'), (7, 'ref72#test.org'),
(8, 'ref10#test.org'), (8, 'ref81#test.org'), (8, 'ref82#test.org')
;
CREATE TABLE IF NOT EXISTS `FinalDecision` (
`ApplicationID` INT NOT NULL,
`Decision` ENUM('Accepted', 'Denied') NOT NULL,
PRIMARY KEY (`ApplicationID`)
);
INSERT INTO FinalDecision (ApplicationID, Decision)
VALUES
(1, 'Accepted'), (2, 'Denied'),
(3, 'Accepted'), (4, 'Denied'),
(5, 'Denied'), (6, 'Denied'),
(7, 'Denied'), (8, 'Accepted')
;
select email, CountDecision
from (
SELECT er.email, COUNT(fd.Decision) AS CountDecision
, max(COUNT(fd.Decision)) over() maxCountDecision
FROM EmailReferences AS er
JOIN FinalDecision AS fd ON er.ApplicationID = fd.ApplicationID
WHERE fd.Decision = 'Accepted'
GROUP BY er.email
) d
where CountDecision = maxCountDecision
email | CountDecision
:------------- | ------------:
ref10#test.org | 2
dbfiddle here

Selecting only the first two items from and order

I need you help regarding something, i have 3 tables ORDERS, ORDER_ITEM, ORDER_ITEM_LINE.
CREATE TABLE orders
(`id` int, `date` datetime)
;
INSERT INTO orders
(`id`, `date`)
VALUES
(78, '2017-01-03 00:00:00'),
(79, '2017-02-03 00:00:00'),
(80, '2017-03-03 00:00:00'),
(81, '2017-04-03 00:00:00'),
(82, '2017-05-03 00:00:00'),
(83, '2017-06-03 00:00:00'),
(84, '2017-07-03 00:00:00')
;
CREATE TABLE order_item
(`id` int, `fk_o_id` int, `sku` int)
;
INSERT INTO order_item
(`id`, `fk_o_id`, `sku`)
VALUES
(10, 78, 123),
(11, 79, 124),
(12, 79, 125),
(13, 80, 126),
(14, 82, 127),
(15, 82, 128),
(16, 82, 129)
;
CREATE TABLE order_item_line
(`id` int, `fk_oi_id` int, `line_id` int)
;
INSERT INTO order_item_line
(`id`, `fk_oi_id`, `line_id`)
VALUES
(33, 10, 1),
(34, 11, 1),
(35, 12, 2),
(36, 13, 1),
(37, 14, 1),
(38, 15, 2),
(39, 16, 3)
;
I would like to display all orders with 2 or more than 2 items but only first two so it will be line_id - 1 and 2.
The outcome should look like:
Outcome
If you have any ideas, thank you in advance.
To get the result you require, you will need to create another table. In this example I created a table called TESTQUERY and inserted data to count how many times the orders id appeared
Table creation
CREATE TABLE TESTQUERY
(`id` int, `count` int)
Data into the test table
INSERT INTO TESTQUERY
(
SELECT o.id, COUNT(o.id) as count FROM orders o
JOIN order_item oi ON oi.fk_o_id = o.id
JOIN order_item_line oil ON oil.fk_oi_id = oi.id
GROUP BY o.id
)
I then queried against all for databases using the query below and it returned your desired outcome
SELECT o.id, oi.sku, oil.line_id FROM orders o
JOIN order_item oi ON oi.fk_o_id = o.id
JOIN order_item_line oil ON oil.fk_oi_id = oi.id
JOIN TESTQUERY t ON t.id = o.id
WHERE t.count > 1 AND oil.line_id < 3
I hope this helps

sum of multiple products with show rows concerned using array

I have a table that has 3 products description (Product_1, Product_2, Product_3) and Each product of some reference AND quantity with location of every row
Iwant show all rows for product_1, product_2, product_3 which the sum of each product on Array variable as an example
$globalProduct = array(21000, 18000, 1000);
$sumProduct_1 = $globalProduct[0];
$sumProduct_2 = $globalProduct[1];
$sumProduct_3 = $globalProduct[2];
Ihave query mysql for one product but I found it difficult to use 3 products.
this query for one product, it's working correctly
SET #runtot=0;
SELECT p_reference, p_id, p_description, p_quantity, p_location, (#runtot := #runtot + p_quantity) AS runningTotal
FROM (
SELECT p_reference, p_id, p_description, p_quantity, p_location
FROM product_table
WHERE p_description = 'product_1'
ORDER BY p_reference, p_quantity
) AS l
WHERE #runtot <= 21000;
this follow source code for create Product_table
CREATE TABLE IF NOT EXISTS `product_table` (
`p_id` int(11) NOT NULL,
`p_description` varchar(50) NOT NULL,
`p_reference` varchar(25) NOT NULL,
`p_location` varchar(25) NOT NULL,
`p_quantity` float(11,3) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=18 ;
--
-- Contenu de la table `product_table`
--
INSERT INTO `product_table` (`p_id`, `p_description`, `p_reference`, `p_location`, `p_quantity`) VALUES
(1, 'Product_1', '1A00001', 'QD01', 7000.000),
(2, 'Product_1', '1A00001', 'QD02', 7000.000),
(3, 'Product_1', '1A00007', 'QD03', 7000.000),
(4, 'Product_1', '1A00008', 'QD04', 7000.000),
(5, 'Product_2', '1A00002', 'AB01', 6500.000),
(6, 'Product_2', '1A00004', 'AB02', 6500.000),
(7, 'Product_2', '1A00005', 'AB03', 6500.000),
(8, 'Product_2', '1A00003', 'DB01', 6500.000),
(9, 'Product_2', '1A00009', 'DB02', 6500.000),
(10, 'Product_2', '1A00002', 'DB03', 6500.000),
(11, 'Product_2', '1A00002', 'DB04', 6500.000),
(12, 'Product_3', '1A00010', 'FD01', 5000.000),
(13, 'Product_3', '1A00015', 'DS02', 5000.000),
(14, 'Product_3', '1A00017', 'DS03', 5000.000),
(15, 'Product_3', '1A00018', 'DS04', 5000.000);
Perhaps it is as easy as this?
SELECT
p_description,
sum(p_quantity) AS runningTotal
FROM
product_table
GROUP BY
p_description
Ok, a bit more complicated then:
SELECT * FROM
(
SELECT
a.*, c.p_description as nextProduct,
(#gc := #gc + a.p_quantity) as uu, if(a.p_description != c.p_description, #gc := 0, 1) as `reset`
FROM
(SELECT #gc := 0) as b, product_table as a
LEFT OUTER JOIN product_table as c ON a.p_id = c.p_id -1
) as a
WHERE uu <= 21000

SELECT data based on result of previous row in table

I have a database of students.
CREATE TABLE classlist
(`id` int, `studentid` int, `subjectid` int, `presentid` 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))
;
INSERT INTO classlist
(`id`, `studentid`, `subjectid`, `presentid`)
VALUES
(1, 111, 1, 1),
(2, 222, 3, 0),
(3, 333, 2, 1),
(4, 111, 4, 1),
(5, 111, 1, 0),
(6, 222, 3, 0),
(7, 333, 2, 1),
(8, 111, 4, 1),
(9, 111, 2, 0),
(10, 111, 4, 1),
(11, 111, 1, 1),
(12, 333, 3, 1),
(13, 333, 2, 1),
(14, 333, 3, 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')
;
And I have a query which shows how many times they have been present or absent.
SELECT
studentid,
students.name AS NAME,
SUM(presentid = 1) AS present,
SUM(presentid = 0) AS absent
FROM classlist
INNER JOIN student as students ON classlist.studentid=students.id
GROUP BY studentid, NAME
See this fiddle below.
http://sqlfiddle.com/#!2/fe0b0/1
There seems to be a trend from looking at this sample data that after someone attends subjectid 4 they are often not coming to the next class. How can I capture this in a query. I want to ONLY show data WHERE last subjectid =4. So in my sample data rows matching my criteria would be.
(5, 111, 1, 0),
(9, 111, 2, 0),
(11, 111, 1, 1),
as these rows are all the next row of a studentid who had a subjectid=4.
My output would be
| STUDENTID | NAME | PRESENT | ABSENT|
| 111 | John | 1 | 2 |
To get the next class for a student, use a correlated subquery:
select cl.*,
(select min(cl2.id) from classlist cl2 where cl2.studentid = cl.studentid and cl2.id > cl.id) as nextcl
from classlist cl
Plugging this into your query example tell you you who is present and absent for the next class:
SELECT students.id, students.name AS NAME,
SUM(cl.presentid = 1) AS present, SUM(cl.presentid = 0) AS absent,
sum(clnext.presentid = 1) as presentnext
FROM (select cl.*,
(select min(cl2.id) from classlist cl2 where cl2.studentid = cl.studentid and cl2.id > cl.id) as nextcl
from classlist cl
) cl INNER JOIN
student as students
ON cl.studentid = students.id left outer join
classlist clnext
on cl.nextcl = clnext.id
GROUP BY students.id, students.NAME
Add a where cl.subjectid = 4 to get the answer for subject 4.
I fixed the query. The SQLFiddle is k.
A quick and dirty solution could be to get the Classlist.Id for all lines where subjectid=4 (let's call them n) then select all the lines where Id = n+1