SQL query to get columns based on value - mysql

I have a table like below
user_id | month, | value
---------+------------+--------
1 | 2013-02-01 | 1
1 | 2013-01-01 | 0
1 | 2013-03-01 | 5
2 | 2013-02-01 | 1
Supposedly a user_id can not have same month more than one.
Let's say I can put the months I want to query in the query statement. like below
SELECT user_id, (bla bla bla) AS '2013-03-01', (bla bla bla) AS '2013-02-01'
How do I get a result like below, with minimum number of queries and post-processing (e.g. using python or php)?
user_id | 2013-03-01 | 2013-02-01 | 2013-01-01
---------+------------+------------+------------
1 | 5 | 1 | 0
2 | NULL | 1 | NULL

You can use conditional aggregates to get the required result set:
SELECT user_id,
MAX(CASE WHEN month = '2013-03-01' THEN value END) AS '2013-03-01',
MAX(CASE WHEN month = '2013-02-01' THEN value END) AS '2013-02-01',
MAX(CASE WHEN month = '2013-01-01' THEN value END) AS '2013-01-01'
FROM mytable
GROUP BY user_id
This works as long there is a predefined set of month values. Otherwise you have to use dynamic SQL.
Fiddle Demo here

Related

MySQL Count and Convert Row to Colum Involve One Table Only

I have a table name histories that record user activities which consists of user_id, branch_id and duration.
The table look like this:
+++++++++++++++++++++++++++++++++++
id | user_id | branch_id | totHours
+++++++++++++++++++++++++++++++++++
|1 | 100 | 1 | 1 |
|2 | 199 | 1 | 1 |
|3 | 121 | 1 | 1 |
|4 | 140 | 1 | 1 |
|5 | 103 | 2 | 3 |
|6 | 107 | 2 | 1 |
|7 | 299 | 1 | 2 |
|8 | 209 | 2 | 2 |
|9 | 119 | 1 | 5 |
I would like to produce an output like this:
+++++++++++++++++++++++++++
Hours | Branch A | Branch B
+++++++++++++++++++++++++++
|1 | 4 | 1 |
|2 | 1 | 1 |
|3 | 0 | 1 |
|4 | 0 | 0 |
|5 | 1 | 0 |
I try make it using this query, but when i use group by on totHours column only, it return error because i need to include the branch_id in the group by.
Here is my query:
select totHours as Hours,
coalesce(case when branch_id = 1 then count(totHours) else 0 end) as 'Branch A',
coalesce(case when branch_id = 2 then count(totHours) else 0 end) as 'Branch B'
from histories
group by totHours, branch_id;
And if the totHours is not in the table (for example in this table 4), it will display 0 for both branch column.
Here is my db fiddle
Update: MySQL version 5.7.22
If you're using MySQL version 8+ (or any version support windows function), you can make use of the recursive common table expression to generate the hour values for you then LEFT JOIN table histories with it. After that you can do SUM() with CASE expression in SELECT to generate your expected output:
WITH RECURSIVE hours AS (
SELECT 1 AS hr, MAX(totHours) AS maxth FROM histories UNION ALL
SELECT hr+1, maxth FROM hours WHERE hr+1 <= maxth)
SELECT hours.hr,
SUM(CASE WHEN histories.branch_id=1 THEN 1 ELSE 0 END) AS Branch_A,
SUM(CASE WHEN histories.branch_id=2 THEN 1 ELSE 0 END) AS Branch_B
FROM hours
LEFT JOIN histories
ON hours.hr=histories.totHours
GROUP BY hours.hr;
If you're using version that doesn't support window function, you can create a subquery to represent the hours (including missing hour). This is a hard-coding approach where you may have to always update the subquery to include new hour value (if any):
SELECT hours.hr,
SUM(CASE WHEN histories.branch_id=1 THEN 1 ELSE 0 END) AS Branch_A,
SUM(CASE WHEN histories.branch_id=2 THEN 1 ELSE 0 END) AS Branch_B
FROM
(SELECT 1 hr UNION
SELECT 2 UNION
SELECT 3 UNION
SELECT 4 UNION
SELECT 5) AS hours
LEFT JOIN histories
ON hours.hr=histories.totHours
GROUP BY hours.hr;
Demo fiddle
Edit the hours subquery to add more, for example if you want until 7, you just add:
(SELECT 1 hr UNION
SELECT 2 UNION
SELECT 3 UNION
SELECT 4 UNION
SELECT 5 UNION
SELECT 6 UNION
SELECT 7) AS hours
to the subquery. Another way is to define the hours beforehand and create a reference table. Let's say you estimate the hour to be until 100, then it's better if you create a table that stores 1-100 as reference for the LEFT JOIN

