Mysql left join is not working as expected - mysql

3 tables.
table_customers - customer_id, name
table_orders - order_id, customer_id, order_datetime
table_wallet - customer_id, amount, type // type 1- credit, type 2- debit
Need to get all customers, their total balance, and their last order date and order id. If customer have not placed any return order date as 0000-00-00 and order id as 0.
This is my query.
SELECT
C.customer_id,
C.name,
COALESCE( SUM(CASE WHEN type = 2 THEN -W.amount ELSE W.amount END), 0) AS value,
COALESCE( max( O.order_id ) , '0' ) AS last_order_id,
COALESCE( max( date( O.order_datetime ) ) , '0000-00-00' ) AS last_order_date
FROM
table_customers as C
LEFT JOIN
table_wallet as W
ON C.customer_id = W.customer_id
LEFT JOIN
table_orders AS O
ON W.customer_id = O.customer_id
group by C.customer_id
ORDER BY C.customer_id
Everything is coming correct except customer's value. From result it seems its getting added multiple times.
I have created the fiddle here. http://sqlfiddle.com/#!9/560f2/1
What is wrong in query? Can anyone help me on this?
Edit: Expected result
customer_id name value last_order_id last_order_date
1 abc 20 3 2016-06-22
2 def 112.55 0 0000-00-00
3 pqrs 0 4 2016-06-15
4 wxyz 0 0 0000-00-00

The issue is that the join between orders and wallet will produce as many rows as there as orders for each wallet, when you really just want one row per wallet from the order table (since you only use the max values). In your test case you get 3 rows for customer 1 which makes the sum 60 (3*20).
One way to solve this is to change to this:
SELECT
C.customer_id,
C.name,
COALESCE( SUM(CASE WHEN type = 2 THEN -W.amount ELSE W.amount END), 0) AS value,
COALESCE( O.order_id , '0' ) AS last_order_id,
COALESCE( DATE( O.order_datetime ) , '0000-00-00' ) AS last_order_date
FROM table_customers AS C
LEFT JOIN table_wallet AS W ON C.customer_id = W.customer_id
LEFT JOIN (
SELECT
customer_id,
MAX(order_id) AS order_id,
MAX(order_datetime) AS order_datetime
FROM table_orders
GROUP BY customer_id
) AS O ON c.customer_id = O.customer_id
GROUP BY C.customer_id
ORDER BY C.customer_id
As you see the orders table is replaced by a derived table that gets you one row per customer.
Running the query above gets you the following result:
| customer_id | name | value | last_order_id | last_order_date |
|-------------|------|--------|---------------|-----------------|
| 1 | abc | 20 | 3 | 2016-06-22 |
| 2 | def | 112.55 | 0 | 0000-00-00 |
| 3 | pqrs | 0 | 4 | 2016-06-15 |
| 4 | wxyz | 0 | 0 | 0000-00-00 |

To further illustrate from the previous answers, if we simply remove your group by statement, you can easily see why you are double counting. The following code:
SELECT
C.*,
O.order_id, O.order_datetime,
W.amount, W.type
FROM
table_customers as C
LEFT JOIN
table_wallet as W
ON C.customer_id = W.customer_id
LEFT JOIN
table_orders AS O
ON W.customer_id = O.customer_id
Will yield the result:
customer_id name order_id order_datetime amount type
1 abc 1 April, 22 2016 23:53:09 20 1
1 abc 2 May, 22 2016 23:53:09 20 1
1 abc 3 June, 22 2016 23:53:09 20 1
2 def (null) (null) 100 1
2 def (null) (null) 12.55 1
3 pqrs (null) (null) (null) (null)
4 wxyz (null) (null) (null) (null)
Note the duplication of Customer ID 1 with amount 20.

