SQL Join Inventory Count Table to Date table - mysql

I have a running inventory table of different products that records the inventory count after every transaction. Transactions do not happen every day, so the table does not have a running daily count.
I need to have all dates listed for each product so that I can sum and average the counts over a period of time.
inventory
DATE ID Qty Count
2014-05-13 123 12 12
2014-05-19 123 -1 11
2014-05-28 123 -1 10
2014-05-29 123 -3 7
2014-05-10 124 5 5
2014-05-15 124 -1 4
2014-05-21 124 -1 3
2014-05-23 124 -3 0
I have a table that includes dates for a Join, but I am not sure how to make the missing dates join over multiple products.
I need the query as follows. It needs to to return the counts over the a period selected, but also include dates inbetween.
DATE ID Qty Count
2013-05-01 123 0 0
2013-05-02 123 0 0
2013-05-03 123 0 0
2013-05-04 123 0 0
2013-05-05 123 0 0
2013-05-06 123 0 0
2013-05-07 123 0 0
2013-05-08 123 0 0
2013-05-09 123 0 0
2013-05-10 123 0 0
2013-05-11 123 0 0
2013-05-12 123 0 0
2014-05-13 123 12 12
2013-05-14 123 0 12
2013-05-15 123 0 12
2013-05-16 123 0 12
2013-05-17 123 0 12
2013-05-18 123 0 12
2014-05-19 123 -1 11
2013-05-20 123 0 11
2013-05-21 123 0 11
2013-05-22 123 0 11
2013-05-23 123 0 11
2013-05-24 123 0 11
2013-05-25 123 0 11
2013-05-26 123 0 11
2013-05-27 123 0 11
2014-05-28 123 -1 10
2014-05-29 123 -3 7
2013-05-30 123 0 7
2013-05-31 123 0 7
2013-05-01 124 0 0
2013-05-02 124 0 0
2013-05-03 124 0 0
2013-05-04 124 0 0
2013-05-05 124 0 0
2013-05-06 124 0 0
2013-05-07 124 0 0
2013-05-08 124 0 0
2013-05-09 124 0 0
2014-05-10 124 5 5
2014-05-11 124 0 5
2014-05-12 124 0 5
2014-05-13 124 0 5
2014-05-14 124 0 5
2014-05-15 124 -1 4
2014-05-16 124 0 4
2014-05-17 124 0 4
2014-05-18 124 0 4
2014-05-19 124 0 4
2014-05-20 124 0 4
2014-05-21 124 -1 3
2014-05-22 124 0 3
2014-05-23 124 -3 0
2014-05-24 124 0 0
2014-05-25 124 0 0
2014-05-26 124 0 0
2014-05-27 124 0 0
2014-05-28 124 0 0
2014-05-29 124 0 0
2014-05-30 124 0 0
2014-05-31 124 0 0

