How to group value within a certain date range or every certain days in SQL server - sql

For example table would be:
Customer OrderDate OrderAmt
-------- ---------- ---------
A1 20140101 920.00
A2 20140111 301.00
A2 20140123 530.00
A1 20140109 800.00
A3 20140110 500.00
A1 20140115 783.00
A3 20140217 500.00
A1 20140219 1650.00
A1 20140225 780.00
...
A3 20140901 637.00
I want to group them and calculate the sum(OrderAmt) within every 20 days and group by customer start from 20140101.

For what it's worth, you can accomplish what you describe with a pretty simple DATEDIFF() / GROUP BY operation, as below: whether or not that is actually what you want might be another question. I suspect that the DateBucket calculation might actually be something else ...
CREATE TABLE #tmpCustomer (Customer VARCHAR(2), OrderDate VARCHAR(10), OrderAmt DECIMAL(6,2))
INSERT INTO #tmpCustomer (Customer, OrderDate, OrderAmt)
SELECT 'A1',20140101, 920.00 UNION
SELECT 'A2',20140111, 301.00 UNION
SELECT 'A2',20140123, 530.00 UNION
SELECT 'A1',20140109, 800.00 UNION
SELECT 'A3',20140110, 500.00 UNION
SELECT 'A1',20140115, 783.00 UNION
SELECT 'A3',20140217, 500.00 UNION
SELECT 'A1',20140219, 1650.00 UNION
SELECT 'A1',20140225, 780.00 UNION
SELECT 'A3',20140901, 637.00
SELECT
Customer,
(DATEDIFF(day, '1/1/2014', CAST(OrderDate AS DATE)) / 20) + 1 AS DateBucket,
SUM(OrderAmt) SumOrderAmt
FROM #tmpCustomer
GROUP BY Customer, (DATEDIFF(day, '1/1/2014', CAST(OrderDate AS DATE)) / 20) + 1
ORDER BY Customer, DateBucket