This is the classic combinatorial explosion problem when you JOIN tables containing unrelated data.
You need to compute each customer's balance in a subquery. That subquery must yield either one row or zero rows per customer_id. It might look like this. (http://sqlfiddle.com/#!9/560f2/8/0)
SELECT customer_id,
SUM(CASE WHEN type = 2 THEN -amount ELSE amount END) AS value
FROM table_wallet
GROUP BY customer_id
Similarly, you need to retrieve each customer's latest order in a subquery (http://sqlfiddle.com/#!9/560f2/10/0) . Again, it needs either one row or zero rows per customer_id.
SELECT customer_id,
MAX(order_id) AS order_id,
DATE(MAX(order_datetime)) AS order_date
FROM table_orders
GROUP BY customer_id
Then, you can LEFT JOIN those two subqueries as if they were tables, to your table_customers. The subqueries are tables; they're virtual tables. (http://sqlfiddle.com/#!9/560f2/12/0)
SELECT c.customer_id,
c.name,
w.value,
o.order_id,
o.order_date
FROM table_customers c
LEFT JOIN (
SELECT customer_id,
SUM(CASE WHEN type = 2 THEN -amount ELSE amount END) AS value
FROM table_wallet
GROUP BY customer_id
) w ON c.customer_id = w.customer_id
LEFT JOIN (
SELECT customer_id,
MAX(order_id) AS order_id,
DATE(MAX(order_datetime)) AS order_date
FROM table_orders
GROUP BY customer_id
) o ON c.customer_id = o.customer_id
Your mistake was this: you joined two tables each with multiple rows for each customer id. For example, a particular customer might have had two orders and three wallet rows. Then, the join results in six rows representing all the possible combinations of wallet and order rows. That's called combinatorial explosion.
The solution I outlined makes sure there's only one row (or maybe no rows) to join for each customer_id, and so eliminates the combinatorial explosion.
Pro tip: Using subqueries like this makes it easy to test your query: you can test each subquery separately.

Related

How can I retrieve Similar Orders In Mysql?

i need a query that should first look the oldest order which has status 0 (zero). and retrieves all the similar orders of that kind(matches exact total qty, itemSku and number of distinct items ordered).
***OrdersTable***
ID OrderNumber CustomerId Status created_at
1 123456 1 0 2018-01-01
2 234567 1 0 2018-01-02
3 345678 1 0 2018-01-03
4 456789 1 0 2018-01-04
***PurchasedProductsTable***
OrderId itemSku Qty
1 1000001 1
1 1000002 2
2 1000001 3
3 1000001 1
3 1000002 2
4 1000001 3
In the above table the query should first look at the oldest (created_at ASC) order (i.e with Id 1) having status 0 (in order table). and along with that order it should retrieves all the other orders that matches the same itemSku, qty and total distinct items count (in purchasedProducts table).
here order 1 and 3 matches the same itemSKu (1000001 and 1000002) and qty ( 1 and 2) and both have (2) distinct items count respectively so order 1 and 3 should be retrived at first.and when i marked order 1 and 3 as shipped (i.e chang status to 2).
and if i run query again it should retrive similar oders. now order 2 and 4 as order 2 and 4 are similar orders. (have same itemSkus (1000001, Qty (3) and distinct items count (1)).
please help thanks
You have to go trough your tables two times :)
Something like this :
SELECT DISTINCT O2.ID
FROM OrdersTable O1
INNER JOIN PurchasedProductsTable P1 ON O1.ID = P1.OrderId
INNER JOIN PurchasedProductsTable P2 ON P1.itemSku = P2.itemSku
AND P1.Qty = P2.Qty
INNER JOIN OrdersTable O2 ON O2.ID = P2.OrderId
WHERE O1.ID =
(SELECT ID FROM OrdersTable WHERE Status = 0
ORDER BY created_at ASC LIMIT 1)
AND (SELECT COUNT(*) FROM PurchasedProductsTable WHERE OrderId = O1.ID)
= (SELECT COUNT(*) FROM PurchasedProductsTable WHERE OrderId = O2.ID)
ORDER BY O2.ID ASC;
https://www.db-fiddle.com/f/65t9GgSfqMpzNVgnrJp2TR/2
You can get the earliest order via a limit and ordered by the date.
Then you can left join to get that order and any other order that at least has the same items.
Then once you have those order id's from the sub-query result, you can get the order details.
SELECT o.*
FROM
(
SELECT DISTINCT ord2.ID as OrderId
FROM
(
SELECT ID, CustomerId, Status
FROM OrdersTable
WHERE Status = 0
ORDER BY created_at
LIMIT 1
) AS ord1
JOIN PurchasedProductsTable AS pprod1
ON pprod1.OrderId = ord1.ID
LEFT JOIN OrdersTable ord2
ON ord2.CustomerId = ord1.CustomerId
AND ord2.Status = ord1.Status
LEFT JOIN PurchasedProductsTable pprod2
ON pprod2.OrderId = ord2.ID
AND pprod2.itemSku = pprod1.itemSku
AND pprod2.Qty = pprod1.Qty
GROUP BY ord1.CustomerId, ord1.ID, ord2.ID
HAVING COUNT(pprod1.itemSku) = COUNT(pprod2.itemSku)
) q
JOIN OrdersTable AS o ON o.ID = q.OrderId;
Test on RexTester here

