SQL: count and sum based on conditions from the related table - mysql

I have two tables;
countries(id, name, region);
1, 'UK', '1';
2, 'USA', '1';
3, 'AUSTRALIA', '1';
4, 'CHINA', '0';
5, 'INDIA', '0';
6, 'SRI LANKA', '0' ;
and
tickets(id, country_id, issued_date, holder, gender, fee, canceled);
100, 2, 2017-08-15, 'Person 1', 'M', 200, '1';
101, 2, 2017-08-15, 'Person 2', 'M', 200, '0';
103, 3, 2017-08-15, 'Person 3', 'M', 200, '0';
104, 5, 2017-08-16, 'Person 1', 'M', 200, '0';
105, 6, 2017-08-16, 'Person 1', 'M', 200, '0';
106, 1, 2017-08-17, 'Person 1', 'M', 200, '0';
107, 3, 2017-08-18, 'Person 1', 'M', 200, '1';
108, 4, 2017-08-18, 'Person 1', 'M', 200, '0';
I want to group all the tickets based on issued_date with some aggregates fields to generate the summary. Here is my query:-
SELECT
issued_date,
COUNT(*) as total_tickets,
COUNT(CASE WHEN canceled = '0' THEN 1 ELSE NULL END) as issued_tickets,
SUM(CASE WHEN canceled = '0' THEN fee ELSE NULL END) as total_amount
FROM tickets
GROUP BY issued_date;
But, how to use COUNT and SUM for related table countries? For example, I want to show how many tickets were sold on a date (2017-08-15) from a country having region = '1'.
I tried the following, but the results are not correct for region_1 field
SELECT
issued_date,
COUNT(*) as total_tickets,
COUNT(CASE WHEN canceled = '0' THEN 1 ELSE NULL END) as issued_tickets,SUM(CASE WHEN canceled = '0' THEN fee ELSE NULL END) as total_amount,
(SELECT COUNT(countries.id) FROM countries WHERE countries.id = tickets.country_id && countries.region = '1') as region_1
FROM tickets
GROUP BY issued_date;

I would use a derived table that is grouped on issue_date, country_id and region and use that derived table in an inner join.
SELECT issued_date
,COUNT(*) AS total_tickets
,COUNT(CASE WHEN canceled = '0' THEN 1 ELSE NULL END) AS issued_tickets
,SUM(CASE WHEN canceled = '0' THEN fee ELSE NULL END) AS total_amount
,tickets_by_region.total_region_1_tickets
FROM tickets
INNER JOIN (
SELECT issued_date
,country_id
,countries.region
,COUNT(*) AS total_region_1_tickets
FROM tickets
INNER JOIN countries ON (countries.id = tickets.country_id)
GROUP BY issued_date
,countries.country_id
,countries.region
) tickets_by_region ON (
tickets_by_region.issued_date = tickets.issued_date
AND tickets_by_region.country_id = tickets.country_id
AND tickets_by_region.region = '1'
) AS region_1
GROUP BY issued_date;
HTH.

You probably need to use INNER JOIN and GROUP BY with HAVING which allow append next table on related keys and add to additional summary or counts you need to use them like sub-query because they need to work with no filtered data.
Approach is prepare data in sub-query and then JOIN the filtered data
to your main table which can do final filtering for final result.
SQL can looks like bellow (tested on local)
SELECT t1.issued_date, COUNT(t1.id) as sum_tickets, t2.region, t3.total_tickets
FROM tickets t1
LEFT JOIN (SELECT id, COUNT(id) as total_tickets FROM tickets) t3 ON t3.id = t1.id
INNER JOIN countries t2 ON t2.id = t1.country_id
GROUP BY t1.issued_date
HAVING (t2.region = '1')
Output is
issued_date, sum_tickets, region, total_tickets
2017-08-15, 3, 1, 8
2017-08-17, 1, 1, null
2017-08-18, 2, 1, null
You can add more conditions to HAVING in query.

I would just use conditional aggregation with a JOIN:
SELECT t.issued_date, COUNT(*) as total_tickets,
SUM(t.canceled = 0) as issued_tickets,
SUM(CASE WHEN t.canceled = 0 THEN t.fee END) as total_amount,
SUM(c.region = 1) as num_region_1
FROM tickets t JOIN
countries c
ON t.country_id = c.id
GROUP BY t.issued_date;

Related

Merge multiple queries into single one

