MYSQL MULTIPLE COUNT AND SUM - php

I hope this is possible in MYSQL, I am scripting with PHP.
I am trying to create multiple column on SUM of values and COUNT on table1 based on each month based with individual conditions and groupings. The tables are already joined through the accountid.
I have two tables monthlyreport(table1) & planters(table2).
Desired Results is in table 1
MONTHLY REPORT (Table 1)
REPORTID|ACCOUNTID|COMPMONTH|SUMtoDATE|COUNTtoDATE|SUMcompDATE|COUNTcompDATE|
1 | 190 | JAN | 150 | 2 | 150 | 2 |
2 | 190 | FEB | 0 | 0 | 100 | 1 |
Planters (Table 2)
PlanterID | ACCOUNTID |PLANTER | SALARY | compDATE | toDATE |
1 | 190 | aaa | 100 | Jan-1-2013 | Jan-05-2013 |
2 | 190 | bbb | 50 | Jan-9-2013 | Jan-12-2013 |
3 | 190 | aaa | 100 | Feb-1-2013 | Mar-12-2013 |
4 | 190 | bbb | 0 | Mar-5-2013 | Mar-12-2013 |
A single query with inner join already works but if I run both I get nothing because I can't seem to get the logic if it is possible.
This is what I have so far from stackoverflow but getting error.
Wish someone can refactor it or make it work.
SELECT *,
(
SELECT COUNT(planters.todate), SUM(planters.todate)
FROM monthlyreport
INNER JOIN planters ON monthlyreport.accountid = planters.accountid
WHERE monthlyreport.accountid = 190 AND MONTH(monthlyreport.compmonth) = MONTH(planters.todate)
GROUP BY monthlyreport.mthreportid, month(planters.todate)
) AS count_1,
(
SELECT COUNT(planters.compdate), SUM(planters.compdate)
FROM monthlyreport
INNER JOIN planters ON monthlyreport.accountid = planters.accountid
WHERE monthlyreport.accountid = 190 AND MONTH(monthlyreport.compmonth) = MONTH(planters.compdate)
GROUP BY monthlyreport.mthreportid, month(planters.compdate)
) AS count_2

Its not very clear, but as far as I can think, what you want is to get the two results in a single query result. Try joining them on the basis of accountID from both the tables.AS:
SELECT *
from
(select accountID,COUNT(planters.todate) as count2date, SUM(planters.todate) as sum2date
-----
-----) count_1
inner join
(SELECT accountID,COUNT(planters.compdate) as countcomp, SUM(planters.compdate) as sumcomp
-----
-----) count_2
using(accountID);
Do not use "AS" before count_1 or count_2. It is better to replace * in the outer select query with more specific attributes, like count_1.count2date or like.
Hope this helps ! If anything else is what you are looking for, do let me know.
-----UPDATE-----
After looking at your file you uploaded, I came up with the following query:
SELECT count1.compmonth, IFNULL( todatecount, 0 ) , IFNULL( todatesum, 0 ) , IFNULL( compdatecount, 0 ) , IFNULL( compdatesum, 0 )
FROM count_1
LEFT JOIN count_2 ON count_1.compmonth = count_2.compmonth
UNION
SELECT count2.compmonth, IFNULL( todatecount, 0 ) , IFNULL( todatesum, 0 ) , IFNULL( compdatecount, 0 ) , IFNULL( compdatesum, 0 )
FROM count_1
RIGHT JOIN count_2 ON count_1.compmonth = count_2.compmonth
You can format the 0's as per your wish. Also, if your database platform supports the "FULL OUTER JOIN", you can use that instead of making a union of left and right joins.
You will have to replace "FROM count_1" with:
FROM (select accountID,COUNT(planters.todate) as count2date, SUM(planters.todate) as sum2date
-----
-----) count_1
Similarly for FROM count_2. I know this looks like a huge query, but all this does is joins the 2 tables on common dates, and all the other fields that don't match are specified NULL.

Related

Get rows above and below (neighbouring rows) a certain row, based on two criteria SQL

