Is it possible to pivot the output from a query? - mysql

I have this output from this query:
select Date,Status, count(distinct persons)from TableA where Date='2014-11-04' group by Status;
+------------+------------------------+-------------------------------+
| Date | Status | count(distinct persons) |
+------------+------------------------+-------------------------------+
| 2014-11-04 | 0 | 45 |
| 2014-11-04 | 1 | 93 |
+------------+------------------------+-------------------------------+
What I wanted to get is that:
+------------+------------------------+-------------------------------+
| Date | 0 | 1 |
+------------+------------------------+-------------------------------+
| 2014-11-04 | 45 | 93 |
+------------+------------------------+-------------------------------+

You can put a condition inside your COUNT function using CASE:
SELECT Date,
COUNT(DISTINCT CASE WHEN status = 0 THEN persons END) AS `0`,
COUNT(DISTINCT CASE WHEN status = 1 THEN persons END) AS `1`
FROM TableA
WHERE Date = '2014-11-04'
GROUP BY Date;

You can use following code -
select Date, [0], [1]
from
(select Date,Status,persons
from TableA
where Date='2014-11-04') AS SourceTable
PIVOT
(
COUNT(persons)
FOR Status IN ([0],[1])
) AS PivotTable;

Related

Get appropriate date with GROUP BY