Use inv join inv to build up at least 31 rows and construct a table of 31 days. Then join the ids, and finally the original table.
select a.d, a.id, a.qty,
if(a.id=#lastid, #count:=#count+a.qty, #count:=a.count) `count`,
#lastid:=a.id _lastid
from (
select a.d, b.id, ifnull(c.qty, 0) qty, ifnull(c.count, 0) `count`
from (
select adddate('2014-05-01', #row) d, #row:=#row+1 i
from inv a
join inv b
join (select #row := 0) c
limit 31) a
join (
select distinct id
from inv) b
left join inv c on a.d = c.date and b.id = c.id
order by b.id, a.d) a
join (select #count := 0, #lastid := 0) b;
fiddle

Here are the steps needed:
Get all dates between the two given dates.
Get the initial stock per ID. This is: get the first date on or after the given start date for that ID, read this record's stock and subtract its transaction quantity.
For every date get the previous stock. If there is a record for this date, then add its transaction quantity and compare the result with its stock quantity. Throw an error if values don't match. (This is because you store data redundantly; a record's quantity must equal the quantity of the previous record plus its own transaction quantity. But data can always be inconsistent, so better check it.) Show the new stock and the difference to the previous stock.
All this would typically be achieved with a recursive CTE for the dates, a derived table for all initial stocks at best using a KEEP DENSE_RANK function, and the LAG function to look into the previous record.
MySQL doesn't support recursive CTEs - or CTEs at all for that matter. You can emulate this with a big enough table and a variable.
MySQL doesn't support the KEEP DENSE_RANK function. You can work with another derived table instead to find the minimum date per ID first.
MySQL doesn't support the LAG function. You can work with a variable in MySQL instead.
Having said this, I suggest to use a programming language instead (Java, C#, PHP, whatever). You would just select the raw data with SQL, use a loop and simply do all processiong on a per record base. This is much more convenient (and readable) than building a very complex query that does all that's needed. You can do this in SQL, even MySQL; I just don't recommend it.

The SQL I ended up using to resolve this question used a combination of #Fabricators answer (which really was a correct answer) and my edits.
I ended up using an existing table to create the date rows instead of a cross join. The cross join had poor performance for how many products I was working with.
SELECT
POSTDATE,
IF(#PROD_ID = PRODUCT_ID, #NEW := 0, #NEW := 1) AS New_Product,
(#PROD_ID := PRODUCT_ID) AS PRODUCT_ID,
QUANTITY,
IF(#NEW = 1, #INVENTORY := QUANTITY, #INVENTORY := #INVENTORY+QUANTITY) AS 'Count'
FROM (
(
SELECT
POSTDATE,
PRODUCT_ID,
QUANTITY
FROM
inventory
)
UNION ALL
(
SELECT
dateslist_sub.TransDate AS POSTDATE,
productlist_sub.PRODUCT_ID,
0 AS QUANTITY,
FROM
(
SELECT
TransDate
FROM
(
SELECT
adddate('2013-05-01', #row) AS TransDate,
#row:=#row+1 i
FROM
any_table,
(SELECT #row := 0) row
) datestable
WHERE
TransDate <= CURDATE()
) dateslist_sub
cross join (
SELECT
PRODUCT_ID
FROM
products_table
ORDER BY
PRODUCT_ID ASC
) productlist_sub
ORDER BY
productlist_sub.PRODUCT_ID ASC,
dateslist_sub.TransDate ASC
)
ORDER BY
PRODUCT_ID ASC,
POSTDATE ASC
) daily_rows_sub

Related

MySQL: Get last value of a non-grouped column in GROUP BY query

I'm running below query to join two tables and select a few columns. There are multiple values of points_balance but as I'm doing GROUP BY, I'm getting very first value of points_balance (which seems default). The use case is to fetch the last value of points_balance which will be the latest one in my case.
What updates below query requires for that? TIA
SELECT DATE(main_table.created_at) AS period, main_reward.customer_id,
main_reward.website_id,
SUM(IF(points_delta > 0, points_delta, 0 )) AS points_added,
SUM(IF(points_delta < 0 && is_expired = 0, ABS(points_delta), 0 )) AS points_used,
SUM(IF(points_delta < 0 && is_expired = 1, ABS(points_delta), 0 )) AS points_expired,
main_table.points_balance
FROM magento_reward_history AS main_table
INNER JOIN magento_reward AS main_reward ON main_table.reward_id = main_reward.reward_id
GROUP BY period, customer_id, website_id
Table schemas with some test data are:
magento_reward
reward_id
customer_id
website_id
points_balance
website_currency_code
75505
218501
1
71
magento_reward_history
history_id
reward_id
website_id
store_id
action
entity
points_balance
points_delta
points_used
points_voided
currency_amount
currency_delta
base_currency_code
additional_data
comment
created_at
expired_at_static
expired_at_dynamic
is_expired
is_duplicate_of
notification_sent
is_processed
313769
75505
1
1
8
949831
64
64
64
0
3.0000
3.0000
USD
2021-05-18 00:47:38
2022-05-18 00:47:38
2022-05-18 00:47:38
0
0
313770
75505
1
1
8
949832
109
45
45
0
5.0000
2.0000
USD
2021-05-18 00:50:18
2022-05-18 00:50:18
2022-05-18 00:50:18
0
0
313775
75505
1
1
8
949835
138
29
11
0
6.0000
1.0000
USD
2021-05-19 16:23:56
2022-05-19 16:23:56
2022-05-19 16:23:56
0
0
313783
75505
1
1
1
18
-120
0
0
0.0000
-6.0000
USD
2021-05-19 23:08:43
2022-05-19 23:08:43
2022-05-19 23:08:43
0
0
313784
75505
1
1
8
949840
71
53
0
0
3.0000
2.0000
USD
2021-05-19 23:08:46
2022-05-19 23:08:46
2022-05-19 23:08:46
0
0
For this data, I need to get 109 as points_balance for 2021-05-18, and 71 for 2021-05-19. Currently, I'm getting 64 and 138 which are the very first values for these dates.

Processing values in a column for each group

I have a MySQL table of customers and the shop branches they have purchased from, similar to the following:
customer_id | branch_id | is_major_branch
-----------------------------------------------
5 24 1
5 83 0
5 241 0
8 66 0
8 72 0
9 15 1
16 31 1
16 61 1
is_major_branch is 1 if that branch is a particularly large store.
How can I delete all rows where a customer has shopped in a minor branch (is_major_branch = 0), except if a customer has only ever shopped in a minor branch? Example result set:
customer_id | branch_id | is_major_branch
-----------------------------------------------
5 241 1
8 66 0
8 72 0
9 15 1
16 31 1
16 61 1
Notice how customer 8 has only ever shopped in a minor branches, so we ignore them from the deletion.
You can delete the rows doing:
delete t
from t join
(select customer_id, max(is_major_branch) as max_is_major_branch
from t
group by customer_id
) tt
on t.customer_id = tt.customer_id
where t.is_major_branch = 0 and tt.max_is_major_branch = 1;
If you just want a select query, then use exists:
select t.*
from t
where not (t.is_major_branch = 0 and
exists (select 1 from t t2 where t2.customer_id = t.customer_id and t2.is_major_branch = 1)
);

Show mysql result of one column in different columns

I'm new to mysql and currently stuggeling with a little weird problem,
I am faced with a list with three rows:
Customernr Type Amount
------------------------------
111 A 10
111 B 5
111 C 21
222 B 12
333 A 20
333 C 14
I need to format the output like:
Customernr A B C
----------------------------------
111 10 5 21
222 0 12 0
333 20 0 14
I have tried it with multiple "if" clauses:
SELECT distinct `Customernr`,
IF(`Type`='A' ,`Amount`, 0) as A,
IF(`Type`='B' ,`Amount`, 0) as B,
IF(`Type`='C' ,`Amount`, 0) as C,
FROM `database`
The result is:
Customernr A B C
----------------------------------
111 10 0 0
111 0 5 0
111 0 0 21
222 0 12 0
333 20 0 0
333 0 0 14
If I add a group by Customern
the result will display only the first value
Customernr A B C
----------------------------------
111 10 0 0
222 0 0 0
333 20 0 0
Maybe my approach is totally wrong but I hope you can understand the problem, not all customers do not have all types but I need to display every customer in one row including all his amounts of types even if the customer does not have all types.
I have searched through stackoverflow and as this is my first post I hop I am doing everything right.
Any help appreciated.
Try this
SELECT Customernr,sum(temp.A) as A,sum(temp.B) as B,sum(temp.C) as C from(
SELECT `Customernr`,
IF(`Type`='A' ,(SELECT SUM(AMOUNT) FROM test_db td2 WHERE TYPE='A' AND td2.Customernr=td.Customernr ), 0) as A,
IF(`Type`='B' ,(SELECT SUM(AMOUNT) FROM test_db td2 WHERE TYPE='B' AND td2.Customernr=td.Customernr ), 0) as B,
IF(`Type`='C' ,(SELECT SUM(AMOUNT) FROM test_db td2 WHERE TYPE='C' AND td2.Customernr=td.Customernr ), 0) as C
FROM test_db td ) as temp GROUP BY Customernr
I got the result
111 10 5 21
222 0 12 0
333 20 0 14

Increment Group Number based on row value SQL Server 2008

I have two tables that aren't really associated, but need to be combined. So I'm using union all on the two tables. The unioned tables are ordered by date, so rows from one table are dispersed among rows from the other table. What I need to do is get a running count of a column so I can group elements.
To explain further, table A holds dates of when a container is emptied, while table B holds daily entries for content of the container. I need to union the two tables so I have one table where I can get the sum of the information for a container before the container is emptied.
So I need something like this:
Table A:
Location_ID Empty Date
123 3/2/13
123 3/10/13
123 4/1/13
Table B:
PSI Entry Date Location_ID
120 2/28/13 123 (same for all)
130 3/1/13
100 3/8/13
110 3/9/13
200 3/18/13
180 3/20/13
So the unioned table after some magic would look like:
Table C...:
Location_ID Date PSI Emptied
123 2/28/13 120 0
123 3/1/13 130 0
123 3/2/13 null 1
123 3/8/13 100 0
123 3/9/13 110 0
123 3/10/13 null 1
123 3/18/13 200 0
123 3/20/13 180 0
123 4/1/13 null 1
What I need to do is have a grouping such that I can have a table like this
Table C_b
Location_ID Date PSI Emptied Group
123 2/28/13 120 0 1
123 3/1/13 130 0 1
123 3/2/13 null 1 1
123 3/8/13 100 0 2
123 3/9/13 110 0 2
123 3/10/13 null 1 2
123 3/18/13 200 0 3
123 3/20/13 180 0 3
123 4/1/13 null 1 3
How can I get that grouping in that way? I have to make it work in SQL Server 2008. I have tried using Count, and Rank, and Row_Number. But the problem with those is that it won't do a running count, it will just say the total count in each row.
Try this query:
DECLARE #MyTable TABLE(
EntryDate DATE NOT NULL,
Emptied BIT NOT NULL
);
INSERT INTO #MyTable (EntryDate,Emptied)
VALUES
('2013-01-01',0),
('2013-01-02',0),
('2013-01-03',1),
('2013-01-04',0),
('2013-01-05',0),
('2013-01-06',1),
('2013-01-07',0),
('2013-01-08',0),
('2013-01-09',1);
DECLARE #TableWithRowNum TABLE(
EntryDate DATE NOT NULL,
Emptied BIT NOT NULL,
RowNum INT PRIMARY KEY
);
INSERT INTO #TableWithRowNum (EntryDate,Emptied,RowNum)
SELECT crt.*,ROW_NUMBER() OVER(ORDER BY crt.EntryDate) AS RowNum
FROM #MyTable crt;
WITH RecCTE
AS(
SELECT
crt.EntryDate,
crt.Emptied,
crt.RowNum,
1 AS Grp
FROM #TableWithRowNum crt
WHERE crt.RowNum=1
UNION ALL
SELECT
crt.EntryDate,
crt.Emptied,
crt.RowNum,
CASE WHEN prev.Emptied=1 THEN prev.Grp+1 ELSE prev.Grp END
FROM #TableWithRowNum crt INNER JOIN RecCTE prev ON crt.RowNum=prev.RowNum+1
)
SELECT * FROM RecCTE
OPTION(MAXRECURSION 0); -- Default value for MAXRECURSION is 100
GO
Results:
EntryDate Emptied RowNum Grp
---------- ------- ------ ---
2013-01-01 0 1 1
2013-01-02 0 2 1
2013-01-03 1 3 1
2013-01-04 0 4 2
2013-01-05 0 5 2
2013-01-06 1 6 2
2013-01-07 0 7 3
2013-01-08 0 8 3
2013-01-09 1 9 3

access query needed

I am looking for an access query, but a sql server 2008 could be sufficient as I can use a passthrough feature in access.
My data looks like this .
--------------------------------------------------------------
id nameid name score diff include
--------------------------------------------------------------
1 0001 SO 100 0 0
2 0001 SO 100 0 0
3 0001 SO 100 0 0
4 0001 SO 100 0 0
5 0001 SO 100 0 0
6 0001 SO 100 0 0
7 0002 MO 10 0 0
8 0002 MO 18 0 1
9 0002 MO 20 0 0
10 0002 MO 14 0 0
11 0002 MO 100 0 0
11 0002 MO 100 0 0
12 0003 MA 10 0 0
13 0003 MA 18 0 1
14 0003 MA 20 0 0
15 0003 MA 14 0 0
16 0003 MA 100 0 1
17 0003 MA 100 0 0
Now what i want is to go through each row and only select the rows where include = 1. THIS IS EASY however ,I don't want the entire row.. I want to select the "group". The group can be identified by the nameid (or name).
So for the above I want the following result:
--------------------------------------------------------------
id nameid name score diff include
--------------------------------------------------------------
7 0002 MO 10 0 0
8 0002 MO 18 0 1
9 0002 MO 20 0 0
10 0002 MO 14 0 0
11 0002 MO 100 0 0
11 0002 MO 100 0 0
12 0003 MA 10 0 0
13 0003 MA 18 0 1
14 0003 MA 20 0 0
15 0003 MA 14 0 0
16 0003 MA 100 0 1
17 0003 MA 100 0 0
Ask your table for row with include = 1.
Then join again with the table to have all the rows corresponding to the first query's nameid :
SELECT DISTINCT m.*
FROM myTable m
INNER JOIN myTable m2
ON m.nameid = m2.nameid
AND m2.include = 1
A join query will work better than an 'in' query for big amount of datas. You still need an index on the field 'nameid', and on 'include' could not hurt too.
An equivalent is with 'WHERE EXISTS' :
SELECT m.*
FROM myTable m
WHERE EXISTS
(
SELECT *
FROM myTable m2
WHERE m2.include = 1
AND m2.nameid = m.nameid
)
You could see the difference here :
Can an INNER JOIN offer better performance than EXISTS
And why you have to use a Where exists when you have a filter with a lot of IDs :
Difference between EXISTS and IN in SQL?
I think this query identifies the nameid values you want included in your main query.
SELECT DISTINCT nameid
FROM YourTable
WHERE include = 1;
If that is true, incorporate it as a subquery and use an INNER JOIN with YourTable to return only those rows for which a nameid value is associated with include = 1 ... in any row of the table.
SELECT id, nameid, name, score, diff, include
FROM
YourTable AS y
INNER JOIN (
SELECT DISTINCT nameid
FROM YourTable
WHERE include = 1
) AS q
ON y.nameid = q.nameid;
The Access query designer will probably substitute square brackets plus a dot in place of the parentheses enclosing the subquery.
SELECT id, nameid, name, score, diff, include
FROM
YourTable AS y
INNER JOIN [
SELECT DISTINCT nameid
FROM YourTable
WHERE include = 1
]. AS q
ON y.nameid = q.nameid;
You need a subquery - as follows:
SELECT *
FROM tablename
WHERE nameid IN
(
SELECT DISTINCT nameid
FROM tablename
WHERE include = 1
)
SELECT * FROM yourTable WHERE nameid IN (SELECT DISTINCT nameid FROM yourTable WHERE include=1)
What you do is, select every row, whose nameid is in your subquery.
The subquery selects the nameid for rows where include=1.