group by error : this is incompatible with sql_mode=only_full_group_by

i have two tables order and order_detail
order
id | total | date
------------------------
1 3500 2018-02-10
2 1000 2018-02-18
order_detail
id | order_id | item_id | quantity
-------------------------------------------------
1 1 4 20
2 1 6 10
3 2 3 50
i am trying to achive
orderCount | itemCount | totalAmount
----------------------------------------
2 80 4500
i have written this query for extracting last 30day summary:
select COUNT(*) as orderCount
, (select SUM(od.quantity)
from order_detail od
where od.order_id = o.id
) as itemCount
, SUM(o.total) as totalSum
from order o
WHERE DATE_SUB(CURDATE(),INTERVAL 30 DAY) <= o.date
this query works fine in local mysql database
but in production server it is generating following error:
#1140 - In aggregated query without GROUP BY, expression #2 of SELECT list contains nonaggregated column 'databaseb.o.id'; this is incompatible with sql_mode=only_full_group_by
The 2nd column:
(select SUM(od.quantity)
from order_detail od
where od.order_id = o.id
) as itemCount
Is not an aggregated value; it's the result of a subquery (which happens to produce aggregated value, but that is irrelevant).
You must add:
group by 2
To your query to conform with sql_mode=only_full_group_by.
Obviously your local database does not have sql_mode=only_full_group_by set.
I suspect you just want a join:
select COUNT(*) as orderCount
, SUM(od.quantity) as itemCount
, SUM(o.total) as totalSum
from order o
left join order_detail od on od.order_id = o.id
WHERE DATE_SUB(CURDATE(),INTERVAL 30 DAY) <= o.date
To achieve your desire output you can simple use below query
select sum(od.quantity) as itemCount,
sum(o.total) as totalAmount,
count(distinct(o.id)) as orderCount
from order_detail od
left join `order` o
on od.id=o.id ;

Mysql query select customers with no orders in each year

