calculate unit in stock of a product - mysql

This is the question:
To find
units in stock of a product, use the InventoryTransactions table, find total quantity
purchased and subtract quantities sold and on hold.
The Inventory Transaction table:
This are the transaction type for each transaction id:
transaction id 1 = purchased, trasaction id 2 = sold, transaction id 3 = on hold
+----+-----------+
| ID | TypeName |
+----+-----------+
| 1 | Purchased |
| 2 | Sold |
| 3 | On Hold |
| 4 | Waste |
+----+-----------+
4 rows in set (0.00 sec)
The product id, quantity, and type of transaction:
SELECT productid, quantity, transactiontype FROM Inventory_Transactions ORDER BY productid;
+-----------+----------+-----------------+
| productid | quantity | transactiontype |
+-----------+----------+-----------------+
| 1 | 40 | 1 |
| 1 | 15 | 2 |
| 1 | 25 | 3 |
| 3 | 100 | 1 |
| 3 | 50 | 2 |
| 4 | 40 | 1 |
| 4 | 10 | 2 |
| 4 | 30 | 2 |
| 5 | 40 | 1 |
| 5 | 25 | 2 |
| 6 | 100 | 1 |
| 6 | 10 | 2 |
| 6 | 90 | 2 |
| 7 | 40 | 1 |
| 7 | 10 | 2 |
| 7 | 30 | 2 |
| 8 | 40 | 1 |
| 8 | 17 | 2 |
| 8 | 25 | 1 |
| 8 | 25 | 2 |
| 8 | 20 | 2 |
| 8 | 3 | 2 |
| 14 | 40 | 1 |
| 17 | 40 | 1 |
| 17 | 40 | 2 |
| 19 | 20 | 1 |
| 19 | 20 | 2 |
| 19 | 30 | 1 |
| 19 | 30 | 2 |
| 19 | 25 | 1 |
| 19 | 10 | 2 |
| 19 | 10 | 1 |
| 19 | 25 | 2 |
| 20 | 40 | 1 |
| 20 | 40 | 2 |
| 21 | 20 | 1 |
| 21 | 20 | 2 |
| 34 | 60 | 1 |
| 34 | 100 | 1 |
| 34 | 100 | 2 |
| 34 | 12 | 3 |
| 34 | 10 | 3 |
| 34 | 1 | 3 |
| 34 | 50 | 1 |
| 34 | 300 | 1 |
| 34 | 300 | 2 |
| 34 | 87 | 2 |
| 40 | 120 | 1 |
| 40 | 50 | 2 |
| 40 | 30 | 2 |
| 40 | 40 | 2 |
| 41 | 40 | 1 |
| 41 | 200 | 1 |
| 41 | 200 | 2 |
| 41 | 30 | 2 |
| 41 | 50 | 1 |
| 41 | 50 | 2 |
| 41 | 10 | 2 |
| 43 | 100 | 1 |
| 43 | 20 | 2 |
| 43 | 300 | 1 |
| 43 | 300 | 2 |
| 43 | 25 | 3 |
| 43 | 250 | 1 |
| 43 | 300 | 3 |
| 43 | 5 | 2 |
| 48 | 100 | 1 |
| 48 | 10 | 2 |
| 48 | 100 | 1 |
| 48 | 100 | 2 |
| 48 | 10 | 2 |
| 48 | 40 | 2 |
| 48 | 40 | 2 |
| 51 | 40 | 1 |
| 51 | 10 | 2 |
| 51 | 30 | 2 |
| 52 | 100 | 1 |
| 52 | 40 | 2 |
| 56 | 120 | 1 |
| 56 | 110 | 3 |
| 57 | 80 | 1 |
| 57 | 100 | 1 |
| 57 | 100 | 2 |
| 65 | 40 | 1 |
| 66 | 80 | 1 |
| 72 | 40 | 1 |
| 72 | 50 | 1 |
| 72 | 50 | 2 |
| 72 | 40 | 2 |
| 74 | 20 | 1 |
| 74 | 20 | 2 |
| 77 | 60 | 1 |
| 80 | 75 | 1 |
| 80 | 30 | 2 |
| 80 | 10 | 2 |
| 80 | 20 | 3 |
| 80 | 15 | 2 |
| 81 | 125 | 1 |
| 81 | 200 | 1 |
| 81 | 200 | 2 |
| 81 | 50 | 3 |
| 81 | 25 | 3 |
+-----------+----------+-----------------+
102 rows in set (0.00 sec)
I will need to calculate unit of stock for each productid by SUM(quantity purchased) - SUM(quantity sold) - SUM(quantity on hold)
My take:
CREATE VIEW purchased AS
SELECT productid, SUM(quantity) quantity
FROM Inventory_Transactions
WHERE transactiontype = 1
GROUP BY productid
ORDER BY productid;
CREATE VIEW sold AS
SELECT productid, SUM(quantity) quantity
FROM Inventory_Transactions
WHERE transactiontype = 2
GROUP BY productid
ORDER BY productid;
CREATE VIEW onhold AS
SELECT productid, SUM(quantity) quantity
FROM Inventory_Transactions
WHERE transactiontype = 3
GROUP BY productid
ORDER BY productid;
SELECT pur.productid, pur.quantity - so.quantity - on.quantity
FROM purchased pur, sold so, onhold on
WHERE pur.productid = so.productid = on.productid;
But I got this error
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'on WHERE pur.productid = so.productid = on.productid' at line 1
These are the individual VIEW I created above:
SELECT * FROM purchased;
+-----------+----------+
| productid | quantity |
+-----------+----------+
| 1 | 40 |
| 3 | 100 |
| 4 | 40 |
| 5 | 40 |
| 6 | 100 |
| 7 | 40 |
| 8 | 65 |
| 14 | 40 |
| 17 | 40 |
| 19 | 85 |
| 20 | 40 |
| 21 | 20 |
| 34 | 510 |
| 40 | 120 |
| 41 | 290 |
| 43 | 650 |
| 48 | 200 |
| 51 | 40 |
| 52 | 100 |
| 56 | 120 |
| 57 | 180 |
| 65 | 40 |
| 66 | 80 |
| 72 | 90 |
| 74 | 20 |
| 77 | 60 |
| 80 | 75 |
| 81 | 325 |
+-----------+----------+
28 rows in set (0.00 sec)
SELECT * FROM sold;
+-----------+----------+
| productid | quantity |
+-----------+----------+
| 1 | 15 |
| 3 | 50 |
| 4 | 40 |
| 5 | 25 |
| 6 | 100 |
| 7 | 40 |
| 8 | 65 |
| 17 | 40 |
| 19 | 85 |
| 20 | 40 |
| 21 | 20 |
| 34 | 487 |
| 40 | 120 |
| 41 | 290 |
| 43 | 325 |
| 48 | 200 |
| 51 | 40 |
| 52 | 40 |
| 57 | 100 |
| 72 | 90 |
| 74 | 20 |
| 80 | 55 |
| 81 | 200 |
+-----------+----------+
23 rows in set (0.01 sec)
SELECT * FROM onhold;
+-----------+----------+
| productid | quantity |
+-----------+----------+
| 1 | 25 |
| 34 | 23 |
| 43 | 325 |
| 56 | 110 |
| 80 | 20 |
| 81 | 75 |
+-----------+----------+
6 rows in set (0.00 sec)
I am using the northwind database
Here are all the table relationship in northwind database.
May I know how do I solve this?