MySQL Trigger Sum

I would like help with a before update trigger that conditionally sums the amount column where day and id match, and only display the sum where variable is ‘total’.
id | day | variable | amount
-: | :------ | :------- | :-----
1 | Monday | Total | null
1 | Monday | null | 1
1 | Monday | null | 2
1 | Monday | null | 3
1 | Tuesday | Total | null
1 | Tuesday | null | 1
1 | Tuesday | null | 2
1 | Tuesday | null | 3
2 | Monday | Total | null
2 | Monday | null | 1
2 | Monday | null | 2
2 | Monday | null | 3
2 | Tuesday | Total | null
2 | Tuesday | null | 1
2 | Tuesday | null | 2
2 | Tuesday | null | 3
Is there a way to control the sum function so that it won’t update each total unless an associated value has been updated? I.E I wouldn’t want the Monday total to recalculate when values change inside a Tuesday row, or for another ID.
Fiddle: https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=fc3939aa508002dab7f2af45611717cf
Note that MySQL has a restriction on updating other rows in the same table in a trigger according to the documentation.
A stored function or trigger cannot modify a table that is already being used (for reading or writing) by the statement that invoked the function or trigger.
If the following SQL statement will work
UPDATE tb1 t1
JOIN (
SELECT id, day, SUM(amount) as total
FROM tb1 t
WHERE id = 1 AND day = 'Monday' AND variable is NULL
GROUP BY id, day
) t2 ON t1.id = t2.id AND t1.day = t2.day AND t1.variable = 'Total'
SET t1.amount = t2.total;
Then its implementation inside the trigger will throw an error
You can't specify target table 'tb1' for update in FROM clause
But you can do it without the trigger and create a view
SELECT
id, day, variable,
CASE WHEN variable = 'Total'
THEN (
SELECT SUM(amount) FROM tb1 t1
WHERE t1.id = t.id AND t1.day = t.day AND t1.variable IS NULL
)
ELSE amount
END AS amount
FROM tb1 t
Or, it is even possible to completely delete all rows with a Total and retrieve them using the WITH ROLLUP clause.
SELECT
id,
day,
CASE WHEN amount IS NULL THEN 'Total' END AS variable,
SUM(amount) AS amount
FROM tb1
GROUP BY id, day, amount
WITH ROLLUP
HAVING id IS NOT NULL AND day IS NOT NULL
db<>fiddle

get specific data in different time period in mysql

