Select distinct in week and distinct overall per week - sql-server

I have an example on sql fiddle. What I am trying to do is divide the overall COUNT(DISTINCT ID) by the weekly COUNT(DISTINCT ID). For example if I have the following conceptual setup of what the result should be.
year week id_set overall_distinct week_distinct result
2016 1 A,A,A,B,B,C 0 3 0
2016 2 A,B,C,C,D 1 4 .25
2016 3 A,B,C,E,F 2 5 .4
The table linked to on sql fiddle has the following schema. Also, in reality I do have multiple values for 'year'.
CREATE TABLE all_ids
([year] int, [week] int, [id] varchar(57))
;
INSERT INTO all_ids
([year], [week], [id])
VALUES
(2016, 1, 'A'),
(2016, 1, 'A'),
(2016, 1, 'A'),
(2016, 1, 'B'),
(2016, 1, 'B'),
(2016, 1, 'C'),
(2016, 2, 'A'),
(2016, 2, 'B'),
(2016, 2, 'C'),
(2016, 2, 'C'),
(2016, 2, 'D'),
(2016, 3, 'A'),
(2016, 3, 'B'),
(2016, 3, 'C'),
(2016, 3, 'E'),
(2016, 3, 'F')
;
Edit
I apologize for the confusion. The above table was just a conceptual example of the result. The actual result only needs to look like the following.
year week overall_distinct week_distinct result
2016 1 0 3 0
2016 2 1 4 .25
2016 3 2 5 .4
there is no need to include id_set

I used dense_rank and max() over () to simulate count (distinct ...) with window functions. You could try to do it with another subquery
select
year, week
, id_set = stuff((
select
',' + a.id
from
all_ids a
where
a.year = t.year
and a.week = t.week
order by a.id
for xml path('')
), 1, 1, '')
, overall_distinct = count(case when cnt = 1 then 1 end)
, week_distinct = count(distinct id)
, result = cast(count(case when cnt = 1 then 1 end) * 1.0 / count(distinct id) as decimal(10, 2))
from (
select
year, week, id, cnt = max(dr) over (partition by id)
from (
select
*, dr = dense_rank() over (partition by id order by year, week)
From
all_ids
) t
) t
group by year, week
Output
year week id_set overall_distinct week_distinct result
--------------------------------------------------------------------------
2016 1 A,A,A,B,B,C 0 3 0.00
2016 2 A,B,C,C,D 1 4 0.25
2016 3 A,B,C,E,F 2 5 0.40

This would be one way, probably not the best one:
;with weekly as
(
select year, week, count(distinct id) nr
from all_ids
group by year, week
),
overall as
(
select a.week, count(distinct a.id) nr
from all_ids a
where a.id not in (select id from all_ids where week <> a.week and id = a.id )
group by week
)
select distinct a.year
, a.week
, stuff((select ', ' + id
from all_ids
where year = a.year and week = a.week
for xml path('')), 1, 1, '') ids
, w.Nr weeklyDistinct
, isnull(t.Nr, 0) overallDistinct
from all_ids a join weekly w on a.year = w.year and a.week = w.week
left join overall t on t.week = a.week

One statement count only
declare #t table (y int, w int, id varchar(57));
INSERT #t (y, w, id)
VALUES
(2016, 1, 'A'),
(2016, 1, 'A'),
(2016, 1, 'A'),
(2016, 1, 'B'),
(2016, 1, 'B'),
(2016, 1, 'C'),
(2016, 2, 'A'),
(2016, 2, 'B'),
(2016, 2, 'C'),
(2016, 2, 'C'),
(2016, 2, 'D'),
(2016, 3, 'A'),
(2016, 3, 'B'),
(2016, 3, 'C'),
(2016, 3, 'E'),
(2016, 3, 'F');
select t1.w, count(distinct t1.id) as wk
, (count(distinct t1.id) - count(distinct t2.id)) as [all]
, (cast(1 as smallmoney) - cast(count(distinct t2.id) as smallmoney) / count(distinct t1.id)) as [frac]
from #t t1
left join #t t2
on t2.id = t1.id
and t2.w <> t1.w
group by t1.w
order by t1.w;

Related

Get data based on repetitive group values

