Query that removes a duplicate entry using mysql - mysql

My target is to create a query that removes a duplicate entry in a row using MySQL 5.7. My current function matches entries with the same weight however, it has a duplicate. (Please, see the 2nd table)
Here is my entry table.
entryID| entryName | weight |
-------------------------------
1 | lamok2 | 1970 |
2 | lamok2 | 1955 |
3 | lamok3 | 1965 |
4 | lamok3 | 1975 |
5 | lamok3 | 1985 |
6 | lamok4 | 1955 |
7 | lamok4 | 1960 |
8 | lamok4 | 1975 |
9 | lamok5 | 1955 |
10 | MA1 | 2000 |
11 | MA2 | 2010 |
After applying my query which is:
SELECT t1.entryName AS mname, t1.weight AS weight,
MIN(t2.entryName) AS wname,MIN(t2.weight) AS weight
FROM entry t1
LEFT JOIN entry t2 ON t1.weight = t2.weight AND t1.entryName != t2.entryName
GROUP BY t1.entryID, t1.entryName, t1.weight
it produces this:
mname| weight | wname | weight|
--------------------------------------------
lamok2 | 1970 | NULL | NULL |
lamok2 | 1955 | lamok4 | 1955 |
lamok3 | 1965 | NULL | NULL |
lamok3 | 1975 | lamok4 | 1975 |
lamok3 | 1985 | NULL | NULL |
lamok4 | 1955 | lamok2 | 1955 |
lamok4 | 1960 | NULL | NULL |
lamok4 | 1975 | lamok3 | 1975 |
lamok5 | 1955 | lamok2 | 1955 |
MA1 | 2000 | NULL | NULL |
MA2 | 2010 | NULL | NULL |
As we can see on the result, Lamok 2 and Lamok 4 had a 2 matches Lamok 3 and Lamok 4 had a 2 matches too... It shouldn't be possible... This is counted as the duplicate fight. How can I avoid this kind of duplication in my query?
This is my target output: (No duplicate fights)
mname| weight | wname | weight|
--------------------------------------------
lamok2 | 1970 | NULL | NULL |
lamok2 | 1955 | lamok4 | 1955 |
lamok3 | 1965 | NULL | NULL |
lamok3 | 1975 | lamok4 | 1975 |
lamok3 | 1985 | NULL | NULL |
lamok4 | 1960 | NULL | NULL |
lamok5 | 1955 | lamok2 | 1955 |
MA1 | 2000 | NULL | NULL |
MA2 | 2010 | NULL | NULL |

I think that you need group by two columns, by entryName and weight.
So this query will return your duplicates:
SELECT *
FROM entry
GROUP BY entryName, weight
HAVING COUNT(*) > 1
When you find duplicated then you can delete them. If you want to delete them with query, then you can use something like this:
DELETE FROM entry
WHERE entryID IN (
SELECT e.entryID
FROM entry AS e
GROUP BY e.entryName, e.weight
HAVING COUNT(*) > 1
)

Related

MySQl Subquery output with null data

