Last order timestamp per product - mysql

I want to find the last payment (or NULL if n/a) made for which specified product_id. Below is a representation of the tables I'm working with (simplified version).
+----------+
|Products |
|----------+
|product_id|
+----------+
+---------------+
|Orders |
+---------------+
|order_id |
|order_timestamp|
|order_status |
+---------------+
+-----------------+
|ProductsOrdersMap|
+-----------------+
|product_id |
|order_id |
+-----------------+
After JOINs, MAXs, GROUP BYs, LEFT JOINs, multiple INNER JOINs to get the greatest-n-per-group, I still can't get to the right result. Most of the times, products with multiple orders are returning multiple rows. The best results I got so far were (I was searching specific products):
product_id order_id order_timestamp order_status
8 NULL NULL NULL
9 NULL NULL NULL
10 NULL NULL NULL
12 NULL NULL NULL
13 NULL NULL NULL
14 11 2013-08-13 07:22:01 finished
15 11 2013-08-13 07:22:01 finished
15 12 2013-08-14 00:00:00 finished
32 11 2013-08-13 07:22:01 finished
83 9 2013-08-13 07:04:02 finished
83 10 2013-08-13 07:11:42 finished
Edit: After PP. anwser, I ended up with the following query:
SELECT p.product_id, o.order_id, MAX(order_timestamp) AS order_timestamp, order_status
FROM Products p LEFT JOIN (ProductsOrdersMap m, Orders o)
ON (p.product_id = m.product_id AND m.order_id = o.order_id)
WHERE p.product_id IN (8,9,10,12,13,14,15,32,83)
GROUP BY p.product_id
Which returns
product_id order_id order_timestamp order_status
8 NULL NULL NULL
9 NULL NULL NULL
10 NULL NULL NULL
12 NULL NULL NULL
13 NULL NULL NULL
14 11 2013-08-13 07:22:01 finished
15 11 2013-08-13 07:22:01 finished
32 11 2013-08-13 07:22:01 finished
83 9 2013-08-13 07:04:02 finished
At first glance, it seems correct but only the products IDs and the timestamps are right. Comparing the two queries above, you can see that, for products 15 and 83, order_id is wrong (order_status might be wrong as well).

This query should return the specified resultset (this is only desk checked, not tested)
to return ALL product_id
SELECT p.product_id
, m.order_d
, m.order_timestamp
, m.order_status
FROM products p
LEFT
JOIN ( SELECT kl.product_id
, MAX(ko.order_timestamp) AS latest_timestamp
FROM orderproductsmap kl
JOIN orders ko
ON ko.order_id = kl.order_id
GROUP
BY kl.product_id
) l
ON l.product_id = p.product_id
LEFT
JOIN ( SELECT ml.product_id
, mo.order_id
, mo.order_timestamp
, mo.order_status
FROM orderproductsmap ml
JOIN orders mo
ON mo.order_id = ml.order_id
) m
ON m.product_id = l.product_id
AND m.order_timestamp = l.latest_timestamp
GROUP
BY p.product_id
The inline view "l" gets us the latest "order_timestamp" for each "product_id". This is joined to inline view "m" to get us the whole row for the order that has the latest timestamp.
If there happens to be more than one order with the same latest "order_timestamp" (i.e. order_timestamp is not guaranteed to be unique for a given product_id) then the outermost GROUP BY ensures that only one of those order rows is returned.
If only particular product_id values need to be returned, add a WHERE clause in the outermost query. For performance, that same predicate can be repeated in the inline views.
to return only SPECIFIC product_id we add three WHERE clauses:
SELECT p.product_id
, m.order_d
, m.order_timestamp
, m.order_status
FROM products p
LEFT
JOIN ( SELECT kl.product_id
, MAX(ko.order_timestamp) AS latest_timestamp
FROM orderproductsmap kl
JOIN orders ko
ON ko.order_id = kl.order_id
WHERE kl.product_id IN (8,9,10,12,13,14,15,32,83)
GROUP
BY kl.product_id
) l
ON l.product_id = p.product_id
LEFT
JOIN ( SELECT ml.product_id
, mo.order_id
, mo.order_timestamp
, mo.order_status
FROM orderproductsmap ml
JOIN orders mo
ON mo.order_id = ml.order_id
WHERE ml.product_id IN (8,9,10,12,13,14,15,32,83)
) m
ON m.product_id = l.product_id
AND m.order_timestamp = l.latest_timestamp
WHERE p.product_id IN (8,9,10,12,13,14,15,32,83)
GROUP
BY p.product_id
Only the WHERE clause on the outermost query is required. The other two are added just to improve performance by limiting the size of each of the derived tables.

SELECT
P.product_id
,MAX(order_timestamp)
FROM
Products P
,Orders O
,ProductsOrdersMap M
WHERE
P.product_id = M.product_id
AND O.order_id = M.order_id
GROUP BY
P.product_id

