Mysql nested left joins with counts - mysql

I have a set of MySQL three tables in a "has many" relationship: deals, orders, and coupons.
Deals
|----|--------------|
| id | title |
|----|--------------|
| 1 | Some deal |
| 2 | Another deal |
|----|--------------|
Orders
|----|---------|-----------|
| id | deal_id | state |
|----|---------|-----------|
| 1 | 1 | purchased |
| 2 | 1 | purchased |
| 3 | 1 | expired |
| 4 | 2 | purchased |
|----|---------|-----------|
Coupons
|----|----------|
| id | order_id |
|----|----------|
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 2 |
| 5 | 2 |
| 6 | 4 |
|----|----------|
So, deals have many orders, which have many coupons.
What I'd like to do is select on the deals table while counting the number of purchased orders and coupons.
I know how to get a count on paid orders already:
SELECT deals.*, count(orders.id) AS orders_purchased_count FROM deals
LEFT JOIN orders ON deals.id=orders.deal_id AND orders.state='purchased'
WHERE deal_id < 3
GROUP BY deals.id
Deals
|----|--------------|------------------------|
| id | title | orders_purchased_count |
|----|--------------|------------------------|
| 1 | Some deal | 2 |
| 2 | Another deal | 1 |
|----|--------------|------------------------|
Similarly, I can get a count of coupons for orders:
SELECT orders.*, count(coupons.id) AS coupons_count FROM orders
LEFT JOIN coupons ON orders.id=couoons.orders_id
WHERE orders.state='purchased'
GROUP BY orders.id
Orders
|----|-----------|---------------|
| id | state | coupons_count |
|----|-----------|---------------|
| 1 | purchased | 3 |
| 2 | purchased | 2 |
| 4 | purchased | 1 |
|----|-----------|---------------|
My question is: How do I combine these so that I can add coupons_count next to orders_purchased_count?
Deals
|----|--------------|------------------------|---------------|
| id | title | orders_purchased_count | coupons_count |
|----|--------------|------------------------|---------------|
| 1 | Some deal | 2 | 5 |
| 2 | Another deal | 1 | 1 |
|----|--------------|------------------------|---------------|
The tricky thing, in my case, will be to run the WHERE deal_id < 3 filter when selecting from deals before I join on orders and to run the WHERE orders.state='purchased' filter when selecting from orders before I join on coupons. It's a large dataset and I don't want to load all my orders and coupons into memory for the purpose of joining.
At a loss for how to do this.

Does this work? I couldn't understand your concerns which part was going to be tricky.
SELECT deals.*, COUNT(DISTINCT(orders.id)) AS orders_purchased_count, COUNT(coupons.id) AS coupons_count
FROM deals
LEFT JOIN orders
ON deals.id=orders.deal_id
AND orders.state='purchased'
LEFT JOIN coupons
ON orders.id=coupons.orders_id
WHERE deal_id < 3
GROUP BY deals.id;

Try this one with a co related subquery and join
SELECT deals.*, count(orders.id) AS orders_purchased_count
(
SELECT count(id) FROM coupons WHERE orders_id = orders.id
) AS coupons_count
FROM deals
LEFT JOIN orders ON deals.id=orders.deal_id AND orders.state='purchased'
WHERE deal_id < 3
GROUP BY deals.id

Related

query to get customer list using JOIN with sum () of the amounts spent in orders