The approach I would take is using something called "Conditional aggregation"
use a summed case expression to determine if it's a purchase
use a summed 2nd case expression to determine if it's a sale/hold
subtract the two
group by product
.
SELECT ProductID,
SUM(case when TransactionType = 1 then quantity else 0 end) -
SUM(case when TransactionType in (2,3) then quantity else 0 end) as AvailableInventory
FROM Inventory_Transactions
GROUP BY ProductID
As to the nature of your error:
you have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'on WHERE pur.productid = so.productid = on.productid' at line 1
on is a reserved word use onh or onhold or something for the alias
using , joins is and old technique use Inner, outer, full outer, cross join and use the on notation to specify how he tables relate
equality checks within where clauses must return true false. To date they are not trinary. If A=B but <> C what would you expect to happen? so break out your productID's so they handle all the needed joins.
you're going to have problems of matching records because not all 3 may have a value so if you match on productID for sold or hold, you may not have a product and it would fall out of your results... thus outer joins are needed if you continue with your current approach.
.
ORIGINAL
SELECT pur.productid, pur.quantity - so.quantity - on.quantity
FROM purchased pur, sold so, onhold on
WHERE pur.productid = so.productid = on.productid;
should be:
SELECT coalesce(pur.productid, so.productid,onhold.ProductID) as productid,
coalesce(pur.quantity,0) - coalesce(so.quantity,0) -
coalesce(onhold.quantity) as AvailableInventory
FROM purchased pur
FULL OUTER JOIN sold so
on pur.productid = so.productid
FULL OUTER JOIN onhold
on pur.productid = onhold.productid
OR so.productid = onhold.productid
GROUP BY coalesce(pur.productid, so.productid,onhold.ProductID);
Breaking this down
the coalesce of productID is because we don't know in which of the 1,2,3 a productID will exist. But, we need the product id to show up for any of the 3 not just purchases.
the coalesce on quantity is because if we try to subtract a NULL value, we get a NULL so we need to ensure a numeric value exists. pretend for a second product ID 1 exists in purchases but has had no sales or holds. 10 purchases - NULL = NULL... not what we want. now if the productID 2 is in sales but not purchases we'd have NULL-10-NULL... again not what we want. Also if we just used the productid from purchases it would be NULL on this record...; also not good.
we replaced the , notation which is a cross join to a full outer so we don't lose records. Now cross join would work but it takes # of purchases * #of sales * number of on hold then limits the records based on your where clause this is WAY more work than the database needs to do and on a large dataset would be VERY slow.
Discussed earlier, where clause can't be trinary it must be a binary result comparing just 2 values.
Now full outer vs left vs inner vs right....
FULL OUTER: include all records from both tables and line them up where they match on Key (productID in our example)
LEFT JOIN include all records from the first table and any that match from the 2nd
INNER Include only records which exist in both tables.
RIGHT include all records from the 2nd table and any that match from the 1st.
CROSS JOIN: all records related to all records (Very slow but there are uses; just not here)
and more...
If we can't assume each of your products has a value in each group: pur, sold, onhold; we need to use full outer joins as above; so we don't exclude any records. However even this can be an issue because we have to ensure each productID ties back to a related product in another table if it exists.
To do this we have to use an OR on the last match (kinda ugly) and as we don't know the source of productID... we have to find one using coalesce.
If we can assume all products must first exist as a purchase, we can left join the other two. A bit cleaner, but we still have to handle the situation where there may be no data in sold or on hold, so we have to coalesce the 0 in for the quantities. we don't have to coalesce the productid since we know a purchase has to exist.
SELECT pur.productid, sum(coalesce(pur.quantity,0) - coalesce(so.quantity,0) -
coalesce(onhold.quantity)) as AvailableInventory
FROM purchased pur
LEFT OUTER JOIN sold so
on pur.productid = so.productid
LEFT OUTER JOIN onhold
on pur.productid = onhold.productid
--Note: we tie back to pur both times as we know it exists there.
GROUP BY pur.productid;
Hopefully you can see the conditional aggregation is the better approach as it simplifies and reduces joins and avoids much of the coalesing we do.