I wish to have total users of each role who make differents type_role of actions.
This is my tables:
t_users : id_user, username, userpass, status, ...
t_action: id_action, id_user, id_role, id_type_role, ...
t_role: id_role, libelle, status
t_type_role: id_type_role, libelle, status
This is my datas:
INSERT INTO `t_users` (`id_user`, `username`, `password`, `status`) VALUES
(1, 'foo', 'pass1', '1'),
(2, 'bar', 'pass2', '0'),
(3, 'fusion', 'pass3', '2');
INSERT INTO `t_role` (`id_role`, `libelle`, `statut`) VALUES
(1, 'Operator', 1),
(2, 'User', 1),
(5, 'Administrator', 1);
INSERT INTO `t_type_role` (`id_type_role`, `libelle`, `statut`) VALUES
(1, 'Executif', 1),
(2, 'home', 1),
(3, 'System', 1);
INSERT INTO `t_action` (`id_action`, `id_user`, `id_role`, `id_type_role`) VALUES
(1, 1, 5, 3),
(2, 2, 5, 3);
Expected result:
ROLE
Executif
home
System
Administrator
0
0
2
Operator
0
0
0
User
0
0
0
For each role, COUNT Users who have make actions WITH EACH column Type_of_role :
this is my queries written on each cell:
ROLE
Type_role_1
Type_role_2
DISTINCT(t_role.libelle)role_1
SELECT count(U.id) Type_role_1FROM t_users UJOIN t_action AON A.id_user=U.idJOIN t_type_role TRON TR.id=A.type_roleJOIN t_role RON R.id=A.id_roleWHERE R.libelle= role_1 AND TR.libelle= Type_role_2
SELECT count(U.id) Type_role_1FROM t_users UJOIN t_action AON A.id_user=U.idJOIN t_type_role TRON TR.id=A.type_roleJOIN t_role RON R.id=A.id_roleWHERE R.libelle= role_1 AND TR.libelle= Type_role_2
DISTINCT(t_role.libelle)role_2
SELECT count(U.id) Type_role_1FROM t_users UJOIN t_action AON A.id_user=U.idJOIN t_type_role TRON TR.id=A.type_roleJOIN t_role RON R.id=A.id_roleWHERE R.libelle= role_2 AND TR.libelle= Type_role_1
SELECT count(U.id) Type_role_2FROM t_users UJOIN t_action AON A.id_user=U.idJOIN t_type_role TRON TR.id=A.type_roleJOIN t_role RON R.id=A.id_roleWHERE R.libelle= role_2 AND TR.libelle= Type_role_2
Please, help
First solution:
As suggested by another user on comment, i've write the single query like this one:
SELECT DISTINCT
R.libelle ROLE,
COUNT( IF( T.libelle= 'Executif', 1, NULL )) AS Executif,
COUNT( IF( T.libelle= 'Home', 1, NULL )) AS Home,
COUNT( IF( T.libelle= 'System', 1, NULL )) AS System
FROM t_users U
JOIN t_action A ON A.id_user = U.id_user
JOIN t_role R ON R.id_role = A.id_role
JOIN t_type_role T ON T.id_type_role = A.id_type_role
GROUP BY ROLE
it works fine.But how about add another COUNT conditions dynamically?
Update:
this conditions should have to be built dynamically. Because, they are based on the table T.libelle wich could grow on time. example: If we add a new value to T.libelle, the query should have to include this new value on query accoding to the syntax of CONDITIONS...
Below,
CONDITIONS = COUNT( IF( T.libelle= 'Executif', 1, NULL )) AS Executif,
COUNT( IF( T.libelle= 'Home', 1, NULL )) AS Home,
COUNT( IF( T.libelle= 'System', 1, NULL )) AS System
So, if we add an entry new_libelle to T.libelle, CONDITIONS should become
CONDITIONS = COUNT( IF( T.libelle= 'Executif', 1, NULL )) AS Executif,
COUNT( IF( T.libelle= 'Home', 1, NULL )) AS Home,
COUNT( IF( T.libelle= 'System', 1, NULL )) AS System
COUNT( IF( T.libelle= 'New_libelle', 1, NULL )) AS New_libelle

Find the longest chain of records not containing 1 record