To return all products, even those without orders, a LEFT JOIN is definitely the way to go. The answer from #PP above uses "old-style" inner joins and is equivalent to this:
SELECT
P.product_id
,MAX(order_timestamp)
FROM Products P
INNER JOIN ProductsOrdersMap M ON P.product_id = M.product_id
INNER JOIN Orders O ON O.order_id = M.order_id
GROUP BY
P.product_id
Starting with this syntax it's a lot easier to get to the LEFT JOIN - just replace INNER with LEFT:
SELECT
P.product_id
,MAX(order_timestamp)
FROM Products P
LEFT JOIN ProductsOrdersMap M ON P.product_id = M.product_id
LEFT JOIN Orders O ON O.order_id = M.order_id
GROUP BY
P.product_id
Addendum: Renato needed something more than just reworking the other answer as a LEFT JOIN because the order_id and order_status have to come along with the maximum timestamp. The easiest approach is to start with a list of product ID's and order ID's where the order has the maximum timestamp by order_id:
SELECT
p2.product_id,
o2.order_id
FROM Products p2
INNER JOIN ProductsOrdersMap m ON p2.product_id = m.product_id
INNER JOIN Orders o2 ON m.order_id = o2.order_id
WHERE (o2.order_id, o2.order_timestamp) IN (
SELECT order_id, MAX(order_timestamp)
FROM Orders
GROUP BY order_id)
Then, instead of using ProductsOrdersMap to resolve products to orders, use the results from the query above:
SELECT
p.product_id,
o.order_id,
o.TS,
o.order_status
FROM Products p
LEFT JOIN (
SELECT
p2.product_id,
o2.order_id
FROM Products p2
INNER JOIN ProductsOrdersMap m ON p2.product_id = m.product_id
INNER JOIN Orders o2 ON m.order_id = o2.order_id
WHERE (o2.order_id, o2.order_timestamp) IN (
SELECT order_id, MAX(order_timestamp)
FROM Orders
GROUP BY order_id)
) MaxTS ON p.product_id = MaxTS.product_id
LEFT JOIN Orders o ON MaxTS.order_id = o.order_id

Related

Getting associated data from max column with multiple tables in MySQL

I have two tables, Orders and OrderDetails. I would like to create an SQL query that retrieves the OrderID, Unit Cost and Process Date for orders with the latest processed T-Shirts for each user.
I have created a very simplified version of my tables in this SQL fiddle.
From my google searches, most of which ended up here on SO, I have managed to figure out SQL to get the associated unit cost for the latest processed order of T-shirts per order, but I'm not sure how to further condense it from here to be for each user and still maintain the correct unit cost and process dates. I know I can't use MAX and GROUP BY and I can't work out how to do a LEFT OUTER JOIN (like how I did the first part in the SQL fiddle) when the UserID and ProcessDate reside in different tables.
For the dataset in the SQL fiddle, the output I would be looking for would be
| UserID | OrderID | UnitCost | ProcessDate |
|-------- |--------- |---------- |------------- |
| 1 | 2 | 3 | 2018-03-01 |
| 2 | 3 | 1 | 2018-04-01 |
| 3 | 5 | 3 | 2018-06-01 |
Above query output is perfectly fine just the case is correlated subquery gets executed everytime for each row which is quite a overhead and in case of large data there will be performance issue.
I have changed the subquery into join which may be helpful.
SELECT O.UserID
, O.OrderID
, OD.UnitCost
, OD.ProcessDate
FROM Orders O
left
JOIN OrderDetails OD
ON O.OrderID = OD.OrderID
left
join
( SELECT o2.userid
, max(od2.processdate) newdate
FROM Orders o2
JOIN OrderDetails od2
ON o2.OrderId = od2.OrderId
WHERE od2.Product = 'T-Shirts'
group
by o2.userid
) temp
on O.userid = temp.userid
WHERE OD.Product = 'T-Shirts'
and OD.processdate = temp.newdate
ORDER
BY OD.OrderDetailID;
Hope it helps.
You can select the "latest" order for each user using a correlated subquery:
SELECT O.UserID, O.OrderID, OD.UnitCost, OD.ProcessDate
FROM OrderDetails OD LEFT JOIN
Orders O
ON O.OrderID = OD.OrderID
WHERE OD.Product = 'T-Shirts' AND
o.OrderID = (SELECT MAX(o2.OrderId)
FROM Orders o2
WHERE o2.UserID = o.UserID
)
ORDER BY OD.OrderDetailID;
This assumes that the latest order is the one with the largest OrderId.
EDIT:
Actually, the above happens to work on your data, because the largest order id for each user has t-shirts. However, you explicitly say that you want the latest order with t-shirts, so that requires a slight modification:
SELECT O.UserID, O.OrderID, OD.UnitCost, OD.ProcessDate
FROM Orders O LEFT JOIN
OrderDetails OD
ON O.OrderID = OD.OrderID
WHERE OD.Product = 'T-Shirts' AND
o.OrderID = (SELECT MAX(o2.OrderId)
FROM Orders o2 JOIN
OrderDetails od2
ON o2.OrderId = od2.OrderId
WHERE od2.Product = 'T-Shirts' AND
o2.UserID = o.UserID
)
ORDER BY OD.OrderDetailID;
Or, if you want to go by process date:
SELECT O.UserID, O.OrderID, OD.UnitCost, OD.ProcessDate
FROM Orders O LEFT JOIN
OrderDetails OD
ON O.OrderID = OD.OrderID
WHERE OD.Product = 'T-Shirts' AND
od.ProcessDate = (SELECT od2.ProcessDate
FROM Orders o2 JOIN
OrderDetails od2
ON o2.OrderId = od2.OrderId
WHERE od2.Product = 'T-Shirts' AND
o2.UserID = o.UserID
ORDER BY od2.ProcessDate DESC
LIMIT 1
)
ORDER BY OD.OrderDetailID;

