How to calculate quantity on hand for every lot (mysql) - mysql

This is the transaction details table
I am trying to design a Mysql inventory database.
I consider every type: 1 row a product lot (batch).
The type column has 1 for IN and 0 for OUT for each transaction.
detail_id is referencing the id column.
How can I get this result:
id item sum(quantity)
1 1 3 [10-(5+2)]
4 1 0 (5-5)
6 2 20 20

WITH cte AS (
SELECT *,
SUM(detail_id IS NULL) OVER (PARTITION BY item ORDER BY id) group_num
FROM details
)
SELECT MIN(id) id,
item,
SUM( CASE type WHEN 1 THEN quantity
WHEN 0 THEN -quantity
END ) `sum(quantity)`
FROM cte
GROUP BY item, group_num;
fiddle

You can use this:
SELECT
lots.id,
MIN(lots.item) AS item,
MIN(lots.quantity) - IFNULL(SUM(details.quantity), 0) AS quantity
FROM (
SELECT id, item, quantity
FROM details
WHERE type = 1
) lots LEFT JOIN details ON lots.id = details.detail_id
GROUP BY lots.id
ORDER BY lots.id
demo on dbfiddle.uk

I have modified your fiddle. I did change one thing to your existing table - you need to specify detail_id, wherever it is null, so that we can group the result on that.
Final query will look like
select detail_id, item,
(in_sum - out_sum) as `sum(quantity)` from
(SELECT
detail_id,
item,
sum(case when type=1 then quantity else 0 end) as in_sum,
sum(case when type=0 then quantity else 0 end) as out_sum
FROM details
group by detail_id, item) tab
Firstly getting the sum of quantity for specified type by grouping over detail_id, item and then use that result to compute the final output.

Related

Get most recent records from the table if a 'ALERT' status is present otherwise most recent record with 'OK' Status - Mysql v5.6.50