Related

Group by with categories created within the query in SQL

I have a table where I want to group by both categories and days, however I want to organize the days into separate buckets and apparently I do not find a way to do it.
Table:
| Days | Category | Values |
| 2 | A | 20 |
| 4 | B | 50 |
| 6 | A | 100 |
| 2 | A | 70 |
| 1 | B | 220 |
| 9 | A | 130 |
| 7 | A | 45 |
| 1 | A | 90 |
| 5 | B | 280 |
| 5 | B | 10 |
| 8 | A | 70 |
| 9 | B | 50 |
| 0 | A | 120 |
| 3 | B | 115 |
| 0 | B | 25 |
| 3 | B | 10 |
| 6 | A | 55 |
The result I would like to get:
| Days | Category | Values |
| 0-4 | A | 300 |
| 0-4 | B | 420 |
| 5-9 | A | 400 |
| 5-9 | B | 340 |
Based on my current knowledge this is how far I can get:
SELECT
Days, Category, Value
FROM
Table
GROUP BY
Days,
Category
But of course I cannot create the day buckets. Can you please help me out with it?
Use the DIV operator (DIV = Integer division. Discards from the division result any fractional part to the right of the decimal point):
SELECT CONCAT(
MIN(Days DIV 5) * 5,
' – ',
(MIN(Days DIV 5) + 1) * 5 - 1
) AS Days,
Category,
SUM(`Values`) AS `Values`
FROM `Table`
GROUP BY Days DIV 5,
Category
sqlfiddle

Where and group conflict?