Count two different rows in mysql query

I have organizations. Each organization can have members and projects.
I want to get list of organizations with number of members and projects.
For example,
Organization | Members | Projects | Action
------------------------------------------
Org 1 | 5 | 6 | Delete - Edit
Org 2 | 2 | 9 | Delete - Edit
I am using this query,
SELECT COUNT(m.id) as members, COUNT(p.id) as projects,
o.status,o.organization_name,o.logo, o.id as id
from tbl_organizations o
LEFT JOIN tbl_organization_members m ON (o.id = m.organization_id)
LEFT JOIN tbl_projects p ON (o.id = p.organization_id)
WHERE o.status= 'active' AND o.created_by= 1
But the output of number of projects is equal to number of members.
How can I make the sample above using query?
Try this way:
SELECT o.id as id, o.organization_name, cnt_ as members, cnt_p as projects
from tbl_organizations o
LEFT JOIN (
SELECT organization_id, COUNT(id) cnt_m
FROM tbl_organization_members
GROUP BY organization_id
) m ON (o.id = m.organization_id)
LEFT JOIN (
SELECT organization_id, COUNT(id) cnt_p
FROM tbl_projects
GROUP BY organization_id
) p ON (o.id = p.organization_id)
WHERE o.status= 'active' AND o.created_by= 1
This way you JOIN to an already aggregated version of member/project tables, so as to get the count of members/projects per organization_id.
Group by the organisation columns and count distinct IDs
SELECT o.status,o.organization_name, o.logo, o.id as id,
COUNT(distinct m.id) as members, COUNT(distinct p.id) as projects,
from tbl_organizations o
LEFT JOIN tbl_organization_members m ON (o.id = m.organization_id)
LEFT JOIN tbl_projects p ON (o.id = p.organization_id)
WHERE o.status= 'active'
AND o.created_by= 1
GROUP BY o.status, o.organization_name, o.logo, o.id
You can co-related subquery:
SELECT
o.id as Organization,
(SELECT COUNT(*) FROM tbl_organization_members WHERE organization_id = o.id) as members,
(SELECT COUNT(*) FROM tbl_projects WHERE organization_id = o.id) as projects
FROM
tbl_organizations o
WHERE
o.status= 'active' AND o.created_by = 1

Mysql query with count(distinct) show nothing if group by has no result

I have a query on 3 tables, settlement, order and passenger.
A settlement has orders, and orders has passengers.
TABLE: Settlemnt
id
TABLE: Order
id settlement_id
TABLE: passenger
id orders_id
If settlement has no orders (and then no passengers), it returns nothing.
What I want, is to return the empty settlement even if it has no orders.
It is the count(passengers) that ruins it for me and returns no settlement if empty.
This is the simplified query:
select s.id, o.id, count(distinct p.id)
from settlement s
left join orders o ON o.settlement_id = s.id
left join passenger p on p.orders_id = o.id
where s.date = '2016-02-02';
group by o.id
How can I make it return settlements with no orders?
Works for me...
SELECT s.id s_id
, o.id o_id
, p.id p_id
FROM settlement s
LEFT
JOIN orders o
ON o.sid = s.id
LEFT
JOIN passengers p
ON p.oid = o.id;
+------+------+------+
| s_id | o_id | p_id |
+------+------+------+
| 1 | NULL | NULL |
| 2 | 1 | 1 |
+------+------+------+
http://sqlfiddle.com/#!9/9a01b/4
The major problem with your query appears to be that you GROUP BY the o.id. Any settlement that has no order will have an o.id of NULL, hence all settlements with no orders will be lumped together into one row. Which s.id that is returned for this o.id is not defined (and effectively random).
Change it to GROUP BY both s.id and o.id
SELECT s.id,
o.id,
COUNT(DISTINCT p.id)
FROM settlement s
LEFT JOIN orders o ON o.settlement_id = s.id
LEFT JOIN passenger p ON p.orders_id = o.id
WHERE s.date = '2016-02-02';
GROUP BY s.id,
o.id