I have a table like this
--------------------------------
|name | time | city |value |
--------------------------------
|a | 2018| rasht | 1.5 |
--------------------------------
|a | 2017| rasht | 2 |
--------------------------------
|a | 2018| tehran| 4 |
--------------------------------
|a | 2017| rasht | 3 |
--------------------------------
|a | 2018| rasht | 5 |
--------------------------------
|a | 2017| rasht | 2 |
--------------------------------
|b | 2018| tehran| 7 |
--------------------------------
i like to get data from this table like this:
name | city | total 2018 | total 2017
a |rasht | 6.5 |7
a |theran| 4 |0
b |theran| 7 |0
I am using query like this:
select A.name,city,A.sum(value) ,B.sum(value) from
(select * from tbl where year=2018 group by name,city)A,
(select * from tbl where year=2017 group by name,city)B
where A.name=B.name and A.city=B.city
but it didn't work and for records like b that dont have value in 2017 it returns nothing
As u can see my example here is very simple i made exact db in fiddle if u can see it in this address Fiddle
I also use this code:
select subzone, mvFeederName, SUM(CASE WHEN faultStartDate >= 13970901 and faultEndDate <= 13971101 and (subzone=10 ) THEN energyLost ELSE 0 END) AS `unplannedCurrentEnergyLost`, SUM(CASE WHEN faultStartDate >= 13960901 and faultEndDate <= 13961101 and (subzone=10 ) THEN energyLost ELSE 0 END) AS `unplannedLastYearEnergyLost`, count(CASE WHEN faultStartDate >= 13970901 and faultEndDate <= 13971101 and (subzone=10 ) THEN mvFeederName ELSE 0 END) AS `unplannedCurrentCount`, count(CASE WHEN faultStartDate >= 13960901 and faultEndDate <= 13961101 and (subzone=10 ) THEN mvFeederName ELSE 0 END) AS `unplannedLastYearCount` from faults_mv group by subzone,mvFeederName
it return zero for energyLost sumation col in two time period and count of mvFeederName in two thime period is the same that is not correct.
Use conditional aggregation -
select name, city,
sum(case when time=2018 then value end) 'total 2018',
sum(case when time=2017 then value end) 'total 2017'
from tablename
group by name, city
You can use conditional aggregation to get the result you want:
SELECT name, city,
SUM(CASE WHEN time = 2018 THEN value ELSE 0 END) AS `total 2018`,
SUM(CASE WHEN time = 2017 THEN value ELSE 0 END) AS `total 2017`
FROM tbl
GROUP BY name, city
Output:
name city total 2018 total 2017
a rasht 6.5 7
a tehran 4 0
b tehran 7 0
Demo on dbfiddle

MySQL calculated column based on 2 conditions

I'm trying to calculate a basic 'attendance' report from a table of logs. My logs table appears like this:
TABLE logs
id | date | log |
------------------------|
123 | 2018-01-01 | 1234 |
123 | 2018-01-02 | | // Missed log
123 | 2018-01-03 | 5678 |
456 | 2018-01-01 | 5678 |
456 | 2018-01-02 | 1234 |
456 | 2018-01-03 | 1234 |
So an empty entry results in a missed date.
The desired SELECT result would be this:
id | perc |
------------|
123 | 0.666 |
456 | 1 |
I've tried the following but I'm not sure where to go from here:
SELECT id, count(*) AS attended WHERE log IS NOT NULL GROUP BY id ORDER BY attended
which yeilds:
id | attended |
---------------|
123 | 2 |
456 | 3 |
This gets me part of the way there, but I'm not sure how I can add a missed column to that, and then calculate the percentage. I'll be joining the result to a 'users' table later on, so names can be associated with the logs ids. Any help would be appreciated!
CASE condition that is suggested in another answer is redundant here as COUNT always counting NOT NULL records only. As a result, you can simplify the query:
SELECT
id
, COUNT(log) AS attended
, COUNT(*) - COUNT(log) AS missed
, IF(COUNT(*) > 0, count(log) / COUNT(*), NULL) AS percentage
FROM
logs
GROUP BY
id
ORDER BY
attended
In addition, I've fixed percentage calculation as per OP.
"conditional aggregates" solve this, basically you place a case expression inside the count() function
SELECT
id
, count(case when log IS NOT NULL then 1 end) AS attended
, count(case when log IS NULL then 1 end) AS Missed
, case when count(case when log IS NOT NULL then 1 end) > 0 then
count(case when log IS NOT NULL then 1 end) / count(*)
else 0
end AS perc
GROUP BY id
ORDER BY attended
If you use count(*) than all rows are counted, but if you count a value that can be NULL then those NULLs are not counted.

Join two table and count, avoid zero if record is not available in second table