I have no idea how to name the question properly, but here is sample data:
CREATE TABLE dbo.test_data
(
row_version VARBINARY(8)
, account_number CHAR(8)
, account_balance DECIMAL(10, 2)
, group_rank BIGINT
, rownum BIGINT
);
INSERT INTO dbo.test_data
VALUES (0x000000000013fd24, '46436663', 123.00, 4, 86)
, (0x000000000013fd23, '46436663', 123.00, 4, 86)
, (0x000000000013fd22, '46436663', 123.00, 4, 85)
, (0x000000000013fd21, '46436663', 123.00, 4, 85)
, (0x000000000013fd20, '46436663', 123.00, 4, 83)
, (0x000000000013fd1f, '46436663', 555.00, 2, 83)
, (0x000000000013fd21, '46436663', 123.00, 4, 85)
, (0x000000000013fd20, '46436663', 123.00, 4, 83)
, (0x000000000013fd21, '46436663', 123.00, 4, 85)
, (0x000000000013fd20, '46436663', 123.00, 4, 83)
, (0x000000000013fd1e, '46436664', 12345.00, 5, 82)
, (0x000000000013fd1d, '46436664', 12345.00, 5, 82)
, (0x000000000013fd1c, '46436664', 12345.00, 5, 82)
, (0x000000000013fd1b, '46436664', 12345.00, 5, 81)
, (0x000000000013fd1a, '46436664', 12345.00, 5, 81)
, (0x000000000013fd19, '46436664', 12345.00, 5, 78)
, (0x000000000013fcb3, '46436664', 123.00, 6, 77)
, (0x000000000013fcb2, '46436664', 123.00, 6, 77)
, (0x000000000013fcb1, '46436664', 123.00, 6, 76)
, (0x000000000013fcb0, '46436664', 123.00, 6, 76);
This is how data look like:
SELECT * FROM dbo.test_data
ORDER BY row_version DESC
Here 1 and 4 (blue) are sequential group numbers, that have minimum sequence of 2 the same values in a roll of you order them by row_version. I need to find first occurrence of different group_rank (2, red) and then check rownum value (3,6, purple) where it is MIN(row_num) for upper group (blue) and row_num for the record that comes just before the group ended (red). If these values differs by 1, then I need to the account_number then I need to return it, otherwise - I don't need to return it.
I'm not interested in what happened below 2 and 5 points (red) for the accounts.
So, by looking to that data, the only account should be returned - 46436664 as for 46436663 rownum value is the same (83).
Interesting problem I guess the first step is finding the problem account_numbers, which you could do like this.
select
*
from
dbo.test_data t1
cross apply (
select top 1
*
from
dbo.test_data
where
account_number = t1.account_number
and rownum = t1.rownum
and row_version > t1.row_version
order by
row_version asc) t2
where
t1.group_rank <> t2.group_rank
order by
t1.row_version;
Then you could do this
select distinct
t0.account_number
from
test_data t0
except
select distinct
t1.account_number
from
dbo.test_data t1
cross apply (
select top 1
*
from
dbo.test_data
where
account_number = t1.account_number
and rownum = t1.rownum
and row_version > t1.row_version
order by
row_version asc) t2
where
t1.group_rank <> t2.group_rank

T-SQL: process consequtive periods and count by group

I deal with big job to find max period by FULL months of enrollment within the year (12 months period), which I did OK if we have 2 periods. Just got stuck at the end while testing if I have 3+ periods. Data below and picture hope will provide all information and easy start. Thanks to all. This is final work table I got at the end of my process, thanks all. Code below produces partially correct results. My global task find MAX period for each member, so some fields are just for easy working.
/*
DROP TABLE IF EXISTS #t;
CREATE TABLE #t ( Cust VARCHAR(10), mm INT, mm_prev INT, rn INT)
INSERT #t values
(123456, 1, NULL, 1), (123456, 2, 1, 2),
(123456, 4, 2, 3), (123456, 5, 4, 4), (123456, 6, 5, 5),
(123456, 8, 6, 6), (123456, 9, 8, 7), (123456, 10, 9, 8), (123456, 11, 10, 9), (123456, 12, 11, 10),
(777 , 1, NULL, 1),(777 , 2, 1, 2)
SELECT * from #t
*/
select
Cust, MIN(mm) mmStart, MAX(mm) mmEnd,
CASE WHEN mm = rn THEN 'Grp A' ELSE 'Grp B' END Grp
,COUNT(*) mm_count
FROM #t
WHERE 1=1
--mm - ISNULL(mm_prev,0) = 1 --check for conseq but we drop mm=6--> start of new period
-- AND mm = rn -- this brings only first group by mm
GROUP BY Cust, CASE WHEN mm = rn THEN 'Grp A' ELSE 'Grp B' END
ORDER BY 1,4
just for the case if somebody prefer to deal with initial raw data I posting it here too with some gap and islands:
CREATE TABLE #tr ( Cust varchar(10), ENR_START date, enr_END date, rn INT); -- SELECT * FROM #t
INSERT #tr VALUES
('123456' , '2018-12-01', '2019-3-1' , 1),
('123456' , '2019-3-28', '2019-6-30' , 2), -- 6 month with 2 periods, island
('123456' , '2019-7-26', '2019-8-20' , 3),
('123456' , '2019-8-15', '2019-12-31' , 4),
('777' , '2018-11-4', '2019-3-3' , 1)
select * from #tr
Screenshot is here:
looks to me, you wanted this. Not really sure what is the purpose of the case statement in your query
with cte as
(
SELECT *,
grp = mm - rn
from #t
)
SELECT Cust, MIN(mm) as mmStart, MAX(mm) as mmEnd, grp,
count(*) as mm_count
FROM cte
GROUP BY Cust, grp
order by Cust, mmStart

How to get the Previous & next row based on condition

