MySQL median subquery in select - mysql

This is what I have at the moment:
http://sqlfiddle.com/#!9/30a97c/10
Schema:
CREATE TABLE IF NOT EXISTS `test` (
`t_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`t_city` varchar(64) NOT NULL,
`t_category` enum('cat1','cat2') NOT NULL,
`t_type` enum('type1','type2') NOT NULL,
`t_num` int(8) NOT NULL,
PRIMARY KEY (`t_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
INSERT INTO `test` (`t_id`, `t_city`, `t_category`, `t_type`, `t_num`) VALUES
(1, 'New York', 'cat1', 'type1', 1056),
(2, 'New York', 'cat1', 'type1', 3756),
(3, 'London', 'cat1', 'type2', 3456),
(4, 'New York', 'cat1', 'type2', 5756),
(5, 'London', 'cat1', 'type2', 3777),
(6, 'New York', 'cat2', 'type1', 3756),
(7, 'New York', 'cat2', 'type1', 3756),
(8, 'London', 'cat2', 'type1', 3777),
(9, 'New York', 'cat2', 'type2', 4556),
(10, 'Berlin', 'cat1', 'type2', 1556),
(11, 'Berlin', 'cat2', 'type2', 9756),
(12, 'London', 'cat1', 'type2', 1756);
Query:
SELECT "cat1" as st_category, "type2" as st_type, t_city as st_name,
(
SELECT (ROUND(AVG(dd.t_num) / 10) * 10) as median
FROM (
SELECT d.t_num, #rownum:=#rownum+1 as `row_number`, #total_rows:=#rownum
FROM test as d, (SELECT #rownum:=0) as r
WHERE d.t_num is NOT NULL
AND `t_category` = "cat1" AND `t_type` = "type2" AND `t_city` = "XXXXX"
ORDER BY d.t_num
) as dd
WHERE dd.row_number IN ( FLOOR((#total_rows+1)/2), FLOOR((#total_rows+2)/2) )
) as st_median,
COUNT(t_num) as st_count
FROM `test` WHERE `t_category` = "cat1" AND `t_type` = "type2"
AND t_city in ("London", "New York")
GROUP BY t_city
ORDER BY st_name ASC
I don't know what to write in place of XXX (in the subquery) so that the median values of "t_num" in the output lines are not NULL. No median function in my MySQL.
OUTPUT NOW:
+-------------+---------+----------+-----------+----------+
| st_category | st_type | st_name | st_median | st_count |
+-------------+---------+----------+-----------+----------+
| cat1 | type2 | London | (null) | 3 |
| cat1 | type2 | New York | (null) | 1 |
+-------------+---------+----------+-----------+----------+
REQUIRED OUTPUT:
+-------------+---------+----------+-----------+----------+
| st_category | st_type | st_name | st_median | st_count |
+-------------+---------+----------+-----------+----------+
| cat1 | type2 | London | 3460 | 3 |
| cat1 | type2 | New York | 5760 | 1 |
+-------------+---------+----------+-----------+----------+

Thank you all for your help! This is the right solution:
SET SESSION GROUP_CONCAT_MAX_LEN = 1000000;
SELECT `t_category` AS st_category,
`t_type` AS st_type,
`t_city` AS st_name,
CASE ( COUNT(*) % 2 )
WHEN 1 THEN
ROUND(SUBSTRING_INDEX(SUBSTRING_INDEX(GROUP_CONCAT(`t_num`
ORDER BY `t_num` SEPARATOR ','), ',', ( COUNT(*) + 1 ) / 2), ',', -1)
/ 10) * 10
ELSE
ROUND((SUBSTRING_INDEX( SUBSTRING_INDEX( GROUP_CONCAT(`t_num`
ORDER BY `t_num` SEPARATOR ','), ',', COUNT(*) / 2), ',', -1)
+ SUBSTRING_INDEX( SUBSTRING_INDEX( GROUP_CONCAT(`t_num`
ORDER BY `t_num` SEPARATOR ','), ',', (COUNT(*) + 1) / 2), ',', -1) ) / 2
/ 10) * 10
END st_median,
COUNT(`t_num`) AS st_count
FROM `test`
WHERE `t_category` = "cat1"
AND `t_type` = "type2"
AND `t_city` IN ( "London", "New York" )
GROUP BY `t_city`
ORDER BY st_name ASC;
SQL Fiddle: http://sqlfiddle.com/#!9/30a97c/13
The GROUP_CONCAT_MAX_LEN setting is only required if you are working a lot of data.
Maximum value is 18,446,744,073,709,551,615 on 64-bit platforms.
On 32-bit platforms: 4,294,967,295.
https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_group_concat_max_len

Related

Find DISTINCT LAST record with SQL LEFT JOIN

I'm running MySQL 5.6.
I have two related tables:
CREATE TABLE Cars (
id INT NOT NULL AUTO_INCREMENT,
plate VARCHAR(16) NOT NULL,
flag TINYINT,
PRIMARY KEY(id)
)
and:
CREATE TABLE Rents (
id INT NOT NULL AUTO_INCREMENT,
out_date DATE NOT NULL,
in_date DATE,
car_id INT,
FOREIGN KEY (car_id) REFERENCES Cars(id),
PRIMARY KEY(id)
)
I can have multiple rents for each car (0 to many).
I need to select all vehicles in table Cars (with flag = 1) along with their status i.e. I need to know if each car is currently unavailable (only out_date is filled) or availabe (out_date and in_date filled) of course also vehicles without any rents are to be considered available.
The result set need to include out_date and in_date values [Update 17/07/2022].
I tought to use something like:
SELECT
*,
IF(Rents.in_date IS NOT NULL AND Rents.out_date IS NOT NULL, 1, IF(Rents.id IS NULL, 1, 0)) AS status
FROM Cars
LEFT JOIN Rents ON Cars.id = Rent.Car_id WHERE Cars.Flag = 1
but this of course will just return all the rows with positive flag match and a status evaluation (0 unavailable, 1 available):
id | plate | flag | id | out_date | in_date | car_id | status
---------------------------------------------------------------------
'1', 'FA787MX', '1', '1', '2022-07-14', '2022-07-15', '1', '1'
'1', 'FA787MX', '1', '2', '2022-07-16', NULL, '1', '0'
'3', 'AB124DF', '1', '4', '2022-07-13', '2022-07-14', '3', '1'
'4', 'CC666VC', '1', NULL, NULL, NULL, NULL, '1'
'5', 'GG435ED', '1', '5', '2022-07-16', NULL, '5', '0'
While I need to have this (edited 17/07/2022):
'1', 'FA787MX', '1', '2', '2022-07-16', NULL, '1', '0'
'3', 'AB124DF', '1', '4', '2022-07-13', '2022-07-14', '3', '1'
'4', 'CC666VC', '1', NULL, NULL, NULL, NULL, '1'
'5', 'GG435ED', '1', '5', '2022-07-16', NULL, '5', '0'
i.e. only the second row of FA787MX car should be mantained since it's the most recent out_date value (no matter if it's id is higher or lower).
For the sake of completeness: There is no guarantee that rental ids will be kept consistent with their rental history. In other words you cannot be sure that for a given car the rental where in_date = NULL is the correct one but you should compare them by out_date value.
Data sample:
INSERT INTO `Cars` (`id`, `plate`, `flag`) VALUES (1, 'FA787MX', 1);
INSERT INTO `Cars` (`id`, `plate`, `flag`) VALUES (2, 'EX431YY', 0);
INSERT INTO `Cars` (`id`, `plate`, `flag`) VALUES (3, 'AB124DF', 1);
INSERT INTO `Cars` (`id`, `plate`, `flag`) VALUES (4, 'CC666VC', 1);
INSERT INTO `Cars` (`id`, `plate`, `flag`) VALUES (5, 'GG435ED', 1);
INSERT INTO `Rents` (`id`, `out_date`, `in_date`, `car_id`) VALUES (1, '2022-07-14', '2022-07-15', 1);
INSERT INTO `Rents` (`id`, `out_date`, `in_date`, `car_id`) VALUES (2, '2022-07-16', NULL, 1);
INSERT INTO `Rents` (`id`, `out_date`, `in_date`, `car_id`) VALUES (3, '2022-07-16', NULL, 2);
INSERT INTO `Rents` (`id`, `out_date`, `in_date`, `car_id`) VALUES (4, '2022-07-13', '2022-07-14', 3);
INSERT INTO `Rents` (`id`, `out_date`, `in_date`, `car_id`) VALUES (5, '2022-07-16', NULL, 5);
One option is to join to find only those rentals that are still outstanding (in_date IS NULL). That will drop the old rentals having in_date not null.
Based on the updated requirements, there are a few ways to do it. One is a simple outer join to find the most recent rental per car to obtain the corresponding in_date as well...
MySQL 5.6 fiddle
SELECT Cars.*
, Rents.out_date
, Rents.in_date
, Rents.id IS NULL OR Rents.in_date IS NOT NULL AS status_final
FROM Cars
LEFT JOIN Rents
ON Cars.id = Rents.Car_id
LEFT JOIN Rents AS r2
ON Rents.out_date < r2.out_date
AND Rents.Car_id = r2.Car_id
WHERE Cars.Flag = 1
AND r2.Car_id IS NULL
ORDER BY Cars.id
;
The result:
id
plate
flag
out_date
in_date
status_final
1
FA787MX
1
2022-07-16
0
3
AB124DF
1
2022-07-13
2022-07-14
1
4
CC666VC
1
1
5
GG435ED
1
2022-07-16
0
Based on the original requirements: Try this (fiddle):
SELECT Cars.*
, Rents.in_date
, CASE WHEN in_date IS NOT NULL OR Rents.id IS NULL THEN 1 ELSE 0 END AS status_final
FROM Cars
LEFT JOIN Rents
ON Cars.id = Rents.Car_id
AND in_date IS NULL
WHERE Cars.Flag = 1
;
and if the results contain only those with in_date IS NULL, this reduces to:
SELECT Cars.*
, out_date
, Rents.in_date
, Rents.id IS NULL AS status_final
FROM Cars
LEFT JOIN Rents
ON Cars.id = Rents.Car_id
AND in_date IS NULL
WHERE Cars.Flag = 1
;
Result:
id
plate
flag
out_date
in_date
status_final
1
FA787MX
1
2022-07-16
0
3
AB124DF
1
1
4
CC666VC
1
1
5
GG435ED
1
2022-07-16
0
If your version of MySql is 8.0+ use ROW_NUMBER() window function to pick the latest row for each car in Rents:
SELECT c.*, r.*,
r.out_date IS NULL OR r.in_date IS NOT NULL status
FROM Cars c
LEFT JOIN (
SELECT *, ROW_NUMBER() OVER (PARTITION BY car_id ORDER BY out_date DESC) rn
FROM Rents
) r ON r.car_id = c.id AND r.rn = 1
WHERE c.flag = 1;
For previous versions use NOT EXISTS:
SELECT c.*, r.*,
r.out_date IS NULL OR r.in_date IS NOT NULL status
FROM Cars c
LEFT JOIN (
SELECT r1.*
FROM Rents r1
WHERE NOT EXISTS (
SELECT *
FROM Rents r2
WHERE r2.car_id = r1.car_id AND r2.out_date > r1.out_date
)
) r ON r.car_id = c.id
WHERE c.flag = 1;
See the demo.
If you imagine the result of your query as a table, you can easily write a query that would give you what you need (the subquery is just yours with the select spelled out to give a unique column name to the second id column, as it seemed useful - the only way to uniquely identify a row):
SELECT MAX(rent_id) FROM (
SELECT
Cars.id as id,
plate,
flag,
Rents.id as rent_id,
out_date,
in_date,
car_id,
IF(Rents.in_date IS NOT NULL AND Rents.out_date IS NOT NULL, 1, IF(Rents.id IS NULL, 1, 0)) AS status
FROM Cars
LEFT JOIN Rents ON Cars.id = Rents.car_id WHERE Cars.Flag = 1
) as rental_status
WHERE status = 0
GROUP BY car_id;
Which tells you which rows are interesting:
+--------------+
| MAX(rent_id) |
+--------------+
| 2 |
| 5 |
+--------------+
Now you can use a join to return the results of your initial query only for the interesting rows. To avoid having to spell out that query all over again, MySQL 8 has a way to stash the results of your core query and use it like a table:
WITH
status_data AS (
SELECT
Cars.id as id,
plate,
flag,
Rents.id as rent_id,
out_date,
in_date,
car_id,
IF(Rents.in_date IS NOT NULL AND Rents.out_date IS NOT NULL, 1, IF(Rents.id IS NULL, 1, 0)) AS status
FROM Cars
LEFT JOIN Rents ON Cars.id = Rents.car_id WHERE Cars.Flag = 1
)
SELECT * from status_data
JOIN (
SELECT MAX(rent_id) as rent_id FROM status_data
WHERE status = 0
GROUP BY car_id
) as ids using(rent_id);
Giving the result:
+---------+----+---------+------+------------+---------+--------+--------+
| rent_id | id | plate | flag | out_date | in_date | car_id | status |
+---------+----+---------+------+------------+---------+--------+--------+
| 2 | 1 | FA787MX | 1 | 2022-07-16 | NULL | 1 | 0 |
| 5 | 5 | GG435ED | 1 | 2022-07-16 | NULL | 5 | 0 |
+---------+----+---------+------+------------+---------+--------+--------+

How to get rows ordered by desc from specific ids

My goal : Getting Purchase requests ordered by most confirmed shipment locations.
Purchase requests are linked to a shipment location (warehouse).
I have a table :
CREATE TABLE IF NOT EXISTS `shipment_locations` (
`id` int(6) primary key,
`name` varchar(200) NOT NULL
);
INSERT INTO `shipment_locations` (`id`, `name`) VALUES
('1', 'france'),
('2', 'usa'),
('3', 'spain'),
('4', 'germany');
CREATE TABLE IF NOT EXISTS `purchase_requests` (
`id` int(6) primary key,
`name` varchar(200) NOT NULL,
`total_cost_confirmed` int(6) NULL,
`shipment_location_id` int(6) NULL,
FOREIGN KEY (`shipment_location_id`) REFERENCES `shipment_locations` (`id`)
);
INSERT INTO `purchase_requests` (`id`, `name`, `total_cost_confirmed`, `shipment_location_id`) VALUES
('1', 'pr1', '109', 1),
('2', 'pr2', '1500', 3),
('3', 'pr3', '3000', 2),
('4', 'pr4', '10000', 2),
('5', 'pr5', '5', 3),
('6', 'pr6', '3000', 2),
('7', 'pr7', '3000', 2),
('8', 'pr8', '1', 3),
('9', 'pr9', '10000000', 3);
For ordering by shipment location that have the most confirmed cost, it's pretty simple :
SELECT shipment_location_id, SUM(total_cost_confirmed) totalConfirmed
FROM purchase_requests
GROUP BY shipment_location_id
ORDER BY totalConfirmed DESC
It works perfectly here : http://sqlfiddle.com/#!9/732f32/2/0
But, then I tried to filter by purchase request id (adding GROUP BY id and WHERE id IN(...)) it gives me the wrong order (because it's taking ids present in the result).
=> (sqlfiddle)
How I can keep the correct order from the first query while filtering by Purchase request id ?
Adding sqlfiddle : Sqlfiddle
Thanks by advance for your help :)
First aggregate to get the sum of totalConfirmed and then join to the table:
SELECT p.id, p.name, p.shipment_location_id, t.totalConfirmed
FROM purchase_requests p
INNER JOIN (
SELECT shipment_location_id, SUM(total_cost_confirmed) totalConfirmed
FROM purchase_requests
GROUP BY shipment_location_id
) t ON t.shipment_location_id = p.shipment_location_id
WHERE p.id IN ('1', '3', '4', '8')
ORDER BY t.totalConfirmed DESC
See the demo.
Results:
> id | name | shipment_location_id | totalConfirmed
> -: | :--- | -------------------: | -------------:
> 4 | pr4 | 2 | 19000
> 3 | pr3 | 2 | 19000
> 8 | pr8 | 1 | 110
> 1 | pr1 | 1 | 110

Getting highest calculated score of GROUP BY in mysql

I'm trying to retrieve the best suited price for a product in each quantity depending on the customer and/or his customer group. To do so, I use a weight based system: the matching customer group is more important than the matching customer, so if two rows collide, we should get the row corresponding to the customer group id.
Here's an example:
Customer n°1 is part of Customer group n°2
Product prices:
A - 90€ for customer n°1 (when buying at least 2 of the same product)
B - 80€ for customer group n°2 (when buying at least 2 of the same product)
So the price shown to the customer n°1 should be 80€
He's my query:
SELECT
MAX(IF(t.customer_id = 1, 10, 0) + IF(t.customer_group_id = 1, 100, 0)) as score,
t.*
FROM tierprice t
WHERE t.product_variant_id = 110
AND (t.customer_id = 1 OR t.customer_id IS NULL)
AND (t.customer_group_id = 1 OR t.customer_group_id IS NULL)
GROUP BY t.product_variant_id, t.qty
The problem I'm having is that the correct score is shown in the result row (here: 100), but the row for the given score is not correct. I'm guessing it has something to do with the MAX in the SELECT and the GROUP BY, but I don't know how to assign the score to the row, and then take the highest.
Here's a fiddle :
CREATE TABLE `tierprice` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_variant_id` int(11) DEFAULT NULL,
`customer_group_id` int(11) DEFAULT NULL,
`price` int(11) NOT NULL,
`qty` int(11) NOT NULL,
`customer_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `no_duplicate_prices` (`qty`,`product_variant_id`,`customer_group_id`),
KEY `IDX_BA5254F8A80EF684` (`product_variant_id`),
KEY `IDX_BA5254F8D2919A68` (`customer_group_id`),
KEY `IDX_BA5254F89395C3F3` (`customer_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `tierprice` (`id`, `product_variant_id`, `customer_group_id`, `price`, `qty`, `customer_id`)
VALUES
(1, 110, NULL, 8000, 2, 1),
(2, 110, 1, 7000, 2, NULL),
(3, 110, 1, 6000, 5, NULL),
(4, 110, NULL, 5000, 5, 1),
(5, 111, 1, 8000, 2, NULL),
(6, 111, NULL, 6000, 2, 1),
(7, 111, 1, 7000, 6, NULL),
(8, 111, NULL, 5000, 6, 1);
http://sqlfiddle.com/#!9/7bc0d9/2
The price ids that should come out in the result should be ID 2 & ID 3.
Thank you for your help.
Provided query is not a valid query from SQL standard's perspective:
SELECT
MAX(IF(t.customer_id = 1, 10, 0) + IF(t.customer_group_id = 1, 100, 0)) as score,
t.*
FROM tierprice t
WHERE t.product_variant_id = 110
AND (t.customer_id = 1 OR t.customer_id IS NULL)
AND (t.customer_group_id = 1 OR t.customer_group_id IS NULL)
GROUP BY t.product_variant_id, t.qty;
Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 't.id' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
Related: Group by clause in mySQL and postgreSQL, why the error in postgreSQL?
It could be rewritten using windowed functions(MySQL 8.0 and above):
WITH cte AS (
SELECT t.*, ROW_NUMBER() OVER(PARTITION BY product_variant_id, qty
ORDER BY IF(t.customer_id=1,10,0)+IF(t.customer_group_id=1,100,0) DESC) AS rn
FROM tierprice t
WHERE t.product_variant_id = 110
AND (t.customer_id = 1 OR t.customer_id IS NULL)
AND (t.customer_group_id = 1 OR t.customer_group_id IS NULL)
)
SELECT *
FROM cte
WHERE rn = 1;
db<>fiddle demo
The only valid columns that your query can return are product_variant_id, qty, which you use in GROUP BY clause, and the aggregated column score.
Because of t.* you get all the columns of the table but the values chosen are nondeterministic, for the other columns, as it is explained in MySQL Handling of GROUP BY.
What you can do is join your query to the table like this:
SELECT t.*
FROM tierprice t
INNER JOIN (
SELECT product_variant_id, qty,
MAX(IF(customer_id = 1, 10, 0) + IF(customer_group_id = 1, 100, 0)) as score
FROM tierprice
WHERE product_variant_id = 110
AND (customer_id = 1 OR customer_id IS NULL)
AND (customer_group_id = 1 OR customer_group_id IS NULL)
GROUP BY product_variant_id, qty
) g ON g.product_variant_id = t.product_variant_id
AND g.qty = t.qty
AND g.score = IF(t.customer_id = 1, 10, 0) + IF(t.customer_group_id = 1, 100, 0)
WHERE (t.customer_id = 1 OR t.customer_id IS NULL)
AND (t.customer_group_id = 1 OR t.customer_group_id IS NULL)
See the demo.
Results:
> id | product_variant_id | customer_group_id | price | qty | customer_id
> -: | -----------------: | ----------------: | ----: | --: | ----------:
> 2 | 110 | 1 | 7000 | 2 | null
> 3 | 110 | 1 | 6000 | 5 | null

Group and rank top N rows by category

I have a table with the columns category and votes. I've tried multiple solutions before with very little success; usually what would happen is that instead of returning the top 3 items in each category, it returns all of the items available.
SELECT `id`, `user_id`, `full_name`, `category`, `year`, `month`, `thumbnail_photo`, `photo_title`, `votes`
FROM
(
SELECT `id`, `user_id`, `full_name`, `category`, `year`, `month`, `thumbnail_photo`, `photo_title`, `votes`,
#category_rank := IF(#current_category = category, #category_rank + 1, 1) AS category_rank,
#current_category := category
FROM `photo_contest`
ORDER BY
`category`,
`votes` DESC
) ranked
WHERE
category_rank <= 3
AND `year` = '2017'
AND `month` = 'April'
AND `votes` > 0
This particular solution was adapted from SQLines. What I ultimately want to do is to turn a table like this:
Name | Category | Votes
--------- | -------- | -----
Name Foo | CatFoo | 0
Name Bar | CatFoo | 1
Name Baz | CatFoo | 10
Name Quux | CatFoo | 200
Name ooF | CatBar | 50
Name raB | CatBar | 300
Name zaB | CatBar | 10
Name xuuQ | CatBar | 200
...to:
Name | Category | Votes
--------- | -------- | -----
Name Quux | CatFoo | 200
Name Baz | CatFoo | 10
Name Bar | CatFoo | 1
Name raB | CatBar | 300
Name xuuQ | CatBar | 200
Name ooF | CatBar | 50
...with the other WHERE statements included. Year, month, and minimum votes.
Your subquery tries to calculate ranking over the entire table. If you only want to rank for the selected year-month with votes > 0, you should copy those conditions into the subquery as its own WHERE conditions.
UPDATE:
Looks like it's the missing ORDER BY in the outer-query that causes the said problem. I've created the following DDL/SQL at sqlfiddle.
CREATE TABLE IF NOT EXISTS `votes` (
`id` INT NOT NULL,
`category` VARCHAR(10) NULL,
`year` VARCHAR(4) NULL,
`month` VARCHAR(2) NULL,
`votes` INT
)
ENGINE = InnoDB;
INSERT INTO `votes` VALUES
(10, 'cat1', '2016', '05', 300),
(10, 'cat1', '2016', '06', 200),
(10, 'cat2', '2016', '05', 500),
(11, 'cat1', '2016', '05', 200),
(11, 'cat2', '2016', '05', 0),
(11, 'cat2', '2016', '06', 100),
(12, 'cat1', '2016', '05', 400),
(12, 'cat2', '2016', '05', 150),
(13, 'cat1', '2016', '05', 350),
(13, 'cat2', '2016', '05', 100),
(13, 'cat2', '2016', '06', 150),
(14, 'cat1', '2016', '05', 0),
(14, 'cat2', '2016', '05', 450);
SELECT `id`, `category`, `year`, `month`, `votes`
FROM (
SELECT `id`, `category`, `year`, `month`, `votes`,
#category_rank := IF(#current_category = category, #category_rank + 1, 1) AS category_rank,
#current_category := category
FROM `votes`
WHERE
`year` = '2016'
AND `month` = '05'
AND `votes` > 0
ORDER BY
`category`,
`votes` DESC
) ranked
WHERE
category_rank <= 3
ORDER BY
`category`,
`votes` DESC;
I'm not an expert with MySQL, so I propose you a different (standard SQL) approach: you can join the table with itself on Category and on Votes being less or equal to the votes of the current row.
select t1.Name, t1.Category, t1.Votes, count(distinct t2.Name) as rank
from photo_contest t1
join photo_contest t2
on t1.Category = t2.Category and
t1.Votes <= t2.Votes
/*where whatever you want*/
group by t1.Name, t1.Category, t1.Votes
having count(distinct t2.Name) <= 3
order by t1.Category, rank
I tested it here and it seems to do what you asked for
It sounds like your PHPMyAdmin needs an upgrade or a replacement. Meanwhile you might want to try #Stefano Zanini's non-MySQL specific SQL:
SELECT
t1.`id`, t1.`category`, t1.`year`, t1.`month`,
t1.`votes`, count(distinct t2.`id`) as rank
FROM photo_contest t1
INNER JOIN photo_contest t2 ON
t1.`category` = t2.`category` AND
t1.`votes` <= t2.`votes`
WHERE t1.`votes` > 0
GROUP BY t1.`id`, t1.`category`, t1.`votes`
HAVING count(distinct t2.`id`) <= 3
ORDER BY t1.`category`, rank;
It's available on sqlfiddle. If you think this solution suits you better please credit #Stefano Zanini's answer instead of this one.

How do I get the sum of each persons best ten scores of the season?

I have a database of results for a season of 25 games. However only each persons best ten scores count.
Can anybody tell me how to sum just the top ten scores of each person AND show the minimum score that was used in that sum (their tenth best).
The database has PlayerName, TournamentID, Points
eg.
- TounamentID PlayerName Points
- 1 Jo 100
- 1 Tel 50
- 1 Kevin 75
- 2 Jo 100
- 2 Tel 50
- 2 Kevin 75
- 3 Jo 100
- 3 Tel 50
- 3 Kevin 75
- 4 Jo 100
- 4 Tel 50
- 4 Kevin 75
- 5 Jo 100
- 5 Tel 50
- 5 Kevin 75 etc
Many thanks in advance
EDIT 1
At the moment I have this kind of working though it doesn't handle duplicate scores very well and can actual end up adding up the top 11 if there's a duplicate;
SELECT X.PlayerName, Sum(X.Points) AS SumOfPoints, Min(X.Points) AS Target
FROM SoP11PreBats AS X
WHERE (((10)>(SELECT count(*)
FROM SoP11PreBats
WHERE PlayerName = X.PlayerName
AND Points > X.Points )))
GROUP BY X.PlayerName
ORDER BY Sum(X.Points) DESC;
Something like this would work for one player at a time:
SELECT SUM(n), MIN(n) FROM
(SELECT points AS n
FROM table
WHERE PlayerName = ?
ORDER BY n DESC
LIMIT 10
)
I'm not sure how to expand it to produce a table for every player.
SELECT test.playername, sum(top10.score), MIN(top10.score)
FROM test
LEFT JOIN (SELECT playername, score FROM test ORDER BY score DESC LIMIT 10) top10
ON top10.playername = test.playername
GROUP BY test.playername
Edit: Turns out the above approach using a subselect and join is not going to do the trick. Because you limit the results in the subselect it is not going to lead to a set of max 10 records PER playername.
The next approach would be to do something like
SELECT pk, name, score from test where
pk IN (SELECT pk FROM test t2 WHERE t2.name = name ORDER BY score DESC LIMIT 10)
This could create the proper set of records to join with. However LIMIT is not supported inside an IN clause (yet). So this won't compile.
EIDT2: The final approach I can think of goes like this:
select distinct test.name
, (SELECT sum(t2.score) FROM (select t3.score FROM test t3 WHERE t3.name = test.name ORDER BY score desc LIMIT 10) t2)
, (SELECT min(t2.score) FROM (select t3.score FROM test t3 WHERE t3.name = test.name ORDER BY score desc LIMIT 10) t2)
FROM test
This one also won't compile because the most inner test.name in the where can't be resolved properly.
I don't think that what you want to do can currently be done in 1 single query on mysql, but I'm curious if someone can proof me wrong.
Ok I got it working but it's about the most nasty sql hack I could think of and I'm not sure you should even consider to put it in production like this. It also only runs on mysql:
select PlayerName, sum(Points), min(Points) from
(select distinct SoP11PreBats.PlayerName, (SELECT t2.Points FROM test t2 WHERE t2.PlayerName = SoP11PreBats.PlayerName ORDER BY Points desc LIMIT 1) as Points FROM test
UNION ALL
select distinct SoP11PreBats.PlayerName, (SELECT t2.Points FROM test t2 WHERE t2.PlayerName = SoP11PreBats.PlayerName ORDER BY Points desc LIMIT 1,1) as Points FROM test
UNION ALL
select distinct SoP11PreBats.PlayerName, (SELECT t2.Points FROM test t2 WHERE t2.PlayerName = SoP11PreBats.PlayerName ORDER BY Points desc LIMIT 2,1) as Points FROM test
UNION ALL
select distinct SoP11PreBats.PlayerName, (SELECT t2.Points FROM test t2 WHERE t2.PlayerName = SoP11PreBats.PlayerName ORDER BY Points desc LIMIT 3,1) as Points FROM test
UNION ALL
select distinct SoP11PreBats.PlayerName, (SELECT t2.Points FROM test t2 WHERE t2.PlayerName = SoP11PreBats.PlayerName ORDER BY Points desc LIMIT 4,1) as Points FROM test
UNION ALL
select distinct SoP11PreBats.PlayerName, (SELECT t2.Points FROM test t2 WHERE t2.PlayerName = SoP11PreBats.PlayerName ORDER BY Points desc LIMIT 5,1) as Points FROM test
UNION ALL
select distinct SoP11PreBats.PlayerName, (SELECT t2.Points FROM test t2 WHERE t2.PlayerName = SoP11PreBats.PlayerName ORDER BY Points desc LIMIT 6,1) as Points FROM test
UNION ALL
select distinct SoP11PreBats.PlayerName, (SELECT t2.Points FROM test t2 WHERE t2.PlayerName = SoP11PreBats.PlayerName ORDER BY Points desc LIMIT 7,1) as Points FROM test
UNION ALL
select distinct SoP11PreBats.PlayerName, (SELECT t2.Points FROM test t2 WHERE t2.PlayerName = SoP11PreBats.PlayerName ORDER BY Points desc LIMIT 8,1) as Points FROM test
UNION ALL
select distinct SoP11PreBats.PlayerName, (SELECT t2.Points FROM test t2 WHERE t2.PlayerName = SoP11PreBats.PlayerName ORDER BY Points desc LIMIT 9,1) as Points FROM test
) top10
group by name
I have a feeling this will not work:
SELECT *
FROM
( SELECT pd.PlayerName
, ( SELECT SUM(t10.Points)
FROM
( SELECT t10.Points
FROM SoP11PreBats AS t10
WHERE t10.PlayerName = pd.PlayerName
ORDER BY t10.Points DESC
LIMIT 10
) AS x
) AS Sum10
, ( SELECT t10.Points
FROM SoP11PreBats AS t10
WHERE t10.PlayerName = pd.PlayerName
ORDER BY t10.Points DESC
LIMIT 1 OFFSET 9
) AS TenthBest
FROM
( SELECT DISTINCT PlayerName
FROM SoP11PreBats
) AS pd
) AS y
ORDER BY Sum10 DESC
But this will:
SELECT pb.PlayerName AS PlayerName
, COALESCE(SUM(p.Points),0) + pb.TenthBest*(10-COUNT(p.Points))
AS SumOfPoints
, pb.TenthBest AS Target
FROM
( SELECT pd.PlayerName
, ( SELECT t10.Points
FROM SoP11PreBats AS t10
WHERE t10.PlayerName = pd.PlayerName
ORDER BY t10.Points DESC
LIMIT 1 OFFSET 9
) AS TenthBest
FROM
( SELECT DISTINCT PlayerName
FROM SoP11PreBats
) AS pd
) AS pb
LEFT JOIN SoP11PreBats AS p
ON p.PlayerName = pb.PlayerName
AND p.Points > pb.TenthBest
GROUP BY pb.PlayerName
ORDER BY SumOfPoints DESC
This works (see test output below):
set #count:=0, #player:='';
SELECT
PlayerName,
SUM(Points) as sum_top_10,
MIN(Points) as min_top_10
FROM (SELECT PlayerName, Points
FROM (SELECT
Points,
#count := if (#player != PlayerName, 0, #count + 1) as count,
#player := PlayerName as PlayerName
FROM (SELECT PlayerName, Points FROM SoP11PreBATS order by 1, 2 desc) x) y
where count < 10) z
group by 1;
Here's the test, using OP's data, plus extra rows for 'Jo' to make more than 10 rows:
create table SoP11PreBATS (TounamentID int, PlayerName text, Points int);
delete from SoP11PreBATS;
insert into SoP11PreBATS values
(1, 'Jo', 100), (1, 'Tel', 50), (1, 'Kevin', 75), (2, 'Jo', 100), (2, 'Tel', 50),
(2, 'Kevin', 75), (3, 'Jo', 100), (3, 'Tel', 50), (3, 'Kevin', 75), (4, 'Jo', 100),
(4, 'Tel', 50), (4, 'Kevin', 75), (5, 'Jo', 100), (5, 'Tel', 50), (5, 'Kevin', 75),
(5, 'Jo', 50), (6, 'Jo', 75), (7, 'Jo', 100), (8, 'Jo', 50), (9, 'Jo', 75),
(10, 'Jo', 50), (11, 'Jo', 75), (12, 'Jo', 100);
select * from SoP11PreBATS where playername = 'Jo' order by points desc;
+-------------+------------+--------+
| TounamentID | PlayerName | Points |
+-------------+------------+--------+
| 1 | Jo | 100 |
| 2 | Jo | 100 |
| 3 | Jo | 100 |
| 4 | Jo | 100 |
| 5 | Jo | 100 |
| 7 | Jo | 100 |
| 12 | Jo | 100 |
| 6 | Jo | 75 |
| 9 | Jo | 75 |
| 11 | Jo | 75 |
| 5 | Jo | 50 |
| 8 | Jo | 50 |
| 10 | Jo | 50 |
+-------------+------------+--------+
-- Inspection shows Jo should have 925 as sum and 75 as min
-- Ran query above and got:
+------------+------------+------------+
| PlayerName | sum_top_10 | min_top_10 |
+------------+------------+------------+
| Jo | 925 | 75 |
| Kevin | 375 | 75 |
| Tel | 250 | 50 |
+------------+------------+------------+
-- Test output correct
I think this works, and it only uses one derived table:
SELECT #row := 0, #pp := NULL, #min := 0;
SELECT Player,
SUM(Points) AS Points,
MIN(Points) AS MinPoints
FROM (
SELECT Player,
Points,
#row := IF(
IFNULL(#pp, '') <> Player AND NOT (#pp := Player),
1,
#row + 1
) AS Row
FROM SoP11PreBats
ORDER BY Player, Points DESC
) tmp
WHERE tmp.Row <= 10
GROUP BY Player;
Test data:
CREATE TABLE `SoP11PreBats` (
`TournamentID` int(11) NOT NULL,
`Player` varchar(255) NOT NULL,
`Points` int(11) NOT NULL
);
INSERT INTO SoP11PreBats (TournamentID, Player, Points) VALUES
(15, 'Jo', 10),
(14, 'Jo', 20),
(13, 'Jo', 30),
(12, 'Jo', 40),
(11, 'Jo', 50),
(10, 'Jo', 60),
( 9, 'Jo', 70),
( 8, 'Jo', 80),
( 7, 'Jo', 90),
( 6, 'Jo', 100),
( 5, 'Jo', 110),
( 4, 'Jo', 120),
( 3, 'Jo', 130),
( 2, 'Jo', 140),
( 1, 'Jo', 150),
( 1, 'Tel', 15),
( 2, 'Tel', 25),
( 3, 'Tel', 35),
( 4, 'Tel', 45),
( 5, 'Tel', 55),
( 6, 'Tel', 65),
( 7, 'Tel', 75),
( 8, 'Tel', 85),
( 9, 'Tel', 95),
(10, 'Tel', 105),
(11, 'Tel', 115),
(12, 'Tel', 125),
(13, 'Tel', 135),
(14, 'Tel', 145),
(15, 'Tel', 155),
( 1, 'Kevin', 10),
( 2, 'Kevin', 20),
( 3, 'Kevin', 30),
( 4, 'Kevin', 40),
( 5, 'Kevin', 50),
( 6, 'Kevin', 60),
( 7, 'Kevin', 70),
( 8, 'Kevin', 80),
( 9, 'Kevin', 90),
(10, 'Kevin', 100),
(11, 'Kevin', 110),
(12, 'Kevin', 120),
(13, 'Kevin', 130),
(14, 'Kevin', 140),
(15, 'Kevin', 150);
Result:
+--------+--------+-----------+
| Player | Points | MinPoints |
+--------+--------+-----------+
| Jo | 1050 | 60 |
| Kevin | 1050 | 60 |
| Tel | 1100 | 65 |
+--------+--------+-----------+