I have following tables products and tests.
select id,pname from products;
+----+---------+
| id | pname |
+----+---------+
| 1 | prd1 |
| 2 | prd2 |
| 3 | prd3 |
| 4 | prd4 |
+----+---------+
select pname,testrunid,testresult,time from tests;
+--------+-----------+------------+-------------+
| pname | testrunid | testresult | time |
+--------+-----------+------------+-------------+
| prd1 | 800 | PASS | 2017-10-02 |
| prd1 | 801 | FAIL | 2017-10-16 |
| prd1 | 802 | PASS | 2017-10-02 |
| prd1 | 803 | NULL | 2017-10-16 |
| prd1 | 804 | PASS | 2017-10-16 |
| prd1 | 805 | PASS | 2017-10-16 |
| prd1 | 806 | PASS | 2017-10-16 |
+--------+-----------+------------+-------------+
I like to count test results for products and if there is no result available,for a product just show a zero for it. something like following table:
+--------+------------+-----------+----------------+---------------+
| pname | total_pass | total_fail| pass_lastweek | fail_lastweek |
+--------+------------+-----------+----------------+---------------+
| prd1 | 5 | 1 | 3 | 1 |
| prd2 | 0 | 0 | 0 | 0 |
| prd3 | 0 | 0 | 0 | 0 |
| prd4 | 0 | 0 | 0 | 0 |
+--------+------------+-----------+----------------++--------------+
I have tried different queries like following, which is just working for one product and is incomplete:
SELECT pname, count(*) as pass_lastweek FROM tests where testresult = 'PASS' AND time
>= '2017-10-11' and pname in (select pname from products) group by pname;
+-------------+---------------+
| pname | pass_lastweek |
+-------------+---------------+
| prd1 | 3 |
+-------------+---------------+
it looks so basic but still I am unable to write it, any idea?
Use conditional aggregation. The COUNT function count NULL values as zeros automatically, therefore, there is no need to take care of that.
select p.pname,
count(case when testresult = 'PASS' then 1 end) as total_pass,
count(case when testresult = 'FAIL' then 1 end) as total_fail,
count(case when testresult = 'PASS' and time >= curdate() - INTERVAL 6 DAY then 1 end) as pass_lastweek ,
count(case when testresult = 'FAIL' and time >= curdate() - INTERVAL 6 DAY then 1 end) as fail_lastweek ,
from products p
left join tests t on t.pname = p.pname
group p.id, p.pname
Generally, you need to LEFT JOIN the first table with the second one before you group. The join will give you a row for each product (even if there are no test results to join it to; INNER JOIN would exclude products with no associated tests) + an additional row for each test result (beyond the first). Then you can group them.
SELECT products.*, tests.* FROM products
LEFT JOIN tests ON products.pname = tests.pname
GROUP BY products.id
Also, I would strongly recommend using a product_id column in the tests table, rather than using pname (if a products.pname changes, your whole DB breaks unless you also update the pname field in kind for every test result). The general query would then look like this:
SELECT products.*, tests.* FROM products
LEFT JOIN tests ON products.id = tests.product_id
GROUP BY products.id
I used 2 queries , the first with conditional count and the second one is to change all null values into 0 :
select pname,
case when total_pass is null then 0 else total_pass end as total_pass,
case when total_fail is null then 0 else total_fail end as total_fail,
case when pass_lastweek is null then 0 else pass_lastweek end as pass_lastweek,
case when fail_lastweek is null then 0 else fail_lastweek end asfail_lastweek from (
select products.pname,
count(case when testresult = 'PASS' then 1 end) as total_pass,
count(case when testresult = 'FAIL' then 1 end) as total_fail,
count(case when testresult = 'PASS' and time >= current_date -7 DAY then 1 end) as pass_lastweek ,
count(case when testresult = 'FAIL' and time >= current_date -7 DAY then 1 end) as fail_lastweek ,
from products
left join tests on tests.pname = products.pname
group 1 ) t1