I have the following tables
table anag (customer registry)
id | surname | name | phone |
----------------------------------------------
1 | Brown | Jack | +3989265781 |
2 | Smith | Bill | +3954872358 |
3 | Rogers | Stan | +3912568453 |
4 | Pickford | Eric | +3948521358 |
----------------------------------------------
table levels (table that connects each customer to his salesperson. For database registration reasons, the link between customer and seller is given by the customer's telephone number)
id | client_phone | id_seller |
--------------------------------------
1 | +3989265781 | 4 |
2 | +3954872358 | 7 |
3 | +3912568453 | 7 |
4 | +3948521358 | 8 |
--------------------------------------
table orders (contains all purchases made by customers, of course)
id | id_client | id_item | id_seller | price | status |
--------------------------------------------------------------------
1 | 1 | 2 | 4 | 12.50 | 2 |
2 | 2 | 2 | 7 | 12.50 | 2 |
3 | 2 | 3 | 7 | 10.00 | 3 |
4 | 2 | 3 | 7 | 10.00 | 3 |
5 | 2 | 4 | 7 | 20.50 | 1 |
6 | 3 | 2 | 7 | 12.50 | 1 |
7 | 3 | 5 | 7 | 19.00 | 3 |
8 | 3 | 7 | 7 | 31.00 | 2 |
9 | 4 | 1 | 8 | 5.00 | 1 |
--------------------------------------------------------------------
What I'm trying to do is get from the JOIN of these tables a complete list by seller of his customers sorted in descending order by the amount spent on orders as long as the order status is 2 or 3
Something like this (example seller id 7):
id | surname | name | amaount |
----------------------------------------
3 | Rogers | Stan | 50.00 |
2 | Smith | Bill | 32.50 |
----------------------------------------
I have tried with this query which seems correct to me, but unfortunately it returns me error in fetch_assoc()
SELECT a.id, a.surname, a.name, o.amount FROM levels AS l
JOIN anag AS a ON a.phone = l.client_phone
JOIN {
SELECT id_client, SUM(price) AS amount FROM orders
WHERE id_seller = '7' AND (status = '2' OR status = '3') GROUP BY id_client
} AS o ON o.id_client = a.id
WHERE l.id_seller = '7'
ORDER BY o.amount DESC
If I separate the subquery from the main query, both return the data I expect and it seems strange to me the JOIN between the two does not work properly
I think the only real error is using curly braces instead of parentheses:
SELECT a.id, a.surname, a.name, o.amount
FROM levels l JOIN
anag a
ON a.phone = l.client_phone JOIN
(SELECT id_client, SUM(price) AS amount
FROM orders
WHERE id_seller = '7' AND status IN ('2', '3'))
GROUP BY id_client
) o
ON o.id_client = a.id
WHERE l.id_seller = '7'
ORDER BY o.amount DESC;
In addition:
You can use IN to shorten an equality comparison to multiple values.
Although I left them in, status and id_seller look like numbers. If so, drop the single quotes. Don't mix data types.
Your question is ambiguous on what to do if the seller in orders differs from the seller in anag for a customer. This keeps your logic (the sellers need to match).
SELECT a.id, a.surname, a.name, sum(o.price) 'amount'
FROM anag a
LEFT JOIN levels l ON l.id =a.id
LEFT JOIN orders of ON o.id_seller = l.id_seller AND o.id_client = l.id
GROUP BY o.id_seller
ORDER BY amount DESC

SQL query that finds the an id that is not associated with another id

I'm currently learning the ropes of SQL and i have an tutorial from school that goes like this:
All stores (storeid) sells (productid, storeid) some products (productid)  
A store is considered a monopoly if every product they sell is not sold by any other store.
How do I find the monopolies?
I was thinking of selecting the storeid from 2 of the same tables, but I'm not sure how to continue from there on.
Tables are below:
Store:
+-----------+
| storeid |
+-----------+
| --------- |
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
+-----------+
Products:
+-------------+
| productid |
+-------------+
| --------- |
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
+-------------+
Sells:
+--------------------------+
| productid | storeid |
+--------------------------+
| -----------+------------ |
| 1 | 1 |
| 2 | 1 |
| 2 | 2 |
| 3 | 2 |
| 1 | 2 |
| 3 | 3 |
| 2 | 4 |
| 4 | 4 |
| 5 | 5 |
| 6 | 5 |
+--------------------------+
So by my count, only store 5 is considered a monopoly, because they sell products that are not available in other stores.
We can try a self join approach combined with aggregation:
SELECT t1.storeid
FROM yourTable t1
LEFT JOIN yourTable t2
ON t2.productid = t1.productid AND
t2.store_id <> t1.storeid
GROUP BY t1.storeid
HAVING COUNT(t2.storeid) = 0;
The approach here is to try to match each row in Sells to some other row on the condition that it is the same product, but is being sold by some other store. A matching store is one for which none of its products are being sold by other stores, so the count of the second table column in the join should be zero.
Use window functions and aggregation:
select s.storeid
from (select s.*,
count(*) over (partition by productid) as num_stores
from sells s
) s
group by s.storeid
having max(num_stores) = 1;
This should be much faster than a self-join. It is also almost a direct translation of your question. The subquery counts the number of stores where each product is sold. The outer query selects stores where all products are sold in one store.

How do flatten or correctly group by a multiple join