I have a table that contains 4 columns - UserID, FromLocation, ToLocation, and Date. I need to pull the "length" of the longest chain (UserID going from FromLocation to ToLocation, as long as the chain does not contain "FAKE_LOCATION").
So, based on the following data set:
CREATE TABLE IF NOT EXISTS `tableA` (
`UserID` int(11) unsigned NOT NULL,
`FromLocation` varchar(20) NOT NULL,
`ToLocation` varchar(20) NOT NULL,
`Date` datetime NOT NULL
) DEFAULT CHARSET=utf8;
INSERT INTO `tableA` (`UserID`, `FromLocation`, `ToLocation`, `Date`) VALUES
(1, 'Loc 1', 'Loc 2', '2022-01-01'),
(1, 'Loc 2', 'Loc 3', '2022-01-02'),
(1, 'Loc 3', 'Loc 5', '2022-01-03'),
(1, 'Loc 5', 'Loc 18', '2022-01-04'),
(1, 'Loc 18', 'Loc 2', '2022-01-05'),
(1, 'Loc 2', 'Loc 4', '2022-01-06'),
(1, 'Loc 4', 'FAKE_LOCATION', '2022-01-07'),
(1, 'FAKE_LOCATION', 'Loc 7', '2022-01-08'),
(1, 'Loc 7', 'Loc 17', '2022-01-09'),
(2, 'Loc 3', 'Loc 4', '2022-01-05'),
(2, 'Loc 4', 'Loc 5', '2022-01-06'),
(2, 'Loc 5', 'FAKE_LOCATIOIN', '2022-01-07'),
(3, 'Loc 3', 'Loc 4', '2022-01-05'),
(3, 'Loc 4', 'FAKE_LOCATIOIN', '2022-01-07'),
(3, 'FAKE_LOCATIOIN', 'Loc 3', '2022-02-07'),
(3, 'Loc 3', 'Loc 5', '2022-02-08');
I'm trying to generate the following data set:
UserID
Longest Chain
1
7
2
3
3
2
For UserID 1, the longest chain is: Loc 1 -> Loc 2 -> Loc 3 -> Loc 5 -> Loc 18 -> Loc 2 -> Loc 4.
For User ID 2, the longest chain is: Loc 3 -> Loc 4 -> Loc 5
For User ID 3, the longest chain is: Loc 3 -> Loc 4. As well as, Loc 3 -> Loc 5
I've created an SQLFiddle for it. Any help shall be appreciated!
Based on the data you provided, I'm assuming that a record that follows another for an ID will always have the same from location as the prior records to location (sorting by UserID and date).
If that's the case, this should solve your problem -
#get max chain count by UserID
SELECT
UserID,
MAX(chain_count) AS LongestChain
FROM(
#count the number of records in each chain, add one for terminal ToLocation for chain
SELECT
UserID,
chained,
COUNT(chained) + 1 AS chain_count
FROM(
#start a new chain every time FAKE_LOCATION is present for a UserID
SELECT
UserID,
FromLocation,
ToLocation,
Date,
(#row_number4:=CASE
WHEN (ToLocation = 'FAKE_LOCATION' OR FromLocation = 'FAKE_LOCATION')
THEN #chain:= #chain + 1
ELSE #chain
END) AS chained
FROM tableA as a, (SELECT #chain:=0,#row_number4:=0) as chain
ORDER BY UserID, Date) chainvalues
WHERE ToLocation != 'FAKE_LOCATION' AND FromLocation != 'FAKE_LOCATION'
GROUP BY UserID, chained) chaincounts
GROUP BY UserID
ORDER BY UserID
;
It was a bit more challenging using MySQL 5.6 than it otherwise could have been since it doesn't allow for window functions.
Here is the fiddle with the query
Here is another example that addresses instances where movement is not necessarily restricted on the next record to the from location for the prior record:
#get max chain count by UserID
SELECT
UserID,
MAX(chain_count) AS LongestChain
FROM(
#count the number of records in each chain, add one for terminal ToLocation for chain
SELECT
UserID,
chained,
COUNT(chained) + 1 AS chain_count
FROM(
#start a new chain every time FAKE_LOCATION is present for a UserID, or b.UserID is null where not the first user record
SELECT
a.UserID,
a.FromLocation,
a.ToLocation,
a.Date,
b.UserID as bUserID,
(#row_number4:=CASE
WHEN (a.ToLocation = 'FAKE_LOCATION' OR a.FromLocation = 'FAKE_LOCATION' OR b.UserID IS NULL) AND userrow != 1
THEN #chain:= #chain + 1
ELSE #chain
END) AS chained,
userrow
FROM (
#get left side of table, userrow increments starting at one for each user instance, num shifts down one to join to b sides to location to ensure date order is followed
SELECT
(#row_number3:=CASE
WHEN #user = UserID
THEN #row_number3 + 1
ELSE 1
END) AS userrow,
#user:=UserID UserID,
FromLocation,
ToLocation,
Date,
(#row_number:=#row_number + 1) - 1 AS num
FROM tableA, (SELECT #row_number:=0) AS t, (SELECT #user:=0,#row_number3:=0) as z
ORDER BY UserID, Date) a
LEFT JOIN (
#right side, effectively shifts tableA up one
SELECT
UserID,
FromLocation,
ToLocation,
Date,
(#row_number2:=#row_number2 + 1) AS num
FROM tableA, (SELECT #row_number2:=0) AS t
ORDER BY UserID, Date) b
ON a.FromLocation = b.ToLocation and a.UserID = b.UserID and a.num = b.num, (SELECT #chain:=0,#row_number4:=0) as chain
ORDER BY a.num) chainvalues
WHERE ToLocation != 'FAKE_LOCATION' AND FromLocation != 'FAKE_LOCATION'
GROUP BY UserID, chained) final
GROUP BY UserID
ORDER BY UserID
;
Here is the fiddle for that example (I added in another record for testing purposes)

Is it possible to fetch needed data in one query?

I have a database containing tickets. Each ticket has a unique number but this number is not unique in the table. So for example ticket #1000 can be multiple times in the table with different other columns (Which I have removed here for the example).
create table countries
(
isoalpha varchar(2),
pole varchar(50)
);
insert into countries values ('DE', 'EMEA'),('FR', 'EMEA'),('IT', 'EMEA'),('US','USCAN'),('CA', 'USCAN');
create table tickets
(
id int primary key auto_increment,
number int,
isoalpha varchar(2),
created datetime
);
insert into tickets (number, isoalpha, created) values
(1000, 'DE', '2021-01-01 00:00:00'),
(1001, 'US', '2021-01-01 00:00:00'),
(1002, 'FR', '2021-01-01 00:00:00'),
(1003, 'CA', '2021-01-01 00:00:00'),
(1000, 'DE', '2021-01-01 00:00:00'),
(1000, 'DE', '2021-01-01 00:00:00'),
(1004, 'DE', '2021-01-02 00:00:00'),
(1001, 'US', '2021-01-01 00:00:00'),
(1002, 'FR', '2021-01-01 00:00:00'),
(1005, 'IT', '2021-01-02 00:00:00'),
(1006, 'US', '2021-01-02 00:00:00'),
(1007, 'DE', '2021-01-02 00:00:00');
Here is an example:
http://sqlfiddle.com/#!9/3f4ba4/6
What I need as output is the number of new created tickets for each day, devided into tickets from USCAN and rest of world.
So for this Example the out coming data should be
Date | USCAN | Other
'2021-01-01' | 2 | 2
'2021-01-02' | 1 | 3
At the moment I use this two queries to fetch all new tickets and then add the number of rows with same date in my application code:
SELECT MIN(ti.created) AS date
FROM tickets ti
LEFT JOIN countries ct ON (ct.isoalpha = ti.isoalpha)
WHERE ct.pole = 'USCAN'
GROUP BY ti.number
ORDER BY date
SELECT MIN(ti.created) AS date
FROM tickets ti
LEFT JOIN countries ct ON (ct.isoalpha = ti.isoalpha)
WHERE ct.pole <> 'USCAN'
GROUP BY ti.number
ORDER BY date
but that doesn't look like a very clean method. So how can I improved the query to get the needed data with less overhead?
Ii is recommended that is works with mySQL 5.7
You may logically combine the queries using conditional aggregation:
SELECT
MIN(CASE WHEN ct.pole = 'USCAN' THEN ti.created END) AS date_uscan,
MIN(CASE WHEN ct.pole <> 'USCAN' THEN ti.created END) AS date_other
FROM tickets ti
LEFT JOIN countries ct ON ct.isoalpha = ti.isoalpha
GROUP BY ti.number
ORDER BY date;
You can create unique entries for each date/country then use that value to count USCAN and non-USCAN
SELECT created,
SUM(1) as total,
SUM(CASE WHEN pole = 'USCAN' THEN 1 ELSE 0 END) as uscan,
SUM(CASE WHEN pole != 'USCAN' THEN 1 ELSE 0 END) as nonuscan
FROM (
SELECT created, t.isoalpha, MIN(pole) AS pole
FROM tickets t JOIN countries c ON t.isoalpha = c.isoalpha
GROUP BY created,isoalpha
) AS uniqueTickets
GROUP BY created
Results:
created total uscan nonuscan
2021-01-01T00:00:00Z 4 2 2
2021-01-02T00:00:00Z 3 1 2
http://sqlfiddle.com/#!9/3f4ba4/45/0
Regarding the answer of SQL Hacks I found the right solution
SELECT created,
SUM(1) as total,
SUM(CASE WHEN pole = 'USCAN' THEN 1 ELSE 0 END) as uscan,
SUM(CASE WHEN pole != 'USCAN' THEN 1 ELSE 0 END) as nonuscan
FROM (
SELECT created, t.isoalpha, MIN(pole) AS pole
FROM tickets t JOIN countries c ON t.isoalpha = c.isoalpha
GROUP BY t.number
) AS uniqueTickets
GROUP BY SUBSTR(created, 1 10)

How to SUM result both SUMs SQL?

Using this query I try to sum result of both SUM function:
select
DAY(created_at) AS day,
SUM(if(status = '1', 1, 0)) AS result,
SUM(if(status = '2', 1, 0)) AS noresult,
SUM(result + noresult)
from `clients` where `doctor_id` = 2 and MONTH(created_at) = MONTH(CURRENT_TIMESTAMP) group by `day`
I try to do that in this line:
SUM(result + noresult)
Try this:
select
DAY(created_at) AS day,
SUM(if(status = '1', 1, 0)) AS result,
SUM(if(status = '2', 1, 0)) AS noresult,
SUM(if(status in ('1', '2'), 1, 0))
from `clients`
where `doctor_id` = 2 and MONTH(created_at) = MONTH(CURRENT_TIMESTAMP)
group by `day`
You can't use alias in select columns name you must repeat the code
select
DAY(created_at) AS day,
SUM(if(status = '1', 1, 0)) AS result,
SUM(if(status = '2', 1, 0)) AS noresult,
SUM(if(status = '1', 1, 0)) + SUM(if(status = '2', 1, 0)) AS all_result
from `clients` where `doctor_id` = 2 and MONTH(created_at) = MONTH(CURRENT_TIMESTAMP) group by `day`
you must repeat the code because the different SQL clause are processed in a specific order (first from then where then select and and group by .... etc.. ) so at the moment of the select parsing the alias are not available to the sql engine
As several other people have stated, you cannot use aliases in your select statement. However, to keep it cleaner, you could combine both conditions rather than summing both SUM fields.
select
DAY(created_at) AS day,
SUM(if(status = '1', 1, 0)) AS result,
SUM(if(status = '2', 1, 0)) AS noresult,
SUM(if(status = '1' OR status = '2', 1, 0)) AS newcolumn
from `clients` where `doctor_id` = 2 and MONTH(created_at) = MONTH(CURRENT_TIMESTAMP) group by `day`

MySQL - conditional select in group

I have a simple table with several rows and I would like to group them by id_room and select value only when condition is true. Problem is that condition is always false even there is a row with right date column year_month.
Here is schema:
CREATE TABLE tbl_account_room (
`id` int,
`year_month` date,
`value` int,
`id_room` int
);
INSERT INTO tbl_account_room
(`id`, `year_month`, `value`, `id_room`)
VALUES
(1, '2016-08-01', 1, 300),
(2, '2016-09-01', 2, 300),
(3, '2016-10-01', 3, 300);
and here query:
SELECT
(case when '2016-10-01' = ar.year_month then ar.value else 0 end) as total
FROM tbl_account_room AS ar
WHERE ar.year_month >= "2016-08-01"
AND ar.year_month <= "2016-11-01"
and ar.id_room = '300'
GROUP BY ar.id_room
LIMIT 10
Here is SQL Fiddle
In column total I'm getting 0 and I would like to get value 3 because year_month is 2016-10-01. Why this happens?
You certainly don't need that CASE condition and include that condition in your WHERE clause rather like
SELECT
ar.value as total,
GROUP_CONCAT(ar.year_month)
FROM tbl_account_room AS ar
WHERE ar.year_month = '2016-10-01'
GROUP BY ar.id_room;
Not sure why you want the result like that, here you can use self join to do that:
SELECT
MAX(t1.value) as total,
GROUP_CONCAT(ar.year_month)
FROM tbl_account_room AS ar
LEFT JOIN tbl_account_room AS t1
ON ar.id_room = t1.id_room
AND ar.year_month = t1.year_month
AND t1.year_month = '2016-10-01'
WHERE ar.year_month >= "2016-08-01"
AND ar.year_month <= "2016-11-01"
and ar.id_room = '300'
GROUP BY ar.id_room
LIMIT 10;
and here is SQLFiddle Demo.