Say I have a table like so:
+---+-------+------+---------------------+
|id | level |score | timestamp |
+---+-------+------+---------------------+
| 4 | 1 | 70 | 2021-01-14 21:50:38 |
| 3 | 1 | 90 | 2021-01-12 15:38:0 |
| 1 | 1 | 20 | 2021-01-14 13:10:12 |
| 5 | 1 | 50 | 2021-01-13 12:32:11 |
| 7 | 1 | 50 | 2021-01-14 17:15:20 |
| 8 | 1 | 55 | 2021-01-14 09:20:00 |
| 10| 2 | 99 | 2021-01-15 10:50:38 |
| 2 | 1 | 45 | 2021-01-15 10:50:38 |
+---+-------+------+---------------------+
What I want to do is show 5 of these rows in a table (in html), with a certain row (e.g. where id=5) in the middle and have the two rows above and below it (in the correct order). Also where level=1. This will be like a score board but only showing the user's score with the two above and two below.
So because scores can be the same, the timestamp column will also need to be used - so if two scores are equal, then the first person to get the score is shown above the other person.
E.g. say the user is id=5, I want to show
+---+-------+------+---------------------+
|id | level |score | timestamp |
+---+-------+------+---------------------+
| 4 | 1 | 70 | 2021-01-14 21:50:38 |
| 8 | 1 | 55 | 2021-01-14 09:20:00 |
| 5 | 1 | 50 | 2021-01-13 12:32:11 |
| 7 | 1 | 50 | 2021-01-14 17:15:20 |
| 2 | 1 | 45 | 2021-01-15 10:50:38 |
| 1 | 1 | 20 | 2021-01-14 13:10:12 |
+---+-------+------+---------------------+
Note that id=7 is below id=5
I am wondering does anyone know a way of doing this?
I have tried this below but it is not outputting what I need (it is outputting where level_id=2 and id=5, and the other rows are not in order)
((SELECT b.* FROM table a JOIN table b ON b.score > a.score OR (b.score = a.score AND b.timestamp < a.timestamp)
WHERE a.level_id = 1 AND a.id = 5 ORDER BY score ASC, timestamp DESC LIMIT 3)
UNION ALL
(SELECT b.* FROM table a JOIN table b ON b.score < a.score OR (b.score = a.score AND b.timestamp > a.timestamp)
WHERE a.level_id = 1 AND a.id = 5 ORDER BY score DESC, timestamp ASC LIMIT 2))
order by score
If it is easier to output all rows in the table, say where level = 1, so it is a full score board.. and then do the getting a certain row and two above and below it using PHP I'd also like to know please :) ! (possibly thinking this may keep the SQL simpler)?
You can use cte and inner join as follows:
With cte as
(select t.*,
dense_rank() over (order by score) as dr
from your_table t)
Select c.*
From cte c join cte cu on c.dr between cu.dr - 2 and cu.dr + 2
Where cu.id = 5
Ordwr by c.dr, c.timestamp
I would suggest window functions:
select t.*
from (select t.*,
max(case when id = 7 then score_rank end) over () as id_rank
from (select t.*,
dense_rank() over (order by score) as score_rank
from t
where level = 1
) t
) t
where score_rank between id_rank - 2 and id_rank + 2;
Note: This returns 5 distinct score values, which may result in more rows depending on duplicates.
Here is a db<>fiddle.
EDIT:
If you want exactly 5 rows using the timestamp, then:
select t.*
from (select t.*,
max(case when id = 7 then score_rank end) over () as id_rank
from (select t.*,
dense_rank() over (order by score, timestamp) as score_rank
from t
where level = 1
) t
) t
where score_rank between id_rank - 2 and id_rank + 2
order by score;
Note: This still treats equivalent timestamps as the same, but they seem to be unique in your data.

Complex query combining

