Aggregate values under multiple conditions - mysql

Given the following tables
+-----------+-------------+----------+
| tours | tour_user | tag_tour |
+-----------+-------------+----------+
| id | tour_id | tag_id |
| startdate | user_id | tour_id |
+-----------+-------------+----------+
I want to achieve a result set like this:
+-----------------+----------------+----------------+
| DATE(startdate) | COUNT(user_id) | COUNT(tour_id) |
+-----------------+----------------+----------------+
| 2017-12-01 | 55 | 32 |
+-----------------+----------------+----------------+
Described in words the amount of users paticipated on a tour and the amount of tours should be aggregated by days.
Additionaly the count of tour and user participation should be filterable via tags which are attachted to tours via tag_tour table (many-to-may-relation).
E.g. I want only the tour and user count of tours which have tag_id 1 AND 2 attaches.
Currently I go with this Query:
SELECT DATE(tours.start) AS acum_date,
COUNT(tour_user.user_id) AS guide_assignments,
A.tour_count
FROM `tour_user`
LEFT JOIN `tours` ON `tours`.`id` = `tour_user`.`tour_id`
LEFT JOIN
(SELECT DATE(tours.start) AS tour_date,
COUNT(DISTINCT tours.id) AS tour_count
FROM tours
GROUP BY DATE(tours.start)) AS A ON `A`.`tour_date` = DATE(tours.start)
GROUP BY `acum_date`
ORDER BY `acum_date` ASC
The problem with this is, that only the total tour/user count is returned and not the filtered one.

The base query is:
select t.startdate, count(tu.user_id) as num_users, count(distinct t.id) as num_tours
from tours t left join
tour_user tu
on tu.tour_id = t.id
group by t.startdate;
In this case, I would recommend using exists for filtering the tags:
select t.startdate, count(tu.user_id) as num_users, count(distinct t.id) as num_tours
from tours t left join
tour_user tu
on tu.tour_id = t.id
where exists (select 1 from tour_tags tt where tt.tour_id = t.tid and tt.tag_id = <tag1>) and
exists (select 1 from tour_tags tt where tt.tour_id = t.tid and tt.tag_id = <tag2>)
group by t.startdate;

Related

Set Alias in SELECT with Inner Join

I have a 2 tables:
People table:
id | name | date
1 | Mika | 2013
2 | Rose | 2015
Work table:
id | user_id | work_name | registers
1 | 1 | rugby | 10
2 | 1 | golf | 3
I use this query to join tables:
SELECT * FROM work INNER JOIN people ON work.user_id = people.id WHERE work_name= :work_name
This work it, but I get duplicate column ID and in php when I try to print the camp id, this show the last column id..
I try with this but dont work it:
SELECT *, id AS 'work_id'
FROM work
INNER JOIN people ON work.user_id = people.id
WHERE work_name= :work_name
That is because you are using * in the SELECT that mean will bring all the fields from boths tables instead you can use
SELECT work.id as work_id,
work.name as work_name,
work.date as work_date,
people.id as people_id,
people.name as people_name,
people.date as people_date
Try to manually list all of columns you need to display, for instance
SELECT w.user_id, w.work_name, w.registers, p.name, p.date FROM work as w INNER JOIN people as p ON work.user_id = people.id WHERE work_name= :work_name

MySql query code to fetch unique data from single ID

How do i List the CUSTNUMs and NAMES of any customer who has only ordered chemical [NUMBER].
ORDERS TABLE
+---------+--------+------------+------+
| CUSTNUM | CHEMNO | DATE | QTY |
+---------+--------+------------+------+
| 123456 | 1234 | 2000-00-00 | 35 |
+---------+--------+------------+------+
CUSTOMER TABLE
+---------+-----------+-----------+
| CUSTNUM | NAME | LOCATION |
+---------+-----------+-----------+
| 123456 | AmChem | New York |
+---------+-----------+-----------+
You could join the CUSTOMER and ORDERS tables containing orders for a particular <chemno> with a subquery for the custnum that buy only a product:
SELECT
CUSTNUM, NAME
FROM
CUSTOMER c
INNER JOIN
ORDERS o ON o.CUSTNUM = c.CUSTNUM and o.CHEMNO = <chemno>
INNER JOIN
( SELECT
CUSTNUM
FROM
ORDERS
GROUP BY
CUSTNUM
HAVING
COUNT(DISTINCT CHEMNO) = 1 ) t ON t.CUSTNUM = o.CUSTNUM
I will approach this with one join between both tables, then grouping by the column CUSTNUM of the ORDERS table and finally adding the required conditions on the HAVING clause, like this:
SELECT
o.CUSTNUM,
c.NAME
FROM
ORDERS AS o
INNER JOIN
CUSTOMER AS c ON c.CUSTNUM = o.CUSTNUM
GROUP BY
o.CUSTNUM
HAVING
( COUNT(DISTINCT o.CHEMNO) = 1 AND MIN(o.CHEMNO) = <some_chemno> )
OK, slow day...
SELECT DISTINCT x.custnum
FROM orders x
LEFT
JOIN orders y
ON y.custnum = x.custnum
AND y.chemno <> x.chemno
WHERE x.chemno = 9377
AND y.order_id IS NULL;
The rest of this task has been left as an exercise for the reader