You need to do two things:
(1) Create some sort of guide hold the '20 day groups' information. A Recursive CTE does this pretty well, and
(2) Recast that varchar date as an actual date for comparison purposes.
Then it's just joining the order data into that daterange grouping and summing the order amounts.
-------------------------
-- Here i'm just recreating your example
-------------------------
DECLARE #customerOrder TABLE (Customer varchar(2), OrderDate varchar(8), OrderAmt decimal(8,2))
INSERT INTO #customerOrder (Customer, OrderDate, OrderAmt)
VALUES
('A1', '20140101', 920.00),
('A2', '20140111', 301.00),
('A2', '20140123', 530.00),
('A1', '20140109', 800.00),
('A3', '20140110', 500.00),
('A1', '20140115', 783.00),
('A3', '20140217', 500.00),
('A1', '20140219', 1650.00),
('A1', '20140225', 780.00),
('A3', '20140901', 637.00)
-------------------------
-- Set up a table that lists off 20 day periods starting from 1/1/2014
-------------------------
DECLARE #startDate datetime, #endDate datetime;
SET #startDate = {d N'2014-01-01'};
SET #endDate = {d N'2014-12-31'};
WITH [dates] ([Sequence], [startDate], [maxExcludedDate]) AS
(SELECT 1 AS [Sequence]
,#startDate AS [startDate]
,DATEADD(d, 20, #startDate) AS [maxExcludedDate]
UNION ALL
SELECT Sequence + 1 AS Sequence
,DATEADD(d, 20, [startDate]) AS [startDate]
,DATEADD(d, 40, [startDate]) AS [maxExcludedDate]
FROM [dates]
WHERE [startDate] < #endDate
)
, dateFrame AS
(
SELECT
[startDate]
,[maxExcludedDate]
FROM [dates]
)
-------------------------
-- Set up a table that holds the orderDates as actual dates
-------------------------
, castData AS
(
SELECT
cast(orderdate as datetime) castDate
,OrderAmt
FROM #customerOrder
)
-------------------------
-- JOIN and sum.
-------------------------
SELECT
[startDate]
, Sum(OrderAmt) perodAmt
FROM
dateFrame df
left join castData cd
on cd.castDate >= df.startDate
and cd.castDate < df.maxExcludedDate
GROUP BY
[startDate]
ORDER by
[startDate]

Assuming that the OrderDate is a numeric field (not varchar). I'm also assuming that you don't need to go much more than a year in the future. If you want the gaps shown, keep the left join, if you don't want the gaps, then make it an inner join. (You can also make the hardcoded date a variable of where to start, I just kept it as the 20140101 that you mentioned.
with Numbers as
(Select 0 as Num
UNION ALL
Select Num+1
FROM Numbers
WHERE Num+1<= 20
)
, DateList AS
(Select Year(DateAdd(dd,20*(Num), Cast('2014-01-01' as date))) * 10000+Month(DateAdd(dd,20*(Num), Cast('2014-01-01' as date)))*100+Day(DateAdd(dd,20*(Num), Cast('2014-01-01' as date))) Groupingdatemin
, Year(DateAdd(dd,20*(Num+1)-1, Cast('2014-01-01' as date)))*10000+ MONTH(DateAdd(dd,20*(Num+1)-1, Cast('2014-01-01' as date)))*100+DAY(DateAdd(dd,20*(Num+1)-1, Cast('2014-01-01' as date))) Groupingdatemax
from Numbers
)
select Customer, sum(orderamt), Groupingdatemin, Groupingdatemax from DateLIst d LEFT JOIN
<yourtable> t on t.orderdate between d.Groupingdatemin and d.Groupingdatemax
group by customer, Groupingdatemin, Groupingdatemax

Related

LEFT outer join returns returns duplicate rows even after adding distinct

Restricting the price change table to select a particular item
select * from price
where item = '13'
results of the query above
item Date_Changed New Old START_DATE end_DATE
13 01/11/2018 00:00 5.61 4.88 01/11/2018 00:00 30/11/2018 00:00
13 30/11/2018 00:00 2.84 5.61 01/11/2018 00:00 17/12/2018 00:00
13 17/12/2018 00:00 2.39 2.84 30/11/2018 00:00 17/12/2018 00:00
sales table
Date Item Qty Amount
05/07/2018 00:00 13 3 14.64
05/07/2018 00:00 13 3 14.64
04/07/2018 00:00 13 3 14.64
02/07/2018 00:00 13 1 4.88
02/07/2018 00:00 13 6 29.28
06/07/2018 00:00 13 7 34.16
03/07/2018 00:00 13 4 19.52
12/07/2018 00:00 13 2 9.76
10/08/2018 00:00 13 1 4.88
Sample code
SELECT distinct a.[Inv]
, (CASE
WHEN a.Date <= b.START_DATE THEN (b.Old * a.Qty)
WHEN a.Date between b.START_DATE and b.dt_end_DATE THEN (b.New * a.Qty)
ELSE 0
END) as calc_amount
,(a.[amount] - (CASE
WHEN a.Date <= b.START_DATE THEN (b.Old * a.Qty)
WHEN a.Date between b.START_DATE and b.end_DATE THEN (b.New * a.Qty)
ELSE 0
END)) as variance
[sales] a
left outer join price b
on a.[Item] = b.item
where b.item = '13'
The script then returns 27 rows instead of 9 rows. can someone assist on how i can improve my script to be more accurate
Use outer apply. I assume you want the most recent start date from price, so this looks like:
select s.*,
(case when s.date <= p.start_date
then p.Old * s.Qty
else p.New * s.Qty
end) as calc_amount
from sales s outer apply
(select top (1) p.*
from prices p
where p.item = s.item and
p.start_date <= s.date
order by p.date desc
) p
I am not sure if this is what you are looking for. but perhaps you can skip the join?
I created 2 sample data with the columns that you might need.
DECLARE #price table (item varchar(2),date_start date, new_price numeric(9,2))
Insert into #price (item , date_start,new_price)
values
( '13', '20190101', '1.00'),
( '13', '20190102', '1.01'),
( '13', '20190103', '1.02')
DECLARE #sales table (item varchar(2),date_sales date,qty int)
Insert into #sales (item , date_sales,qty)
values
( '13', '20190101', '5'),
( '13', '20190101', '2'),
( '13', '20190102', '5'),
( '13', '20190102', '2'),
( '13', '20190103', '5'),
( '13', '20190103', '2')
declare #item as varchar(2) = '13'
SELECT (select top (1) new_price from #price b where a.date_sales>=b.date_start and b.item = #item order by b.date_start desc ) * a.qty as 'new_price* qty'
from #sales a
where a.item = #item
I have not tested this in a table with a huge data set, so I also can't vouch for the speed of this query. I believe it would be better to have some kind of other ID to join the table
Your question is not clear... You added sample data, but I doubt this is correct...
Your price table is open to erronous data. It would be better to store just the price and a validFrom-date. In this case you can pick the price on a give date easily. Your format is open to overlapping periodes and there is no good reason to store the former price once again. That's why I ignore all fields you should not use...
Try this. I've changed the dates in a way to simulate validity periodes.
A mock-up scenario (please to this the next time for us):
CREATE TABLE priceMock(item INT, Date_Changed DATE, New DECIMAL(10,4), Old DECIMAL(10,4), [START_DATE] DATE, end_DATE DATE);
SET DATEFORMAT dmy;
INSERT INTO priceMock VALUES
(13,'01/11/2018 00:00',5.61,4.88,'01/07/2018 00:00','06/07/2018 00:00')
,(13,'30/11/2018 00:00',2.84,5.61,'07/07/2018 00:00','10/07/2018 00:00')
,(13,'17/12/2018 00:00',2.39,2.84,'11/07/2018 00:00','15/08/2018 00:00');
GO
CREATE TABLE salesMock ([Date] DATE, Item INT, Qty INT, Amount DECIMAL(10,4));
SET DATEFORMAT dmy;
INSERT INTO salesMock VALUES
('05/07/2018 00:00',13,3,14.64)
,('05/07/2018 00:00',13,3,14.64)
,('04/07/2018 00:00',13,3,14.64)
,('02/07/2018 00:00',13,1,4.88 )
,('02/07/2018 00:00',13,6,29.28)
,('06/07/2018 00:00',13,7,34.16)
,('03/07/2018 00:00',13,4,19.52)
,('10/07/2018 00:00',13,2,9.76 )
,('10/08/2018 00:00',13,1,4.88 );
GO
I'd add an inline-table-valued-function to get exactly one single line back.
CREATE FUNCTION dbo.GetPriceForItemOnDate(#item INT,#ValidOn DATE)
RETURNS TABLE
AS
RETURN
SELECT TOP 1 *
FROM priceMock
WHERE item=#item
AND [START_DATE] <= #ValidOn
ORDER BY [START_DATE] DESC
GO
--This query will combine your sales data with the price valid on the given date
SELECT s.[Date]
,s.Item
,s.Qty
,p.New AS CurrentPrice
,s.Qty * p.New AS ComputedAmount
FROM salesMock s
OUTER APPLY dbo.GetPriceForItemOnDate(s.item,s.[Date]) p
GO
--Clean up (carefull with real data)
DROP FUNCTION dbo.GetPriceForItemOnDate;
DROP TABLE priceMock;
DROP TABLE salesMock;
The idea in short:
The function will first filter to price lines for the given item. The second filter will cut the list and return just the prices for the given date and before the given date. As we sort this by the date in descending order we will get the latest price on top. By using TOP 1 we return just the one single line we want.
General remark: I use a validFrom-approach here. But you can turn this to the opposite and use a validTo-approach. The idea is the same.

Any one help me to solve this i try my best but did not solve this?

ItemName Price CreatedDateTime
New Card 50.00 2014-05-26 19:17:09.987
Recharge 110.00 2014-05-26 19:17:12.427
Promo 90.00 2014-05-27 16:17:12.427
Membership 70.00 2014-05-27 16:17:12.427
New Card 50.00 2014-05-26 19:20:09.987
Out Put : Need a query which Sum the sale of Current hour and
sale of item which have maximum sale in that hour in breakdownofSale
Column.
Hour SaleAmount BreakDownOfSale
19 210 Recharge
16 160 Promo
This should do it
create table #t
(
ItemName varchar(50),
Price decimal(18,2),
CreatedDateTime datetime
);
set dateformat ymd;
insert into #t values('New Card', 50.00, '2014-05-26 19:17:09.987');
insert into #t values('Recharge', 110.00, '2014-05-26 19:17:12.427');
insert into #t values('Promo', 90.00, '2014-05-27 16:17:12.427');
insert into #t values('Membership', 70.00, '2014-05-27 16:17:12.427');
insert into #t values('New Card', 50.00, '2014-05-26 19:20:09.987');
with cte as
(
select datepart(hh, CreatedDateTime) as [Hour],
ItemName,
Price,
sum(Price) over (partition by datepart(hh, CreatedDateTime)) SaleAmount,
ROW_NUMBER() over (partition by datepart(hh, CreatedDateTime) order by Price desc) rn
from #t
)
select Hour,
SaleAmount,
ItemName
from cte
where rn = 1
Though i am not clear with the question, based on your desired output, you may use the query as below.
SELECT DATEPART(HOUR,CreatedDateTime) AS Hour, sum(Price) AS Price, ItemName AS BreakDownOfSale from TableName WHERE BY ItemName,DATEPART(HOUR,CreatedDateTime)
Replace table name and column name with the actual one.
Hope this helps!
Here is the sample query.
You can use SQL Server Windows functions to get the result you need.
DECLARE #Table TABLE
(
ItemName NVARCHAR(40),
Price DECIMAL(10,2),
CreatedDatetime DATETIME
)
-- Fill table.
INSERT INTO #Table
( ItemName, Price, CreatedDatetime )
VALUES
( N'New Card' , 50.00 , '2014-05-26 19:17:09.987' ),
( N'Recharge' , 110.00 , '2014-05-26 19:17:12.427' ) ,
( N'Promo' , 90.00 , '2014-05-27 16:17:12.427' ) ,
( N'Membership' , 70.00 , '2014-05-27 16:17:12.427' ) ,
( N'New Card' , 50.00 , '2014-05-26 19:20:09.987' )
-- Check record(s).
SELECT * FROM #Table
-- Get record(s) in required way.
;WITH T1 AS
(
SELECT
DATEPART(HOUR, T.CreatedDatetime) AS Hour,
CONVERT(DATE, T.CreatedDatetime) AS Date,
T.ItemName AS BreakDownOfSales,
-- Date and hour both will give unique record(s)
SUM(Price) OVER (PARTITION BY CONVERT(DATE, T.CreatedDatetime), DATEPART(HOUR, CreatedDateTime)) AS SaleAmount,
ROW_NUMBER() OVER(PARTITION BY CONVERT(DATE, T.CreatedDatetime), DATEPART(HOUR, T.CreatedDatetime) ORDER BY T.Price DESC) AS RN
FROM
#Table T
)
SELECT
T1.Date ,
T1.Hour ,
T1.SaleAmount,
T1.BreakDownOfSales
FROM
T1
WHERE T1. RN = 1
ORDER BY
T1.Hour
Check this simple solution, Please convert it to SQL Server Query.
This will give you perfect result even if you have multiple date data.
SELECT HOUR(CreatedDateTime), SUM(Price),
(SELECT itemname FROM t it WHERE HOUR(ot.CreatedDateTime) = HOUR(it.CreatedDateTime) AND
DATE(ot.CreatedDateTime) = DATE(it.CreatedDateTime)
GROUP BY itemname
ORDER BY price DESC
LIMIT 1
) g
FROM t ot
GROUP BY HOUR(CreatedDateTime);

Calculate Average Qty On Hand of Inventory

I'm trying to find the average qty on hand of my inventory over a date range from parameter #StartDate by averaging the ending qty from each day. I have three tables: a part table, a part transaction table, and a warehouse table, mocked up below.
PartNum | PartNum TranDate TranQty | PartNum OnHandQty
---------- | ------------------------------------ | --------------------
P1 | P1 6/28/2016 5 | P1 30
P2 | P1 6/26/2016 3 | P2 2
| P1 6/26/2016 -1 |
| P1 6/15/2016 2 |
| P2 6/15/2016 1 |
If today is 6/30/2016 and #StartDate = 6/1/2016, I expect a result like:
PartNum AverageOnHand
------------------------
P1 22.9
P2 1.5
However, I don't know what function would best allow me to get to an appropriate weighted sum which I could divide by the difference in dates. Is there a SumProduct function or similar that I can use here? My code, so far, is below:
select
[Part].[PartNum] as [Part_PartNum],
(max(PartWhse.OnHandQty)*datediff(day,max(PartTran.TranDate),Constants.Today)) as [Calculated_WeightedSum],
(WeightedSum/DATEDIFF(day, #StartDate, Constants.Today)) as [Calculated_AverageOnHand]
from Erp.Part as Part
right outer join Erp.PartTran as PartTran on
Part.PartNum = PartTran.PartNum
inner join Erp.PartWhse as PartWhse on
Part.PartNum = PartWhse.PartNum
group by [Part].[PartNum]
Here is a sql-server 2012 + method that is interesting.
;WITH cte AS (
SELECT
p.PartNum
,CAST(t.TranDate AS DATE) AS TranDate
,i.OnHandQty
--,SUM(SUM(t.TranQty)) OVER (PARTITION BY p.PartNum ORDER BY CAST(t.TranDate AS DATE) DESC) AS InventoryChange
,i.OnHandQty - SUM(SUM(t.TranQty)) OVER (PARTITION BY p.PartNum ORDER BY CAST(t.TranDate AS DATE) DESC) AS InventoryOnDate
,DATEDIFF(day,
CAST(ISNULL(LAG(MAX(TranDate)) OVER (PARTITION BY p.PartNum ORDER BY CAST(t.TranDate AS DATE) ASC),#StartDate) AS DATE)
,CAST(t.TranDate AS DATE)
) AS DaysAtInventory
FROM
#Parts p
LEFT JOIN #Transact t
ON p.PartNum = t.PartNum
LEFT JOIN #Inventory i
ON p.PartNum = i.PartNum
GROUP BY
p.PartNum
,CAST(t.TranDate AS DATE)
,i.OnHandQty
)
SELECT
PartNum
,(SUM(ISNULL(DaysAtInventory,0) * ISNULL(InventoryOnDate,0))
+ ((DATEDIFF(day,MAX(TranDate),CAST(GETDATE() AS DATE)) + 1) * ISNULL(MAX(OnHandQty),0)))
/((DATEDIFF(day,CAST(#StartDate AS DATE),CAST(GETDATE() AS DATE)) + 1) * 1.00) AS AvgDailyInventory
FROM
cte
GROUP BY
PartNum
This one actually gave me the 22.9 but 1.53333 the 333 gets introduced because 1 day has to get put somewhere so I stuck it as the current inventory.
Here is a previous method I answered with and this one it is a little easier to conceptualize the data..... I would be curious about performance differences between the 2 methods.
Some of these steps can be combined to be a little more concise but this works (although I got 22.6 not .1 or .9....) I rounded everything to a whole date while doing this so that you don't have to worry about beginning and end of day.
DECLARE #StartDate DATETIME = '6/1/2016'
;WITH cteDates AS (
SELECT #StartDate AS d
UNION ALL
SELECT
d + 1 AS d
FROM
cteDates c
WHERE c.d + 1 <= CAST(CAST(GETDATE() AS DATE) AS DATETIME)
--get dates to today beginning of day
)
, ctePartsDaysCross AS (
SELECT
d.d
,p.PartNum
,ISNULL(i.OnHandQty,0) AS OnHandQty
FROM
cteDates d
CROSS JOIN #Parts p
LEFT JOIN #Inventory i
ON p.PartNum = i.PartNum
)
, cteTransactsQuantityByDate AS (
SELECT
CAST(t.TranDate AS DATE) as d
,t.PartNum
,TranQty = SUM(t.TranQty)
FROM
#Transact t
GROUP BY
CAST(t.TranDate AS DATE)
,t.PartNum
)
,cteDailyInventory AS (
SELECT
c.d
,c.PartNum
,c.OnHandQty - SUM(ISNULL(t.TranQty,0)) OVER (PARTITION BY c.PartNum ORDER BY c.d DESC) AS DailyOnHand
FROM
ctePartsDaysCross c
LEFT JOIN cteTransactsQuantityByDate t
ON c.d = t.d
AND c.PartNum = t.PartNum
)
SELECT
PartNum
,AVG(CAST(DailyOnHand AS DECIMAL(6,3)))
FROM
cteDailyInventory
GROUP BY
PartNum
Here is the test data:
IF OBJECT_ID('tempdb..#Parts') IS NOT NULL
BEGIN
DROP TABLE #Parts
END
IF OBJECT_ID('tempdb..#Transact') IS NOT NULL
BEGIN
DROP TABLE #Transact
END
IF OBJECT_ID('tempdb..#Inventory') IS NOT NULL
BEGIN
DROP TABLE #Inventory
END
CREATE TABLE #Parts (
PartNum CHAR(2)
)
CREATE TABLE #Transact (
AutoId INT IDENTITY(1,1) NOT NULL
,PartNum CHAR(2)
,TranDate DATETIME
,TranQty INT
)
CREATE TABLE #Inventory (
PartNum CHAR(2)
,OnHandQty INT
)
INSERT INTO #Parts (PartNum) VALUES ('P1'),('P2'),('P3')
INSERT INTO #Transact (PartNum, TranDate, TranQty)
VALUES ('P1','6/28/2016',5),('P1','6/26/2016',3),('P1','6/26/2016',-1)
,('P1','6/15/2016',2) ,('P2','6/15/2016',1)
INSERT INTO #Inventory (PartNum, OnHandQty) VALUES ('P1',30),('P2',2)
I am thinking 1 recursive cte might be simpler might post that as an update.
Reverse the transactions to compute daily quantities. Add in the missing dates and look backward to the most recent date to fill in the daily quantities. I think I'm going to try for a better solution than this one.
http://rextester.com/JLD19862
with trn as (
select PartNum, TranDate, TranQty from PartTran
union all
select PartNum, cast('20160601' as date), 0 from PartWhse
union all
select PartNum, cast('20160630' as date), 0 from PartWhse
), qty as (
select
t.PartNum, t.TranDate,
-- assumes that end date corresponds with OnHandQty
min(w.OnHandQty) + sum(t.TranQty)
- sum(sum(t.TranQty))
over (partition by t.PartNum order by t.TranDate desc) as DailyOnHand,
coalesce(
lead(t.TranDate) over (partition by t.PartNum order by t.TranDate),
dateadd(day, 1, t.TranDate)
) as NextTranDate
-- if lead() isn't available...
-- coalesce(
-- (
-- select min(t2.TranDate) from trn as t2
-- where t2.PartNum = t.PartNum and t2.TranDate > t.TranDate
-- ),
-- dateadd(day, 1, t.TranDate)
-- ) as NextTranDate
from PartWhse as w inner join trn as t on t.PartNum = w.PartNum
where t.TranDate between '20160601' and '20160630'
group by t.PartNum, t.TranDate
)
select
PartNum,
sum(datediff(day, TranDate, NextTranDate) * DailyOnHand) * 1.00
/ sum(datediff(day, TranDate, NextTranDate)) as DailyAvg
from qty
group by PartNum;
I was able to solve this with a sum. First, I multiplied the final quantity on hand by the number of days in the range. Next, I multiplied each change in inventory by the time from #StartDate until the TransDate.
select
[Part].[PartNum] as [Part_PartNum],
(max(PartWhse.OnHandQty)*datediff(day,#StartDate,Constants.Today)-
sum(PartTran.TranQty*datediff(day,#StartDate,PartTran.TranDate))) as [Calculated_WeightedSum],
(WeightedSum/DATEDIFF(day, #StartDate, Constants.Today)) as [Calculated_AverageOnHand]
from Erp.Part as Part
right outer join Erp.PartTran as PartTran on
Part.PartNum = PartTran.PartNum
inner join Erp.PartWhse as PartWhse on
Part.PartNum = PartWhse.PartNum
group by [Part].[PartNum]
Thanks for your help everyone! You really helped me think it through.

How to get date difference statistics by group with TSQL

I have a table with columns of product and sold date, and want to query the statistics of sold interval of each product group(max interval, min interval ...) , is there any good advice to make it, appreciate~
Prod SaleDate
-------------------
A 2013-02-05
D 2013-02-24
B 2013-03-01
A 2013-03-12
D 2013-03-22
A 2013-04-03
D 2013-04-08
. . .
Sold interval means days interval between two adjacent date.
Sold interval of A:
DATEDIFF(d, '2013-02-05', '2013-03-12')
DATEDIFF(d, '2013-03-12', '2013-04-03')
...
Sold interval of D:
DATEDIFF(d, '2013-02-24', '2013-03-22')
DATEDIFF(d, '2013-03-22', '2013-04-08')
and I want get the average, max and min value of sold interval.
Prod IntervalAvg IntervalMax IntervalMin
-----------------------------------------------------
A xxx xxx xxx
B xxx xxx xxx
C
. . .
Thanks Kahn's answer give me a hint. I re-implement my code for sql server 2000 by "left outer join".
DECLARE #DATA TABLE (Prod CHAR(1), SaleDate SMALLDATETIME)
INSERT INTO #DATA VALUES ('A','2013-02-05')
INSERT INTO #DATA VALUES ('D','2013-02-24')
INSERT INTO #DATA VALUES ('B','2013-03-01')
INSERT INTO #DATA VALUES ('A','2013-03-12')
INSERT INTO #DATA VALUES ('D','2013-03-22')
INSERT INTO #DATA VALUES ('A','2013-04-03')
INSERT INTO #DATA VALUES ('D','2013-04-08')
SELECT
t.Prod
, MAX(t.Interval) IntervalMax
, MIN(t.Interval) IntervalMin
, AVG(t.Interval) IntervalAvg
FROM
(
SELECT t1.*, DATEDIFF(dd, MAX(t2.SaleDate), t1.SaleDate) Interval
FROM #DATA t1
LEFT OUTER JOIN #DATA t2 ON t1.Prod = t2.Prod AND t1.SaleDate > t2.SaleDate
GROUP BY t1.Prod, t1.SaleDate
)t
GROUP BY t.Prod
ORDER BY t.Prod
Here's one way that should work:
-- Test data
DECLARE #DATA TABLE (Prod CHAR(1), SaleDate DATE)
INSERT INTO #DATA VALUES ('A','2013-02-05')
,('D','2013-02-24')
,('B','2013-03-01')
,('A','2013-03-12')
,('D','2013-03-22')
,('A','2013-04-03')
,('D','2013-04-08')
-- Actual query
;WITH CTE AS
(SELECT D.*, CA.NextSaleDate
, DATEDIFF(DD, SaleDate, NextSaleDate) DDiff
FROM #DATA D
OUTER APPLY (SELECT MIN(SaleDate) NextSaleDate FROM #DATA B WHERE B.Prod = D.Prod AND B.SaleDate > D.SaleDate) CA)
SELECT DISTINCT Prod, AvgInterval, MaxInterval, MinInterval
FROM CTE C
CROSS APPLY (SELECT AVG(DDiff) AvgInterval, MAX(DDiff) MaxInterval, MIN(DDiff) MinInterval FROM CTE B WHERE B.Prod = C.Prod) CA

Sql query to overlay date/period overrides over default dates

Any ideas on building a Sql Server (2008) query that will give me say the "date specific prices for an item based on the default or override where exists".
So a Default table might look like this - columns Price, StartDate, EndDate (yyyy-M-d):
Default: $10, 2010-1-1, 2010-2-1
The Override table like this:
Override: $12, 2010-1-5, 2010-1-8
And the query would return:
Result: $10, 2010-1-1, 2010-1-4
$12, 2010-1-5, 2010-1-8
$10, 2010-1-9, 2010-2-1
Probably I'd wrap this in a stored proc or function and call it for a specific date range.
Soemthing like:
SELECT
D.Price, ISNULL(O.StartDate, D.StartDate), ISNULL(O.EndDate, D.EndDate)
FROM
Default D
LEFT JOIN
Override O ON D.Price= O.Price
The proper design will be to have only one table. You don't want the override table at all. Just keep everyhing in the single table - constrained by the date range. The query becomes much simpler as well.
Your table structure becomes
CREATE TABLE Rates
(ID INT NOT NULL,
Rate Decimal NOT NULL,
FromDate NOT NULL,
ToDate NOT NULL,
CONSTRAINT PK_RATES (ID,FromDate,ToDate))
Then the query becomes
SELECT Rate FROM Rates WHERE ID = #ID AND FromDate = (SELECT MAX(FromDate) FROM Rates WHERE ID = #ID AND FromDate <= #Date) AND ToDate = (SELECT MIN(ToDate) FROM Rates WHERE ID = #ID AND ToDate >=#Date)
I think you're going to have to do something like this (assuming that you want to keep the override dates separate, and that you want to avoid anything procedural):
define and populate a utility table, listing each individual date which could be relevant to you
construct a SELECT that "marks" each date from this utility table as either i) belonging to the override dates or ii) belonging to the default dates
group the results of this SELECT by date and "mark"
join these results back to the respective price info
A bit late, but was looking for a solution for similar problem and did not find an answer so tried to cook one up.
One solution can be to try to adjust the first set of data to fit in the second set, and in the last step we will union the two sets, the adjusted data with the overlay data.
So we need to adjust the default set and that will require several steps of work.
Assumptions:-
The Default periods don’t overlap on their self.
Overlap periods don’t overlap on their self.
The overlap data must have gaps that are more than one day, to ensure that any adjustments will not affect another overlay period; we will not lose the overlay values associated as we will get them back in the last step.
For default periods that are partially overlapped (the overlap is larger or equal to a default period)
If a period start date overlaps with any overlay period we will change the start date to the end of the overlap period plus one day
Default: |<-------- P1 ------>|
Overlap: |<-----------O1------>|
=================================================================
Output: |<-----P1 ---->|
If a period end date overlaps with any overlay period we will change the end date to the start of the overlap period minus one day
Default: |<-------- P1 ------>|
Overlap: |<-----------O1------>|
===================================================================
Output: |<--- P1 ---->|
Remove any default period if it’s completely covered by an overlap period
Default: |<--- P1 --->|
Overlap: |<--------O1------>|
===================================================================
Output: nothing
For default periods that are broken by overlapped periods(the overlap is smaller than the periods)
Get the first part (from the beginning of the period to the first overlap period)
Get the intermediate parts, and that is from the end of any overlapped period to the start of the next overlapped period.
Get the last part( from the last overlapped to the end of the period)
Default: |<---------------------------- P1 ------------------------------>|
Overlap: |<---O1--->| |<---O2-->| |<--O3->|
===================================================================
Output: |<-P1->| |<-P1->| |<-P1->| |<-P1->|
Finally merge the data and get the result
Over lapping types that we are considering
Default: |<---- P1 --->|
Overlap: |<-----------O1------>|
===================================================================
Output: |<---- P1 --->||<-----------O1------>|
Lets build the T-SQL
With [System] as (
Select 1 [RowNum],cast('Jan 01,2017' as date) [StartDate],dateadd(day,-1,cast('Feb 01,2017' as date)) [EndDate],0500 [Value] union all
Select 2 [RowNum],cast('Feb 01,2017' as date) [StartDate],dateadd(day,-1,cast('Mar 01,2017' as date)) [EndDate],0700 [Value] union all
Select 3 [RowNum],cast('Mar 01,2017' as date) [StartDate],dateadd(day,-1,cast('Apr 01,2017' as date)) [EndDate],0900 [Value] union all
Select 4 [RowNum],cast('Apr 01,2017' as date) [StartDate],dateadd(day,-1,cast('May 01,2017' as date)) [EndDate],0700 [Value] union all
Select 5 [RowNum],cast('May 01,2017' as date) [StartDate],dateadd(day,-1,cast('Jun 01,2017' as date)) [EndDate],0900 [Value] union all
Select 6 [RowNum],cast('Jun 01,2017' as date) [StartDate],dateadd(day,-1,cast('Jul 01,9999' as date)) [EndDate],1500 [Value]
),Overrides as (
Select 1 [RowNum],cast('Feb 12,2017' as date) [StartDate],cast('Mar 25,2017' as date) [EndDate],1 [Value] union all
Select 2 [RowNum],cast('Mar 28,2017' as date) [StartDate],cast('May 15,2017' as date) [EndDate],2 [Value] union all
Select 3 [RowNum],cast('May 18,2017' as date) [StartDate],cast('May 20,2017' as date) [EndDate],3 [Value] union all
Select 4 [RowNum],cast('Jun 05,2017' as date) [StartDate],cast('Jun 08,2017' as date) [EndDate],4 [Value] union all
Select 5 [RowNum],cast('Jun 09,2017' as date) [StartDate],cast('Jun 16,2017' as date) [EndDate],5 [Value] union all
Select 6 [RowNum],cast('Jun 17,2017' as date) [StartDate],cast('Jun 22,2017' as date) [EndDate],6 [Value] union all
Select 7 [RowNum],cast('Jun 23,2017' as date) [StartDate],cast('Jun 27,2017' as date) [EndDate],7 [Value]
),PrepareOverridePeriods as (--if override periods have no gabs betwwen we need to merge them
Select p1.StartDate, p1.EndDate
from Overrides p1
left join Overrides p2 on p1.StartDate = DATEADD(day,1,p2.EndDate)
where p2.StartDate is null
union all
select p1.StartDate,p2.EndDate
from PrepareOverridePeriods p1
inner join Overrides p2 on p1.EndDate = DATEADD(day,-1,p2.StartDate)
),OverridePeriods as (
select ROW_NUMBER() over (order by StartDate) [RowNum],StartDate,MAX(EndDate) as EndDate
from PrepareOverridePeriods group by StartDate
),AdjustedPeriods as (
select s.RowNum,'Adj.' [type]
,isnull(dateadd(day,1,ShiftRight.EndDate),s.StartDate) [StartDate]
,isnull(dateadd(day,-1,ShiftLeft.StartDate),s.EndDate) [EndDate]
,s.Value
from System s
left outer join OverridePeriods ShiftRight on s.StartDate between ShiftRight.StartDate and ShiftRight.EndDate
left outer join OverridePeriods ShiftLeft on s.EndDate between ShiftLeft.StartDate and ShiftLeft.EndDate
left outer join OverridePeriods RemovePeriod on s.StartDate between RemovePeriod.StartDate and RemovePeriod.EndDate and s.EndDate between RemovePeriod.StartDate and RemovePeriod.EndDate
where RemovePeriod.StartDate is null
),SmallOverrides as ( --TODO: change SystemCalculated to AdjustSystemCalculatedPeriods
select ROW_NUMBER() over (partition by s.RowNum order by o.StartDate ) [RowNum],
o.RowNum [OverrideRowNum],o.StartDate [OverrideStartDate],o.EndDate [OverrideEndDate],s.Value [Value]
,s.RowNum [SystemRowNum],s.StartDate [SystemStartDate],s.EndDate [SystemEndDate]
from OverridePeriods o
inner join AdjustedPeriods s on o.StartDate between s.StartDate and s.EndDate and o.EndDate between s.StartDate and s.EndDate
)
--,FirstAndLastParts as (
--select [SystemRowNum],[type]
-- ,case when [type]='First' then min([SystemStartDate]) else dateadd(day,1,max(OverrideEndDate)) end [StartDate]
-- ,case when [type]='First' then dateadd(day,-1,min(OverrideStartDate)) else max([SystemEndDate]) end [EndDate]
-- ,min(Value) [Value]
-- from (select *,'First' [type] from SmallOverrides o union all
-- select *,'Last' [type] from SmallOverrides o) data
-- group by [SystemRowNum],[type]
--)
,FirstParts as (
select [SystemRowNum],'First' [type]
,min([SystemStartDate]) [StartDate]
,dateadd(day,-1,min(OverrideStartDate)) [EndDate]
,min(Value) [Value]
from SmallOverrides
group by [SystemRowNum]
),LastParts as (
select [SystemRowNum],'Last' [type]
,dateadd(day,1,max(OverrideEndDate)) [StartDate]
,max([SystemEndDate]) [EndDate]
,min(Value) [Value]
from SmallOverrides
group by [SystemRowNum]
),IntermediatParts as (
select s.SystemRowNum [RowNum],'Inter.' [type]
,dateadd(day,1,s.OverrideEndDate) [StartDate]
,dateadd(day,-1,e.OverrideStartDate) [EndDate]
,s.Value
from SmallOverrides s
left outer join SmallOverrides e on e.SystemRowNum=s.SystemRowNum and s.RowNum+1=e.RowNum
where e.RowNum is not null --remove the first and lasts
),AdjustedPeriodsFiltered as (--remove blocks that are broken to smaller pieces
select s.*
from AdjustedPeriods s
left outer join OverridePeriods o on o.StartDate between s.StartDate and s.EndDate and o.EndDate between s.StartDate and s.EndDate
where o.StartDate is null
),AllParts as (
select * from IntermediatParts union all --order by SystemRowNum,OverrideStartDate
select * from FirstParts union all
select * from LastParts union all
select * from AdjustedPeriodsFiltered
),Merged as (
select [RowNum],[type] [Source],StartDate,EndDate,Value,'System' [RecordType] from AllParts
union all
select [RowNum],'override' [Source],StartDate,EndDate,Value,'Override' [RecordType] from Overrides
)
select * from Merged order by StartDate
Can we adjust the data set in a different way, well yes, another approach is to get all the expected values from the start and end dates for the default periods and the overlay periods, then reconstruct a new set of periods, link it to the default values, merge it with the overlay, and we got it.
The same assumptions and step are taken as below:-
With [System] as (
Select 1 [RowNum],cast('Jan 01,2017' as date) [StartDate],dateadd(day,-1,cast('Feb 01,2017' as date)) [EndDate],0500 [Value] union all
Select 2 [RowNum],cast('Feb 01,2017' as date) [StartDate],dateadd(day,-1,cast('Mar 01,2017' as date)) [EndDate],0700 [Value] union all
Select 3 [RowNum],cast('Mar 01,2017' as date) [StartDate],dateadd(day,-1,cast('Apr 01,2017' as date)) [EndDate],0900 [Value] union all
Select 4 [RowNum],cast('Apr 01,2017' as date) [StartDate],dateadd(day,-1,cast('May 01,2017' as date)) [EndDate],0700 [Value] union all
Select 5 [RowNum],cast('May 01,2017' as date) [StartDate],dateadd(day,-1,cast('Jun 01,2017' as date)) [EndDate],0900 [Value] union all
Select 6 [RowNum],cast('Jun 01,2017' as date) [StartDate],dateadd(day,-1,cast('Jul 01,9999' as date)) [EndDate],1500 [Value]
),Overrides as (
Select 1 [RowNum],cast('Feb 12,2017' as date) [StartDate],cast('Mar 25,2017' as date) [EndDate],1 [Value] union all
Select 2 [RowNum],cast('Mar 28,2017' as date) [StartDate],cast('May 15,2017' as date) [EndDate],2 [Value] union all
Select 3 [RowNum],cast('May 18,2017' as date) [StartDate],cast('May 20,2017' as date) [EndDate],3 [Value] union all
Select 4 [RowNum],cast('Jun 05,2017' as date) [StartDate],cast('Jun 08,2017' as date) [EndDate],4 [Value] union all
Select 5 [RowNum],cast('Jun 09,2017' as date) [StartDate],cast('Jun 16,2017' as date) [EndDate],5 [Value] union all
Select 6 [RowNum],cast('Jun 17,2017' as date) [StartDate],cast('Jun 22,2017' as date) [EndDate],6 [Value] union all
Select 7 [RowNum],cast('Jun 23,2017' as date) [StartDate],cast('Jun 27,2017' as date) [EndDate],7 [Value]
),PrepareOverridePeriods as (--if override periods have no gabs between we need to merge them
Select p1.StartDate, p1.EndDate
from Overrides p1
left join Overrides p2 on p1.StartDate = DATEADD(day,1,p2.EndDate)
where p2.StartDate is null
union all
select p1.StartDate,p2.EndDate
from PrepareOverridePeriods p1
inner join Overrides p2 on p1.EndDate = DATEADD(day,-1,p2.StartDate)
),OverridePeriods as (
select ROW_NUMBER() over (order by StartDate) [RowNum],StartDate,MAX(EndDate) as EndDate
from PrepareOverridePeriods group by StartDate
)
,AllDates as (
select ROW_NUMBER() over (order by [Date]) [RowNum],data.Date from (
select dateadd(day,-1,OverridePeriods.StartDate) [Date] from OverridePeriods union all
select dateadd(day,+1,OverridePeriods.EndDate) [Date] from OverridePeriods union all
select StartDate [Date] from [System] union all
select EndDate [Date] from [System] ) as data
)
,NewPeriods as (
select sy.RowNum, s.[Date] [StartDate],n.[Date] [EndDate] ,sy.Value
from AllDates s
left outer join AllDates n on n.RowNum=s.RowNum+1
left outer join OverridePeriods o on s.[Date] between o.StartDate and o.EndDate and n.[Date] between o.StartDate and o.EndDate
left outer join [System] sy on s.[Date] between sy.StartDate and sy.EndDate
where
s.RowNum % 2 =1 and o.StartDate is null--group it by 2 and remove overriden areas
)
,Merged2 as (
select [RowNum], StartDate,EndDate,Value,'System' [RecordType] from NewPeriods
union all
select [RowNum], StartDate,EndDate,Value,'Override' [RecordType] from Overrides
)
select * from Merged2 order by StartDate
Im sure there may be another way to achieve the result required with some recursive approach, but for now this works for me.
For a last step we can try to merge the results if the value is the same, but I dont think that was requested.