MySQL Group by column, then count by another column - php

I have a challenging MySQL problem that is beyond my basic knowledge, I would really appreciate any help.
I currently have the following query:
select users.userid, CAST(posts.time AS DATE)
FROM users INNER JOIN posts ON users.post_id = posts.id
Sample output:
userid | CAST(posts.time AS DATE)
1............2015-01-05
2............2015-02-06
2............2015-04-07
2............2015-04-07
3............2015-04-07
1............2015-02-06
7............2015-01-05
userid can repeat itself, there could be 10 different rows with userid = 1; same goes for the date column. I would like to count how many rows each userid had for each distinct date. Based on the above data, the output should be:
-----------------------1----------2--------3---------4--------5--------6-------7
2015-01-05.............1..........0........0.........0........0........0.......1
2015-02-06.............1..........1........0.........0........0........0.......0
2015-04-07.............0..........2........1.........0........0........0.......0
I have 7 users in total. I would like to further replace the user id with a name that I define; e.g. I would define 1 in the heading/title to be displayed as Mike, 2 to be displayed as George, and so forth...
Is it possible? Thanks everyone.

If you have 7 users only, and only ever will, pivoting the data is not too difficult:
select date(posts.time),
count(case when userid = 1 then userid end) as `1`,
count(case when userid = 2 then userid end) as `2`,
count(case when userid = 3 then userid end) as `3`,
count(case when userid = 4 then userid end) as `4`,
count(case when userid = 5 then userid end) as `5`,
count(case when userid = 6 then userid end) as `6`,
count(case when userid = 7 then userid end) as `7`
users INNER JOIN posts ON users.post_id = posts.id
group by date(posts.time)
demo here
If your number of users is variable, or prone to change - it becomes annoying and you'd be better off looking to your application language to take care of it.

Here's what I have (I didn't complete it for you):
SELECT date, SUM(id_1) AS Mike, SUM(id_2) AS George FROM (SELECT CASE id WHEN 1 THEN 1 ELSE 0 END as id_1, CASE id WHEN 2 THEN 1 ELSE 0 END as id_2, date FROM test_dates) as tmp GROUP BY date;
+------------+------+--------+
| date | Mike | George |
+------------+------+--------+
| 2015-01-05 | 1 | 0 |
| 2015-02-06 | 1 | 1 |
| 2015-04-07 | 0 | 2 |
+------------+------+--------+
The trick of substituting a summation of 1s when what you want is a count is a common reporting trick that is worth remembering. Blew my mind when I first saw it.

Related

Mysql users compare to itself

I'm trying to create a overview Who owe's who on call hours.
below the table i have.
so user 2 has done 9 hours for user 1. but user 1 has done 3 hours for user 2 so the total owing from user 1 is 6 hours. and user 2 is owing user 3, 2 on call hours. how can i make a cross reference table from all users in Mysql?
First, this is not a natural thing to do in SQL. But it is possible. To make a cross-reference table, you need to generate the rows and then fill the columns:
select user_id_a,
sum(case when user_id_b = 1 then hours_oncall else 0 end) as user_1,
sum(case when user_id_b = 2 then hours_oncall else 0 end) as user_2,
sum(case when user_id_b = 3 then hours_oncall else 0 end) as user_3
from ((select user_id_a from t
) union
(select user_id_b from t
)
) u left join
t
on t.user_id_a = u.user_id_a
group by u.user_id_a;
In plain SQL you can't have a dynamic number of columns that adjusts to the number of users, though in your code you could generate SQL that does that. But I think you are better off just getting rows showing who owes whom how many hours and transforming the data like a pivot table in your code.
To get the basic data, assuming your table is called hours_oncall:
select ower,owed,sum(hours_oncall) hours
from (
select user_id_a ower,user_id_b owed,hours_oncall from hours_oncall
union all
select user_id_b,user_id_a,-hours_oncall from hours_oncall
) hours_oncall_union
group by 1,2
having hours>0;
which for your sample data, returns:
+------+------+-------+
| ower | owed | hours |
+------+------+-------+
| 1 | 2 | 6 |
| 2 | 3 | 2 |
+------+------+-------+

Joining most recent column entry to MySQL query result