MYSQL - Group Contact rows with records NOT IN

My case looks simple but i'm messing around with this..
I have 4 tables: User, Macros, Categories, and another one that relate users with categories. One Macro have many Categories.
What i need, is a query that based on the Macro, get the users and the Categories where user is NOT IN.
Example: I have a macro named VEICULES, with categories CAR,TRUCK and Motorcycle. User José is on category CAR and User Julio on category CAR and TRUCK, so my query should return:
José | TRUCK,Motorcycle
Julio | Motorcycle
Tables:
prd_users
id | name | Email
---------------------------
1 | José | jose#email.com
2 | Júlio | julio#email.com
3 | André | andre#email.com
cat_macros
macro_id | macro_name
-----------------------
1 | Veicules |
cat_categories
category_id | category_name | macro_id
---------------------------------------
1 | Cars | 1
2 | Trucks | 1
3 | Motorcycles | 1
prd_tr_rabbit_catg
id | category_id | tasker_user_id
---------------------------------------
1 | 1 | 1
2 | 1 | 2
3 | 2 | 2
I'm stucked on just getting the categories where the user already is ..
SELECT prd_users.id, prd_users.name,
prd_users.email,cat_macros.macro_name as macro,
GROUP_CONCAT(cat_categories.category_name SEPARATOR ', ') as in_categories
FROM prd_users
INNER JOIN prd_tr_rabbit_catg ON prd_tr_rabbit_catg.tasker_user_id = prd_users.id
INNER JOIN cat_categories ON cat_categories.category_id = prd_tr_rabbit_catg.category_id
INNER JOIN cat_macros ON cat_macros.macro_id = cat_categories.macro_id
WHERE cat_macros.macro_id = '45'
GROUP BY prd_users.id;
To solve this problem it's necessary to create a list of all users joined with all categories for the given macro category. This can be done with a CROSS JOIN:
SELECT *
FROM prd_users u
CROSS JOIN (SELECT m.macro_id, m.macro_name, c.category_name, c.category_id
FROM cat_macros m
JOIN cat_categories c ON c.macro_id = m.macro_id) c
This can then be LEFT JOINed to the prd_tr_rabbit_catg table and by selecting those rows where there is no matching entry in the prd_tr_rabbit_catg table, we can find the users who don't have an entry for the given category:
SELECT c.macro_name, u.id AS user_id, u.name, u.Email, GROUP_CONCAT(c.category_name) AS missing_cats
FROM prd_users u
CROSS JOIN (SELECT m.macro_id, m.macro_name, c.category_name, c.category_id
FROM cat_macros m
JOIN cat_categories c ON c.macro_id = m.macro_id) c
LEFT JOIN prd_tr_rabbit_catg x ON x.tasker_user_id = u.id AND x.category_id = c.category_id
WHERE x.id IS NULL
AND c.macro_id = 1
GROUP BY c.macro_name, u.id
For your sample data, this gives:
macro_name user_id name Email missing_cats
Veicules 1 José jose#email.com Motorcycles,Trucks
Veicules 2 Júlio julio#email.com Motorcycles
Veicules 3 André andre#email.com Cars,Motorcycles,Trucks
Update
To exclude users who don't have any of the categories, add a HAVING clause:
HAVING COUNT(*) < (SELECT COUNT(*) FROM cat_categories WHERE macro_id = 1)
Demo on SQLFiddle

Get all rows from table - with the latest row from another table, with another table based on the latest row