I have a table like this:
mysql> select * from studentscore;
+------------+-----------+-------+
| student_id | cource_id | score |
+------------+-----------+-------+
| 1 | 1 | 80 |
| 1 | 2 | 90 |
| 1 | 3 | 85 |
| 1 | 4 | 78 |
| 2 | 2 | 53 |
| 2 | 3 | 77 |
| 2 | 5 | 80 |
| 3 | 1 | 71 |
| 3 | 2 | 70 |
| 3 | 4 | 80 |
| 3 | 5 | 65 |
| 3 | 6 | 75 |
| 4 | 2 | 90 |
| 4 | 3 | 80 |
| 4 | 4 | 70 |
| 4 | 6 | 95 |
| 5 | 1 | 60 |
| 5 | 2 | 70 |
| 5 | 5 | 80 |
| 5 | 6 | 69 |
| 6 | 1 | 76 |
| 6 | 2 | 88 |
| 6 | 3 | 87 |
| 7 | 4 | 80 |
| 8 | 2 | 71 |
| 8 | 3 | 58 |
| 8 | 5 | 68 |
| 9 | 2 | 88 |
| 10 | 1 | 77 |
| 10 | 2 | 76 |
| 10 | 3 | 80 |
| 10 | 4 | 85 |
| 10 | 5 | 83 |
| 11 | 3 | 80 |
| 12 | 4 | 99 |
| 13 | 5 | 74 |
+------------+-----------+-------+
I want to show student_id and students' average scores that are higher than 80.
The output I want is like this:
+------------+-------------------+
| student_id | Average |
+------------+-------------------+
| 1 | 83.25 |
| 4 | 83.75 |
| 6 | 83.66666666666667 | // and how can I make this result shorter like 83.67?
| 7 | 80 |
| 9 | 88 |
| 10 | 80.2 |
| 11 | 80 |
| 12 | 99 |
+------------+-------------------+
I've tried the following codes
mysql> select student_id, avg(score) as average_score
-> from studentscore
-> group by student_id
-> where avg(score) >= 80;
and it gave me an syntax error.
I know by rules the where clause should go before the group by clause but I can't because the where clause depends on the result from the group by clause, and if I switch their position it will give me another error("Invalid use of group function").
Can some one tell me how to get the table I want?
use "having" instead of "where"
use having instead of where.
Here's the difference:
with where you can write a predicate that will be applied to each row
with having you can write a predicate that will applied to each group
and in your case, the 2nd is the only solution that can work.
select student_id, avg(score) as average_score
from studentscore
group by student_id
having avg(score) >= 80;
where applies a filter to your data before grouping has taken place, whereas having applies a filter post-grouping. round(,2) will format as you also ask:
select student_id, round(avg(score), 2) as average_score
from studentscore
group by student_id
having average_score >= 80;

MySQL get multiple rows into columns

I have a table called visits where concat(s_id, c_id) is unique and id is the primary key. s_id is the ID number of a website and c_id is a campaign ID number. I want to show all the hits each campaign is getting and group by the site. I want each site on a single row
+-----+------+------+------+
| id | s_id | c_id | hits |
+-----+------+------+------+
| 1 | 13 | 8 | 245 |
| 2 | 13 | 8 | 458 |
| 3 | 13 | 3 | 27 |
| 4 | 13 | 4 | 193 |
| 5 | 14 | 1 | 320 |
| 6 | 14 | 1 | 183 |
| 7 | 14 | 3 | 783 |
| 8 | 14 | 4 | 226 |
| 9 | 5 | 8 | 671 |
| 10 | 5 | 8 | 914 |
| 11 | 5 | 3 | 548 |
| 12 | 5 | 4 | 832 |
| 13 | 22 | 8 | 84 |
| 14 | 22 | 1 | 7 |
| 15 | 22 | 3 | 796 |
| 16 | 22 | 4 | 0 |
+----+------+------+-------+
I would like to have the following result set:
s_id | hits | hits | hits| hits
13 | 245 | 458 | 27 | 193
14 | 320 | 183 | 783 | 226
5 | 671 | 914 | 548 | 832
22 | 84 | 7 | 796 | 0
Here is what I have tried which does not pull all the hits columns back.
SELECT v.*, v2.* FROM visits v
INNER JOIN visits v2 on v.s_id = v2.s_id
GROUP BY s_id
How can I get multiple rows into columns?
If your'e data set is not crazy huge and you are just trying to get the multiple rows as a single row.... one way to do this...
SELECT
s_id,
GROUP_CONCAT(hits SEPARATOR ',') as hits_list
FROM
visits
GROUP BY s_id
Since it doesn't use any joins or subqueries etc, i find this way to be quite fast.
you can later split/explode the data based on the ',' separator in PHP or whatever language you are using.
$hits = explode($hits_list, ','); //get them in an array

Group rows with sum and join (Build summary)