I have a MySQL database with vehicles, issued_fuel and mileage data (vs issued fuel) and want to get the following desired output.
Desired Output
I want to get results including above related data as follows :
+---------------+------------+------------+---------+----------+
| registered_no | issue_date | date | mileage | fuel_qty |
+---------------+------------+------------+---------+----------+
| SP KR-3503 | 2021-02-22 | null | null | 40 |
| SP KR-3503 | 2021-02-26 | null | null | 30 |
| null | 2021-03-03 | null | null | 40 |
| null | 2021-03-15 | null | null | 45 |
| SP KR-3503 | 2021-03-18 | null | null | 40 |
| null | 2021-03-25 | null | null | 45 |
| null | 2021-04-04 | null | null | 35 |
| SP KE-6794 | 2021-04-25 | 2021-04-25 | 150 | 40 |
+---------------+------------+------------+---------+----------+
Because the table "tbl_trip_details" includes only one mileage (150km) for vehicle_no "SP KE-6794 for the date 2021-04-25.
My tables as follows :
tbl_vehicle
+------------+---------------+--------+
| vehicle_id | registered_no | status |
+------------+---------------+--------+
| 1 | SP KR-3503 | 1 |
| 2 | SP KE-6794 | 1 |
+------------+---------------+--------+
tbl_trip_details
+---------+------------+------------+---------+--------+
| trip_id | vehicle_id | date | mileage | status |
+---------+------------+------------+---------+--------+
| 1 | 1 | 2021-04-25 | 125 | 1 |
| 2 | 1 | 2021-04-26 | 100 | 1 |
| 3 | 2 | 2021-04-25 | 150 | 1 |
+---------+------------+------------+---------+--------+
tbl_direct_fuel
+----------------+---------+------------+
| direct_fuel_id | vehicle | issue_date |
+----------------+---------+------------+
| 1 | 1 | 2021-02-22 |
| 2 | 1 | 2021-02-26 |
| 3 | 3 | 2021-03-03 |
| 4 | 3 | 2021-03-15 |
| 5 | 1 | 2021-03-18 |
| 6 | 3 | 2021-03-25 |
| 7 | 3 | 2021-04-04 |
| 8 | 2 | 2021-04-25 |
+----------------+---------+------------+
tbl_direct_fuel_details
+-----------------------------+----------------+------+----------+
| tbl_deirect_fuel_details_id | direct_fuel_id | item | fuel_qty |
+-----------------------------+----------------+------+----------+
| 1 | 1 | 1 | 40 |
| 2 | 2 | 1 | 30 |
| 3 | 3 | 1 | 40 |
| 4 | 4 | 1 | 45 |
| 5 | 5 | 1 | 40 |
| 6 | 6 | 1 | 45 |
| 7 | 7 | 1 | 35 |
| 8 | 8 | 1 | 40 |
+-----------------------------+----------------+------+----------+
I used the following query
select tv.registered_no, df.issue_date, td.date, td.mileage, df.fuel_qty
from tbl_trip_details trip
left join tbl_vehicle tv on tv.vehicle_id=trip.vehicle_id
join (select tbl_direct_fuel.direct_fuel_id, tbl_vehicle.registered_no, tbl_direct_fuel.vehicle, tbl_direct_fuel.issue_date, item, tbl_direct_fuel_details.fuel_qty, tbl_direct_fuel_details.fuel_price
from tbl_direct_fuel_details
left join tbl_direct_fuel on tbl_direct_fuel_details.direct_fuel_id = tbl_direct_fuel.direct_fuel_id
join tbl_vehicle on tbl_vehicle.vehicle_id = tbl_direct_fuel.vehicle
where tbl_direct_fuel.status=1
order by tbl_vehicle.registered_no
) AS df on df.vehicle = tv.vehicle_id
join (select tbl_trip_details.trip_id, tbl_vehicle.registered_no, tbl_trip_details.vehicle_id, tbl_trip_details.date, tbl_trip_details.mileage
from tbl_trip_details
left join tbl_vehicle on tbl_vehicle.vehicle_id = tbl_trip_details.vehicle_id
where tbl_trip_details.status=1
order by tbl_vehicle.registered_no) AS td on td.vehicle_id = tv.vehicle_id
Since the date & mileage are repeated for other records and didn't get the expected result. So the query outs the following output:
+---------------+------------+------------+---------+----------+
| registered_no | issue_date | date | mileage | fuel_qty |
+---------------+------------+------------+---------+----------+
| SP KR-3503 | 2021-02-22 | 2021-04-25 | 150 | 40 |
| SP KR-3503 | 2021-02-26 | 2021-04-25 | 150 | 30 |
| null | 2021-03-03 | 2021-04-25 | 150 | 40 |
| null | 2021-03-15 | 2021-04-25 | 150 | 45 |
| SP KR-3503 | 2021-03-18 | 2021-04-25 | 150 | 40 |
| null | 2021-03-25 | 2021-04-25 | 150 | 45 |
| null | 2021-04-04 | 2021-04-25 | 150 | 35 |
| SP KE-6794 | 2021-04-25 | 2021-04-25 | 150 | 40 |
+---------------+------------+------------+---------+----------+
What may be going wrong in my query ? Can anyone help ?
Try this query:
SELECT tv.registered_no, tdf.issue_date, ttd.date, ttd.mileage, tdfd.fuel_qty
FROM tbl_direct_fuel tdf
LEFT JOIN tbl_vehicle tv
ON tdf.vehicle=tv.vehicle_id
LEFT JOIN tbl_trip_details ttd
ON tdf.vehicle=ttd.vehicle_id
AND tdf.issue_date=ttd.date
LEFT JOIN tbl_direct_fuel_details tdfd
ON tdf.direct_fuel_id=tdfd.direct_fuel_id;
Demo fiddle
Rather than having two separate sub-queries, I try to directly get the result with a bunch of LEFT JOINs. From what I can see in the updated expected result, tbl_direct_fuel is enough to be the reference table for all the others. I's just a matter of figuring the correct ON condition.
Also, I mentioned about the missing tbl_direct_fuel.status and tbl_direct_fuel_details.fuel_price from the sample data. I'm not concerned about tbl_direct_fuel_details.fuel_price in particular because it wasn't included in the end SELECT result however, tbl_direct_fuel.status was being used in WHERE might have some impact. But after constructing the possible query, I assume that in this example, it's not required to include tbl_direct_fuel.status at all. Anyway, that's for you to figure out.

