top of page

SQL CHALLENGE: Calculating the number of remaining inventories

Jul 16, 2024

3 min read

We want to generate an inventory age report which would show the distribution of remaining inventory across the length of time the inventory has been sitting at the warehouse. We are trying to classify the inventory on hand across the below 4 buckets to denote the time the inventory has been lying the warehouse. (Full code: my github)


0-90 days old

91-180 days old

181-270 days old

271–365 days old


For example, the warehouse received 100 units yesterday and shipped 30 units today, then there are 70 units which are a day old.


The warehouses use FIFO (first in first out) approach to manage inventory, i.e., the inventory that comes first will be sent out first.


WAREHOUSE Table:

SQL CHALLENGE: Calculating the number of remaining inventories

RESULT Table:

Your output must look like this:

SQL CHALLENGE: Calculating the number of remaining inventories

 

Solution

The items that come in first will go out first. Imagine all items listed in the order of their arrival dates. Now, each time an item is sold (OutBound), we cross off one of the (InBound) items. To implement this logic, we can expand our table to include a column of 1's, with one 1 for each InBound item. We will also add another column with one 1 for each OutBound item. By putting these two columns together and subtracting the outbound values from the InBound values, the 1's in the InBound column that are not subtracted to zero represent the remaining items.


  1. The CTE starts by listing each InBound and OutBound event types with an initial quantity of 1.

WITH CTE AS (
    SELECT 1 AS QUANTITY, event_datetime, event_type
    FROM WAREHOUSE
SQL CHALLENGE: Calculating the number of remaining inventories

  1. The recursive part of the CTE expands this to multiple rows, one for each unit up to the total quantity (OnHandQuantityDelta).

--ADD ROWS UNTIL THE THRESHOLD (OnHandQuantityDelta #) IS MET 
WITH CTE AS (
    SELECT 1 AS QUANTITY, event_datetime, event_type --BASE 
    FROM WAREHOUSE
    UNION ALL --ITERATION
    SELECT CTE.QUANTITY + 1, CTE.event_datetime, CTE.event_type
    FROM CTE
    JOIN WAREHOUSE W 
        ON CTE.event_datetime = W.event_datetime 
       AND CTE.QUANTITY + 1 <= W.OnHandQuantityDelta
),
SQL CHALLENGE: Calculating the number of remaining inventories

  1. Each row (representing a single unit) is ranked by its event_datetime within its event_type.

--ADD 1'S COLUMN AND ROW NUMBER 
RANKED AS (
    SELECT 1 AS QUANTITY, event_datetime, event_type, 
           ROW_NUMBER() OVER (PARTITION BY event_type ORDER BY event_datetime) AS rank
    FROM CTE
),
SQL CHALLENGE: Calculating the number of remaining inventories

  1. Each row of the InBound unit is joined to its corresponding OutBound unit where they have equal row numbers.

REMAINED AS (
    SELECT R1.QUANTITY AS In_QUANTITY, R1.event_type AS In_event_type, R1.rank AS In_rank, 
           R2.QUANTITY AS Out_QUANTITY, R2.event_type AS Out_event_type, R2.rank AS Out_rank
    FROM RANKED R1
    LEFT JOIN RANKED R2 
        ON R1.rank = R2.rank 
       AND R1.event_type != R2.event_type
	   WHERE R1.event_type = 'Inbound'
SQL CHALLENGE: Calculating the number of remaining inventories

  1. We then substract these two columns, and calculate remaining items.

--CALCULATE REMAINED ITEMS 
REMAINED AS (
    SELECT R1.event_datetime, 
           SUM(R1.QUANTITY - ISNULL(R2.QUANTITY, 0)) AS remained 
    FROM RANKED R1
    LEFT JOIN RANKED R2 
        ON R1.rank = R2.rank 
       AND R1.event_type != R2.event_type
    WHERE R1.event_type = 'Inbound'
    GROUP BY R1.event_datetime
), 
SQL CHALLENGE: Calculating the number of remaining inventories

  1. Now, we assign each remained row to an event_period based on the age of the event_datetime, and relative to the latest event_datetime.

--CRETAE THE 4 PERIOD INTERVALS 
PERIODS AS (
    SELECT remained, 
           CASE 
               WHEN event_datetime < DATEADD(DAY, -365, latest) THEN 0
               WHEN event_datetime < DATEADD(DAY, -270, latest) 
                    AND event_datetime >= DATEADD(DAY, -365, latest) THEN 1
               WHEN event_datetime < DATEADD(DAY, -180, latest) 
                    AND event_datetime >= DATEADD(DAY, -270, latest) THEN 2
               WHEN event_datetime < DATEADD(DAY, -90, latest) 
                    AND event_datetime >= DATEADD(DAY, -180, latest) THEN 3
               ELSE 4
           END AS event_period
    FROM REMAINED 
    CROSS JOIN (
        SELECT MAX(event_datetime) AS latest 
        FROM WAREHOUSE
    ) MAX_DAY 
), 
SQL CHALLENGE: Calculating the number of remaining inventories

  1. Lastly, we aggregate the remained column, and add columns for each age period.

--ADD THE RIGHT COLUMNS FOR OUTPUT 
RESULT AS (
    SELECT SUM(remained) AS total_remained, event_period
    FROM PERIODS 
    GROUP BY event_period
)
--REDUCE THE RESULT TABLE TO ONE ROW 
SELECT MAX(CASE WHEN event_period = 4 THEN total_remained ELSE 0 END) AS '0-90 days old',
       MAX(CASE WHEN event_period = 3 THEN total_remained ELSE 0 END) AS '91-180 days old',
       MAX(CASE WHEN event_period = 2 THEN total_remained ELSE 0 END) AS '181-270 days old',
       MAX(CASE WHEN event_period = 1 THEN total_remained ELSE 0 END) AS '271-365 days old'
FROM RESULT 
OPTION (MAXRECURSION 0); --INFINITE LOOP CTE
SQL CHALLENGE: Calculating the number of remaining inventories

Comments

Share Your ThoughtsBe the first to write a comment.

© Jasmine Abtahi 2025

bottom of page