I need to get all the details from the orders table, with the latest status ID in the orders statuses table, and then the name of that status from the states table.
orders
id | customer | product
-----------------------
1 | David | Cardboard Box
Order_to_statuses
id | order | status | updated_at
--------------------------------
1 | 1 | 1 | 2017-05-30 00:00:00
2 | 1 | 3 | 2017-05-28 00:00:00
3 | 1 | 4 | 2017-05-29 00:00:00
4 | 1 | 2 | 2017-05-26 00:00:00
5 | 1 | 5 | 2017-05-05 00:00:00
order_states
id | name
---------
1 | Pending
2 | Paid
3 | Shipped
4 | Refunded
In this instance, I would need to get the customer and product, with the latest status ID from the order statuses table, and then the name of that state.
How can I do this?
I'd break this down by first getting the max(updated_at) for each order, then work to everything else you need. You can get the max date for each order by using subquery:
select
s.`order`,
s.`status`,
s.updated_at
from order_to_statuses s
inner join
(
select
`order`,
max(updated_at) as updated_at
from order_to_statuses
group by `order`
) m
on s.`order` = m.`order`
and s.updated_at = m.updated_at
Once you get this you now have the order, the status id, and the most recent date. Using this you can then JOIN to the other tables, making your full query:
select
o.customer,
o.product,
ots.updated_at,
os.name
from orders o
inner join
(
select
s.`order`,
s.`status`,
s.updated_at
from order_to_statuses s
inner join
(
select
`order`,
max(updated_at) as updated_at
from order_to_statuses
group by `order`
) m
on s.`order` = m.`order`
and s.updated_at = m.updated_at
) ots
on o.Id = ots.`order`
inner join order_states os
on ots.`status` = os.id;
See a demo
It may have some typo, but the idea of the query should be something like this:
select orders.id, orders.customer, orders.product,
order_to_status.status, staus.name
from orders, order_to_status, status
where orders.id = order_to_status.order
and order_to_status.status = status.id
and order_to_status.updated_at in (
SELECT MAX(order_to_status.updated_at)
FROM order_to_status
where order_to_status.order = orders.id
group by order_to_status.order
);
I ussually don't use joins but with joins it should be like this:
select orders.id, orders.customer, orders.product,
order_to_status.status, staus.name
from orders
JOIN order_to_status ON orders.id = order_to_status.order
JOIN status ON order_to_status.status = status.id
where
order_to_status.updated_at in (
SELECT MAX(order_to_status.updated_at)
FROM order_to_status
where order_to_status.order = orders.id
group by order_to_status.order
);
Note I added a group by I had missed.
EDIT 2
I had an error in the subquery condition.
changed to where order_to_status.order = orders.id
also moved the group by after the where clause.

Joining Max Row where Col

I have this query:
SELECT
`shift`.`uid`,
`shift`.`activity`,
`users`.`fname`,
`users`.`lname`
FROM `shift`, `users`
WHERE `shift`.`uid` = `users`.`id`
It works fine just like that, but I need to add a new column from another table and order by it.
times :
| uid | User | time |
+++++++++++++++++++++
| 3 | bob | 1231 |
| 3 | bob | 1291 |
| 4 | ned | 1651 |
| 5 | ted | 5679 |
| 6 | joe | 7665 |
| 6 | joe | 7864 |
How can I include the maximum time from the time table for each user (WHERE times.uid = shift.uid) and then order by that column?
Trouble is, all the other tables have one row per user but the time table has multiple and I can't figure out the correct combination of joins and group by.
You could join on an aggregate query:
SELECT `shift`.`uid`,
`shift`.`activity`,
`users`.`fname`,
`users`.`lname` ,
t.max_time
FROM `shift`
JOIN `users` ON `shift`.`uid` = `users`.`id`
JOIN (SELECT `uid`, MAX(`time`) AS max_time
FROM `times`
GROUP BY `uid`) t ON shift.uid = t.uid
ORDER BY t.max_time
A pretty simple way to approach this is using a correlated subquery:
SELECT s.`uid`, s.`activity`, u.`fname`, u.`lname`,
(SELECT MAX(tt.time)
FROM timetable tt
WHERE tt.uid = u.id
) as maxtime
FROM `shift` s JOIN
`users` u
ON s.`uid` = u.`id`;
The advantage of this approach is performance. With an index on timetable(uid, time), this should work better than doing an aggregation at the outer level (because the query will take advantage of the index).
SELECT s.uid,
s.activity,
u.fname,
u.lname,
MAX(t.time) as maxtime
FROM shift s,
INNER JOIN users u ON u.id = s.uid
INNER JOIN times t ON t.uid = u.id
GROUP BY s.uid,
s.activity,
u.fname,
u.lname
ORDER BY maxtime