SQL : For each ID, only display the highest value from another column (can't group by)

I have this table here :
+---------+----------+------------+------------+
| idStep | idProj | dateStart | dateEnd |
+---------+----------+------------+------------+
| 1 | 1 | 2011-07-01 | 2011-09-01 |
| 1 | 2 | 2012-05-01 | 2012-05-10 |
| 1 | 3 | 2011-11-01 | 2012-01-20 |
| 2 | 1 | 2011-09-02 | 2011-11-30 |
| 2 | 2 | 2012-05-11 | 2012-06-01 |
| 2 | 3 | 2012-01-21 | 2012-04-01 |
| 3 | 1 | 2011-12-01 | 2012-07-07 |
| 3 | 2 | 2012-06-02 | 2012-07-01 |
| 3 | 3 | 2012-04-02 | NULL |
| 4 | 1 | 2012-07-08 | NULL |
| 4 | 2 | 2012-07-01 | 2012-07-21 |
| 5 | 2 | 2012-07-22 | 2012-07-23 |
+---------+----------+------------+------------+
I need to find the current step of each project by searching for the highest idStep of each idProject without using Group By, which is where I'm completely stuck. Without GROUP BY I just cannot get there.
Basically, the output should be this :
+---------+----------+------------+------------+
| idStep | idProj | dateStart | dateEnd |
+---------+----------+------------+------------+
| 3 | 3 | 2012-04-02 | NULL |
| 4 | 1 | 2012-07-08 | NULL |
| 5 | 2 | 2012-07-22 | 2012-07-23 |
+---------+----------+------------+------------+
I want to use a Query built like this
SELECT idProj,idStep
FROM table
WHERE idStep = (SELECT max(idStep) FOR EACH idProj)
I know that FOR EACH isn't SQL, I'm only trying to make my desired query structure readable.
You want a correlated subuqery:
SELECT idProj, idStep
FROM table t
WHERE t.idStep = (SELECT max(idStep)
FROM table t2
WHERE t2.idProj = t.idProj
);

Inequality in Mysql with count()

I have the following structure :
Table Author :
idAuthor,
Name
+----------+-------+
| idAuthor | Name |
+----------+-------+
| 1 | Renee |
| 2 | John |
| 3 | Bob |
| 4 | Bryan |
+----------+-------+
Table Publication:
idPublication,
Title,
Type,
Date,
Journal,
Conference
+---------------+--------------+------+-------------+------------+-----------+
| idPublication | Title | Date | Type | Conference | Journal |
+---------------+--------------+------+-------------+------------+-----------+
| 1 | Flower thing | 2008 | book | NULL | NULL |
| 2 | Bees | 2009 | article | NULL | Le Monde |
| 3 | Wasps | 2010 | inproceding | KDD | NULL |
| 4 | Whales | 2010 | inproceding | DPC | NULL |
| 5 | Lyon | 2011 | article | NULL | Le Figaro |
| 6 | Plants | 2012 | book | NULL | NULL |
| 7 | Walls | 2009 | proceeding | KDD | NULL |
| 8 | Juices | 2010 | proceeding | KDD | NULL |
| 9 | Fruits | 2010 | proceeding | DPC | NULL |
| 10 | Computers | 2010 | inproceding | DPC | NULL |
| 11 | Phones | 2010 | inproceding | DPC | NULL |
| 12 | Creams | 2010 | proceeding | DPC | NULL |
| 13 | Love | 2010 | proceeding | DPC | NULL |
+---------------+--------------+------+-------------+------------+-----------+
Table author_has_publication :
Author_idAuthor,
Publication_idPublication
+-----------------+---------------------------+
| Author_idAuthor | Publication_idPublication |
+-----------------+---------------------------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
| 1 | 5 |
| 2 | 5 |
| 3 | 5 |
| 3 | 6 |
| 4 | 7 |
| 4 | 8 |
| 4 | 9 |
| 4 | 10 |
| 3 | 11 |
| 3 | 12 |
| 2 | 13 |
+-----------------+---------------------------+
I want to obtain the list of all authors having published at least 2 times at conference DPC in 2010.
I achieved to get the list of autors that have published something, and the number of publication for each, but I can't get my 'at least 2' factor.
My following query
SELECT author.name, COUNT(name) FROM author INNER JOIN author_has_publication ON author.idAuthor=author_has_publication.Author_idAuthor INNER JOIN publication ON author_has_publication.Publication_idPublication=publication.idPublication AND publication.date=2010 AND publication.conference='DPC'GROUP BY author.name;
returns the following result (which is good)
+-------+-------------+
| name | COUNT(name) |
+-------+-------------+
| Bob | 2 |
| Bryan | 3 |
| John | 1 |
+-------+-------------+
but when I try to select only the one with a count(name)>=2, i got an error.
I tried this query :
SELECT author.name, COUNT(name) FROM author INNER JOIN author_has_publication ON author.idAuthor=author_has_publication.Author_idAuthor INNER JOIN publication ON author_has_publication.Publication_idPublication=publication.idPublication AND publication.date=2010 AND publication.conference='DPC'GROUP BY author.name WHERE COUNT(name)>=2;
When you use aggregation funcion you can filter with a proper operator named HAVING
Having worok on the result of the query (then pn the aggrgated result like count() ) instead of where that work on the original value of the tables rows
SELECT author.name, COUNT(name)
FROM author INNER JOIN author_has_publication
ON author.idAuthor=author_has_publication.Author_idAuthor
INNER JOIN publication
ON author_has_publication.Publication_idPublication=publication.idPublication
AND publication.date=2010 AND publication.conference='DPC'
GROUP BY author.name
HAVING COUNT(name)>=2;

SQL IN () is including NULL values

Employee table:
+--------+----------+-----------+------------+----------+-----------------+---------+--------------------+--------------------+
| emp_id | fname | lname | start_date | end_date | superior_emp_id | dept_id | title | assigned_branch_id |
+--------+----------+-----------+------------+----------+-----------------+---------+--------------------+--------------------+
| 1 | Michael | Smith | 2005-06-22 | NULL | NULL | 3 | President | 1 |
| 2 | Susan | Barker | 2006-09-12 | NULL | 1 | 3 | Vice President | 1 |
| 3 | Robert | Tyler | 2005-02-09 | NULL | 1 | 3 | Treasurer | 1 |
| 4 | Susan | Hawthorne | 2006-04-24 | NULL | 3 | 1 | Operations Manager | 1 |
| 5 | John | Gooding | 2007-11-14 | NULL | 4 | 2 | Loan Manager | 1 |
| 6 | Helen | Fleming | 2008-03-17 | NULL | 4 | 1 | Head Teller | 1 |
| 7 | Chris | Tucker | 2008-09-15 | NULL | 6 | 1 | Teller | 1 |
| 8 | Sarah | Parker | 2006-12-02 | NULL | 6 | 1 | Teller | 1 |
| 9 | Jane | Grossman | 2006-05-03 | NULL | 6 | 1 | Teller | 1 |
| 10 | Paula | Roberts | 2006-07-27 | NULL | 4 | 1 | Head Teller | 2 |
| 11 | Thomas | Ziegler | 2004-10-23 | NULL | 10 | 1 | Teller | 2 |
| 12 | Samantha | Jameson | 2007-01-08 | NULL | 10 | 1 | Teller | 2 |
| 13 | John | Blake | 2004-05-11 | NULL | 4 | 1 | Head Teller | 3 |
| 14 | Cindy | Mason | 2006-08-09 | NULL | 13 | 1 | Teller | 3 |
| 15 | Frank | Portman | 2007-04-01 | NULL | 13 | 1 | Teller | 3 |
| 16 | Theresa | Markham | 2005-03-15 | NULL | 4 | 1 | Head Teller | 4 |
| 17 | Beth | Fowler | 2006-06-29 | NULL | 16 | 1 | Teller | 4 |
| 18 | Rick | Tulman | 2006-12-12 | NULL | 16 | 1 | Teller | 4 |
+--------+----------+-----------+------------+----------+-----------------+---------+--------------------+--------------------+
Query:
SELECT emp_id, fname, lname, title
FROM employee
WHERE emp_id IN (SELECT superior_emp_id FROM employee);
Query Result:
+--------+---------+-----------+--------------------+
| emp_id | fname | lname | title |
+--------+---------+-----------+--------------------+
| 1 | Michael | Smith | President |
| 3 | Robert | Tyler | Treasurer |
| 4 | Susan | Hawthorne | Operations Manager |
| 6 | Helen | Fleming | Head Teller |
| 10 | Paula | Roberts | Head Teller |
| 13 | John | Blake | Head Teller |
| 16 | Theresa | Markham | Head Teller |
+--------+---------+-----------+--------------------+
Subquery result:
+-----------------+
| superior_emp_id |
+-----------------+
| NULL |
| 1 |
| 1 |
| 3 |
| 4 |
| 4 |
| 4 |
| 4 |
| 4 |
| 6 |
| 6 |
| 6 |
| 10 |
| 10 |
| 13 |
| 13 |
| 16 |
| 16 |
+-----------------+
If the subquery SELECT superior_emp_id FROM employee returns NULL for Michael Smith how is it that the IN() operator returns it in the final result set? I thought nothing was equal to null.
If the subquery SELECT superior_emp_id FROM employee returns NULL for Michael Smith how is it that the IN() operator returns it in the final result set?
Short answer, it doesn't.
The subquery effectively returns the whole set of superior_emp_ids [NULL, 1, 1, 3, 3, 4, 4, 6, 6, 6, 4, 10, 10, 4, 13, 13, 4, 16, 16] for each row.
Your WHERE clause tests each emp_id to see if it is IN this set. And IN is basically a series of equals comparisons OR'd together.
Michael's emp_id is 1 and his row is returned because 1 = NULL OR 1 = 1 .... which can be written as FALSE OR TRUE .... returns TRUE.
You are correct in assuming that NULL doesn't equal anything, including NULL, so WHERE NULL IN (NULL, 1, FALSE, ... anything you like ...) will return FALSE. But that is not what's happening in your example.
N.B. To avoid any confusion it is much better to avoid NULL records on either side of an IN clause where possible as referenced by #Donal
SELECT superior_emp_id FROM employee
returns [NULL, 1, 3, 4, 6, 10, 13, 16]. I do not see the problem here.
Have a look at the ANSI_NULLS setting in SQL Server.
Transact-SQL supports an extension that allows for the comparison
operators to return TRUE or FALSE when comparing against null values.
This option is activated by setting ANSI_NULLS OFF. When ANSI_NULLS is
OFF, comparisons such as ColumnA = NULL return TRUE when ColumnA
contains a null value and FALSE when ColumnA contains some value
besides NULL.
Taken from here.
If you don't want the NULL value you will need to add a WHERE clause to the sub query. For example:
SELECT emp_id, fname, lname, title
FROM employee
WHERE emp_id IN (SELECT superior_emp_id FROM employee WHERE superior_emp_id IS NOT NULL);

Complex MySQL query summing joined records where parent and child ids are equal

Maybe this will be an easy one for some of you MySQL masters who see this stuff like a level 3 children's book.
I have multiple tables that I'm joining to produce statistical data for a report and I'm getting tripped up at the moment trying to figure it out. It's obviously imperative the figures are correct because it impacts a number of decisions going forward.
Here's the lay of the land (not the full picture, but you'll get the point):
Affiliate Table
+----+-----------+------------+---------------------+
| id | firstname | lastname | created_date |
+----+-----------+------------+---------------------+
| 1 | Mike | Johnson | 2010-11-22 17:44:37 |
| 2 | Trevor | Wilson | 2010-12-23 16:24:24 |
| 3 | Bob | Parker | 2011-11-04 10:33:49 |
+----+-----------+------------+---------------------+
Now our query should only find results for Bob Parker (id 3) so I'll only show example results for Bob.
Affiliate Link Table
+-----+-----------+--------------+-----------+----------+---------------------+
| id | parent_id | affiliate_id | link_type | linkhash | created_date |
+-----+-----------+--------------+-----------+----------+---------------------+
| 21 | NULL | 3 | PRODUCT | fa2e82a7 | 2011-06-15 16:18:37 |
| 27 | NULL | 3 | PRODUCT | 55de2ae7 | 2011-06-23 01:03:00 |
| 28 | NULL | 3 | PRODUCT | 02cae72f | 2011-06-23 01:03:00 |
| 29 | 27 | 3 | PRODUCT | a4dfb2c8 | 2011-06-23 01:03:00 |
| 30 | 28 | 3 | PRODUCT | 72cea1b2 | 2011-06-23 01:03:00 |
| 36 | 21 | 3 | PRODUCT | fa2e82a7 | 2011-06-23 01:07:03 |
| 59 | 21 | 3 | PRODUCT | ec33413f | 2011-11-04 17:49:17 |
| 60 | 27 | 3 | PRODUCT | f701188c | 2011-11-04 17:49:17 |
| 69 | 21 | 3 | PRODUCT | 6dfb89fd | 2011-11-04 17:49:17 |
+-----+-----------+--------------+-----------+----------+---------------------+
Affiliate Stats
+--------+--------------+--------------------+----------+---------------------+
| id | affiliate_id | link_id | order_id | type | created_date |
+--------+--------------+---------+----------+----------+---------------------+
| 86570 | 3 | 21 | NULL | CLICK | 2013-01-01 00:07:31 |
| 86574 | 3 | 21 | NULL | PAGEVIEW | 2013-01-01 00:08:53 |
| 86579 | 3 | 21 | 411 | SALE | 2013-01-01 00:09:52 |
| 86580 | 3 | 36 | NULL | CLICK | 2013-01-01 00:09:55 |
| 86582 | 3 | 36 | NULL | PAGEVIEW | 2013-01-01 00:09:56 |
| 86583 | 3 | 28 | NULL | CLICK | 2013-01-01 00:11:04 |
| 86584 | 3 | 28 | NULL | PAGEVIEW | 2013-01-01 00:11:04 |
| 86586 | 3 | 30 | NULL | CLICK | 2013-01-01 00:30:18 |
| 86587 | 3 | 30 | NULL | PAGEVIEW | 2013-01-01 00:30:20 |
| 86611 | 3 | 69 | NULL | CLICK | 2013-01-01 00:40:19 |
| 86613 | 3 | 69 | NULL | PAGEVIEW | 2013-01-01 00:40:19 |
| 86619 | 3 | 69 | 413 | SALE | 2013-01-01 00:42:12 |
| 86622 | 3 | 60 | NULL | CLICK | 2013-01-01 00:46:00 |
| 86624 | 3 | 60 | NULL | PAGEVIEW | 2013-01-01 00:46:01 |
| 86641 | 3 | 60 | NULL | PAGEVIEW | 2013-01-01 00:55:58 |
| 86642 | 3 | 30 | 415 | SALE | 2013-01-01 00:56:35 |
| 86643 | 3 | 28 | NULL | PAGEVIEW | 2013-01-01 00:56:43 |
| 86644 | 3 | 60 | 417 | SALE | 2013-01-01 00:56:52 |
+--------+--------------+---------+----------+----------+---------------------+
Orders
+------+--------------+---------+---------------------+
| id | affiliate_id | total | created_date |
+------+--------------+---------+---------------------+
| 411 | 3 | 138.62 | 2013-01-01 00:09:50 |
| 413 | 3 | 312.87 | 2013-01-01 00:09:52 |
| 415 | 3 | 242.59 | 2013-01-01 00:09:55 |
| 417 | 3 | 171.18 | 2013-01-01 00:09:55 |
+------+--------------+---------+---------------------+
Now the results that I need should look like this (only show main/parent link id)
+---------+---------+
| link_id | total |
+---------+---------+
| 21 | 451.49 | <- 1 order from parent (21), 1 from child (69)
| 27 | 171.18 | <- 1 order from child (69)
| 28 | 242.59 | <- 1 order from child (30)
+---------+---------+
I'm not quite sure how to write the query so that I can sum where affiliate_link.id and affiliate_link.parent_id are combined. Is this even possible with a couple of JOINs and GROUPing?
I'm not too sure why you have denormalised affiliate_id (by placing it in each table) and, therefore, whether one can rely on all Stats and Orders that stem from a particular Link to have the same affiliate_id as that Link.
If it's possible, I'd suggest changing the AffiliateLink.parent_id column such that parent records point to themselves (rather than NULL):
UPDATE AffiliateLink SET parent_id = id WHERE parent_id IS NULL
Then it's a simple case of joining and grouping:
SELECT AffiliateLink.parent_id AS link_id,
SUM(Orders.total) AS total
FROM AffiliateLink
JOIN AffiliateStats ON AffiliateStats.link_id = AffiliateLink.id
JOIN Orders ON Orders.id = AffiliateStats.order_id
WHERE AffiliateLink.affiliate_id = 3
GROUP BY AffiliateLink.parent_id
See it on sqlfiddle.
If it's not possible to make the change, you can effectively create the resulting AffiliateLink table using UNION (but beware the performance implications, as MySQL will not be able to use indexes on the result):
(
SELECT parent_id, id, affiliate_id FROM AffiliateLink WHERE parent_id IS NOT NULL
UNION ALL
SELECT id , id, affiliate_id FROM AffiliateLink WHERE parent_id IS NULL
) AS AffiliateLink
See it on sqlfiddle.