There is a rowset like this:
| ID | OP_CODE | OWNER | MEASURE | COUNT |
|----|-------------|-------|----------|-------|
| 1 | Operation 1 | 1 | Geometry | 42 |
| 2 | Operation 1 | 1 | Geometry | 48 |
| 3 | Operation 1 | 1 | Vacuum | 29 |
| 4 | Operation 1 | 1 | Electro | 14 |
| 5 | Operation 1 | 2 | Geometry | 87 |
| 6 | Operation 1 | 2 | Geometry | 112 |
| 7 | Operation 1 | 2 | Vacuum | 78 |
| 8 | Operation 1 | 3 | Vacuum | 56 |
| 9 | Operation 1 | 3 | Electro | 78 |
I want to group rows by Owner and merge/join other Measures (values of column MEASURE) with sum of column Count to this result like this:
| OWNER | GEOMETRY_CNT | VACUUM_CNT | ELECTRO_CNT | TOTAL_CNT |
|-------|--------------|------------|-------------|-----------|
| 1 | 90 | 29 | 14 | 133 |
| 2 | 199 | 78 | (null) | 277 |
| 3 | (null) | 56 | 78 | 134 |
In this case Geometry_cnt, Vacuum_cnt, Electro_cnt is a sum of corresponding values in first table:
Owner_1_Geometry_cnt=42+48=90;
Owner_1_Vacuum_cnt=29;
Owner_1_Electro_cnt=14;
Owner_1_TOTAL=29+14+90=133;
How can I get this rowset?
SQL Fiddle
Try this:
SELECT a.OWNER, SUM(IF(a.MEASURE = 'Geometry', a.COUNT, 0)) GEOMETRY_CNT,
SUM(IF(a.MEASURE = 'Vacuum', a.COUNT, 0)) VACUUM_CNT,
SUM(IF(a.MEASURE = 'Electro', a.COUNT, 0)) ELECTRO_CNT,
SUM(a.COUNT) TOTAL_CNT
FROM operations_schedule a
GROUP BY a.OWNER
Check the SQL FIDDLE DEMO
OUTPUT
| OWNER | GEOMETRY_CNT | VACUUM_CNT | ELECTRO_CNT | TOTAL_CNT |
|-------|--------------|------------|-------------|-----------|
| 1 | 90 | 29 | 14 | 133 |
| 2 | 199 | 78 | 0 | 277 |
| 3 | 0 | 56 | 78 | 134 |

MySQL - How to use GROUP BY / ORDER BY with "nested" dataset?

My (sub)query results in following dataset:
+---------+------------+-----------+
| item_id | version_id | relevance |
+---------+------------+-----------+
| 1 | 1 | 30 |
| 1 | 2 | 30 |
| 2 | 3 | 22 |
| 3 | 4 | 30 |
| 4 | 5 | 18 |
| 3 | 6 | 30 |
| 2 | 7 | 22 |
| 1 | 8 | 30 |
| 5 | 9 | 48 |
| 4 | 10 | 18 |
| 5 | 11 | 48 |
| 3 | 12 | 30 |
| 3 | 13 | 31 |
| 4 | 14 | 19 |
| 2 | 15 | 22 |
| 1 | 16 | 30 |
| 5 | 17 | 49 |
| 2 | 18 | 22 |
+---------+------------+-----------+
18 rows in set (0.00 sec)
Items and versions are stored in separate InnoDB-tables.
Both tables have auto-incrementing primary keys.
Versions have a foreign key to items (item_id).
My question: How do I get a subset based on relevance?
I would like to fetch the following subset containing the most relevant versions:
+---------+------------+-----------+
| item_id | version_id | relevance |
+---------+------------+-----------+
| 1 | 16 | 30 |
| 2 | 18 | 22 |
| 3 | 13 | 31 |
| 4 | 14 | 19 |
| 5 | 17 | 49 |
+---------+------------+-----------+
It would be even more ideal to fetch the MAX(version_id) in case of equal relevance.
I tried grouping, joining, ordering, etcetera in many ways but I'm not able to get the desired result.
Some of the things I tried is:
SELECT item_id, version_id, relevance
FROM (subquery) a
GROUP BY item_id
ORDER BY relevance DESC, version_id DESC
But of course the ordering happens after the fact, so that both relevance and MAX(version_id) information is lost.
Please advice.
This is how you can do this:
SELECT t1.item_id, max(t1.version_id), t1.relevance FROM t t1
LEFT JOIN t t2 ON t1.item_id = t2.item_id AND t1.relevance < t2.relevance
WHERE t2.relevance IS NULL
GROUP BY t1.item_id
ORDER BY t1.item_id, t1.version_id
Output:
| ITEM_ID | VERSION_ID | RELEVANCE |
|---------|------------|-----------|
| 1 | 16 | 30 |
| 2 | 18 | 22 |
| 3 | 13 | 31 |
| 4 | 14 | 19 |
| 5 | 17 | 49 |
Fiddle here.