Union Two SQL Queries

I have a query like this
SELECT o.product_id,
p.name,
count(o.product_id) AS total_products_sold,
(SELECT count(o.id)
FROM ebay.`order` o) AS total_orders
FROM ebay.`order` o
INNER JOIN product p ON o.product_id = p.id
GROUP BY o.product_id
The total_orders is rerun when executed for each which not i want. I
Question:
I want the total_orders combines with every result set from the outer query.
I tried this but it only return 1 row
SELECT tps.product_id,
tps.name,
tps.total_products_sold,
count(oo.id) AS total_orders
FROM ebay.`order` oo
INNER JOIN
( SELECT o.id,
o.product_id,
p.name,
count(o.product_id) AS total_products_sold
FROM ebay.`order` o
INNER JOIN product p ON o.product_id = p.id
GROUP BY o.product_id ) AS tps ON oo.product_id = tps.product_id
Any better solution ?
Thanks.
You can use with rollup which will give you the total without changing the actual query
It wont give you the result in column of every row but you will get the result of total orders in the last row.
SELECT
o.product_id,
p.name,
count(distinct o.id) AS totalorder
FROM
ebay.`order` o
INNER JOIN
product p
ON
o.product_id = p.id
GROUP BY
o.product_id
WITH ROLLUP
For example
+-----------+------+------------+
| product_id| name | totalorder |
+-----------+------+------------+
| 2000 | A | 10 |
| 2001 | B | 20 |
| NULL | NULL | 30 | <--- Last row is having the Total order
+-----------+------+------------+
WITH ROLLUP
SELECT tps.product_id,
tps.name,
tps.total_products_sold,
s.total_orders
FROM ebay.`order` oo
INNER JOIN
(
SELECT o.id,
o.product_id,
p.name,
count(o.product_id) AS total_products_sold
FROM ebay.`order` o
INNER JOIN product p
ON o.product_id = p.id
GROUP BY o.product_id
) AS tps ON oo.product_id = tps.product_id
CROSS JOIN
(
SELECT count(id) total_orders
FROM ebay.`order`
) s

mysql group by not doing what i am expecting?

SELECT
bp.product_id,bs.step_number,
p.price, pd.name as product_name
FROM
builder_product bp
JOIN builder_step bs ON bp.builder_step_id = bs.builder_step_id
JOIN builder b ON bp.builder_id = b.builder_id
JOIN product p ON p.product_id = bp.product_id
JOIN product_description pd ON p.product_id = pd.product_id
WHERE b.builder_id = '74' and bs.optional != '1'
group by bs.step_number
ORDER by bs.step_number, p.price
but here is my results
88 1 575.0000 Lenovo Thinkcentre POS PC
244 2 559.0000 Touchscreen with MSR - Firebox 15"
104 3 285.0000 Remote Order Printer - Epson
97 4 395.0000 Aldelo Lite
121 5 549.0000 Cash Register Express - Pro
191 6 349.0000 Integrated Payment Processing
155 7 369.0000 Accessory - Posiflex 12.1" LCD Customer Display
That's not how GROUP BY is supposed to work. If you group by a number of columns, your select can only return:
The columns you group by
Aggregation functions from other columns, such as MIN(), MAX(), AVG()...
So you'd need to do this:
SELECT
bs.step_number,
MIN(p.price) AS min_price, pd.name as product_name
FROM
builder_product bp
JOIN builder_step bs ON bp.builder_step_id = bs.builder_step_id
JOIN builder b ON bp.builder_id = b.builder_id
JOIN product p ON p.product_id = bp.product_id
JOIN product_description pd ON p.product_id = pd.product_id
WHERE b.builder_id = '74' and bs.optional != '1'
group by bs.step_number, pd.name
ORDER by bs.step_number, min_price
(MySQL allows a very relaxed syntax and will happily remove random rows for each group but other DBMS will trigger an error with your original query.)
Join to a sub select of the tables which only contain the min value of each group
In this example. the mygroup min(amt) returns the lowest dollar item for a group
I then join this back to the main table as a full inner join to limit the records only to that minimum.
Select A.myGROUP, A.Amt
from mtest A
INNER JOIN (Select myGroup, min(Amt) as minAmt from mtest group by mygroup) B
ON B.myGroup=A.mygroup
and B.MinAmt = A.Amt
Yes. Each different group key is returned only once. This problem is not easily solved. Run two distinct queries and combine results afterwards. IF this is not an option create a temporary table for the minimum price for each step join the tables in the query.