In Mysql I have the following table - property_alert_status having columns :
id (primary), propertyId, status, updatedAt
All record - select * from property_alert_status
id
propertyId
status
updatedAt
1
1
ALERT
1658304031
2
2
OK
1658300273
3
3
ALERT
1658312336
4
3
ALERT
1658313979
5
3
OK
1658312336
6
2
OK
1658312336
From the above table, I want to fetch the most recent record for the property based on status. If Status is 'ALERT' then most recent 'ALERT' record otherwise Most recent 'OK' record.
Ex - For propertyId '3' there are three records but most recent alert status is of id 4 so the output for the above propertyId 3 should be:
id
propertyId
status
updatedAt
4
3
ALERT
1658313979
Expected Output should be:
id
propertyId
status
updatedAt
1
1
ALERT
1658304031
4
3
ALERT
1658313979
6
2
OK
1658312336
I have made one query but the output is not as expected:
Select mainStatus.* from (
SELECT *
FROM property_alert_status
ORDER BY
(CASE
WHEN status = "ALERT" THEN 0
ELSE 1
END) ASC, updatedAt DESC
) mainStatus group by propertyId;
Innerquery is giving the right result but when selecting only a single record by grouping propertyId, giving the wrong result.
Inner query giving result:
id
propertyId
status
updatedAt
4
3
ALERT
1658313979
3
3
ALERT
1658312336
1
1
ALERT
1658304031
5
3
OK
1658312336
6
2
OK
1658312336
2
2
OK
1658300273
The final query gives result:
id
propertyId
status
updatedAt
1
1
ALERT
1658304031
2
2
OK
1658300273
3
3
ALERT
1658312236
Note: Using Mysql v5.6.50.
Tables in SQL are unordered data set. A query result is a table. So the ORDER BY clause in your subquery doesn't have to sort the rows. Don't rely on it. Some DBMS even raise an error when you have an ORDER BY at the end of a subquery.
Moreover, select * from ... group by ... is invalid. If you group by a column, you can select that column plus aggregates, i.e. sums, maximums, averages and so on. You cannot select other original column values (except for the case they are functionally dependent on your group, such as a person's name when you group by the person's ID). MySQL should raise an error, and if it doesn't, this probably means that you are working in a cheat mode that MySQL invented in their early days. Make sure to always SET sql_mode = 'ONLY_FULL_GROUP_BY'; when working with MySQL in order to have the DBMS help you with invalid aggregation queries.
As to the task: You can rank your rows with ROW_NUMBER.
SELECT *
FROM
(
SELECT
s.*,
ROW_NUMBER() OVER (PARTITION BY propertyid ORDER BY status, updatedat DESC) AS rn
FROM vk_property_temperature_alert_status s
WHERE temperature_status IN ('ALERT', 'OK')
) ranked
WHERE rn = 1;
For old MySQL versions I see two approaches. Either select only those rows for which not exists a better row or select those rows that are the best for their group. The second approach seems easier. It's basically writing a subquery that determines the top row for the property ID, so you can check whether the row you are looking at is a top row.
SELECT *
FROM vk_property_temperature_alert_status s
WHERE id =
(
SELECT s2.id
FROM vk_property_temperature_alert_status s2
WHERE s2.temperature_status IN ('ALERT', 'OK')
AND s2.propertyid = s.propertyid
ORDER BY s2.status, s2.updatedat DESC
LIMIT 1
);
I don't know if this will work, wrote just for fun as the question was very interesting:
SELECT
MAX(maxId),
propertyId,
`status`,
MAX(dates) updatedAt
FROM
(
SELECT
firstResult.*,
(CASE WHEN #running_propertyId=0 THEN #running_propertyId:=propertyId ELSE #running_propertyId:=#running_propertyId END) runningPro,
(CASE WHEN #running_status='' THEN #running_status:=`status` ELSE #running_status:=#running_status END) runningStat,
(CASE WHEN #running_variable >0 AND #running_propertyId =propertyId THEN #running_variable:=#running_variable+1 ELSE #running_variable:=1 END )var,
(CASE WHEN #running_variable =1 THEN #running_date:=updatedAt ELSE (CASE WHEN `status`='ALERT' THEN #running_date:=updatedAt ELSE
( CASE WHEN #running_status=`status` THEN #running_date:=updatedAt ELSE #running_date:=#running_date END) END) END )dates,
(CASE WHEN #running_variable =1 THEN #running_id:=id ELSE (CASE WHEN `status`='ALERT' THEN #running_id:=id ELSE
( CASE WHEN #running_status=`status` THEN #running_id:=id ELSE #running_id:=#running_id END) END) END )maxId,
#running_propertyId:=propertyId,
#running_status:=`status`
FROM (SELECT
a.*,
#running_propertyId:=0,
#running_status:='',
#running_variable:=0,
#running_date:=0,
#running_id:=0
FROM
property_alert_status a
ORDER BY
`propertyId`
,`updatedAt`) firstResult
) final
GROUP BY propertyId
By combining my query with some changes into the #Thorsten Kettner's query -
Following Query giving expected result:
SELECT
*
FROM
property_alert_status s
WHERE
id = (SELECT
s2.id
FROM
property_alert_status s2
WHERE
s2.propertyId = s.propertyId
ORDER BY (CASE
WHEN s2.status = 'ALERT' THEN 0
ELSE 1
END) ASC, s2.updatedAt DESC
LIMIT 1);

Get sum of values for multiple categories for a given list of dates

I need to write a query to get the sum of values for each category for a list of given dates. If a value doesn't exist for a category, we should get the value from the previous date. Basically something like "max per category per date". The end goal is a trend chart. If a previous value for a category doesn't exist, setting the value to 0 is fine.
See tables and result below:
Category
id
name
1
savings
2
cash
3
stocks
Item
id
categoryId
value
createdAt
1
1
100
2022-01-01
2
2
20
2022-01-01
3
3
500
2022-01-01
4
2
0
2022-01-02
5
3
1000
2022-01-03
Result
createdAt
total
2022-01-01
620
2022-02-02
600
2022-02-03
1100
To get a result for a single date I could do something like this:
SELECT SUM(value) as total
FROM Category
LEFT JOIN (
SELECT id, categoryId, value
FROM Item
WHERE id IN (
SELECT MAX(id) FROM Item WHERE createdAt <= '2022-01-10' GROUP BY categoryId)
) items ON Category.id = items.categoryId;
I have absolutely no clue on how to approach doing this for multiple dates, eg. if my input would be every day in the month of January 2022. I'm running on MySQL 8.0.23. Also, if this is not feasible with a single query, I'm up for ideas. Do you have any suggestions?
Try this:
with u as
(select id as categoryId from Category),
v as
(select distinct createdAt from Item),
w as
(select * from u cross join v),
x as
(select createdAt,
categoryId,
(select value
from Item
where categoryId = w.categoryId and createdAt <= w.createdAt
order by createdAt desc
limit 1) as value
from w)
select createdAt, sum(value) as total
from x
group by createdAt
Basically getting all the combinations of the creation dates with the categoryIds, then using a subquery to get the value of the closest or equal date for each categoryId.
A Fiddle.
One option uses window functions such as SUM() OVER () and LAG() such as
WITH i AS
(
SELECT SUM(`value`) OVER (PARTITION BY `createdAt`,`categoryId` ORDER BY `createdAt`) AS total_sofar,
LAG(`value`,1,0) OVER (PARTITION BY `categoryId` ORDER BY `createdAt`) AS lg,
`createdAt`
FROM Item
)
SELECT DISTINCT `createdAt`,
SUM(total_sofar) OVER (ORDER BY `createdAt`)-SUM(lg) OVER (ORDER BY `createdAt`) AS total
FROM i
ORDER BY `createdAt`
as you have MySQL DBMS of version 8.0. The trick is grouping(partitioning by categoryId along with the LAG at the first query)
Demo

Mysql - most efficient way to retrieve data based on multiple selects and wheres

I'm having trouble finding the most efficient way of retrieving various different sumed values from a Mysql table.
Let's say I've got 4 columns - userid, amount, paid, referral.
I'd like to retrieve the following based on a user id:
1 - the sum of amount that is paid (marked as 1)
2 - the sum of amount that is unpaid (marked as 0)
3 - the sum of amount that is paid and referral (marked as 1 on both paid and referral columns)
4 - the sum of amount that unpaid and referral (marked as 0 on paid and 1 on referral columns)
I've tried an embedded select statement like this:
SELECT (
SELECT sum(payout)
FROM table1
WHERE ispaid = 0 and userid = '100'
) AS unpaid
(
SELECT sum(payout)
FROM table1
WHERE ispaid = 1 and userid = '100'
) AS paid,
(
SELECT sum(payout)
FROM table1
WHERE ispaid = 0 and isreferral = 1 and userid = '100'
) AS refpending,
(
SELECT sum(payout)
FROM table1
WHERE ispaid = 1 and isreferral = 1 and userid = '100'
) AS refpaid
This works, but its slow (or at least feels like it could be quicker) on my server, around 1.5 seconds.
I'm sure there is a better way of doing this with a group statement but can't get my head around it!
Any help is much appreciated.
Thanks
You can use conditional expressions inside SUM():
SELECT
SUM(CASE WHEN ispaid=0 THEN payout END) AS unpaid,
SUM(CASE WHEN ispaid=1 THEN payout END) AS paid,
SUM(CASE WHEN ispaid=0 AND isreferral=1 THEN payout END) AS refpending,
SUM(CASE WHEN ispaid=0 AND isreferral=1 THEN payout END) AS refpaid
FROM table1
WHERE userid = '100'
If a given row is not matched by any CASE...WHEN clause, then the value of the expression is NULL, and SUM() ignores NULLs. You could also have an ELSE 0 clause in there if you want to be more explicit, since SUM() will not be increased by a 0.
Also make sure you have an index on userid in this table to select only the rows you need.

Add a column from subquery to the SELECT of main query without applying filteres from WHERE in main query

I need a help with following problem:
So I have a table TABLE1 with columns Date, Name, STAT1, STAT2, PROBLEM1
I have a query like this:
SELECT Date, Name, sum(STAT1), sum(STAT2)
FROM TABLE1
WHERE PROBLEM1 <> 0
GROUP BY Date, Name
The result of this query is what I want but I also need to add 2 columns: TOTALCOUNT which is basically the number of rows for each group without applying the filter (PROBLEM1 <> 0 ) and COUNTERRORS which is count for each group where PROBLEM1 = 0.
So to give you further example. For a Date A, and Name B I have 1000 rows. 300 of them have PROBLEM1 = 0.
I run the first query I mentioned above and it calculates my sum(STAT1), sum(STAT2) based on 700 rows because of the filter WHERE PROBLEM1 <> 0. And I need to find a way to add two columns to the result so in the end my table would look like:
DATE NAME sum(STAT1) sum(STAT2) TOTALCOUNT COUNTERRORS
A B 50 3.5 1000 300
Is it possible to do? I was trying using subqueries but without a success.
You can do conditional aggregation :
SELECT Date, Name,
sum(case when PROBLEM1 <> 0 then stat1 else 0 end) as stat1,
sum(case when PROBLEM1 <> 0 then stat2 else 0 end) as stat2,
count(*) as TOTALCOUNT,
sum(PROBLEM1 = 0) as COUNTERRORS
FROM TABLE1
GROUP BY Date, Name;

select case when not working as expected

I'm trying to understand how this works but can't figure it out yet.
I have made this simple uery to test the case-when-then-end clause...
SELECT case when quantity > 3
then count(*) end the_count_a,
case when quantity <= 3
then count(*) end the_count_b
FROM STOCK
my stock table has 30 items with different quantities, only 10 items have quantity over 3 but this is always returning 30.... WHY?
I think it should be returning two columns with values: 10 and 20
Any help will be appreciated!
Thx,
Leo
The value of count(*) means the count of all records (in the current group), regardless of where it is placed. If you want to count records that match a condition, you need to invert your case statement:
select count(case when quantity > 3 then 1 end) the_count_a,
count(case when quantity <= 3 then 1 end) the_count_b
from stock
SELECT
count(case when quantity > 3 then 1 else null end) end the_count_a,
count(case when quantity <= 3 then 1 else null end) end the_count_b
FROM STOCK
The aggregate function COUNT() in absense of a GROUP BY will return all rows in the table which have not been filtered by a WHERE clause. In your case, what you actually need are two subselects or a UNION, depending if you want columns or rows back:
/* Return columns with subselects */
SELECT
(SELECT COUNT(*) FROM STOCK WHERE quantity > 3) AS the_count_a
(SELECT COUNT(*) FROM STOCK WHERE quantity <= 3) AS the_count_b
MySQL is lenient about the presence of a FROM clause, so it can be omitted from the outer query.
/* Return rows instead of columns with UNION */
SELECT
COUNT(*) AS the_count,
'the_count_a'
FROM STOCK WHERE quantity > 3
UNION ALL
SELECT
COUNT(*) AS the_count,
'the_count_b'
FROM STOCK WHERE quantity <= 30