i have 2 table's one with customers and one with orders
SELECT customers.customer_name, orders.order_date
FROM customers
Left
Join orders on (customers.customer_id = orders.customer_id)
WHERE not orders.customer_id IN (SELECT customer_id from orders where Year(order_date) = Year(#Parameter1))
and not orders.order_date is null
this works but i want to do this for each year to get somthing like this as result
|Year | customer_id |
|2010 | 1 |
|2010 | 2 |
|2011 | 2 |
|2011 | 3 |
|2012 | 1 |
You want a list showing customers and years that are not present in the orders table. So get a list of all customers combined with all years and then subtract the customers and years that you find in the orders table:
select o.yr, c.customer_id
from customers c
cross join (select distinct year(order_date) as yr from orders) o
where (c.customer_id, o.yr) not in (select customer_id, year(order_date) from orders);
Scorpio is sort of right, you do have to use the year function:
SELECT Year(orders.order_date), customers.customer_id
FROM customers
LEFT JOIN orders
ON (customers.customer_id = orders.customer_id)
WHERE NOT orders.customer_id
IN (
SELECT customer_id
FROM orders
WHERE Year(order_date) = Year(#Parameter1)
)
AND
NOT orders.order_date is NULL
You can use the year method in the SELECT section of your query
Update:
To display all the customers who didn't order in a year, you don't need the join. You can do it with a single subselect:
SELECT #Parameter1 AS Year, customer_id
FROM customers
WHERE customers.customer_id NOT IN (
SELECT customer_id
FROM orders
WHERE Year(order_date) = Year(#Parameter1)
)

MySQL includes a specific row with order by

Given 2 tables, I want to generate top 3 highest amount from [Purchase] table.
Additional criteria is [Crocs] must be included in top 3 of the records.
I have following SQL, but it cannot generates the result as I wanted (Result A), please guide me on how to pull out the result in Result B. Thank you.
Table (Purchase):
Purchase_ID | StoreID | Amount
------------|---------|--------
1 | 21 | 22
2 | 23 | 13
3 | 25 | 6
4 | 26 | 23
5 | 28 | 18
Table (Store):
Store_ID | StoreName
---------|----------
21 | Adidas
22 | Nike
23 | Puma
24 | New Balance
25 | Crocs
26 | Converse
SQL:
SELECT IF(SUM(amount) IS NULL, 0, SUM(amount)) as totalAmount
FROM (
SELECT a.amount
FROM purchase a
INNER JOIN store b
ON a.store_id = b.storeid
GROUP BY a.amount
HAVING b.StoreName = 'Crocs'
ORDER BY a.amount DESC
LIMIT 3
) t
Result A: $6
Explanation A: Amount of Crocs is $6
Result B: $51
Explanation B: Total Amount of top 3 = $22 (Adidas) + 23 (Puma) + $6 (Crocs)
The answer from scaisEdge is almost right, but the first query could also return a row with crocs and the sorting is wrong (order by max(a.amount) limit 2 means that the lowest 2 results will be shown). Additionally you could wrap the query in another select query to sort the results
SELECT * FROM (
SELECT b.storename, max(a.amount) as maxAmount
FROM purchase a
INNER JOIN store b ON a.store_id = b.storeid
WHERE b.storename != 'crocks'
GROUP BY a.storename
ORDER BY max(a.amount) DESC
LIMIT 2
UNION
SELECT b.storename, a.amount as maxAmount
FROM purchase a
INNER JOIN store b
ON a.store_id = b.storeid
WHERE b.storename='crocks'
ORDER BY a.amount DESC
LIMIT 1
) ORDER BY maxAmount DESC
You could use an union
SELECT b.storename, max(a.amount)
FROM purchase a
INNER JOIN store b
ON a.store_id = b.storeid
GROUP BY a.storename
order by max(a.amount) limit 2
union
SELECT b.storename, a.amount
FROM purchase a
INNER JOIN store b
ON a.store_id = b.storeid
where b.storename='crocks'
try this one:
SELECT sum(amount)as sum_amount,a.store_id,storename,category from
(select amount,store_id from tbl_purchase) as a
inner JOIN
(select store_id,storename,category from tbl_store)as b on a.store_id = b.store_id where b.category = 'supermarket' GROUP BY category

Complicated sum() with group and order by date

There are two tables:
* ORDER
- id
- pay_type
* ORDER_PRICE
- order_id
- dt
- price
Order price can be changed, for example:
order_id | price | dt
1 | 100.3 | 2013-10-25
1 | 105.7 | 2013-10-28
2 | 207.4 | 2013-09-13
4 | 98.0 | 2013-10-03
I can select price history for any date like that:
SELECT
o.`id`,
(SELECT op.`price` FROM `order_price` op
WHERE op.`order_id`=o.`id` AND op.`dt` <= '2013-10-26'
ORDER BY op.`dt` DESC LIMIT 1) order_price
FROM `order` o
It gives me right prices for given date
order_id | price | dt
1 | 100.3 | 2013-10-25
2 | 207.4 | 2013-09-13
4 | 98.0 | 2013-10-03
But i need the sum of the second column (no matter what order number, only one number - 405.7 in this case).
Is there a solution for such a situation? There can be thousands of orders, so i think it will be wrong to sum records outside the mysql. Maybe it's the wrong all the way from the start and i need other structure? Thank you for your time and help.
I suspect the core query should look more like this...
SELECT o.order_id
, op.price
, op.dt
FROM orders o
JOIN order_price op
ON op.order_id = o.order_id
JOIN
( SELECT order_id, MAX(dt) max_dt FROM order_price WHERE dt < '2013-10-26' GROUP BY order_id ) x
ON x.order_id = op.order_id
AND x.max_dt = op.dt;
...which can be rewritten this way, to give totals
SELECT o.order_id
, SUM(op.price) price
, op.dt
FROM orders o
JOIN order_price op
ON op.order_id = o.order_id
JOIN
( SELECT order_id, MAX(dt) max_dt FROM order_price WHERE dt < '2013-10-26' GROUP BY order_id ) x
ON x.order_id = op.order_id
AND x.max_dt = op.dt
GROUP
BY order_id,dt
WITH ROLLUP;
...or this way
SELECT SUM(price) total
FROM
( SELECT o.order_id
, op.price
, op.dt
FROM orders o
JOIN order_price op
ON op.order_id = o.order_id
JOIN
( SELECT order_id, MAX(dt) max_dt FROM order_price WHERE dt < '2013-10-26' GROUP BY order_id ) x
ON x.order_id = op.order_id
AND x.max_dt = op.dt
) z;
...or just pull the total at the application level.