Values present in a group on a range of numbers (SQL) - sql

I would like to know how much of a model (let's say t-shirts) with a given range of sizes (let's say 3: X, Y, Z) I have on a given date and on a given store (let's say 3: A, B, C) in stock.
Where:
X = between 40 and 50
Y = between 30 and 60
Z = between 20 and 70
The final output would look something like this (but with a lot of results):
Date | Store | Model | Availability X | Availability Y | Availability Z
02/26 | A | shirt | Yes | Yes | No
02/26 | B | shirt | Yes | No | No
02/26 | C | shirt | Yes | Yes | Yes
The availablity means I have to have in stock ALL the sizes between the given range of sizes.
I'm still trying to figure out a way to do that. The tables I have are right now designed like this (some illustrative info):
Table "sets"
id | name | initial_value | final_value
1 | X | 40 | 50
2 | Y | 30 | 60
3 | Z | 20 | 70
Table "items"
id | date | store | model | size | in_stock
1 | 02/26 | A | shirt | 40 | 1
2 | 02/26 | A | shirt | 50 | 2
3 | 02/26 | A | shirt | 30 | 0
4 | 02/26 | B | shirt | 30 | 1
I appreciate any help! Thanks.

Here is the output for SQL Server, I don't know about postgresql.
-- Create SETS
create table dbo.test_sets
(
id int not null,
name varchar(255),
initial_value int not null default (0),
final_value int not null default(0)
)
go
insert into dbo.test_sets( id, name, initial_value, final_value)
values (1, 'X', 40, 50)
insert into dbo.test_sets( id, name, initial_value, final_value)
values (2, 'Y', 30, 60)
insert into dbo.test_sets( id, name, initial_value, final_value)
values (3, 'Z', 20, 70)
go
-- Create ITEMS
create table dbo.test_items
(
id int not null,
[date] date,
store varchar(255) not null,
model varchar(255) not null,
size int not null default (0),
in_stock int not null default(0)
)
go
insert into dbo.test_items( id, [date], store, model, size, in_stock)
values (1, '02/26/2016', 'A', 'shirt', 40, 1)
insert into dbo.test_items( id, [date], store, model, size, in_stock)
values (2, '02/26/2016', 'A', 'shirt', 50, 2)
insert into dbo.test_items( id, [date], store, model, size, in_stock)
values (3, '02/26/2016', 'A', 'shirt', 30, 0)
insert into dbo.test_items( id, [date], store, model, size, in_stock)
values (4, '02/26/2016', 'B', 'shirt', 30, 1)
insert into dbo.test_items( id, [date], store, model, size, in_stock)
values (5, '02/26/2016', 'C', 'shirt', 80, 1)
go
-- Create NUMBERS LOOKUP
create table dbo.test_numbers
(
id int not null
)
go
declare #first as int
declare #step as int
declare #last as int
select #first = 1, #step = 1, #last = 100
BEGIN TRANSACTION
WHILE(#first <= #last)
BEGIN
INSERT INTO dbo.test_numbers VALUES(#first) SET #first += #step
END
COMMIT TRANSACTION
go
-- Query to provide required output
;with unique_store_models as
(
select distinct store, model from dbo.test_items
),
set_sizes as
(
select ts.id, ts.name as size_group, tn.id as size
from
dbo.test_sets ts
inner join dbo.test_numbers tn on
tn.id between ts.initial_Value and ts.final_value
),
items_by_sizes_flat as
(
select
ti.[date],
usm.store,
usm.model,
ss.size_group,
ss.size,
ti.in_stock
from
unique_store_models usm
left outer join dbo.test_items ti on
ti.store = usm.store
and ti.model = usm.model
left outer join set_sizes ss on
ss.size = ti.size
),
items_by_sizes_pivoted as
(
select
*
from
(
select
[date],
store,
model,
size_group,
--size,
in_stock
from
items_by_sizes_flat
) as p
PIVOT
(
count(in_stock) for size_group in ([X], [Y], [Z])
) as pv
)
select
[date],
store,
model,
case
when [X] > 0 then 'Yes' else 'No'
end as [Availability X],
case
when [Y] > 0 then 'Yes' else 'No'
end as [Availability Y],
case
when [Z] > 0 then 'Yes' else 'No'
end as [Availability Z]
from
items_by_sizes_pivoted
Here is the output for the above input:

Maybe you could try something like this:
SELECT date,
store,
model,
SUM(
CASE
WHEN size BETWEEN (SELECT initial_value FROM Table "sets" WHERE id = 1) AND (SELECT final_value FROM Table "sets" WHERE id = 1)
THEN in_stock
ELSE 0
END
) as "Availability X",
SUM(
CASE
WHEN size BETWEEN (SELECT initial_value FROM Table "sets" WHERE id = 2) AND (SELECT final_value FROM Table "sets" WHERE id = 2)
THEN in_stock
ELSE 0
END
) as "Availability Y",
CASE
WHEN size BETWEEN (SELECT initial_value FROM Table "sets" WHERE id = 3) AND (SELECT final_value FROM Table "sets" WHERE id = 3)
THEN in_stock
ELSE 0
END
) as "Availability Z"
FROM Table "items"
WHERE date > '02/26/2016'
AND
date < '02/26/2016'
AND
Model = 'shirt'
GROUP BY date, store, model
I think that would give you the information you're after, though if you wanted output exactly as you have it, then you could wrap another case statement around each availability case statement, or use a CTE like below:
WITH
data AS
(
SELECT date,
store,
model,
SUM(
CASE
WHEN size BETWEEN (SELECT initial_value FROM Table "sets" WHERE id = 1) AND (SELECT final_value FROM Table "sets" WHERE id = 1)
THEN in_stock
ELSE 0
END
) as availability_x,
SUM(
CASE
WHEN size BETWEEN (SELECT initial_value FROM Table "sets" WHERE id = 2) AND (SELECT final_value FROM Table "sets" WHERE id = 2)
THEN in_stock
ELSE 0
END
) as availability_y,
SUM(
CASE
WHEN size BETWEEN (SELECT initial_value FROM Table "sets" WHERE id = 3) AND (SELECT final_value FROM Table "sets" WHERE id = 3)
THEN in_stock
ELSE 0
END
) as availability_z
FROM Table "items"
WHERE date > '02/26/2016'
AND
date < '02/26/2016'
AND
Model = 'shirt'
GROUP BY date, store, model
)
SELECT date, store, model,
CASE
WHEN availability_x > 0 THEN "Yes"
ELSE "No"
END as "Availability X",
CASE
WHEN availability_y > 0 THEN "Yes"
ELSE "No"
END as "Availability Y",
CASE
WHEN availability_z > 0 THEN "Yes"
ELSE "No"
END as "Availability Z"
FROM data

This classic example of crosstab() use case.
Sample data:
-- DDL and data
CREATE TABLE items(
id SERIAL PRIMARY KEY,
"Date" DATE,
store TEXT,
model TEXT,
size INTEGER,
in_stock INTEGER
);
INSERT INTO items VALUES
(1, '02/26/2016':: DATE, 'A', 'shirt', 40, 1),
(2, '02/26/2016':: DATE, 'A', 'shirt', 50, 2),
(3, '02/26/2016':: DATE, 'A', 'shirt', 30, 0),
(4, '02/26/2016':: DATE, 'B', 'shirt', 30, 1);
CREATE TABLE sets(
id SERIAL PRIMARY KEY,
name TEXT,
initial_value INTEGER,
final_value INTEGER
);
INSERT INTO sets VALUES
(1, 'X', 40, 50),
(2, 'Y', 30, 60),
(3, 'Z', 20, 70);
For selecting I used int4range(start,end,inclusion) function to configure size ranges inclusion.Query itself:
SELECT * FROM crosstab(
'SELECT i.store,i."Date",i.model,s.name,
bool_or(CASE WHEN size_range #> i.size
THEN TRUE
ELSE FALSE
END)
FROM items i,sets s,int4range(s.initial_value, s.final_value, ''[)'') AS size_range
WHERE i.in_stock > 0
GROUP BY 1,2,3,4
ORDER BY 1,2',
'SELECT DISTINCT(name) FROM sets ORDER BY 1')
AS output(store TEXT,"Date" DATE,model TEXT,"Availability X" BOOLEAN,"Availability Y" BOOLEAN,"Availability Z" BOOLEAN);
Result:
store | Date | model | Availability X | Availability Y | Availability Z
-------+------------+-------+----------------+----------------+----------------
A | 2016-02-26 | shirt | t | t | t
B | 2016-02-26 | shirt | f | t | t
(2 rows)

Related

SQL How to update previous and next row value without using lead and lag?

I am trying to flag some rows based on a value in a a column, but I also need to put same flag for previous and next row as well based on the current row value.
so below is my table
-- create a table
CREATE TABLE table1 (
id INTEGER PRIMARY KEY,
time INTEGER,
event varchar NOT NULL
);
-- insert some values
INSERT INTO table1 VALUES (1, '1', 'r');
INSERT INTO table1 VALUES (2, '2', 'r');
INSERT INTO table1 VALUES (3, '3', 's');
INSERT INTO table1 VALUES (4, '4', 'r');
INSERT INTO table1 VALUES (5, '5', 'r');
INSERT INTO table1 VALUES (6, '6', 'r');
INSERT INTO table1 VALUES (7, '7', 's');
INSERT INTO table1 VALUES (8, '8', 'r');
INSERT INTO table1 VALUES (9, '9', 'r');
INSERT INTO table1 VALUES (10, '10', 's');
I want to add a column flag that contains 0 for event='s' and also for it's previous and next row as well. but cannot use lead or lag or temp table due to system constraints.
so my final output looks like this
+-----------+--------+------+
| timestamp | events | flag |
+-----------+--------+------+
| 1 | r | 1 |
| 2 | r | 0 |
| 3 | s | 0 |
| 4 | r | 0 |
| 5 | r | 1 |
| 6 | r | 1 |
| 7 | r | 0 |
| 8 | s | 0 |
| 9 | r | 0 |
| 10 | r | 0 |
| 11 | s | 0 |
+-----------+--------+------+
what I have tried so far is following
SELECT a.time, a.event, 0 as flag
FROM table1 AS a
JOIN table1 AS b
ON b.event = 's' AND abs(a.id - b.id) <= 1
I get all the rows which I need to flag as 0 but missing out on 1
TimeStamp is ordered time but for ease of solving converted it to integer.
Try the following:
With CTE As
(
Select id, time, event,
Case
When event='r' then -10 else id
End as f
From table1
)
Select id, time, event,
Case
when id in
(select f from cte where f<>-10
union
select f+1 from cte where f<>-10
union
select f-1 from cte where f<>-10) then 0 else 1
End As flag
From CTE
Where the -10 in When event='r' then -10 else id is any integer value not existed in the id column even if it has been added by 1.
See a demo from db<>fiddle.
Update to cover the gaps in the id column:
With CTE As
(
Select M.id, M.time, M.event,
Case
When M.event='r' then -10 else id
End as f,
Case
when M.event='s' then
(select top 1 T.id from table1 T where T.id > M.id order by T.id)
else -10
End As Lead_val,
Case
when M.event='s' then
(select top 1 T.id from table1 T where T.id < M.id order by T.id desc)
else -10
End As Lag_val
From table1 M
)
Select T.id, T.time, T.event,
Case
when T.id in (
select f from cte
union
select Lead_val from cte
union
select Lag_val from cte
)
then 0 else 1
End as flag
From table1 T
See a demo from db<>fiddle.
Another alternative. To address the possibility of gaps, I use row_number to generate a gap-less sequence and then use a self-join to avoid LEAD and LAG.
with cte as (select *, ROW_NUMBER() over (order by time) as rno from table1
)
select main.*,
case when main.event = 's' then 0
when main.event <> 's' and after.event = 's' then 0
when main.event <> 's' and prior.event = 's' then 0
else 1 end as [flag],
prior.rno as [r-1], prior.id as [prior id], prior.event as [prior event],
after.rno as [r+1], after.id as [after id], after.event as [after event]
from cte as main
left join cte as prior on main.rno = prior.rno + 1
left join cte as after on main.rno = after.rno - 1
order by main.rno;
fiddle to demonstrate - containing some extra rows with gaps to illustrate. It is not clear what logic is most appropriate for choosing the prior/next rows so I used the "time" column.

How do I join 2 tables to allocate items?

I've created 2 tables that have inventory information (item, location, qty). One of them NeedInv has item/location(s) that need X number of items. The other HaveInv has item/locations(s) with excess X number of items.
I'm trying to join or combine the 2 tables to output which items should be transferred between which locations. I have code that does this for a single distribution location & I've attempted to modify it and add logic to have it work with multiple distribution locations, but it still fails in certain situations.
I've created a [sqlfiddle]1, but the sample data is like so:
CREATE TABLE NeedInv
(item int, location varchar(1), need int)
INSERT INTO NeedInv
(item, location, need)
VALUES
(100, 'A', 4), (100, 'B', 0), (100, 'C', 2), (200, 'A', 0), (200, 'B', 1), (200, 'C', 1), (300, 'A', 3), (300, 'B', 5), (300, 'C', 0)
CREATE TABLE HaveInv
(item int, location varchar(1), have int)
INSERT INTO HaveInv
(item, location, have)
VALUES
(100, 'A', 0), (100, 'B', 3), (100, 'C', 0), (100, 'D', 3), (200, 'A', 1), (200, 'B', 0), (200, 'C', 0), (200, 'D', 1), (300, 'A', 0), (300, 'B', 0), (300, 'C', 20), (300, 'D', 5)
CREATE TABLE DesiredOutput
(item int, SourceLocation varchar(1), TargetLocation varchar(1), Qty int)
INSERT INTO DesiredOutput
(item, SourceLocation, TargetLocation, Qty)
VALUES
(100, 'B', 'A', 3), (100, 'D', 'A', 1), (100, 'D', 'C', 2), (200, 'A', 'B', 2), (200, 'A', 'C', 3), (200, 'D', 'C', 1), (300, 'C', 'A', 3), (300, 'C', 'B', 3)
I was trying to output something like this as a result of joining the tables:
+------+----------------+----------------+-----+
| item | SourceLocation | TargetLocation | Qty |
+------+----------------+----------------+-----+
| 100 | B | A | 3 |
| 100 | D | A | 1 |
| 100 | D | C | 2 |
| 200 | A | B | 2 |
| 200 | A | C | 3 |
| 200 | D | C | 1 |
| 300 | C | A | 3 |
| 300 | C | B | 3 |
+------+----------------+----------------+-----+
My current query to join the 2 tables looks like so:
select
n.*,
(case when Ord <= Remainder and (RemaingNeed > 0 and RemaingNeed < RemainingInv) then Allocated + RemaingNeed else case when RemaingNeed < 0 then 0 else Allocated end end) as NeedToFill
from (
select
n.*,
row_number() over(partition by item order by RN, (case when need > Allocated then 0 else 1 end)) as Ord,
n.TotalAvail - sum(n.Allocated) over (partition by item) as Remainder
from (
select
n.*,
n.TotalAvail - sum(n.Allocated) over (partition by item order by RN) as RemainingInv,
n.need - sum(n.Allocated) over (partition by item, location order by RN) as RemaingNeed
from (
select
n.*,
case when Proportional > need then need else Proportional end as Allocated
from (
select
row_number() over(order by need desc) as RN,
n.*,
h.location as Source,
h.have,
h.TotalAvail,
convert(int, floor(h.have * n.need * 1.0 / n.TotalNeed), 0) as Proportional
from (
select n.*, sum(need) over (partition by item) as TotalNeed
from NeedInv n) n
join (select h.*, sum(have) over (partition by item) as TotalAvail from HaveInv h) h
on n.item = h.item
and h.have > 0
) n
) n
) n
) n
where n.need > 0
It seems to work for most cases except when Allocated is set to zero, but there's still items that could be transferred. This can be seen for item 200 1 where location B only needs 1 but is going to receive 2 items, while location C which also needs 1 item will receive 0.
Any help/guidance would be appreciated!
Your query looks a little complicated for what it needs to do, IMO.
As far as I can tell, this is just a simple matter of building the logic into a query using running totals of inventory. Essentially, it's just a matter of building in rules such that if what you need can be taken from a source location, you take it, otherwise you take as much as possible.
For example, I believe the following query contains the logic required:
SELECT N.Item,
SourceLocation = H.Location,
TargetLocation = N.Location,
Qty =
CASE
WHEN N.TotalRunningRequirement <= H.TotalRunningInventory -- If the current source location has enough stock to fill the request.
THEN
CASE
WHEN N.TotalRunningRequirement - N.Need < H.TotalRunningInventory - H.Have -- If stock required has already been allocated from elsewhere.
THEN N.TotalRunningRequirement - (H.TotalRunningInventory - H.Have) -- Get the total running requirement minus stock allocated from elsewhere.
ELSE N.Need -- Otherwise just take how much is needed.
END
ELSE N.Need - (N.TotalRunningRequirement - H.TotalRunningInventory) -- Current source doesn't have enough stock to fulfil need, so take as much as possible.
END
FROM
(
SELECT *, TotalRunningRequirement = SUM(need) OVER (PARTITION BY item ORDER BY location)
FROM NeedInv
WHERE need > 0
) AS N
JOIN
(
SELECT *, TotalRunningInventory = SUM(have) OVER (PARTITION BY item ORDER BY location)
FROM HaveInv
WHERE have > 0
) AS H
ON H.Item = N.Item
AND H.TotalRunningInventory - (N.TotalRunningRequirement - N.need) > 0 -- Join if stock in source location can be taken
AND H.TotalRunningInventory - H.Have - (N.TotalRunningRequirement - N.need) < N.TotalRunningRequirement
;
Note: Your desired output doesn't seem to match your sample data for Item 200 as far as I can tell.
I was wondering if a Recursive CTE could be used for allocations.
But it turned out a bit more complicated.
The result doesn't completely align with those expected result in the question.
But since the other answer returns the same results, I guess that's fine.
So see it as just an extra method.
Test on db<>fiddle here
It basically loops through the haves and needs in the order of the calculated row_numbers.
And assigns what's still available for what's still needed.
declare #HaveNeedInv table (
item int,
rn int,
loc varchar(1),
have int,
need int,
primary key (item, rn, loc, have, need)
);
insert into #HaveNeedInv (item, loc, have, need, rn)
select item, location, sum(have), 0 as need,
row_number() over (partition by item order by sum(have) desc)
from HaveInv
where have > 0
group by item, location;
insert into #HaveNeedInv (item, loc, have, need, rn)
select item, location, 0 as have, sum(need),
row_number() over (partition by item order by sum(need) desc)
from NeedInv
where need > 0
group by item, location;
;with ASSIGN as
(
select h.item, 0 as lvl,
h.rn as hrn, n.rn as nrn,
h.loc as hloc, n.loc as nloc,
h.have, n.need,
iif(h.have<=n.need,h.have,n.need) as assign
from #HaveNeedInv h
join #HaveNeedInv n on (n.item = h.item and n.need > 0 and n.rn = 1)
where h.have > 0 and h.rn = 1
union all
select t.item, a.lvl + 1,
iif(t.have>0,t.rn,a.hrn),
iif(t.need>0,t.rn,a.nrn),
iif(t.have>0,t.loc,a.hloc),
iif(t.need>0,t.loc,a.nloc),
iif(a.have>a.assign,a.have-a.assign,t.have),
iif(a.need>a.assign,a.need-a.assign,t.need),
case
when t.have > 0
then case
when t.have > (a.need - a.assign) then a.need - a.assign
else t.have
end
else case
when t.need > (a.have - a.assign) then a.have - a.assign
else t.need
end
end
from ASSIGN a
join #HaveNeedInv t
on t.item = a.item
and iif(a.have>a.assign,t.need,t.have) > 0
and t.rn = iif(a.have>a.assign,a.nrn,a.hrn) + 1
)
select
item,
hloc as SourceLocation,
nloc as TargetLocation,
assign as Qty
from ASSIGN
where assign > 0
order by item, hloc, nloc
option (maxrecursion 1000);
Result:
100 B A 3
100 D A 1
100 D C 2
200 A B 1
200 D C 1
300 C A 3
300 C B 5
Changing the order in the row_numbers (to fill #NeedHaveInv) will change the priority, and could return a different result.

How to calculate the a third column based on the values of two previous columns?

My sample data is as follows:
Table T1:
+------+-------+
| Item | Order |
+------+-------+
| A | 30 |
| B | 3 |
| C | 15 |
| A | 10 |
| B | 2 |
| C | 15 |
+------+-------+
Table T2:
+------+-------+----------+--------+
| Item | Stock | Released | Packed |
+------+-------+----------+--------+
| A | 30 | 10 | 0 |
| B | 20 | 0 | 5 |
| C | 10 | 5 | 5 |
+------+-------+----------+--------+
Now, my requirement is to fetch the data in the following form:
+------+-------+-----------+----------------+
| Item | Order | Available | Availability % |
+------+-------+-----------+----------------+
| A | 40 | 20 | 50.00 |
| B | 5 | 15 | 100.00 |
| C | 30 | 0 | 0.00 |
+------+-------+-----------+----------------+
I am able to get the data of the first three columns using:
SELECT
T1.Item AS Item, SUM(T1.Order) AS Order, T2.Stock - T2.Released - T2.Packed AS Available
FROM T1 INNER JOIN T2 ON T1.Item = T2.Item
GROUP BY T1.Item, T2.Stock, T2.Released, T2.Packed
My question is: Is there a way to calculate the third column using the calculated values of columns 2 and 3 instead of writing down the entire formulas used to calculate those 2 columns? The reason is that the formula for calculating the third column is not small and uses the values of 2 and 3 multiple times.
Is there a way to do something like:
(CASE WHEN Available = 0 THEN 0
ELSE (CASE WHEN Available > Order THEN 100 ELSE Available/Order END) END) AS [Availability %]
What would you suggest?
Note: Please ignore the syntax used in the CASE expressions used above, I have used it just to explain the formula.
by usuing sub-query you can do that
with cte as
(
SELECT
T1.Item AS Item,
SUM(T1.Order) AS Order,
T2.Stock - T2.Released, T2.Packed AS Available
FROM T1 INNER JOIN T2 ON T1.Item = T2.Item
GROUP BY T1.Item, T2.Stock, T2.Released, T2.Packed
) select cte.*,
(
CASE WHEN Available = 0 THEN 0
ELSE (CASE WHEN Available > Order THEN 100 ELSE
100/(Order/Available)*1.00 END
) END) AS [Availability %] from cte
If you dont wont to use a CTE you can do it like this
declare #t1 table (item varchar(1), orderqty int)
declare #t2 table (item varchar(1), stock int, released int, packed int)
insert into #t1 values ('A', 30), ('B', 3), ('C', 15), ('A', 10), ('B', 2), ('C', 15)
insert into #t2 values ('A', 30, 10, 0), ('B', 20, 0, 5), ('C', 10, 5, 5)
select q.Item,
q.orderqty,
q.available,
case when q.available = 0 then 0
when q.available > q.orderqty then 100
else 100 / (q.orderqty / q.available) -- or whatever formula you need
end as [availability %]
from ( select t1.Item,
sum(t1.orderqty) as orderqty,
t2.Stock - t2.Released - t2.Packed as available
from #t1 t1
left outer join #t2 t2 on t1.Item = t2.Item
group by t1.Item, t2.Stock, t2.Released, t2.Packed
) q
The result is
Item orderqty available availability %
---- -------- --------- --------------
A 40 20 50
B 5 15 100
C 30 0 0
I think that your result table have some mistakes.. But you can have the required result by typing:
Select final_tab.Item,
final_tab.ordered,
final_tab.Available,
CASE WHEN final_tab.Available = 0 THEN 0
ELSE
(CASE WHEN final_tab.Available > final_tab.ordered THEN 100
ELSE convert(float,final_tab.Available)/convert(float,final_tab.ordered)*100 END)
END AS [Availability %]
from
(Select tab1.Item,tab1.ordered,
(Table_2.Stock-Table_2.Released-Table_2.Packed)as Available
from
( SELECT Item,sum([Order]) as ordered
FROM Table_1
group by Item )as tab1
left join
Table_2
on tab1.Item=Table_2.Item)as final_tab
You can try to play with below as well, make sure you have tested output.
declare #t1 table ([item] char(1), [order] int)
insert into #t1
values ('A', 30),
('B', 3),
('C', 30),
('A', 15),
('A', 10),
('B', 2),
('C', 15)
declare #t2 table ([item] char(1), [stock] int, [released] int, [packed] int)
insert into #t2
values ('A',30,10,0),
('B',20,0,5),
('C',10,5,5)
SELECT
T1.Item AS Item,
SUM(T1.[Order]) AS [Order],
T2.Stock - T2.Released as Available,
case when SUM(T1.[Order]) < (T2.Stock - T2.Released) then 100
else cast(cast((T2.Stock - T2.Released) as decimal) / cast(SUM(T1.[Order]) as decimal) * 100 as decimal(4,2))
end AS AvailablePercentage
FROM #T1 t1
INNER JOIN #T2 t2 ON T1.Item = T2.Item
GROUP BY T1.Item, T2.Stock, T2.Released, T2.Packed

GroupBy with respect to record intervals on another table

I prepared a sql fiddle for my question. Here it is There is a working code here. I am asking whether there exists an alternative solution which I did not think.
CREATE TABLE [Product]
([Timestamp] bigint NOT NULL PRIMARY KEY,
[Value] float NOT NULL
)
;
CREATE TABLE [PriceTable]
([Timestamp] bigint NOT NULL PRIMARY KEY,
[Price] float NOT NULL
)
;
INSERT INTO [Product]
([Timestamp], [Value])
VALUES
(1, 5),
(2, 3),
(4, 9),
(5, 2),
(7, 11),
(9, 3)
;
INSERT INTO [PriceTable]
([Timestamp], [Price])
VALUES
(1, 1),
(3, 4),
(7, 2.5),
(10, 3)
;
Query:
SELECT [Totals].*, [PriceTable].[Price]
FROM
(
SELECT [PriceTable].[Timestamp]
,SUM([Value]) AS [TotalValue]
FROM [Product],
[PriceTable]
WHERE [PriceTable].[Timestamp] <= [Product].[Timestamp]
AND NOT EXISTS (SELECT * FROM [dbo].[PriceTable] pt
WHERE pt.[Timestamp] <= [Product].[Timestamp]
AND pt.[Timestamp] > [PriceTable].[Timestamp])
GROUP BY [PriceTable].[Timestamp]
) AS [Totals]
INNER JOIN [dbo].[PriceTable]
ON [PriceTable].[Timestamp] = [Totals].[Timestamp]
ORDER BY [PriceTable].[Timestamp]
Result
| Timestamp | TotalValue | Price |
|-----------|------------|-------|
| 1 | 8 | 1 |
| 3 | 11 | 4 |
| 7 | 14 | 2.5 |
Here, my first table [Product] contains the product values for different timestamps. And second table [PriceTable] contains the prices for different time intervals. A given price is valid until a new price is set. Therefore the price with timestamp 1 is valid for Products with timestamps 1 and 2.
I am trying to get the total number of products with respect to given prices. The SQL on the fiddle produces what I expect.
Is there a smarter way to get the same result?
By the way, I am using SQLServer 2014.
DECLARE #Product TABLE
(
[Timestamp] BIGINT NOT NULL
PRIMARY KEY ,
[Value] FLOAT NOT NULL
);
DECLARE #PriceTable TABLE
(
[Timestamp] BIGINT NOT NULL
PRIMARY KEY ,
[Price] FLOAT NOT NULL
);
INSERT INTO #Product
( [Timestamp], [Value] )
VALUES ( 1, 5 ),
( 2, 3 ),
( 4, 9 ),
( 5, 2 ),
( 7, 11 ),
( 9, 3 );
INSERT INTO #PriceTable
( [Timestamp], [Price] )
VALUES ( 1, 1 ),
( 3, 4 ),
( 7, 2.5 ),
( 10, 3 );
WITH cte
AS ( SELECT * ,
LEAD(pt.[Timestamp]) OVER ( ORDER BY pt.[Timestamp] ) AS [lTimestamp]
FROM #PriceTable pt
)
SELECT cte.[Timestamp] ,
( SELECT SUM(Value)
FROM #Product
WHERE [Timestamp] >= cte.[Timestamp]
AND [Timestamp] < cte.[lTimestamp]
) AS [TotalValue],
cte.[Price]
FROM cte
Idea is to generate intervals from price table like:
1 - 3
3 - 7
7 - 10
and sum up all values in those intervals.
Output:
Timestamp TotalValue Price
1 8 1
3 11 4
7 14 2.5
10 NULL 3
You can simply add WHERE clause if you want to filter out rows where no orders are sold.
Also you can indicate the default value for LEAD window function if you want to close the last interval like:
LEAD(pt.[Timestamp], 1, 100)
and I guess it would be something like this in production:
LEAD(pt.[Timestamp], 1, GETDATE())
I think I've got a query which is easier to read. Does this work for you?
select pt.*,
(select sum(P.Value) from Product P where
P.TimeStamp between pt.TimeStamp and (
--get the next time stamp
select min(TimeStamp)-1 from PriceTable where TimeStamp > pt.TimeStamp
)) as TotalValue from PriceTable pt
--exclude entries with timestamps greater than those in Product table
where pt.TimeStamp < (select max(TimeStamp) from Product)
Very detailed question BTW
You could use a cte
;with cte as
(
select p1.[timestamp] as lowval,
case
when p2.[timestamp] is not null then p2.[timestamp] - 1
else 999999
end hival,
p1.price
from
(
select p1.[timestamp],p1.price,
row_number() over (order by p1.[timestamp]) rn
from pricetable p1 ) p1
left outer join
(select p1.[timestamp],p1.price,
row_number() over (order by p1.[timestamp]) rn
from pricetable p1) p2
on p2.rn = p1.rn + 1
)
select cte.lowval as 'timestamp',sum(p1.value) TotalValue,cte.price
from product p1
join cte on p1.[Timestamp] between cte.lowval and cte.hival
group by cte.lowval,cte.price
order by cte.lowval
It's a lot easier to understand and the execution plan compares favourably with your query (about 10%) cheaper

How to pivot rows to columns with known max number of columns

I have a table structured as such:
Pricing_Group
GroupID | QTY
TestGroup1 | 1
TestGroup1 | 2
TestGroup1 | 4
TestGroup1 | 8
TestGroup1 | 22
TestGroup2 | 2
TestGroup3 | 2
TestGroup3 | 5
What I'm looking for is a result like this:
Pricing_Group
GroupID | QTY1 | QTY2 | QTY3 | QTY4 | QTY5
TestGroup1 | 1 | 2 | 4 | 8 | 22
TestGroup2 | 2 | NULL | NULL | NULL | NULL
TestGroup3 | 2 | 5 | NULL | NULL | NULL
Note that there can only ever be a maximum of 5 different quantities for a given GroupID, there's just no knowing what those 5 quantities will be.
This seems like an application of PIVOT, but I can't quite wrap my head around the syntax that would be required for an application like this.
Thanks for taking the time to look into this!
Perfect case for pivot and you don't need a CTE:
Declare #T Table (GroupID varchar(10) not null,
QTY int)
Insert Into #T
Values ('TestGroup1', 1),
('TestGroup1', 2),
('TestGroup1', 4),
('TestGroup1', 8),
('TestGroup1', 22),
('TestGroup2', 2),
('TestGroup3', 2),
('TestGroup3', 5)
Select GroupID, [QTY1], [QTY2], [QTY3], [QTY4], [QTY5]
From (Select GroupID, QTY,
RowID = 'QTY' + Cast(ROW_NUMBER() Over (Partition By GroupID Order By QTY) as varchar)
from #T) As Pvt
Pivot (Min(QTY)
For RowID In ([QTY1], [QTY2], [QTY3], [QTY4], [QTY5])
) As Pvt2
You can pivot on a generated rank;
;with T as (
select
rank() over (partition by GroupID order by GroupID, QTY) as rank,
GroupID,
QTY
from
THE_TABLE
)
select
*
from
T
pivot (
max(QTY)
for rank IN ([1],[2],[3],[4],[5])
) pvt
>>
GroupID 1 2 3 4 5
----------------------------------------
TestGroup1 1 2 4 8 22
TestGroup2 2 NULL NULL NULL NULL
TestGroup3 2 5 NULL NULL NULL
You can also use case statement to perform the pivot:
declare #t table ( GroupID varchar(25), QTY int)
insert into #t
values ('TestGroup1', 1),
('TestGroup1', 2),
('TestGroup1', 4),
('TestGroup1', 8),
('TestGroup1', 22),
('TestGroup2', 2),
('TestGroup3', 2),
('TestGroup3', 5)
;with cte_Stage (r, GroupId, QTY)
as ( select row_number() over(partition by GroupId order by QTY ),
GroupId,
QTY
from #t
)
select GroupId,
[QTY1] = sum(case when r = 1 then QTY else null end),
[QTY2] = sum(case when r = 2 then QTY else null end),
[QTY3] = sum(case when r = 3 then QTY else null end),
[QTY4] = sum(case when r = 4 then QTY else null end),
[QTY5] = sum(case when r = 5 then QTY else null end),
[QTYX] = sum(case when r > 5 then QTY else null end)
from cte_Stage
group
by GroupId;