I have a PHP/MySQL based solution for displaying an ID and the number of entries that ID has on a MySQL database table in the past day, and year to date.
The information is simply output to a table.
We would also be able to quickly see the most recent value for a particular column associated with each ID on this same table.
The tables in use have the following descriptions:
TABLE: tags
id int auto_increment
Tag int(8)
Timestamp timestamp
Battery varchar(3) // Status for rfid tags battery condition
TABLE: tag_vehicle
id int auto_increment
Tag int(8)
VehicleId varchar(10)
And here is my MySQL query
SELECT tags.Tag,
sum(CASE WHEN year(Timestamp) = year(CURDATE()) then 1 else 0 end) as ytd,
sum(CASE WHEN date(Timestamp) = date(CURDATE()) then 1 else 0 end) as today,
tag_vehicle.VehicleId,
MAX(Timestamp) as latest
FROM tags
INNER JOIN tag_vehicle
ON tags.Tag = tag_vehicle.Tag
GROUP BY VehicleNumber;
This generates a table with these columns
| Tag | ytd | today | VehicleId | Timestamp |
|1234 | 300 | 12 | BUS1234 | 2014-09-22 |
I'm simply looking to add to my table the latest value of Battery that matches each Tag. I've been trying all morning to produce this result but I haven't had any luck.
| Tag | ytd | today | VehicleId | Timestamp | Battery |
|1234 | 300 | 12 | BUS1234 | 2014-09-22 | ok |
I'm not an expert on MySQL and its starting to feel like the query is too messy. I'm having a hard time figuring out how to get this battery value.
How should I go about obtaining the latest entry for Battery and matching it to the right row?
Is there some way to do it all in 1 (perhaps cleaner) query, or should I make 2 queries and match Battery columns to Tag columns?
You should be able to do this with the substring_index()/group_concat() trick:
SELECT t.tag,
sum(CASE WHEN year(Timestamp) = year(CURDATE()) then 1 else 0 end) as ytd,
sum(CASE WHEN date(Timestamp) = date(CURDATE()) then 1 else 0 end) as today,
tv.VehicleId,
MAX(Timestamp) as latest,
substring_index(group_concat(t.battery order by t.timestamp desc), ',', 1)
FROM tags t INNER JOIN
tag_vehicle tv
ON t.Tag = tv.Tag
GROUP BY VehicleNumber;
The tag column doesn't look right because it comes from an indeterminate row.

Sql count Average Limit By 2 each row [duplicate]