First of all, thank you for reading my problem. I am very thankful for the people in the community helping others learn more about coding. As this SQL and PHP is part of my hobby, I am limited by Google and fora where I can find answers to my questions. Unfortunately, I can't find a good answer for a problem encountered.
What am I trying to do?
I run a website where you can note your kilometers and liters you filled your car and see your statistics. I want to do a big search in MySQL with a query. I know how to get the results in multiple queries (3 to be exact), but I need it to be in one single query.
What does my table look look like?
For an example I am using this table to make the problem more visual, it's named 'fillings' for now.
+---------+---------+------------+--------+--------+------------+
| tank_id | user_id | kilometers | liters | car_id | date |
+---------+---------+------------+--------+--------+------------+
| 1 | 2 | 450 | 20 | 2 | 2017-11-01 |
+---------+---------+------------+--------+--------+------------+
| 2 | 1 | 500 | 30 | 1 | 2017-12-15 |
+---------+---------+------------+--------+--------+------------+
| 3 | 2 | 490 | 25 | 2 | 2017-11-05 |
+---------+---------+------------+--------+--------+------------+
| 4 | 3 | 260 | 19 | 3 | 2017-11-07 |
+---------+---------+------------+--------+--------+------------+
| 5 | 3 | 610 | 30 | 3 | 2017-12-03 |
+---------+---------+------------+--------+--------+------------+
| 6 | 3 | 100 | 4 | 3 | 2017-12-07 |
+---------+---------+------------+--------+--------+------------+
This is a basic idea of my main table where all the fill-ups are being registrated. In reality someone can have multiple cars, but for now let's asume that a user only has one car.
What are the SQL queries?
The query that I want to make should have this result:
+---------+----------------------+---------------------+
| user_id | consumption november | consumption overall |
+---------+----------------------+---------------------+
| 2 | 20,9 | 20,9 |
+---------+----------------------+---------------------+
| 1 | | 16,7 |
+---------+----------------------+---------------------+
| 3 | 13,7 | 18,3 |
+---------+----------------------+---------------------+
As you can see, the fillings of november are taking and a consumption is being calculated. Also, an overall consumption is being calculated of all fillings from a certain type of user. I know how to make this result, devided in two queries. That's:
SELECT user_id, SUM(kilometers) / SUM(liters) as consumption overall
FROM fillings
GROUP BY user_id
And also the query:
SELECT user_id, SUM(kilometers) / SUM(liters) as consumption november
FROM fillings
WHERE 'date'
BETWEEN '2017-11-01' AND '2017-11-31'
GROUP BY user_id
So right now I want to combine the queries into one, so that I can get the result that I want. Note that in the example, someone can have a null consumption in a month, because hé or she did not fill up their car in that month.
I've looked into the options of UNION and JOIN but I can't seem to find a way to make this working.
I hope everything is clear, and if not, don't hesitate to ask and I will explain in further detail.
Thanks in advance.
Since it is possible that the records are not in range within the month, a LEFT JOIN is recommended for the date specific query:
SELECT
t1.user_id, t2.consumption_november, t1.consumption_overall
FROM (
SELECT user_id, SUM(kilometers) / SUM(liters) as consumption_overall
FROM fillings
GROUP BY user_id
) t1
LEFT JOIN
(
SELECT user_id, SUM(kilometers) / SUM(liters) as consumption_november
FROM fillings
WHERE `date` BETWEEN '2017-11-01' AND '2017-11-31'
GROUP BY user_id
) t2
ON t1.user_id = t2.user_id;
http://sqlfiddle.com/#!9/3fdce84/16
Just use conditional aggregation:
SELECT user_id,
(SUM(CASE WHEN date >= '2017-11-01' AND date < '2017-12-01'
THEN kilometers ELSE 0
END) /
SUM(CASE WHEN date >= '2017-11-01' AND date < '2017-12-01'
THEN liters
END)
) as consumption_november,
SUM(kilometers) / SUM(liters) as consumption_overall
FROM fillings
GROUP BY user_id
You can try something like
SELECT t1.user_id,
t1.consumption_overall as consumption overall,
t2.consumption_november as consumption november
FROM (
SELECT user_id, SUM(kilometers) / SUM(liters) as consumption_overall
FROM fillings
GROUP BY user_id
) as t1, (
SELECT user_id, SUM(kilometers) / SUM(liters) as consumption_november
FROM fillings
WHERE 'date'
BETWEEN '2017-11-01' AND '2017-11-31'
GROUP BY user_id
) as t2
WHERE t1.user_id = t2.user_id
It will take the results of you two queries and put them in the same table.
For the extended answer (to your question in my first answer, since it's too long to leave as a comment), use :
SELECT
t1.user_id, t2.consumption_november, t3.consumption_q, t1.consumption_overall
FROM (
SELECT user_id, SUM(kilometers) / SUM(liters) as consumption_overall
FROM fillings
GROUP BY user_id
) t1
LEFT JOIN
(
SELECT user_id, SUM(kilometers) / SUM(liters) as consumption_november
FROM fillings
WHERE [date] BETWEEN '2017-11-01' AND '2017-11-31'
GROUP BY user_id
) t2
ON t1.user_id = t2.user_id
LEFT JOIN
(
SELECT user_id, SUM(kilometers) / SUM(liters) as consumption_q
FROM fillings
WHERE [date] BETWEEN '2017-08-01' AND '2017-11-31'
GROUP BY user_id
) t3
ON t1.user_id = t3.user_id;
for a subsequent left join, just remove the semicolon from the final left join and extend the pattern, and add the new field in the SELECT clause.

Delete all rows, except last 10 for each client that has related row(s) in the table in one query?

So my situation is this:
Clients table - has client data etc, not too exciting
Recently Viewed table - table that has recently viewed things for the client(s), And has structure like this:
( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
, client_id INT NOT NULL
, cookie_user_id INT NOT NULL
, hotel_id INT NOT NULL
, added DATETIME NOT NULL
, comment TEXT
,status TINYINT NOT NULL DEFAULE 1
);
I currently have a partially working SQL to delete rows in the recently viewed table that right now globally limits number of latest remaining undeleted records in it. This is how it looks like now
DELETE FROM `recently_viewed`
WHERE `recently_viewed`.`id` NOT IN (
SELECT id
FROM (
SELECT `id`
FROM `recently_viewed`
WHERE `client_id` IN (SELECT `id` FROM `klijenti`)
ORDER BY `id` DESC
LIMIT 5
) x
)
AND `client_id` <> 0
"LIMIT 5" part should limit to the N records to remain in recently viewed table on a "per client" basis. Right now it limits records in recently viewed table to 5 no matter how many clients actually have records there. So if I have 10 clients, each of them has 8 records in that table, I would like this query to delete as many oldest records as needed to leave only 5 newest recently viewed items for EACH client and not just leave 5 overall in the table, ignoring the "per each client" logic. Hope that makes sense to you :)
Currently, this query would be ok if I would first fetch all clients in the app and then do a foreach loop to make another query for each client and leave 5 of his latest recently viewed items, but would like to do this in one SQL query instead.
How could this be done ? Thank you
You can do it like this:
DELETE FROM `recently_viewed`
WHERE `recently_viewed`.`id` NOT IN (
SELECT id
FROM (
SELECT t.`id`,count(*) as rnk
FROM `recently_viewed` t
INNER JOIN `recently_viewed` s
ON(t.`client_id` = s.`client_id` and t.added <= s.added)
WHERE t.`client_id` IN (SELECT `id` FROM `klijenti`)
GROUP BY t.`ID`
) x
WHERE rnk <= 5
)
AND `client_id` <> 0
You can use vartiables to in order to count the 5 more recent records per client_id:
DELETE FROM `recently_viewed`
WHERE `recently_viewed`.`id` NOT IN
(
SELECT id
FROM (
SELECT `id`,
#rn := IF(#cid = `client_id`, #rn + 1,
IF(#cid := `client_id`, 1, 1)) AS rn
FROM `recently_viewed`
CROSS JOIN (SELECT #rn := 0, #cid := 0) AS vars
WHERE `client_id` IN (SELECT `id` FROM `klijenti`)
ORDER BY `client_id`, `id` DESC) x
WHERE x.rn <= 5
)
Giorgos's answer is faster, but here's another method...
Consider the following...
SELECT * FROM my_table ORDER BY x,i;
+---+------+
| i | x |
+---+------+
| 2 | A |
| 3 | A |
| 6 | A |
| 8 | A |
| 1 | B |
| 5 | B |
| 4 | C |
| 7 | C |
| 9 | C |
+---+------+
Let's say we want to select the two latest i for each x. Here's one way to do that...
SELECT m.* FROM my_table m JOIN my_table n ON n.x = m.x AND n.i >= m.i GROUP BY m.i HAVING COUNT(*) <= 2;
+---+------+
| i | x |
+---+------+
| 1 | B |
| 5 | B |
| 6 | A |
| 7 | C |
| 8 | A |
| 9 | C |
+---+------+
The inverse of this set can be found as follows....
SELECT m.* FROM my_table m JOIN my_table n ON n.x = m.x AND n.i >= m.i GROUP BY m.i HAVING COUNT(*) > 2;
+---+------+
| i | x |
+---+------+
| 2 | A |
| 3 | A |
| 4 | C |
+---+------+
...which in turn can be incorporated in a DELETE. Here's a crude method for doing that...
DELETE a FROM my_table a
JOIN
( SELECT m.* FROM my_table m JOIN my_table n ON n.x = m.x AND n.i >= m.i GROUP BY m.i HAVING COUNT(*) > 2 ) b
ON b.i = a.i;
Query OK, 3 rows affected (0.03 sec)
SELECT * FROM my_table ORDER BY x,i;
+---+------+
| i | x |
+---+------+
| 6 | A |
| 8 | A |
| 1 | B |
| 5 | B |
| 7 | C |
| 9 | C |
+---+------+
As I say, if performance is critical, then look at a solution along the lines that Giorgos has provided.

MySQL Multiple Conditions on Group By / Having Clause

I have three tables that are all inter-related with the following structure.
ModuleCategory Table:
+------------------+----------------+------------+
| ModuleCategoryID | ModuleCategory | RequireAll |
+------------------+----------------+------------+
| 90 | Cat A | YES |
| 91 | Cat B | NO |
+------------------+----------------+------------+
ModuleCategorySkill Table:
+------------------+---------+
| ModuleCategoryID | SkillID |
+------------------+---------+
| 90 | 1439 |
| 90 | 3016 |
| 91 | 1440 |
| 91 | 3016 |
+------------------+---------+
EmployeeSkill Table:
+---------+---------+
| EmpName | SkillID |
+---------+---------+
| Emp1 | 1439 |
| Emp1 | 3016 |
| Emp2 | 1440 |
| Emp2 | 3016 |
| Emp3 | 1439 |
| Emp4 | 3016 |
+---------+---------+
Desired Output:
+------------------+-------+
| ModuleCategory | Count |
+------------------+-------+
| Cat A | 1 |
| Cat B | 3 |
+------------------+-------+
I am trying to group by ModuleCategoryID's and get the count of employees which have the skills being tracked.
Normally, I can do the following query to obtain the numbers:
select mc.ModuleCategory, Count(*) as Count from ModuleCategory as mc
join ModuleCategorySkill as mcs on mc.ModuleCategoryID = mcs.ModuleCategoryID join EmployeeSkill as es on es.SkillID= mcs.SkillID
group by mc.ModuleCategoryID
However, I have a column RequireAll in the ModuleCategory table which if it is set to 'YES' should only count employees as 1 only if they have all the skills in the category. If it is set to NO then it can count each row normally and increase the count by the number of rows it groups by.
I can achieve this by writing separate queries for each modulecategoryID and using a having Count() > 1 (which will find me anyone that has all the skills for ModuleCategoryID 90). If there were 3 skills than I would have to change it to Having Count() > 2. If there isn't anyone that has all the skills specified, the count should be 0.
I need a dynamic way of being able to do this since there is a lot of data and writing one query for each ModuleCategoryID isn't the proper approach.
Also, I am using PHP so I can loop through and create a sql string that can help me achieve this. But I know I will run into performance issues on big tables with a lot of skills and modulecategoryID's.
Any guidance on how to achieve this is much appreciated.
You can do it by joining on the total category counts, and then using conditional aggregation:
select modulecategory,
count(case when requireall = 'yes'
then if(s = t, 1, null)
else s
end)
from (
select modulecategory,empname, requireall, count(*) s, min(q.total) t
from employeeskill e
inner join modulecategoryskill mcs
on e.skillid = mcs.skillid
inner join modulecategory mc
on mcs.modulecategoryid = mc.modulecategoryid
inner join (
select modulecategoryid, count(*) total
from modulecategoryskill
group by modulecategoryid
) q
on mc.modulecategoryid = q.modulecategoryid
group by modulecategory, empname
) qq
group by modulecategory;
demo here
This operates under the assumption an employee isn't going to be allocated the same skill twice, if that is something that may happen, this query is alterable to support it, but it seems like a broken scenario to me.
What we have here is an inner query that collates all the information we need (category name, employee name, whether or not all skills are required, how many skills are in the group per employee, and how many there in the group total), with an outer query that uses a conditional count to change how the rows are tallied, based on the value of requireall.

GROUP_CONCAT With Nested Set Model

I have an application that uses a nested set model class to organise my data, however I'm trying to write a query that will group_concat my results. I know I need to put some sub select statements somewhere but I can't figure it out!
Here's my structure at the moment:
table: person
-----------+------------+-----------
|Person_ID | Name | Age |
-----------+------------+-----------
| 1 | Mark Vance | 19 |
| 2 | Michael Tsu| 22 |
| 3 | Mark Jones | 29 |
| 4 | Sara Young | 25 |
-----------+------------+-----------
table: person_to_group
----+------------+-----------
|ID | Person_ID | Group_ID |
----+------------+-----------
| 1 | 3 | 1 |
| 2 | 3 | 2 |
| 3 | 1 | 2 |
| 4 | 4 | 3 |
----+------------+-----------
table: groups
----------+--------------+--------------+-------------
|Group_ID | Group_Name | Group_Left | Group_Right |
----------+--------------+--------------+-------------
| 1 | Root | 1 | 6 |
| 2 | Node | 2 | 5 |
| 3 | Sub Node | 3 | 4 |
----------+--------------+--------------+-------------
I need to render something like this with my results:
//Grab the group_IDs for this person and put them in the class tag...
<li class="2 3">Sara Young is in the Sub Node Group</li>
Notice that although Sara is in the Sub Node group, she is still being given the id for Node aswell because she is a child of Node.
The following is the query that I am working with as a starting point.
SELECT *, GROUP_CONCAT( CAST( gg.Group_ID AS CHAR ) SEPARATOR ' ' ) Group_IDs
FROM groups gg
LEFT JOIN person_to_group AS t1 ON gg.Group_ID = t1.Group_ID
LEFT JOIN person AS t2 ON t2.Person_ID = t1.Person_ID
GROUP BY t2.per_ID
ORDER BY t2.Name ASC
Any help would be much appreciated!
Here's how I'd write the query:
SELECT p.Name,
GROUP_CONCAT( g.Group_Name ) AS Group_List,
GROUP_CONCAT( CAST( gg.Group_ID AS CHAR ) SEPARATOR ' ' ) AS Group_ID_List
FROM person AS p
INNER JOIN person_to_group AS pg ON p.Person_ID = pg.Person_ID
INNER JOIN groups AS g ON pg.Group_ID = g.Group_ID
INNER JOIN groups AS gg ON g.Group_Left BETWEEN gg.Group_Left AND gg.Group_Right
GROUP BY p.Name
ORDER BY p.Name ASC
Note that if you group by person name, you also need to GROUP_CONCAT the list of group names. According to your schema, a person could belong to multiple groups, because of the many-to-many relationship.
I also recommend against using SELECT * in general. Just specify the columns you need.
This was little bit interesting as I do programming in both MsSQL and MySql. In SQL I have used function called STUFF. In MySQL you can use a function called INSERT. I tried out the below query in MsSQL. Don't have a MySQL handy to try out my query. If I have time I will post the MySQL version of the query.
DECLARE #person TABLE (Person_ID INT, Name VARCHAR(50), Age INT)
INSERT INTO #person VALUES
(1,'Mark Vance',19),
(2,'Michael Tsu',22),
(3,'Mark Jones',29),
(4,'Sara Young',25)
DECLARE #groups TABLE (Group_ID INT, Group_Name VARCHAR(50), Group_Left INT, Group_Right INT)
INSERT INTO #groups VALUES
(1,'Root',1,6),
(2,'Node',2,5),
(3,'Sub Node',3,4)
DECLARE #person_to_group TABLE (ID INT, Person_ID INT, Group_ID INT)
INSERT INTO #person_to_group VALUES
(1,3,1),
(2,3,2),
(3,1,1),
(4,4,1),
(4,1,1)
SELECT *,STUFF((SELECT ',' + CAST(g.Group_ID AS VARCHAR) FROM #groups g
JOIN #person_to_group pg ON g.Group_ID = pg.Group_ID AND pg.Person_ID = a.Person_ID FOR XML PATH('')) , 1, 1, '' ) FROM #person a
Function: INSERT(str,pos,len,newstr)
Documentation: http://dev.mysql.com/doc/refman/5.0/en/string-functions.html#function_insert

Categories