I have a table where I track the duration of watched films by a user for each day.
Now I would like to calculate a unique view count based on date.
So the conditions are:
For each user max view count is 1
View = 1 if one user's SUM(duration) >= 120
Date should be fixed once SUM(duration) reaches 120
But the issue is here to get a correct date row. For example row1.duration + row2.duration >= 120 and thus view count = 1 should be applied for 2021-10-16
| id | user_id | duration | created_at | film_id |
+----+---------+----------+------------+---------+
| 1 | 1 | 80 | 2021-10-15 | 1 |
| 2 | 1 | 70 | 2021-10-16 | 1 |
| 3 | 1 | 200 | 2021-10-17 | 2 |
| 4 | 2 | 50 | 2021-10-18 | 1 |
| 5 | 2 | 90 | 2021-10-18 | 1 |
| 6 | 3 | 140 | 2021-10-18 | 2 |
| 7 | 4 | 10 | 2021-10-19 | 3 |
Expected result:
| cnt | created_at |
+-------+------------+
| 0 | 2021-10-15 |
| 1 | 2021-10-16 |
| 0 | 2021-10-17 |
| 2 | 2021-10-18 |
| 0 | 2021-10-19 |
This is what I tried, but it choses first date, and ignores 0 count.
Here is the fiddle with populated data
SELECT count(*) AS cnt,
created_at
FROM
(SELECT user_id,
sum(duration) AS total,
created_at
FROM watch_time
GROUP BY user_id) AS t
WHERE t.total >= 120
GROUP BY created_at;
Is there any chance to have this work via SQL or it's should be done in application level?
Thanks in advance!
Update:
Version: AWS RDS MySQL 5.7.33
But I'm ok to switch to Postgres if that can help.
Much appreciated even there is a way to have MIN(date) but with the all dates(included 0 views).
Better than this one.
SELECT IFNULL(cnt, 0) as cnt,
t3.created_at
FROM
(SELECT count(*) AS cnt,
created_at
FROM
(SELECT user_id,
sum(duration) AS total,
created_at
FROM watch_time
GROUP BY user_id) AS t
WHERE t.total >= 120
GROUP BY created_at) AS t2
RIGHT JOIN
(SELECT distinct(created_at)
FROM watch_time) AS t3
ON t2.created_at = t3.created_at;
which returns:
| cnt | created_at |
+-------+------------+
| 1 | 2021-10-15 |
| 0 | 2021-10-16 |
| 0 | 2021-10-17 |
| 2 | 2021-10-18 |
| 0 | 2021-10-19 |
But I'm not sure whether the date(2021-10-15) has taken randomly or its always the lowest date
Update 2:
Is it possible to include the film_id as well? Like considering user_id, film_id as a unique view instead of only grouping by user_id.
So in this case:
row1 & row2 both has user_id: 1 and film_id: 1, and the result is 1 view, because the sum of their durations is >= 120. so the date in this case will be 2021-10-16.
but row3 has user_id: 1 and film_id: 2, and with duration >= 120 it's also a 1 view with date 2021-10-17
| id | user_id | duration | created_at | film_id |
+----+---------+----------+------------+---------+
| 1 | 1 | 80 | 2021-10-15 | 1 |
| 2 | 1 | 70 | 2021-10-16 | 1 |
| 3 | 1 | 200 | 2021-10-17 | 2 |
| 4 | 2 | 50 | 2021-10-18 | 1 |
| 5 | 2 | 90 | 2021-10-18 | 1 |
| 6 | 3 | 140 | 2021-10-18 | 2 |
| 7 | 4 | 10 | 2021-10-19 | 3 |
Expected result:
| cnt | created_at |
+-------+------------+
| 0 | 2021-10-15 |
| 1 | 2021-10-16 |
| 1 | 2021-10-17 |
| 2 | 2021-10-18 |
| 0 | 2021-10-19 |
Using MySQL variables, it can implement your count logic, it basically orders the table rows by user_id and created_at, and calculate row by row
http://sqlfiddle.com/#!9/569088/14
SELECT created_at, SUM(CASE WHEN duration >= 120 THEN 1 ELSE 0 END) counts
FROM (
SELECT user_id, created_at,
CASE WHEN #UID != user_id THEN #SUM_TIME := 0 WHEN #SUM_TIME >= 120 AND #DT != created_at THEN #SUM_TIME := 0 - duration ELSE 0 END SX,
#SUM_TIME := #SUM_TIME + duration AS duration,
#UID := user_id,
#DT := created_at
FROM watch_time
JOIN ( SELECT #SUM_TIME :=0, #DT := NOW(), #UID := '' ) t
ORDER BY user_id, created_at
) f
GROUP BY created_at
I think I misunderstood the requirement in my first attempt.
Second attempt
MySql >= 8.0 (or Postgresl) using window functions
I know you are working with MySql 5.7, I add an answer for it next.
I am not sure if I understand correctly your requirement. Do you want the cumulative sum of time watch by user and the first time some user exceed 119 minutes count one that day?
First, I get cumulative sum by user (cte subquery) ordered by date. In subquery cte1 with a CASE statement I set one the first time a user reach 120 minutes (view column). Finally I group by created_at (date) and count() ones in view column:
WITH cte AS (SELECT *, SUM(duration) OVER (PARTITION BY user_id ORDER BY created_at ASC, film_id) as cum_duration
FROM watch_time),
cte1 AS (SELECT *, CASE WHEN cum_duration >= 120 AND COALESCE(LAG(cum_duration) OVER (PARTITION BY user_id ORDER BY created_at ASC), 0) < 120 THEN 1 END AS view
FROM cte)
SELECT created_at, COUNT(view) AS cnt
FROM cte1
GROUP BY created_at;
created_at
cnt
2021-10-15
0
2021-10-16
1
2021-10-17
0
2021-10-18
2
2021-10-19
0
MySql 5.7
I get the cumulative sum for each user and filter cumulative duration >= 120, then I group by user_id and get MIN(created_at). Finally I group by min_created_at and count records.
SELECT min_created_at AS date, count(*) AS cnt
FROM (SELECT user_id, MIN(created_at) AS min_created_at
FROM (SELECT wt1.user_id, wt1.created_at, SUM(wt2.duration) AS cum_duration
FROM (SELECT user_id, created_at, SUM(duration) AS duration FROM watch_time GROUP BY user_id, created_at) wt1
INNER JOIN (SELECT user_id, created_at, SUM(duration) AS duration FROM watch_time GROUP BY user_id, created_at) wt2 ON wt1.user_id = wt2.user_id AND wt1.created_at >= wt2.created_at
GROUP BY wt1.user_id, wt1.created_at
HAVING SUM(wt2.duration) >= 120) AS sq
GROUP BY user_id) AS sq2
GROUP BY min_created_at;
date
cnt
2021-10-16
1
2021-10-18
2
You can JOIN my query (RIGHT JOIN) with the original table (GROUP BY created_at) to get the rest of the dates with count equal to 0.
First attempt
I understood that you want count one each time a user reach 120 minutes per day.
First, I get the total movie watch time by user and date (subquery sq), then with a CASE statement I set one each time a user in a date exceed 119 minutes, I group by created_at (date) and count() ones in CASE statement:
SELECT created_at, COUNT(CASE WHEN total_duration >= 120 THEN 1 END) cnt
FROM (SELECT created_at, user_id, SUM(duration) AS total_duration
FROM watch_time
GROUP BY created_at, user_id) AS sq
GROUP BY created_at;
Output (with sample data from the question):
reated_at
cnt
2021-10-15
0
2021-10-16
0
2021-10-17
1
2021-10-18
2
2021-10-19
0

SQL Query Group by column values, and get o/p as separate column

I have a table like this
+----------------+---------------------+--------------+--------+
| student_serial | reg_date | batch_serial | status |
+----------------+---------------------+--------------+--------+
| 1 | 2019-10-31 10:25:17 | 1 | C |
| 2 | 2019-10-31 10:32:45 | 3 | A |
| 3 | 2019-11-04 10:57:51 | 1 | A |
+----------------+---------------------+--------------+--------+
And I want the o/p as
batch_serial count_a count_c
1 1 1
3 1 0
i.e. the o/p must group by the values of status column, and display it as separate column
You can do conditional aggregation:
select
batch_serial
sum(status = 'A') count_a,
sum(status = 'C') count_c
from mytable
group by batch_serial
Use conditional aggregation. In MySQL, this looks like:
select batch_serial, sum( status = 'A' ) as count_a, sum( status = 'C' ) as count_c
from t
group by batch_serial;

mysql group by date and left join table used group by date

I am going to query two tables now, use the left connection, first use the group by date in the main table, the date is grouped according to the week, and now I also want to use the same date to group in the join table of join. How to query this?
now I am doing this
select
case
when order.time BETWEEN '2018-7-27' and '2018-8-3' then '7.27-8.2'
when order.time BETWEEN '2018-8-3' and '2018-8-10' then '8.3-8.9'
else "--" end ptime,
payment.uid, payment.time
from order
left join
(select uid, time, from payment where create_time between '2018-7-27' and '2018-8-10') payment on payment.uid = order.uid
where order.time between '2018-7-13' and '2018-8-10'
+----------+-------+-----------------------+
| date | uid | time |
+----------+-------+-----------------------+
| 7.27-8.2 | 42 | 2018-07-27 22:08:22 |
| 7.27-8.2 | 42 | 2018-07-27 22:08:22 |
| 8.3-8.9 | 50 | 2018-08-04 14:19:00 |
| 8.3-8.9 | 50 | 2018-08-04 14:19:00 |
| 8.3-8.9 | 76 | 2018-07-28 14:20:00 |
| 8.3-8.9 | 76 | 2018-07-28 14:12:00 |
| 8.3-8.9 | 76 | 2018-07-28 13:12:00 |
| 8.3-8.9 | 88 | 2018-07-28 19:29:00 |
| 8.3-8.9 | 98 | 2018-08-09 14:39:00 |
+----------+-------+-----------------------+
except output:
+----------+-------+-----+
| date | uid | time|
+----------+-------+-----+
| 7.27-8.2 | 42 | 2 |
| 8.3-8.9 | 50 | 2 |
| 8.3-8.9 | 76 | 3 |
| 8.3-8.9 | 88 | 0 |
| 8.3-8.9 | 98 | 1 |
+----------+-------+-----+
But in this way, the payment time is not grouped according to the date of the order.
Grouping rules are still on the order table.
How do I apply a date grouping to two tables, and do the same grouping of the pay table deduplication? Thank you very much!
This is my solution:
select p1.ptime, p2.uid, count(p2.uid) (
select
case
when order.time BETWEEN '2018-7-27' and '2018-8-3' then '7.27-8.2'
when order.time BETWEEN '2018-8-3' and '2018-8-10' then '8.3-8.9'
else "--" end ptime
from order
where order.time between '2018-7-13' and '2018-8-10' group by ptime) p1
left join
(select
case
when payment.time BETWEEN '2018-7-27' and '2018-8-3' then '7.27-8.2'
when payment.time BETWEEN '2018-8-3' and '2018-8-10' then '8.3-8.9'
else "--" end p2time,
count(payment.uid) uid_cnt
from order
left join
(select uid, time, from payment where create_time between '2018-7-27' and
'2018-8-10') payment on payment.uid = order.uid
where order.time between '2018-7-13' and '2018-8-10' group by p2time) p2
on p2.p2time = p1.ptime
You just need aggregate function count
select uid,date(ptime) as date,count(payment.time) as time
from
( select
case
when order.time BETWEEN '2018-7-27' and '2018-8-3' then '7.27-8.2'
when order.time BETWEEN '2018-8-3' and '2018-8-10' then '8.3-8.9'
else "--" end ptime,
payment.uid, payment.time
from order
left join
(select uid, time, from payment where create_time between '2018-7-27' and '2018-8-10') payment on payment.uid = order.uid
where order.time between '2018-7-13' and '2018-8-10'
) as t group by uid,date(ptime)

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

Multiple SQL rows merge into single row if the id is same

What is the mysql I need to achieve the result below given this table:
table:
+----+-------+--------------
| name| id | items | vol
+----+-------+---------------
| A | 1111 | 4 | 170
| A | 1111 | 5 | 100
| B | 2222 | 6 | 200
| B | 2222 | 7 | 120
+----+-------+-----------------
Above table is the result of union query
SELECT * FROM imports
union all
SELECT * FROM exports
ORDER BY name;
I want to create a temporary view that looks like this
desired result:
+----+---------+---------+-------------------
| name| id | items | vol | items1 | vol2
+-----+--------+-------+--------------------
| A | 1111 | 4 | 170 | 5 | 100
| B | 2222 | 6 | 200 | 7 | 120
+----+---------+---------+-------------------
any help would be greatly appreciated! -Thanks
Use PIVOT:
SELECT name,id,
SUM( CASE WHEN typ = 'imports' THEN items ELSE 0 END) as imports_items,
SUM( CASE WHEN typ = 'imports' THEN vol ELSE 0 END) as imports_vol,
SUM( CASE WHEN typ = 'exports' THEN items ELSE 0 END) as exports_items,
SUM( CASE WHEN typ = 'exports' THEN vol ELSE 0 END) as exports_vol
FROM (
SELECT 'imports' as typ, t.* FROM imports t
union all
SELECT 'exports' as typ, t.* FROM exports t
) x
GROUP BY name,id
ORDER BY name;
This should give you the table you are looking for:
SELECT
a.name,
a.id,
a.items,
a.vol,
b.items as items2,
b.vol as vol2
FROM imports a
INNER JOIN exports b on b.id = a.id;