The following is the simplest possible example, though any solution should be able to scale to however many n top results are needed:
Given a table like that below, with person, group, and age columns, how would you get the 2 oldest people in each group? (Ties within groups should not yield more results, but give the first 2 in alphabetical order)
+--------+-------+-----+
| Person | Group | Age |
+--------+-------+-----+
| Bob | 1 | 32 |
| Jill | 1 | 34 |
| Shawn | 1 | 42 |
| Jake | 2 | 29 |
| Paul | 2 | 36 |
| Laura | 2 | 39 |
+--------+-------+-----+
Desired result set:
+--------+-------+-----+
| Shawn | 1 | 42 |
| Jill | 1 | 34 |
| Laura | 2 | 39 |
| Paul | 2 | 36 |
+--------+-------+-----+
NOTE: This question builds on a previous one- Get records with max value for each group of grouped SQL results - for getting a single top row from each group, and which received a great MySQL-specific answer from #Bohemian:
select *
from (select * from mytable order by `Group`, Age desc, Person) x
group by `Group`
Would love to be able to build off this, though I don't see how.
Here is one way to do this, using UNION ALL (See SQL Fiddle with Demo). This works with two groups, if you have more than two groups, then you would need to specify the group number and add queries for each group:
(
select *
from mytable
where `group` = 1
order by age desc
LIMIT 2
)
UNION ALL
(
select *
from mytable
where `group` = 2
order by age desc
LIMIT 2
)
There are a variety of ways to do this, see this article to determine the best route for your situation:
http://www.xaprb.com/blog/2006/12/07/how-to-select-the-firstleastmax-row-per-group-in-sql/
Edit:
This might work for you too, it generates a row number for each record. Using an example from the link above this will return only those records with a row number of less than or equal to 2:
select person, `group`, age
from
(
select person, `group`, age,
(#num:=if(#group = `group`, #num +1, if(#group := `group`, 1, 1))) row_number
from test t
CROSS JOIN (select #num:=0, #group:=null) c
order by `Group`, Age desc, person
) as x
where x.row_number <= 2;
See Demo
In other databases you can do this using ROW_NUMBER. MySQL doesn't support ROW_NUMBER but you can use variables to emulate it:
SELECT
person,
groupname,
age
FROM
(
SELECT
person,
groupname,
age,
#rn := IF(#prev = groupname, #rn + 1, 1) AS rn,
#prev := groupname
FROM mytable
JOIN (SELECT #prev := NULL, #rn := 0) AS vars
ORDER BY groupname, age DESC, person
) AS T1
WHERE rn <= 2
See it working online: sqlfiddle
Edit I just noticed that bluefeet posted a very similar answer: +1 to him. However this answer has two small advantages:
It it is a single query. The variables are initialized inside the SELECT statement.
It handles ties as described in the question (alphabetical order by name).
So I'll leave it here in case it can help someone.
Try this:
SELECT a.person, a.group, a.age FROM person AS a WHERE
(SELECT COUNT(*) FROM person AS b
WHERE b.group = a.group AND b.age >= a.age) <= 2
ORDER BY a.group ASC, a.age DESC
DEMO
How about using self-joining:
CREATE TABLE mytable (person, groupname, age);
INSERT INTO mytable VALUES('Bob',1,32);
INSERT INTO mytable VALUES('Jill',1,34);
INSERT INTO mytable VALUES('Shawn',1,42);
INSERT INTO mytable VALUES('Jake',2,29);
INSERT INTO mytable VALUES('Paul',2,36);
INSERT INTO mytable VALUES('Laura',2,39);
SELECT a.* FROM mytable AS a
LEFT JOIN mytable AS a2
ON a.groupname = a2.groupname AND a.age <= a2.age
GROUP BY a.person
HAVING COUNT(*) <= 2
ORDER BY a.groupname, a.age DESC;
gives me:
a.person a.groupname a.age
---------- ----------- ----------
Shawn 1 42
Jill 1 34
Laura 2 39
Paul 2 36
I was strongly inspired by the answer from Bill Karwin to Select top 10 records for each category
Also, I'm using SQLite, but this should work on MySQL.
Another thing: in the above, I replaced the group column with a groupname column for convenience.
Edit:
Following-up on the OP's comment regarding missing tie results, I incremented on snuffin's answer to show all the ties. This means that if the last ones are ties, more than 2 rows can be returned, as shown below:
.headers on
.mode column
CREATE TABLE foo (person, groupname, age);
INSERT INTO foo VALUES('Paul',2,36);
INSERT INTO foo VALUES('Laura',2,39);
INSERT INTO foo VALUES('Joe',2,36);
INSERT INTO foo VALUES('Bob',1,32);
INSERT INTO foo VALUES('Jill',1,34);
INSERT INTO foo VALUES('Shawn',1,42);
INSERT INTO foo VALUES('Jake',2,29);
INSERT INTO foo VALUES('James',2,15);
INSERT INTO foo VALUES('Fred',1,12);
INSERT INTO foo VALUES('Chuck',3,112);
SELECT a.person, a.groupname, a.age
FROM foo AS a
WHERE a.age >= (SELECT MIN(b.age)
FROM foo AS b
WHERE (SELECT COUNT(*)
FROM foo AS c
WHERE c.groupname = b.groupname AND c.age >= b.age) <= 2
GROUP BY b.groupname)
ORDER BY a.groupname ASC, a.age DESC;
gives me:
person groupname age
---------- ---------- ----------
Shawn 1 42
Jill 1 34
Laura 2 39
Paul 2 36
Joe 2 36
Chuck 3 112
Snuffin solution seems quite slow to execute when you've got plenty of rows and Mark Byers/Rick James and Bluefeet solutions doesn't work on my environnement (MySQL 5.6) because order by is applied after execution of select, so here is a variant of Marc Byers/Rick James solutions to fix this issue (with an extra imbricated select):
select person, groupname, age
from
(
select person, groupname, age,
(#rn:=if(#prev = groupname, #rn +1, 1)) as rownumb,
#prev:= groupname
from
(
select person, groupname, age
from persons
order by groupname , age desc, person
) as sortedlist
JOIN (select #prev:=NULL, #rn :=0) as vars
) as groupedlist
where rownumb<=2
order by groupname , age desc, person;
I tried similar query on a table having 5 millions rows and it returns result in less than 3 seconds
If the other answers are not fast enough Give this code a try:
SELECT
province, n, city, population
FROM
( SELECT #prev := '', #n := 0 ) init
JOIN
( SELECT #n := if(province != #prev, 1, #n + 1) AS n,
#prev := province,
province, city, population
FROM Canada
ORDER BY
province ASC,
population DESC
) x
WHERE n <= 3
ORDER BY province, n;
Output:
+---------------------------+------+------------------+------------+
| province | n | city | population |
+---------------------------+------+------------------+------------+
| Alberta | 1 | Calgary | 968475 |
| Alberta | 2 | Edmonton | 822319 |
| Alberta | 3 | Red Deer | 73595 |
| British Columbia | 1 | Vancouver | 1837970 |
| British Columbia | 2 | Victoria | 289625 |
| British Columbia | 3 | Abbotsford | 151685 |
| Manitoba | 1 | ...
Check this out:
SELECT
p.Person,
p.`Group`,
p.Age
FROM
people p
INNER JOIN
(
SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`
UNION
SELECT MAX(p3.Age) AS Age, p3.`Group` FROM people p3 INNER JOIN (SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`) p4 ON p3.Age < p4.Age AND p3.`Group` = p4.`Group` GROUP BY `Group`
) p2 ON p.Age = p2.Age AND p.`Group` = p2.`Group`
ORDER BY
`Group`,
Age DESC,
Person;
SQL Fiddle: http://sqlfiddle.com/#!2/cdbb6/15
WITH cte_window AS (
SELECT movie_name,director_id,release_date,
ROW_NUMBER() OVER( PARTITION BY director_id ORDER BY release_date DESC) r
FROM movies
)
SELECT * FROM cte_window WHERE r <= <n>;
Above query will returns latest n movies for each directors.
I wanted to share this because I spent a long time searching for an easy way to implement this in a java program I'm working on. This doesn't quite give the output you're looking for but its close. The function in mysql called GROUP_CONCAT() worked really well for specifying how many results to return in each group. Using LIMIT or any of the other fancy ways of trying to do this with COUNT didn't work for me. So if you're willing to accept a modified output, its a great solution. Lets say I have a table called 'student' with student ids, their gender, and gpa. Lets say I want to top 5 gpas for each gender. Then I can write the query like this
SELECT sex, SUBSTRING_INDEX(GROUP_CONCAT(cast(gpa AS char ) ORDER BY gpa desc), ',',5)
AS subcategories FROM student GROUP BY sex;
Note that the parameter '5' tells it how many entries to concatenate into each row
And the output would look something like
+--------+----------------+
| Male | 4,4,4,4,3.9 |
| Female | 4,4,3.9,3.9,3.8|
+--------+----------------+
You can also change the ORDER BY variable and order them a different way. So if I had the student's age I could replace the 'gpa desc' with 'age desc' and it will work! You can also add variables to the group by statement to get more columns in the output. So this is just a way I found that is pretty flexible and works good if you are ok with just listing results.
In SQL Server row_numer() is a powerful function that can get result easily as below
select Person,[group],age
from
(
select * ,row_number() over(partition by [group] order by age desc) rn
from mytable
) t
where rn <= 2
There is a really nice answer to this problem at MySQL - How To Get Top N Rows per Each Group
Based on the solution in the referenced link, your query would be like:
SELECT Person, Group, Age
FROM
(SELECT Person, Group, Age,
#group_rank := IF(#group = Group, #group_rank + 1, 1) AS group_rank,
#current_group := Group
FROM `your_table`
ORDER BY Group, Age DESC
) ranked
WHERE group_rank <= `n`
ORDER BY Group, Age DESC;
where n is the top n and your_table is the name of your table.
I think the explanation in the reference is really clear. For quick reference I will copy and paste it here:
Currently MySQL does not support ROW_NUMBER() function that can assign
a sequence number within a group, but as a workaround we can use MySQL
session variables.
These variables do not require declaration, and can be used in a query
to do calculations and to store intermediate results.
#current_country := country This code is executed for each row and
stores the value of country column to #current_country variable.
#country_rank := IF(#current_country = country, #country_rank + 1, 1)
In this code, if #current_country is the same we increment rank,
otherwise set it to 1. For the first row #current_country is NULL, so
rank is also set to 1.
For correct ranking, we need to have ORDER BY country, population DESC
SELECT
p1.Person,
p1.`GROUP`,
p1.Age
FROM
person AS p1
WHERE
(
SELECT
COUNT( DISTINCT ( p2.age ) )
FROM
person AS p2
WHERE
p2.`GROUP` = p1.`GROUP`
AND p2.Age >= p1.Age
) < 2
ORDER BY
p1.`GROUP` ASC,
p1.age DESC
reference leetcode

SQL joins. I got confused. Which one i need to use? [duplicate]

The following is the simplest possible example, though any solution should be able to scale to however many n top results are needed:
Given a table like that below, with person, group, and age columns, how would you get the 2 oldest people in each group? (Ties within groups should not yield more results, but give the first 2 in alphabetical order)
+--------+-------+-----+
| Person | Group | Age |
+--------+-------+-----+
| Bob | 1 | 32 |
| Jill | 1 | 34 |
| Shawn | 1 | 42 |
| Jake | 2 | 29 |
| Paul | 2 | 36 |
| Laura | 2 | 39 |
+--------+-------+-----+
Desired result set:
+--------+-------+-----+
| Shawn | 1 | 42 |
| Jill | 1 | 34 |
| Laura | 2 | 39 |
| Paul | 2 | 36 |
+--------+-------+-----+
NOTE: This question builds on a previous one- Get records with max value for each group of grouped SQL results - for getting a single top row from each group, and which received a great MySQL-specific answer from #Bohemian:
select *
from (select * from mytable order by `Group`, Age desc, Person) x
group by `Group`
Would love to be able to build off this, though I don't see how.
Here is one way to do this, using UNION ALL (See SQL Fiddle with Demo). This works with two groups, if you have more than two groups, then you would need to specify the group number and add queries for each group:
(
select *
from mytable
where `group` = 1
order by age desc
LIMIT 2
)
UNION ALL
(
select *
from mytable
where `group` = 2
order by age desc
LIMIT 2
)
There are a variety of ways to do this, see this article to determine the best route for your situation:
http://www.xaprb.com/blog/2006/12/07/how-to-select-the-firstleastmax-row-per-group-in-sql/
Edit:
This might work for you too, it generates a row number for each record. Using an example from the link above this will return only those records with a row number of less than or equal to 2:
select person, `group`, age
from
(
select person, `group`, age,
(#num:=if(#group = `group`, #num +1, if(#group := `group`, 1, 1))) row_number
from test t
CROSS JOIN (select #num:=0, #group:=null) c
order by `Group`, Age desc, person
) as x
where x.row_number <= 2;
See Demo
In other databases you can do this using ROW_NUMBER. MySQL doesn't support ROW_NUMBER but you can use variables to emulate it:
SELECT
person,
groupname,
age
FROM
(
SELECT
person,
groupname,
age,
#rn := IF(#prev = groupname, #rn + 1, 1) AS rn,
#prev := groupname
FROM mytable
JOIN (SELECT #prev := NULL, #rn := 0) AS vars
ORDER BY groupname, age DESC, person
) AS T1
WHERE rn <= 2
See it working online: sqlfiddle
Edit I just noticed that bluefeet posted a very similar answer: +1 to him. However this answer has two small advantages:
It it is a single query. The variables are initialized inside the SELECT statement.
It handles ties as described in the question (alphabetical order by name).
So I'll leave it here in case it can help someone.
Try this:
SELECT a.person, a.group, a.age FROM person AS a WHERE
(SELECT COUNT(*) FROM person AS b
WHERE b.group = a.group AND b.age >= a.age) <= 2
ORDER BY a.group ASC, a.age DESC
DEMO
How about using self-joining:
CREATE TABLE mytable (person, groupname, age);
INSERT INTO mytable VALUES('Bob',1,32);
INSERT INTO mytable VALUES('Jill',1,34);
INSERT INTO mytable VALUES('Shawn',1,42);
INSERT INTO mytable VALUES('Jake',2,29);
INSERT INTO mytable VALUES('Paul',2,36);
INSERT INTO mytable VALUES('Laura',2,39);
SELECT a.* FROM mytable AS a
LEFT JOIN mytable AS a2
ON a.groupname = a2.groupname AND a.age <= a2.age
GROUP BY a.person
HAVING COUNT(*) <= 2
ORDER BY a.groupname, a.age DESC;
gives me:
a.person a.groupname a.age
---------- ----------- ----------
Shawn 1 42
Jill 1 34
Laura 2 39
Paul 2 36
I was strongly inspired by the answer from Bill Karwin to Select top 10 records for each category
Also, I'm using SQLite, but this should work on MySQL.
Another thing: in the above, I replaced the group column with a groupname column for convenience.
Edit:
Following-up on the OP's comment regarding missing tie results, I incremented on snuffin's answer to show all the ties. This means that if the last ones are ties, more than 2 rows can be returned, as shown below:
.headers on
.mode column
CREATE TABLE foo (person, groupname, age);
INSERT INTO foo VALUES('Paul',2,36);
INSERT INTO foo VALUES('Laura',2,39);
INSERT INTO foo VALUES('Joe',2,36);
INSERT INTO foo VALUES('Bob',1,32);
INSERT INTO foo VALUES('Jill',1,34);
INSERT INTO foo VALUES('Shawn',1,42);
INSERT INTO foo VALUES('Jake',2,29);
INSERT INTO foo VALUES('James',2,15);
INSERT INTO foo VALUES('Fred',1,12);
INSERT INTO foo VALUES('Chuck',3,112);
SELECT a.person, a.groupname, a.age
FROM foo AS a
WHERE a.age >= (SELECT MIN(b.age)
FROM foo AS b
WHERE (SELECT COUNT(*)
FROM foo AS c
WHERE c.groupname = b.groupname AND c.age >= b.age) <= 2
GROUP BY b.groupname)
ORDER BY a.groupname ASC, a.age DESC;
gives me:
person groupname age
---------- ---------- ----------
Shawn 1 42
Jill 1 34
Laura 2 39
Paul 2 36
Joe 2 36
Chuck 3 112
Snuffin solution seems quite slow to execute when you've got plenty of rows and Mark Byers/Rick James and Bluefeet solutions doesn't work on my environnement (MySQL 5.6) because order by is applied after execution of select, so here is a variant of Marc Byers/Rick James solutions to fix this issue (with an extra imbricated select):
select person, groupname, age
from
(
select person, groupname, age,
(#rn:=if(#prev = groupname, #rn +1, 1)) as rownumb,
#prev:= groupname
from
(
select person, groupname, age
from persons
order by groupname , age desc, person
) as sortedlist
JOIN (select #prev:=NULL, #rn :=0) as vars
) as groupedlist
where rownumb<=2
order by groupname , age desc, person;
I tried similar query on a table having 5 millions rows and it returns result in less than 3 seconds
If the other answers are not fast enough Give this code a try:
SELECT
province, n, city, population
FROM
( SELECT #prev := '', #n := 0 ) init
JOIN
( SELECT #n := if(province != #prev, 1, #n + 1) AS n,
#prev := province,
province, city, population
FROM Canada
ORDER BY
province ASC,
population DESC
) x
WHERE n <= 3
ORDER BY province, n;
Output:
+---------------------------+------+------------------+------------+
| province | n | city | population |
+---------------------------+------+------------------+------------+
| Alberta | 1 | Calgary | 968475 |
| Alberta | 2 | Edmonton | 822319 |
| Alberta | 3 | Red Deer | 73595 |
| British Columbia | 1 | Vancouver | 1837970 |
| British Columbia | 2 | Victoria | 289625 |
| British Columbia | 3 | Abbotsford | 151685 |
| Manitoba | 1 | ...
Check this out:
SELECT
p.Person,
p.`Group`,
p.Age
FROM
people p
INNER JOIN
(
SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`
UNION
SELECT MAX(p3.Age) AS Age, p3.`Group` FROM people p3 INNER JOIN (SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`) p4 ON p3.Age < p4.Age AND p3.`Group` = p4.`Group` GROUP BY `Group`
) p2 ON p.Age = p2.Age AND p.`Group` = p2.`Group`
ORDER BY
`Group`,
Age DESC,
Person;
SQL Fiddle: http://sqlfiddle.com/#!2/cdbb6/15
WITH cte_window AS (
SELECT movie_name,director_id,release_date,
ROW_NUMBER() OVER( PARTITION BY director_id ORDER BY release_date DESC) r
FROM movies
)
SELECT * FROM cte_window WHERE r <= <n>;
Above query will returns latest n movies for each directors.
I wanted to share this because I spent a long time searching for an easy way to implement this in a java program I'm working on. This doesn't quite give the output you're looking for but its close. The function in mysql called GROUP_CONCAT() worked really well for specifying how many results to return in each group. Using LIMIT or any of the other fancy ways of trying to do this with COUNT didn't work for me. So if you're willing to accept a modified output, its a great solution. Lets say I have a table called 'student' with student ids, their gender, and gpa. Lets say I want to top 5 gpas for each gender. Then I can write the query like this
SELECT sex, SUBSTRING_INDEX(GROUP_CONCAT(cast(gpa AS char ) ORDER BY gpa desc), ',',5)
AS subcategories FROM student GROUP BY sex;
Note that the parameter '5' tells it how many entries to concatenate into each row
And the output would look something like
+--------+----------------+
| Male | 4,4,4,4,3.9 |
| Female | 4,4,3.9,3.9,3.8|
+--------+----------------+
You can also change the ORDER BY variable and order them a different way. So if I had the student's age I could replace the 'gpa desc' with 'age desc' and it will work! You can also add variables to the group by statement to get more columns in the output. So this is just a way I found that is pretty flexible and works good if you are ok with just listing results.
In SQL Server row_numer() is a powerful function that can get result easily as below
select Person,[group],age
from
(
select * ,row_number() over(partition by [group] order by age desc) rn
from mytable
) t
where rn <= 2
There is a really nice answer to this problem at MySQL - How To Get Top N Rows per Each Group
Based on the solution in the referenced link, your query would be like:
SELECT Person, Group, Age
FROM
(SELECT Person, Group, Age,
#group_rank := IF(#group = Group, #group_rank + 1, 1) AS group_rank,
#current_group := Group
FROM `your_table`
ORDER BY Group, Age DESC
) ranked
WHERE group_rank <= `n`
ORDER BY Group, Age DESC;
where n is the top n and your_table is the name of your table.
I think the explanation in the reference is really clear. For quick reference I will copy and paste it here:
Currently MySQL does not support ROW_NUMBER() function that can assign
a sequence number within a group, but as a workaround we can use MySQL
session variables.
These variables do not require declaration, and can be used in a query
to do calculations and to store intermediate results.
#current_country := country This code is executed for each row and
stores the value of country column to #current_country variable.
#country_rank := IF(#current_country = country, #country_rank + 1, 1)
In this code, if #current_country is the same we increment rank,
otherwise set it to 1. For the first row #current_country is NULL, so
rank is also set to 1.
For correct ranking, we need to have ORDER BY country, population DESC
SELECT
p1.Person,
p1.`GROUP`,
p1.Age
FROM
person AS p1
WHERE
(
SELECT
COUNT( DISTINCT ( p2.age ) )
FROM
person AS p2
WHERE
p2.`GROUP` = p1.`GROUP`
AND p2.Age >= p1.Age
) < 2
ORDER BY
p1.`GROUP` ASC,
p1.age DESC
reference leetcode

Select row in which the sum of the times the same value came up is less than X

this one been puzzling me for a couple of searching hours.
So I have a campaign table and a vendor Table. The vendor might have several campaigns.
I want to select all campaigns if the vendor has enough credits.
Problem is I don't know how many campaigns are going to be selected from the same vendor which means that the vendor might still have credits for two campaigns but not for the rest of them.
Example
tblvendors
+---------+------------+---------------+
|vendorId | vendorName | vendorCredits |
+---------+------------+---------------+
| 1 | a | 5 |
| 2 | b | 100 |
+---------+------------+---------------+
tblproducts
+-----------+---------------+------------+
| productId | productName | vendorId |
+-----------+---------------+------------+
| 1 | c | 1 |
| 2 | e | 2 |
| 3 | f | 1 |
| 4 | g | 1 |
| 5 | h | 1 |
+-----------+---------------+------------+
tblcampaigns
+------------+---------------+------------+
| campaignId | productId | vendorId |
+------------+---------------+------------+
| 1 | 1 | 1 |
| 2 | 2 | 2 |
| 3 | 3 | 1 |
| 4 | 4 | 1 |
| 5 | 5 | 1 |
+------------+---------------+------------+
Now considering that everytime a row is selected the vendor looses 2 credits since vendor 'a' only has 5 credits left only campaigns 1 2 and 3 should be returned.
My current Query is this:
SET #maxCampaignId = (SELECT MAX(campaignId) FROM tblCampaigns);
SELECT
#maxCampaignId,
t0.campaignId,
t0.productId,
productName,
productDescription,
productImage,
(CASE WHEN campaignId > (SELECT configValue FROM tblconfiguration WHERE configKey = 'lastHomeCampaignId')
THEN campaignId ELSE campaignId + #maxCampaignId END) AS orderField
FROM tblcampaigns AS t0
INNER JOIN tblproducts AS t1 ON t0.productId = t1.productId
INNER JOIN tblvendors AS t2 ON t1.vendorId = t2.vendorId
WHERE
campaignType = 'homeFeature' AND
t0.isActive = 1 AND
t2.vendorCredits > (SELECT configValue FROM tblconfiguration WHERE configKey = 'campaignHomeFeatureCost' LIMIT 1)
ORDER BY orderField ASC
LIMIT 4
The problem as you can see is int the line that compares the vendorCredits. Obviously as is the query selects more campaigns than the vendor can afford.
I wanted to avoid doing this in PHP as I think it should be possible to do this straight out of the database.
Check this post, it may help - group by and having clauses. I'll try to do some test later
Using COUNT(*) in the WHERE clause
UPDATE:
select t2.vendorId, vendorCredits from tblcampaigns AS t0 JOIN tblproducts AS t1 ON t0.productId = t1.productId JOIN tblvendors AS t2 ON t1.vendorId = t2.vendorId group by t2.vendorId having t2.vendorCredits = count(t2.vendorId)
If I correctly understood the question: This query will select all vendors having more campains than credits.
Ok found it.
Thanks to this post: How do I limit the number of rows per field value in SQL?
What I did was Selecting the rows I wanted in the order I wanted as a subquery and its respective row number so that I could reorder it back in the end.
Then I made a second subquery ordered by the vendorId so that I could count the number of times it turned up and returning the row_count to the main query.
Finally in the main query I reordered it back to the row number in the deepest subquery but now I have the value I wanted to compare which is the value of credits per row * the current row number for a particular vendor.
Anyways maybe the code is cleared and here it goes:
SET #creditsCost = (SELECT configValue FROM tblconfiguration WHERE configKey = 'campaignHomeFeatureCost' LIMIT 1);
SET #maxCampaignId = (SELECT MAX(campaignId) FROM tblCampaigns);
SET #curRow = 0;
SELECT * FROM
(
SELECT *,
#num := if(#first_column = vendorId, #num:= #num + 1, 1) as row_num,
#first_column:=vendorId as c
FROM
(SELECT
#curRow := #curRow + 1 AS row_number,
#maxCampaignId,
t0.campaignId,
t0.productId,
t2.vendorId,
t2.vendorCredits,
productName,
productDescription,
productImage,
(CASE WHEN campaignId > (SELECT configValue FROM tblconfiguration WHERE configKey = 'lastHomeCampaignId')
THEN campaignId ELSE campaignId + #maxCampaignId END) AS orderField
FROM tblcampaigns AS t0
INNER JOIN tblproducts AS t1 ON t0.productId = t1.productId
INNER JOIN tblvendors AS t2 ON t1.vendorId = t2.vendorId
WHERE
campaignType = 'homeFeature' AND
t0.isActive = 1
ORDER BY orderField ASC) AS filteredCampaigns
ORDER BY vendorId
) AS creditAllowedCampaigns
WHERE
row_num * #creditsCost <= vendorCredits
ORDER BY row_number
Anyhow I still appreciate Who took the time to answer and try to help, and will be listening to future comments since I think this is not the best way performance wise.

Categories