My Structure
I have three (hypothetical) tables; Users, movies, sessions.
> SELECT * FROM users
+----+---------------------------+
| id | email |
+----+---------------------------+
| 5 | abcdefghijklmno#gmail.com |
+----+---------------------------+
> SELECT * FROM movies
+----+---------+---------+--------------+
| id | title | user_id | total_watches|
+----+---------+---------+--------------+
| 1 | X-men | 1 | 1 |
| 2 | Blade | 1 | 1 |
| 3 | Goonies | 1 | 1 |
+----+---------+---------+--------------+
> SELECT * FROM sessions
+----+---------+---------+------------+
| id | user_id | show_id | total_time |
+----+---------+---------+------------+
| 1 | 1 | 1 | 5 |
| 2 | 1 | 1 | 30 |
| 3 | 1 | 1 | 5 |
+----+---------+---------+------------+
What I want
I want to get an overview of a user's movie activity in one query, so would like to retrieve the data in the following format:
+----+---------------------------+---------------+----------------+
| id | email | total_time | total_watches |
+----+---------------------------+---------------+----------------+
| 5 | abcdefghijklmno#gmail.com | 40 | 3 |
+----+---------------------------+---------------+----------------+
What I've tried
SELECT users.id, users.email, SUM(movies.total_watches) AS total_watches, SUM(sessions.total_time) AS total_time
FROM users
JOIN movies ON users.id = movies.user_id
JOIN sessions ON users.id = sessions.user_id
GROUP BY users.id
This returns (minus a couple of columns):
+------------------+---------------+---------------+
| email | total_watches | total_time |
+------------------+---------------+---------------+
| abcdef#gmail.com | 9 | 120 |
+------------------+---------------+---------------+
Summary
I understand that the extra session join creates three rows for every movie and therefore trebles the SUM results, so how do I get the 'flattened' data? I have tried other group by combinations with no luck.
As suggested in comments above, your current table structure requires further normalization.
Now, for this table structure, one hacky way is to divide the SUM by Count of rows from the other table (causing duplication due to JOIN), to counter the effect of duplication.
So, the SUM of total_watches can be divided by the Count of rows from the sessions table for a user id. Similarly, the SUM of total_time can be divided by the Count of rows from the movies tables.
SELECT users.id,
SUM(movies.total_watches)/COUNT(DISTINCT sessions.id) AS total_watches,
SUM(sessions.total_time)/COUNT(DISTINCT movies.id) AS total_time
FROM users
JOIN movies ON users.id = movies.user_id
JOIN sessions ON users.id = sessions.user_id
GROUP BY users.id
Result
| id | total_watches | total_time |
| --- | ------------- | ---------- |
| 1 | 3 | 40 |
View on DB Fiddle

How to sum numbers through a relationship table?

I have a table of PRODUCTS
+------------+---------------+---------------+
| ProductCod | unitPrice | name |
+------------+---------------+---------------+
| 1 | 30 | some |
| 2 | 20 | poor |
| 3 | 10 | example |
+------------+---------------+---------------+
Another of SALES (which I believe it's not needed) and some other to register a n..m relationship
+------------+----------+------------+
| quantity | sellCode | productCod |
+------------+----------+------------+
| 3 | 1 | 1 |
| 5 | 1 | 2 |
| 4 | 2 | 2 |
| 4 | 2 | 3 |
| 4 | 2 | 3 |
+------------+----------+------------+
How can I select a list of products and how many were sold at all registers?
I would like something like:
+---------+------+
| name | sold |
+---------+------+
| some | 3 |
| poor | 9 |
| example | 8 |
+---------+------+
Using a INNER JOIN is more exact here.
Query
SELECT
products.name
, SUM(sales.quantity) AS sold
FROM
products
INNER JOIN
sales
USING(productCod)
GROUP BY
products.name
ORDER BY
products.ProductCod ASC
Or
SELECT
products.name
, SUM(sales.quantity) AS sold
FROM
products
INNER JOIN
sales
ON
products.productCod = sales.productCod
GROUP BY
products.name
ORDER BY
products.ProductCod ASC
Results
| name | sold |
|---------|------|
| some | 3 |
| poor | 9 |
| example | 8 |
see demo http://sqlfiddle.com/#!9/10cd3e/5
select p.Name, sum(s.Quantity) as sold
from Products p
left join Sales s on p.ProductCod = s.ProductCod
group by p.ProductCod, p.Name;
What we are doing is first to join two tables using productCod common field. We are using LEFT join, because there might be products that are not sold at all yet. Then we sum the quantities grouping by productCod (and Name. We had to include it in the list because it is not an aggregation expression - and there is a single Name per productCod). This works right, because there is a 1 to many relation between products and sales. If there were a many to many relation then the result would be wrong.
EDIT: Check this SQLFiddle sample for a good formatting.

Using left join and inner join to get data from three tables

I have three tables.
managersTbl
ID | NAME |
1 | Ana |
2 | Elsa |
3 | Olaf |
4 | Belle|
gigsTbl
ID | GIG | EARNING | MANAGER | ARTIST |
1 | sing | 500 | 2 | 1 |
2 | act | 100 | 2 | 3 |
3 | modelling | 250 | 3 | 4 |
4 | dance | 10 | 1 | 1 |
artistsTbl
ID | NAME |
1 | Haley |
2 | Aw |
3 | Fire |
4 | Finn |
What I want to accomplish is get all the managers, and get the sum of their gigs earning.
SELECT id, name
ifnull(b.earning, 0) AS earning
FROM doctorsTbl
LEFT JOIN(
SELECT id, name
SUM(earning) AS earning
FROM gigsTbl
GROUP BY manager_id
) b on (a.id = b.manager_id)
I have already achieved getting all the managers, and the sum of their gigs earnings with the above query. Now, I want to get all the artists, without getting repeated, from the gigsTbl grouped by their manager.
I've tried using inner join like this
SELECT id, name
ifnull(b.earning, 0) AS earning
FROM doctorsTbl
LEFT JOIN(
SELECT id, name
SUM(earning) AS earning
FROM gigsTbl
INNER JOIN artistsTbl d
on b.artist = d.id
GROUP BY manager_id
) b on (a.id = b.manager_id)
I want to do this without creating a new query for the artists because I'd like to be able to search the tables with using the artist identification.