I am trying to get the statement on fetching the previous and next rows of a selected row.
Declare #OderDetail table
(
Id int primary key,
OrderId int,
ItemId int,
OrderDate DateTime2,
Lookup varchar(15)
)
INSERT INTO #OderDetail
VALUES
(1, 10, 1, '2018-06-11', 'A'),
(2, 10, 2, '2018-06-11', 'BE'), --this
(3, 2, 1, '2018-06-04', 'DR'),
(4, 2, 2, '2018-06-04', 'D'), --this
(5, 3, 2, '2018-06-14', 'DD'), --this
(6, 4, 2, '2018-06-14', 'R');
DECLARE
#ItemId int = 2,
#orderid int = 10
Required output:
Input for the procedure is order id =10 and item id =2 and i need to check item-2 is in the any other order i.e only previous and next item of matched record/order as per order date
Is this what your after? (Updated to reflect edit [OrderDate] to question)
Declare #OderDetail table
(
Id int primary key,
OrderId int,
ItemId int,
OrderDate DateTime2,
Lookup varchar(15)
)
INSERT INTO #OderDetail
VALUES
(1, 10, 1, '2018-06-11', 'A'),
(2, 10, 2, '2018-06-11', 'BE'), --this
(3, 2, 1, '2018-06-04', 'DR'),
(4, 2, 2, '2018-06-04', 'D'), --this
(5, 3, 2, '2018-06-14', 'DD'), --this
(6, 4, 2, '2018-06-14', 'R');
declare #ItemId int=2 , #orderid int = 10;
Query
With cte As
(
Select ROW_NUMBER() OVER(ORDER BY OrderDate) AS RecN,
*
From #OderDetail Where ItemId=#ItemId
)
Select Id, OrderId, ItemId, [Lookup] From cte Where
RecN Between ((Select Top 1 RecN From cte Where OrderId = #orderid) -1) And
((Select Top 1 RecN From cte Where OrderId = #orderid) +1)
Order by id
Result:
Id OrderId ItemId Lookup
2 10 2 BE
4 2 2 D
5 3 2 DD
Another possible approach is to use LAG() and LEAD() functions, that return data from a previous and subsequent row form the same resul tset.
-- Table
DECLARE #OrderDetail TABLE (
Id int primary key,
OrderId int,
ItemId int,
OrderDate DateTime2,
Lookup varchar(15)
)
INSERT INTO #OrderDetail
VALUES
(1, 10, 1, '2018-06-11', 'A'),
(2, 10, 2, '2018-06-11', 'BE'), --this
(3, 2, 1, '2018-06-04', 'DR'),
(4, 2, 2, '2018-06-04', 'D'), --this
(5, 3, 2, '2018-06-14', 'DD'), --this
(6, 4, 2, '2018-06-14', 'R');
-- Item and order
DECLARE
#ItemId int = 2,
#orderid int = 10
-- Statement
-- Get previois and next ID for every order, grouped by ItemId, ordered by OrderDate
;WITH cte AS (
SELECT
Id,
LAG(Id, 1) OVER (PARTITION BY ItemId ORDER BY OrderDate) previousId,
LEAD(Id, 1) OVER (PARTITION BY ItemId ORDER BY OrderDate) nextId,
ItemId,
OrderId,
Lookup
FROM #OrderDetail
)
-- Select current, previous and next order
SELECT od.*
FROM cte
CROSS APPLY (SELECT * FROM #OrderDetail WHERE Id = cte.Id) od
WHERE (cte.OrderId = #orderId) AND (cte.ItemId = #ItemId)
UNION ALL
SELECT od.*
FROM cte
CROSS APPLY (SELECT * FROM #OrderDetail WHERE Id = cte.previousId) od
WHERE (cte.OrderId = #orderId) AND (cte.ItemId = #ItemId)
UNION ALL
SELECT od.*
FROM cte
CROSS APPLY (SELECT * FROM #OrderDetail WHERE Id = cte.nextId) od
WHERE (cte.OrderId = #orderId) AND (cte.ItemId = #ItemId)
Output:
Id OrderId ItemId OrderDate Lookup
2 10 2 11/06/2018 00:00:00 BE
4 2 2 04/06/2018 00:00:00 D
5 3 2 14/06/2018 00:00:00 DD
Update to given this data set: I see where you are going with this. Note that in SOME cases there IS no row before the given one - so it only returns 2 not 3. Here I updated the CTE version. Un-comment the OTHER row to see 3 not 2 as there is then one before the selected row with that Itemid.
Added a variable to demonstrate how this is better allowing you to get 1 before and after or 2 before/after if you change that number (i.e. pass a parameter) - and if less rows, or none are before or after it gets as many as it can within that constraint.
Data setup for all versions:
Declare #OderDetail table
(
Id int primary key,
OrderId int,
ItemId int,
OrderDate DateTime2,
Lookup varchar(15)
)
INSERT INTO #OderDetail
VALUES
(1, 10, 1, '2018-06-11', 'A'),
(2, 10, 2, '2018-06-11', 'BE'), --this
(3, 2, 1, '2018-06-04', 'DR'),
(4, 2, 2, '2018-06-04', 'D'), --this
(5, 3, 2, '2018-06-14', 'DD'), --this
(9, 4, 2, '2018-06-14', 'DD'),
(6, 4, 2, '2018-06-14', 'R'),
--(10, 10, 2, '2018-06-02', 'BE'), -- un-comment to see one before
(23, 4, 2, '2018-06-14', 'R');
DECLARE
#ItemId int = 2,
#orderid int = 2;
CTE updated version:
DECLARE #rowsBeforeAndAfter INT = 1;
;WITH cte AS (
SELECT
Id,
OrderId,
ItemId,
OrderDate,
[Lookup],
ROW_NUMBER() OVER (ORDER BY OrderDate,Id) AS RowNumber
FROM #OderDetail
WHERE
ItemId = #itemId -- all matches of this
),
myrow AS (
SELECT TOP 1
Id,
OrderId,
ItemId,
OrderDate,
[Lookup],
RowNumber
FROM cte
WHERE
ItemId = #itemId
AND OrderId = #orderid
)
SELECT
cte.Id,
cte.OrderId,
cte.ItemId,
cte.OrderDate,
cte.[Lookup],
cte.RowNumber
FROM ctE
INNER JOIN myrow
ON ABS(cte.RowNumber - myrow.RowNumber) <= #rowsBeforeAndAfter
ORDER BY OrderDate, OrderId;
You probably want the CTE method (See an original at the end of this) however:
Just to point out, this gets the proper results but is probably not what you are striving for since it is dependent on the row order and the item id not the actual row with those two values:
SELECT TOP 3
a.Id,
a.OrderId,
a.ItemId,
a.Lookup
FROM #OderDetail AS a
WHERE
a.ItemId = #ItemId
To fix that, you can use an ORDER BY and TOP 1 with a UNION, kind of ugly. (UPDATED with date sort and != on the id.)
SELECT
u.Id,
u.OrderId,
u.OrderDate,
u.ItemId,
u.Lookup
FROM (
SELECT
a.Id,
a.OrderId,
a.OrderDate,
a.ItemId,
a.Lookup
FROM #OderDetail AS a
WHERE
a.ItemId = #ItemId
AND a.OrderId = #orderid
UNION
SELECT top 1
b.Id,
b.OrderId,
b.OrderDate,
b.ItemId,
b.Lookup
FROM #OderDetail AS b
WHERE
b.ItemId = #ItemId
AND b.OrderId != #orderid
ORDER BY b.OrderDate desc, b.OrderId
UNION
SELECT top 1
b.Id,
b.OrderId,
b.OrderDate,
b.ItemId,
b.Lookup
FROM #OderDetail AS b
WHERE
b.ItemId = #ItemId
AND b.OrderId != #orderid
ORDER BY b.OrderDate asc, b.OrderId
) AS u
ORDER BY u.OrderDate asc, u.OrderId
I think its simple, you can check with min(Id) and Max(id) with left outer join or outer apply
like
Declare #ItemID int = 2
Select * From #OderDetail A
Outer Apply (
Select MIN(A2.Id) minID, MAX(A2.Id) maxID From #OderDetail A2
Where A2.ItemId =#ItemID
) I05
Outer Apply(
Select * From #OderDetail Where Id=minID-1
Union All
Select * From #OderDetail Where Id=maxID+1
) I052
Where A.ItemId =#ItemID Order By A.Id
Let me know if this helps you or you face any problem with it...
Regards,

distribution of income over time based on rule set

Not sure if I was able to properly describe the title of my question but I haven't done anything this complex in T-Sql. Here's what I'm trying to achieve.
After several queries and table building, I got the data (below) in a temp table which probably will become a permanent table.
The part I need help with is, Need to create another table to report part of the sale from the data below.
Table 1
LOC# ITEM sale_total FiscalYear FiscalPrd TYPE O_R DESC LFT_EXP METHOD PRD_MO AG_PCNT PLAN_ID YEARS STATUS
33 17879 12203.57 2017 3 A O Plan A L W 120 100.00 6 10 N
33 17903 579.97 2017 3 A O Plan A L W 120 100.00 6 10 N
33 18870 799.60 2017 3 A O Plan A L W 120 100.00 6 10 N
33 19739 319.95 2017 3 A B Plan C E F 36 100.00 8 3 N
33 21585 559.96 2017 3 B O Plan B E N 0 100.00 10 0 N
A percentage of the sale_total from table above needs to be distributed based on the table below (I can also include the YR01-YR10 columns in the table above if it make things simpler).
Table 2
PLAN_ID EFDATE MONTHS METHOD EPERCENT YR01 YR02 YR03 YR04 YR05 YR06 YR07 YR08 YR09 YR10
6 2007-08-01 120 W 100.00 40.00 20.00 11.00 8.00 6.00 5.00 4.00 3.00 2.00 1.00
8 2012-11-12 36 F 100.00 33.00 33.00 34.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
10 2015-10-01 0 N 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
So my end result would look like below for LOC#33 and item 18870: (So the calculation is based on the plan. For this specific example, item 18870 falls under plan 6 (table 1). Table 2 has the distribution of all plans. We can see it spreads over 120 months and YR01-YR10 has the yearly percent distribution. Now from the sales number for 18870 from table 1, 799.60, distribution math for year 1 is (799.60 x 0.4) / 12 = 26.65. Same method follows up to year 10 for this plan and up to year 3 for plan 8. For the 10 year plan, it will create 120 input records. 1 for each month for each item, location)
LOC# ITEM FiscalYear FiscalPrd SALES
33 18870 2017 4 26.65
33 18870 2017 5 26.85
33 18870 2017 6 26.85
.............................................
.............................................
33 18870 2017 11 26.85
33 18870 2017 12 26.85
33 18870 2018 1 13.33
33 18870 2018 2 13.33
.............................................
.............................................
33 18870 2018 11 13.33
33 18870 2018 12 13.33
.............................................
.............................................
33 18870 2027 2 0.67
33 18870 2027 3 0.67
It will distribute that income over 120 months at the percentage dictated by the table 2 for planID 6. for planID 8, it will do the same math over 36 months.
The percentages can change over time and a user will manage the plans. There are other locations and the first table will include all location for the current fiscal period and all items (3 max as shows in Table 2). I created a status columns so I can mark if once the data is processed.
When comes the next fiscal period sales, the program will update the above result set sales numbers to include new sales. It will be run once a month to generate the above result.
Would greatly appreciate some direction as I got up to this point and not sure how to approach from here.
Thanks a bunch.
#ZLK I saved the first run in a temp table, then added the following rows
(33, 17879, 10000.00, 2017, 4, 6, 10),
(33, 17903, 500.00, 2017, 4, 6, 10),
(33, 18870, 800.00, 2017, 4, 6, 10),
(33, 19739, 300.00, 2017, 4, 8, 3),
(33, 21585, 500.00, 2017, 4, 10, 0);
This the save first set of item, location with different sales number in fiscal period 4. the first set was inserted into a table #finalDist, the second uses the following query to insert and update.
begin tran
if exists (select * FROM #finalDist AGE
INNER JOIN #dist PrdAg on PrdAg.Loc = AGE.Loc
and PrdAg.Item = AGE.Item
and PrdAg.FiscalYear = AGE.FiscalYear
and PrdAg.FiscalPrd = AGE.FiscalPrd)
begin
UPDATE AGE
SET AGE.SALES = AGE.SALES + PrdAg.SALES
FROM #finalDist AGE
INNER JOIN #dist PrdAg on PrdAg.Loc = AGE.Loc
and PrdAg.Item = AGE.Item
and PrdAg.FiscalYear = AGE.FiscalYear
and PrdAg.FiscalPrd = AGE.FiscalPrd
end
else
begin
INSERT INTO #finalDist (Loc, Item, FiscalYear, FiscalPrd, SALES)
SELECT Loc, Item, FiscalYear, FiscalPrd, SALES from #dist PrdAg
WHERE NOT EXISTS (SELECT AGE.Item FROM #finalDist AGE WHERE PrdAg.Loc = AGE.Loc
and PrdAg.Item = AGE.Item
and PrdAg.FiscalYear = AGE.FISCALYear
and PrdAg.FiscalPrd = AGE.FiscalPrd)
end
commit tran
I realize I can't do if else as I need to do both insert and update. It will update everything except the fiscal period 3, then it will insert four rows for this case
Loc Item FiscalYear FiscalPrd SALES
33 18870 2027 4 0.66666666666
33 19739 2020 4 8.50000000000
33 17903 2027 4 0.41666666666
33 17879 2027 4 8.33333333333
update look like this
33 17879 2017 4 406.78566666666 -- this row wasn't updated
33 17879 2017 5 740.11899999999
One way you can do it is like this:
DECLARE #T TABLE (Loc INT, Item INT, Sales_Total DECIMAL(10, 2), FiscalYear INT, FiscalPrd INT, Plan_ID INT, YEARS INT);
INSERT #T (Loc, Item, Sales_Total, FiscalYear, FiscalPrd, Plan_ID, YEARS) VALUES
(33, 17879, 12203.57, 2017, 3, 6, 10),
(33, 17903, 579.97, 2017, 3, 6, 10),
(33, 18870, 799.60, 2017, 3, 6, 10),
(33, 19739, 319.95, 2017, 3, 8, 3),
(33, 21585, 559.96, 2017, 3, 10, 0);
DECLARE #T2 TABLE (PLAN_ID INT, YR01 DECIMAL(4, 2), YR02 DECIMAL(4, 2), YR03 DECIMAL(4, 2), YR04 DECIMAL(4, 2), YR05 DECIMAL(4, 2), YR06 DECIMAL(4, 2), YR07 DECIMAL(4, 2), YR08 DECIMAL(4, 2), YR09 DECIMAL(4, 2), YR10 DECIMAL(4, 2));
INSERT #T2 (PLAN_ID, YR01, YR02, YR03, YR04, YR05, YR06, YR07, YR08, YR09, YR10) VALUES
(6, 40.00, 20.00, 11.00, 8.00, 6.00, 5.00, 4.00, 3.00, 2.00, 1.00),
(8, 33.00, 33.00, 34.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00),
(10, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00);
WITH CTE(N) AS (SELECT 1 FROM (VALUES (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) AS A(B)),
CTE2(N) AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM CTE AS A CROSS JOIN CTE) -- Numbers 1-121
SELECT T.Loc,
T.Item,
FiscalYear = DATEPART(YEAR, DATEADD(MONTH, CTE2.N, DATEFROMPARTS(T.FiscalYear, T.FiscalPrd, 1))),
FiscalPrd = DATEPART(MONTH, DATEADD(MONTH, CTE2.N, DATEFROMPARTS(T.FiscalYear, T.FiscalPrd, 1))),
SALES = T.Sales_Total * C.Vals / 100 / 12
FROM #T AS T
JOIN CTE2
ON CTE2.N <= T.YEARS * 12
JOIN
(
SELECT T.PLAN_ID, C.Y, C.Vals
FROM #T2 AS T
CROSS APPLY (VALUES (1, YR01), (2, YR02), (3, YR03), (4, YR04), (5, YR05), (6, YR06), (7, YR07), (8, YR08), (9, YR09), (10, YR10)) AS C(Y, Vals) -- Unpivot
) AS C
ON C.PLAN_ID = T.PLAN_ID
AND C.Y = (CTE2.N + 11) / 12
ORDER BY T.Loc, T.Item, CTE2.N;
Essentially you want to get a list of numbers (up to 120 here but if there's more than 10 years, you would need more) and cross join it to your table. Then unpivot the values in table2 and link them to the numbers table in (grouping them by years).
EDIT:
Taking into account that values can change and assuming the end date is still the same and doesn't increase, this is what I believe you're looking for:
DECLARE #T TABLE (Loc INT, Item INT, Sales_Total DECIMAL(10, 2), FiscalYear INT, FiscalPrd INT, Plan_ID INT, YEARS INT);
INSERT #T (Loc, Item, Sales_Total, FiscalYear, FiscalPrd, Plan_ID, YEARS) VALUES
(33, 17879, 12203.57, 2017, 3, 6, 10),
(33, 17903, 579.97, 2017, 3, 6, 10),
(33, 18870, 799.60, 2017, 3, 6, 10),
(33, 19739, 319.95, 2017, 3, 8, 3),
(33, 21585, 559.96, 2017, 3, 10, 0),
(33, 17879, 10000.00, 2017, 4, 6, 10),
(33, 17903, 500.00, 2017, 4, 6, 10),
(33, 21585, 500.00, 2017, 4, 10, 0),
(33, 17879, 5000.00, 2017, 5, 6, 10),
(33, 17879, 6900.00, 2017, 7, 6, 10);
DECLARE #T2 TABLE (PLAN_ID INT, YR01 DECIMAL(4, 2), YR02 DECIMAL(4, 2), YR03 DECIMAL(4, 2), YR04 DECIMAL(4, 2), YR05 DECIMAL(4, 2), YR06 DECIMAL(4, 2), YR07 DECIMAL(4, 2), YR08 DECIMAL(4, 2), YR09 DECIMAL(4, 2), YR10 DECIMAL(4, 2));
INSERT #T2 (PLAN_ID, YR01, YR02, YR03, YR04, YR05, YR06, YR07, YR08, YR09, YR10) VALUES
(6, 40.00, 20.00, 11.00, 8.00, 6.00, 5.00, 4.00, 3.00, 2.00, 1.00),
(8, 33.00, 33.00, 34.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00),
(10, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00);
WITH CTE(N) AS (SELECT 1 FROM (VALUES (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) AS A(B)),
CTE2(N) AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 FROM CTE AS A CROSS JOIN CTE) -- Numbers 0-120
SELECT T.Loc,
T.Item,
FiscalYear = DATEPART(YEAR, DATEADD(MONTH, CTE2.N, T.FirstDate)),
FiscalPrd = DATEPART(MONTH, DATEADD(MONTH, CTE2.N, T.FirstDate)),
SALES = T.TotalToDate * C.Vals / 100 / 12
FROM
(
SELECT T.Loc,
T.Item,
T.YEARS,
T.Plan_ID,
C.TotalToDate,
FirstDate = CAST(DATEADD(MONTH, 1, CAST(MIN(FiscalYear) OVER (PARTITION BY Loc, Item) AS CHAR(4)) + '-' +
CAST(MIN(FiscalPrd) OVER (PARTITION BY Loc, Item) AS VARCHAR(2)) + '-01') AS DATE),
CurrentDate = CAST(DATEADD(MONTH, 1, CAST(FiscalYear AS CHAR(4)) + '-' + CAST(FiscalPrd AS VARCHAR(2)) + '-01') AS DATE),
C2.NextDate
FROM #T AS T
CROSS APPLY
(
SELECT SUM(Sales_Total)
FROM #T
WHERE Loc = T.Loc
AND Item = T.Item
AND FiscalYear * 12 + FiscalPrd <= T.FiscalYear * 12 + T.FiscalPrd
) AS C(TotalToDate)
OUTER APPLY
(
SELECT CAST(DATEADD(MONTH, 1, CAST(MIN(FiscalYear) AS CHAR(4)) + '-' +
CAST(MIN(FiscalPrd) AS VARCHAR(2)) + '-01') AS DATE)
FROM #T
WHERE Loc = T.Loc
AND Item = T.Item
AND FiscalYear * 12 + FiscalPrd > T.FiscalYear * 12 + T.FiscalPrd
) AS C2(NextDate)
) AS T
JOIN CTE2
ON CTE2.N <= T.YEARS * 12 - 1
AND CTE2.N >= DATEDIFF(MONTH, T.FirstDate, T.CurrentDate)
AND DATEADD(MONTH, CTE2.N, T.FirstDate) < COALESCE(T.NextDate, DATEADD(MONTH, T.YEARS * 12, T.FirstDate))
JOIN
(
SELECT T.PLAN_ID, C.Y, C.Vals
FROM #T2 AS T
CROSS APPLY (VALUES (1, YR01), (2, YR02), (3, YR03), (4, YR04), (5, YR05), (6, YR06), (7, YR07), (8, YR08), (9, YR09), (10, YR10)) AS C(Y, Vals) -- Unpivot
) AS C
ON C.PLAN_ID = T.PLAN_ID
AND C.Y = (CTE2.N + 12) / 12
ORDER BY T.Loc, T.Item, CTE2.N;
SECOND EDIT (Modified to have the start date be the month declared, e.g. March 2017 means the plan starts in March 2017 instead of April 2017. The only difference is adjusting the row_number joins):
/*
#PlanTable: A static table for plans.
*/
DECLARE #PlanTable TABLE (PLAN_ID INT, YR01 DECIMAL(4, 2), YR02 DECIMAL(4, 2), YR03 DECIMAL(4, 2), YR04 DECIMAL(4, 2), YR05 DECIMAL(4, 2), YR06 DECIMAL(4, 2), YR07 DECIMAL(4, 2), YR08 DECIMAL(4, 2), YR09 DECIMAL(4, 2), YR10 DECIMAL(4, 2));
INSERT #PlanTable (PLAN_ID, YR01, YR02, YR03, YR04, YR05, YR06, YR07, YR08, YR09, YR10) VALUES
(6, 40.00, 20.00, 11.00, 8.00, 6.00, 5.00, 4.00, 3.00, 2.00, 1.00),
(8, 33.00, 33.00, 34.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00),
(10, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00);
/*
#RecordsToInsert: The values you're inserting into your final table. Starting with FiscalYear 2017 and FiscalPrd 3 as an example.
*/
DECLARE #RecordsToInsert TABLE (Loc INT, Item INT, Sales_Total DECIMAL(10, 2), FiscalYear INT, FiscalPrd INT, Plan_ID INT, YEARS INT);
INSERT #RecordsToInsert (Loc, Item, Sales_Total, FiscalYear, FiscalPrd, Plan_ID, YEARS) VALUES
(33, 17879, 12203.57, 2017, 3, 6, 10),
(33, 17903, 579.97, 2017, 3, 6, 10),
(33, 18870, 799.60, 2017, 3, 6, 10),
(33, 19739, 319.95, 2017, 3, 8, 3),
(33, 21585, 559.96, 2017, 3, 10, 0);
/*
#FinalTable: This is the table you're using to insert the data.
*/
DECLARE #FinalTable TABLE (Loc INT, Item INT, FiscalYear INT, FiscalPrd INT, SALES DECIMAL(10, 2));
/*
Start by inserting the first lot of information into the final table.
*/
WITH CTE(N) AS (SELECT 1 FROM (VALUES (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) AS A(B)),
CTE2(N) AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 FROM CTE AS A CROSS JOIN CTE) -- Numbers 0-120
INSERT #FinalTable (Loc, Item, FiscalYear, FiscalPrd, SALES)
SELECT T.Loc,
T.Item,
FiscalYear = DATEPART(YEAR, DATEADD(MONTH, CTE2.N, CAST(T.FiscalYear AS CHAR(4)) + '-' + CAST(T.FiscalPrd AS VARCHAR(2)) + '-01')),
FiscalPrd = DATEPART(MONTH, DATEADD(MONTH, CTE2.N, CAST(T.FiscalYear AS CHAR(4)) + '-' + CAST(T.FiscalPrd AS VARCHAR(2)) + '-01')),
SALES = T.Sales_Total * C.Vals / 100 / 12
FROM #RecordsToInsert AS T
JOIN CTE2
ON CTE2.N <= T.YEARS * 12 - 1
JOIN
(
SELECT T.PLAN_ID, C.Y, C.Vals
FROM #PlanTable AS T
CROSS APPLY (VALUES (1, YR01), (2, YR02), (3, YR03), (4, YR04), (5, YR05), (6, YR06), (7, YR07), (8, YR08), (9, YR09), (10, YR10)) AS C(Y, Vals) -- Unpivot
) AS C
ON C.PLAN_ID = T.PLAN_ID
AND C.Y = CTE2.N / 12 + 1
ORDER BY T.Loc, T.Item, CTE2.N;
/*
Next we want to insert records for the next FiscalPrd, so we get rid of the last ones and put in FiscalYear 2017, FiscalPrd 4.
*/
DELETE #RecordsToInsert;
INSERT #RecordsToInsert (Loc, Item, Sales_Total, FiscalYear, FiscalPrd, Plan_ID, YEARS) VALUES
(33, 17879, 10000.00, 2017, 4, 6, 10),
(33, 17903, 500.00, 2017, 4, 6, 10),
(33, 18870, 800.00, 2017, 4, 6, 10),
(33, 19739, 300.00, 2017, 4, 8, 3),
(33, 21585, 500.00, 2017, 4, 10, 0);
/*
Then we update the final table with the new sales total, ignoring periods that were prior to the fiscal year and period being inserted.
*/
UPDATE T
SET SALES = SALES_NEW
FROM (
SELECT FT.Loc,
FT.Item,
FT.FiscalYear,
FT.FiscalPrd,
FT.SALES,
SALES_NEW = (SUM(FT.SALES) OVER (PARTITION BY FT.Loc, FT.Item) + R.Sales_Total) * C.Vals / 100 / COUNT(1) OVER (PARTITION BY FT.Loc, FT.Item, (FT.RN + 11) / 12)
FROM (SELECT *, RN = ROW_NUMBER() OVER (PARTITION BY Loc, Item ORDER BY FiscalYear, FiscalPrd) FROM #FinalTable) AS FT
JOIN #RecordsToInsert AS R
ON R.Loc = FT.Loc
AND R.Item = FT.Item
AND R.FiscalYear * 12 + R.FiscalPrd <= FT.FiscalYear * 12 + FT.FiscalPrd
JOIN
(
SELECT T.PLAN_ID, C.Y, C.Vals
FROM #PlanTable AS T
CROSS APPLY (VALUES (1, YR01), (2, YR02), (3, YR03), (4, YR04), (5, YR05), (6, YR06), (7, YR07), (8, YR08), (9, YR09), (10, YR10)) AS C(Y, Vals) -- Unpivot
) AS C
ON C.PLAN_ID = R.PLAN_ID
AND C.Y = (FT.RN + 11) / 12
) T;
/*
For the first year, the sales for 2017/5 onwards have changed to (totalsales - firstmonth) * 40% / 11
For the second years onward, the sales have changed to totalsales * whatever% / 12
The first month of the first year remains the same.
*/
SELECT Loc, Item, FiscalYear, FiscalPrd, SALES
FROM #FinalTable
ORDER BY Loc, Item, FiscalYear, FiscalPrd;

Finding a date gap (missing date range) within a SQL table

On this SQL Server 2008 database I have a table of attendance, students come into school every day and check in, the table looks something like this:
SchoolID | StudentID | Date
There will be a record for every day for every student on this table. What I want to find out is, given a start date, an end date, and a number of days (gap), find any student that has not checked into school for that number of days. So for example, I need to know which students missed 3 days straight during the month of December, and spit out the list of StudentIDs.
How can I accomplish something like that?
You can produce date ranges from startdate to enddate
then outer join this data with your table, if the student wouldn't have come consider it as 1 then summarize this data.
for generating date range you can use this function as below
CREATE FUNCTION [dbo].[DateRange]
(
#Increment CHAR(1),
#StartDate DATETIME,
#EndDate DATETIME
)
RETURNS
#SelectedRange TABLE
(IndividualDate DATETIME)
AS
BEGIN
;WITH cteRange (DateRange) AS (
SELECT #StartDate
UNION ALL
SELECT
CASE
WHEN #Increment = 'd' THEN DATEADD(dd, 1, DateRange)
WHEN #Increment = 'w' THEN DATEADD(ww, 1, DateRange)
WHEN #Increment = 'm' THEN DATEADD(mm, 1, DateRange)
END
FROM cteRange
WHERE DateRange <=
CASE
WHEN #Increment = 'd' THEN DATEADD(dd, -1, #EndDate)
WHEN #Increment = 'w' THEN DATEADD(ww, -1, #EndDate)
WHEN #Increment = 'm' THEN DATEADD(mm, -1, #EndDate)
END)
INSERT INTO #SelectedRange (IndividualDate)
SELECT DateRange
FROM cteRange
OPTION (MAXRECURSION 3660);
RETURN
END
GO
then
select sum(isAbsent) absentDays, s.studentid from
(
select case when studentid is null then 1 else 0 end isAbsent,individualDate,s.studentid from DateRange('d', '01/11/2014', '30/11/2014') d
cross join tblstudent s
left outer join yourtable on yourtable.Date = d.IndividualDate and yourtable.studentid = s.studentid
) x
group by s.studentid
having sum(isAbsent) > 3
Just look at this. I think you will be able to figure out you own answer from that.This solution take care of the weekend days and holidays :
SQL Fiddle
MS SQL Server 2008 Schema Setup:
CREATE TABLE attendance
([SchoolID] int, [StudentID] int, [Date] datetime)
;
INSERT INTO attendance
([SchoolID], [StudentID], [Date])
VALUES
(1, 1, '2014-12-01 00:00:00'),
(1, 1, '2014-12-02 00:00:00'),
(1, 1, '2014-12-03 00:00:00'),
(1, 1, '2014-12-04 00:00:00'),
(1, 1, '2014-12-05 00:00:00'),
(1, 1, '2014-12-08 00:00:00'),
(1, 1, '2014-12-09 00:00:00'),
(1, 1, '2014-12-10 00:00:00'),
(1, 1, '2014-12-11 00:00:00'),
(1, 1, '2014-12-12 00:00:00'),
(1, 1, '2014-12-15 00:00:00'),
(1, 1, '2014-12-16 00:00:00'),
(1, 1, '2014-12-17 00:00:00'),
(1, 1, '2014-12-18 00:00:00'),
(1, 1, '2014-12-19 00:00:00'),
(1, 2, '2014-12-01 00:00:00'),
(1, 2, '2014-12-02 00:00:00'),
(1, 2, '2014-12-08 00:00:00'),
(1, 2, '2014-12-09 00:00:00'),
(1, 2, '2014-12-10 00:00:00'),
(1, 2, '2014-12-11 00:00:00'),
(1, 2, '2014-12-12 00:00:00'),
(1, 2, '2014-12-15 00:00:00'),
(1, 2, '2014-12-16 00:00:00'),
(1, 2, '2014-12-17 00:00:00'),
(1, 2, '2014-12-18 00:00:00'),
(1, 2, '2014-12-19 00:00:00')
;
CREATE TABLE holidays
([Date] datetime)
;
INSERT INTO holidays
([Date])
VALUES
('2014-12-22 00:00:00'),
('2014-12-23 00:00:00'),
('2014-12-24 00:00:00'),
('2014-12-25 00:00:00'),
('2014-12-26 00:00:00'),
('2014-12-29 00:00:00'),
('2014-12-30 00:00:00'),
('2014-12-31 00:00:00')
;
CREATE TABLE students
([StudentID] int, [Name] varchar(5))
;
INSERT INTO students
([StudentID], [Name])
VALUES
(1, 'John'),
(2, 'Peter')
;
Query 1:
DECLARE #start DATE, #end DATE
SELECT #start = '20141201', #end = '20141231'
;WITH tdate AS
(
SELECT TOP (DATEDIFF(DAY, #start, #end) + 1)
n = ROW_NUMBER() OVER (ORDER BY [object_id])
FROM sys.all_objects
)
SELECT DISTINCT Name
FROM students s
INNER JOIN attendance a ON s.StudentID = a.StudentID
INNER JOIN tdate ON DATEADD(DAY, n-1, #start) = a.Date
GROUP BY NAME
HAVING
(SELECT count(*)
FROM tdate
LEFT OUTER JOIN holidays h ON DATEADD(DAY, n-1, #start) = h.Date
WHERE h.date is null
AND DATEPART(dw,DATEADD(DAY, n-1, #start)) not in (1,7))
- COUNT(*) >= 3
Results:
| NAME |
|-------|
| Peter |
UPDATE
SELECT s.StudentID, d.Date
FROM students s
INNER JOIN (
SELECT DATEADD(DAY, n-1, #start) as Date
FROM tdate
LEFT OUTER JOIN holidays h ON DATEADD(DAY, n-1, #start) = h.Date
WHERE h.date is null
AND DATEPART(dw,DATEADD(DAY, n-1, #start)) not in (1,7)) d ON 1 = 1
LEFT OUTER JOIN attendance a ON s.StudentID = a.StudentID AND d.Date = a.Date
WHERE a.StudentID IS NULL
ORDER BY s.StudentID, d.Date
Results:
| STUDENTID | DATE |
|-----------|------------|
| 2 | 2014-12-03 |
| 2 | 2014-12-04 |
| 2 | 2014-12-05 |

Resources