SQL frequency count - sql-server

I have a table in SSMS:
Id Date Value
111 1/1/18 x
111 1/2/18 x
111 1/3/18 y
111 1/4/18 y
111 1/5/18 x
111 1/6/18 x
222 1/3/18 z
222 1/6/18 y
222 1/8/18 y
I want to count for the frequency of latest value . So the output will be:
Id Value Days
111 x 2 *(for 1/5/18 & 1/6/18)*
222 y 3 *(for 1/6/18 & 1/8/18; Here I assume 1/7/18 is a weekend or holiday. Even though my table skips the weekend, we still want to count days for the weekend)*
How would this be done? Many thanks!

Use lag to get the previous row's value and then a running sum to assign groups. Thereafter count the number in the first group.
select id,val,datediff(day,min(date),max(date))+1 as days
from (select t.*,sum(case when val=prev_val then 0 else 1 end) over(partition by id order by date desc) as grp
from (select t.*,lag(val) over(partition by id order by date desc) as prev_val
from tbl t
) t
) t
where grp=1
group by id,val

Try:
SELECT COUNT(*) FROM Table1 WHERE Value =
(
SELECT Value FROM Table1 WHERE Id = MAX(Id)
)

I hope you want this
select Id, count(Date) as "Days", Value from SSMS
group by ID, Value
correct me if I'm wrong

This answer should account for the weekends and holiday assumptions you have made (with another test case).
SELECT
T.Id, T.val, DATEDIFF(DD, COALESCE(T.MaxSwitch, T.MinMatch, T.MaxDate), T.MaxDate) + 1 AS [Days]
FROM (
SELECT
T.Id,
MAX(CASE WHEN T.LastValue IS NULL THEN T.val ELSE '' END) AS [val],
MAX(T.Date) AS [MaxDate],
MAX(CASE WHEN t.val <> t.LastValue THEN T.RunningDate ELSE NULL END) AS [MaxSwitch],
MIN(CASE WHEN t.val = t.LastValue THEN T.[Date] ELSE NULL END) AS [MinMatch]
FROM (SELECT *, LAG(val) OVER (PARTITION BY Id ORDER BY DATE DESC) AS LastValue,
LAG([Date]) OVER (PARTITION BY Id ORDER BY DATE DESC) AS RunningDate FROM #T) T
GROUP BY
T.Id
) T
This approach uses LAG to track previous value and date so that it can determine (1) the last value to get running match, (2) the latest date when value switched to most recent value, and (3) the earliest date with value matching final date. It then calculates the date difference to account for skipping days in table from priority of (A) latest date value switched to recent value, (B) or if no switch occurred, then earliest date with value matching final date.
For the sample data below:
DECLARE #T TABLE (
Id INT, [Date] DATE, val VARCHAR(10)
)
INSERT #T VALUES
('111', '1/1/18', 'x'),
('111', '1/2/18', 'x'),
('111', '1/3/18', 'y'),
('111', '1/4/18', 'y'),
('111', '1/5/18', 'x'),
('111', '1/6/18', 'x'),
('222', '1/2/18', 'y'),
('222', '1/3/18', 'z'),
('222', '1/6/18', 'y'),
('222', '1/8/18', 'y'),
('333', '1/9/18', 'a')
The following output is given:
Id val Days
----------- ---------- -----------
111 x 2 (from OP example)
222 y 3 (from OP example)
333 a 1 (case of single value)

Related

SQL query to get start and end date from a result set

I am working on one of requirement the raw data is in following format
Requirement - Startdate should be the date when status changed to 1 and enddate should be the 1st date after the record status changed from 1 to any other number.
Customer
Status
Date
A123
0
7/2/2021
A123
0
7/15/2021
A123
0
7/22/2021
A123
1
8/18/2021
A123
1
9/8/2021
A123
0
12/1/2021
A123
0
1/21/2022
A123
1
3/6/2022
A123
1
3/7/2022
A123
0
3/15/2022
B123
1
1/1/2022
B123
0
1/6/2022
C123
1
1/2/2022
C123
2
1/8/2022
C123
0
1/9/2022
expected output
Customer
StartDate
EndDate
A123
8/18/2021
12/1/2021
A123
9/8/2021
12/1/2021
A123
3/6/2022
3/15/2022
A123
3/7/2022
3/15/2022
B123
1/1/2022
1/6/2022
C123
1/2/2022
1/8/2022
Query I tried to get the output is below, I am getting the output for Customer B123 and C123, but not for A123 as expected.
Query Explanation - In 1st part of query I am taking all the records with status = 1 and in next part taking only those records where status is not equal to 1, and joining these 2 datasets based on Customer and row number generated.
SELECT A.[Customer],A.StartDate,B.EndDate
from
(
SELECT [Customer],MIN(Date) AS STARTDATE,[Status],RANK() OVER (PARTITION BY [STATUS] ORDER BY Date ASC) AS ROWNUM
FROM table1
WHERE [STATUS] = 1
GROUP BY Customer,Date,[Status]
) A
LEFT JOIN
(
SELECT [Customer],MIN(Date) AS ENDDATE,[Status],RANK() OVER (PARTITION BY [STATUS] ORDER BY Date ASC) AS ROWNUM
FROM table1
WHERE [STATUS] != 1
AND Date>(
SELECT MIN(Date) AS STARTDATE
FROM table1
WHERE [STATUS] = 1
)
GROUP BY Customer,Date,[Status]
) B
ON
(
A.[Customer] = B.[Customer]
AND A.RowNum = B.RowNum
)
ORDER BY A.Startdate
First you list the rows where Status = 1 and then use CROSS APPLY to get the corresponding minimum Date where the Status is not equal to 1
select s.[Customer],
StartDate = s.[Date],
EndDate = e.[Date]
from Table1 s
cross apply
(
select [Date] = min(e.[Date])
from Table1 e
where e.[Customer] = s.[Customer]
and e.[Date] > s.[Date]
and e.[Status] <> 1
) e
where s.[Status] = 1
order by s.[Customer], s.[Date]
Here is a more efficient way to do this without a self-join.
WITH cte01only AS
( SELECT *, CASE Status WHEN 1 THEN 1 ELSE 0 END AS Status1 FROM table1 ),
cteDifference AS
(
SELECT *,
ROW_NUMBER() OVER (PARTITION BY Customer ORDER BY Date, Status1)
- ROW_NUMBER() OVER (PARTITION BY Customer, Status1 ORDER BY Date) AS StatusGroup
FROM cte01only
),
cteGroup AS
(
SELECT Customer, StatusGroup, Status1, MIN(Date) As StartDate
FROM cteDifference
GROUP BY Customer, StatusGroup, Status1
),
cteNextDate AS
(
SELECT Customer, StatusGroup, Status1, StartDate,
LEAD(StartDate, 1, NULL) OVER (PARTITION BY Customer ORDER BY StatusGroup) AS EndDate
FROM cteGroup
)
SELECT Customer, StartDate, EndDate
FROM cteNextDate
WHERE Status1 = 1
ORDER BY Customer, StateDate
The key trick here is the second CTE which uses the difference of two ROW_NUMBER() functions to tag the customer records (with the StatusGroup column) into separate partitions by contiguous runs of records whose status is 1 or not 1. After that they can be grouped according to that tag to get the start dates, and then use the LEAD() function to get the following group's StartDate as the current groupings EndDate.
(There may be a more compact way to express this, but I like to layout each stage as a separate CTE.)

Create data ranges based on list of dates

I have a list of dates like this (no gaps, each calendar date):
DateKey
Valid
2021-01-01
1
2021-01-02
1
2021-01-03
1
2021-01-04
0
2021-01-05
0
2021-01-06
1
2021-01-07
1
I would like to convert them using T-SQL to date ranges considering valid dates only.
So the results would be:
ValidFrom
ValidTo
2021-01-01
2021-01-03
2021-01-06
2021-01-07
Grouping simply by Valid flag rtutns wrong results:
select min(dateKey),max(dateKey)
from #t
group by Valid
If I knew how to assign a unique value for each continuous segment of valid dates, that would solve my problem. Is there anyone that can help me with this?
Just another option using the window function sum() over()
Select ValidFrom = min(DateKey)
,ValidTo = max(DateKey)
From (
Select *
,Grp = sum(case when Valid=0 then 1 else 0 end) over (order by DateKey)
from YourTable
) A
Where Valid=1
Group By Grp
Returns
ValidFrom ValidTo
2021-01-01 2021-01-03
2021-01-06 2021-01-07
Something like the following may work for you:
DECLARE #Dates TABLE (Dt DATE, Valid BIT)
INSERT #Dates
VALUES('2021-01-01', 1),
('2021-01-02', 1),
('2021-01-03', 1),
('2021-01-04', 0),
('2021-01-05', 0),
('2021-01-06', 1),
('2021-01-07', 1)
SELECT MIN(dt.Dt) AS BeginRange,
MAX(dt.Dt) AS EndRange
FROM (
SELECT d.Dt,
DATEDIFF(D, ROW_NUMBER() OVER(ORDER BY d.Dt), d.Dt) AS DtRange
FROM #Dates d
WHERE Valid = 1
) AS dt
GROUP BY dt.DtRange;
I think I've just found the solution of my problem:
https://dba.stackexchange.com/questions/197972/convert-list-of-dates-in-a-date-range-in-sql-server
DECLARE #t TABLE (dt DATE);
INSERT INTO #t (dt)
VALUES ('20180202')
,('20180203')
,('20180204')
,('20180205')
,('20180209')
,('20180212')
,('20180213');
WITH c
AS (
SELECT dt
,dateadd(day, - 1 * dense_rank() OVER (orderby dt), dt) AS grp
FROM #t
)
SELECT min(dt) AS start_range
,max(dt) AS end_range
FROM c
GROUP BY grp;

How to update rows between two different sets of criteria in SQL Server without using a loop

Issue: How to update rows between two different sets of criteria in SQL Server without using a loop (SQL Server 2014). In other words, for each row in a result set, how to update every row between the first occurrence (with one criterion) and the second occurrence (with different criteria). I think part of the issue is trying to run a TOP N query for every row in the query.
Specifically:
In the example starting table below, how can I update the last 2 columns of dates where:
Update rows between the null Category rows and the last consecutive "M" Category row if the null Category row is preceded by a "S" Category. Category can contain any order of "S", "M", or null.
Set StartDate = IDEndDate+1 day of the "S" row preceding the null row.
Set EndDate = IDEndDate of the last row with a "M" Category.
Here is a SQLFiddle.
Notes: I have done this in the past with a loop (fetch..) but I am trying to do this with a few queries instead kind of like:
step 1: Get work: select all valid null rows (beginning of range)
step 2: for each row above, select the related last "M" row (end of range) and then run a query to update the StartDate, EndDates in each range.
Starting Table:
ID IDStartDate IDEndDate Category
------------------------------------
11 2017-01-01 2017-01-31 S
11 2017-02-02 2017-02-03 null
11 2017-02-03 2017-03-31 M
11 2017-04-01 2017-04-30 M
22 2017-05-01 2017-06-15 S
22 2017-06-16 2017-06-20 null
22 2017-06-21 2017-06-25 M
22 2017-06-26 2017-06-27 null
22 2017-06-28 2017-06-29 S
22 2017-06-30 2017-07-05 M
33 2017-06-30 2017-07-14 M
33 2017-07-15 2017-07-20 S
33 2017-07-21 2017-07-25 null
44 2018-06-30 2018-07-14 S
44 2018-07-15 2018-07-20 M
44 2018-07-21 2018-07-25 null
Desired Ending Table:
ID IDStartDate IDEndDate Category StartDate EndDate
----------------------------------------------------------
11 2017-01-01 2017-01-31 S
11 2017-02-02 2017-02-03 null 2017-02-01 2017-04-30
11 2017-02-03 2017-03-31 M 2017-02-01 2017-04-30
11 2017-04-01 2017-04-30 M 2017-02-01 2017-04-30
22 2017-05-01 2017-06-15 S
22 2017-06-16 2017-06-20 null 2017-06-16 2017-06-25
22 2017-06-21 2017-06-25 M 2017-06-16 2017-06-25
22 2017-06-26 2017-06-27 null
22 2017-06-28 2017-06-29 S
22 2017-06-30 2017-07-05 M
33 2017-06-30 2017-07-14 M
33 2017-07-15 2017-07-20 S
33 2017-07-21 2017-07-25 null
44 2018-06-30 2018-07-14 S
44 2018-07-15 2018-07-20 M
44 2018-07-21 2018-07-25 null
Below is some SQL to create the table and view the query results that I have started. I tried cte, cross apply, outer apply, inner joins... with no luck.
thanks so much!
CREATE TABLE test (
ID INT,
IDStartDate date,
IDEndDate date,
Category VARCHAR (2),
StartDate date,
EndDate date
);
INSERT INTO test (ID, IDStartDate, IDEndDate, Category)
VALUES
(11, '2017-01-01', '2017-01-31', 'S')
,(11, '2017-02-02', '2017-02-03', null)
,(11, '2017-02-03', '2017-03-31', 'M')
,(11, '2017-04-01', '2017-04-30', 'M')
,(22, '2017-05-01', '2017-06-15', 'S')
,(22, '2017-06-16', '2017-06-20', null)
,(22, '2017-06-21', '2017-06-25', 'M')
,(22, '2017-06-26', '2017-06-27', null)
,(22, '2017-06-28', '2017-06-29', 'S')
,(22, '2017-06-30', '2017-07-05', 'M')
,(33, '2017-06-30', '2017-07-14', 'M')
,(33, '2017-07-15', '2017-07-20', 'S')
,(33, '2017-07-21', '2017-07-25', null)
,(44, '2018-06-30', '2018-07-14', 'S')
,(44, '2018-07-15', '2018-07-20', 'M')
,(44, '2018-07-21', '2018-07-25', null);
--**************************
--results: shows first rows of each range
--**************************
;with cte as
(
select *
,ROW_NUMBER() OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS RowNum
,LAG(IDEndDate) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS lastIDEndDate
,LAG(Category) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS lastCategory
,LEAD(Category) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS nextCategory
from test
)
select * --select first row of each range to update
from cte
where Category is null and lastCategory = 'S' and nextCategory = 'M'
--*******************************
--6 of 8 "new" values are correct (missing NewEndDate for first range)
--*******************************
;with cte as
(
SELECT *
,ROW_NUMBER() OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS RowNum
,LAG(IDEndDate) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS lastIDEndDate
,LAG(Category) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS lastCategory
,LEAD(Category) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS nextCategory
FROM test
), cte2 as
(
select * --find the first/start row of each range
,LAG(RowNum) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS lastRowNum
,IIF(Category is null and lastCategory = 'S' and nextCategory = 'M', DateAdd(day, 1, lastIDEndDate), null) as NewStartDate
,IIF(Category is null and lastCategory = 'S' and nextCategory = 'M', RowNum, null) as NewStartRowNum
from cte
)
select t1.*, t3.*
from cte2 t1
outer apply
(
select top 1 --find the last/ending row of each range
t2.lastIDEndDate as NewEndDate
,t2.lastRowNum as NewEndRowNum
from cte2 t2
where t1.ID = t2.ID
and t1.NewStartRowNum < t2.RowNum
and t2.nextCategory <> 'M'
order by t2.ID, t2.RowNum
) t3
order by t1.ID, t1.RowNum
Here's an attempt on this SQL puzzle.
Basically, it updates from a CTE.
First it calculates a Cummulative sum. To create some kind of ranking.
Then only for rank 2 & 3 it'll calculate the dates.
;WITH CTE AS
(
SELECT ID, IDStartDate, IDEndDate, Category, StartDate, EndDate,
DATEADD(day,1, FIRST_VALUE(IDEndDate) OVER (PARTITION BY ID ORDER BY IDStartDate)) AS NewStartDate,
FIRST_VALUE(IDEndDate) OVER (PARTITION BY ID ORDER BY IDStartDate DESC) AS NewEndDate
FROM
(
SELECT ID, IDStartDate, IDEndDate, Category, StartDate, EndDate,
SUM(CASE WHEN Category = 'S' THEN 2 WHEN Category IS NULL THEN 1 END) OVER (PARTITION BY ID ORDER BY IDStartDate) AS cSum
FROM test t
) q
WHERE cSum IN (2, 3)
)
UPDATE CTE
SET
StartDate = NewStartDate,
EndDate = NewEndDate
WHERE (Category IS NULL OR Category = 'M');
A test on rextester here
I answered my own question. I had two major errors:
1) A Cross Apply (or Outer Apply) is needed for the Top N query to work properly.
Using a cross apply, the Top N query will be run for each row from the inner query.
Using an inner join (or left join), all rows will be returned first from the inner query and the Top N query runs only once.
2) Filtering on "[column] <> 'M'" messed me up as it did not exclude NULL's. I had to use instead "[column] = 'S' or [column] is null"
Final SQL found in rextester
Working code below:
;with cte as
(
SELECT *
,ROW_NUMBER() OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS RowNum
,LAG(IDEndDate) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS lastIDEndDate
,LAG(Category) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS lastCategory
,LEAD(Category) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS nextCategory
FROM test
), cte2 as
(
select t1.ID, t1.IDStartDate, t1.IDEndDate --find the first/start row of the range
,IIF(Category is null and lastCategory = 'S' and nextCategory = 'M', DateAdd(day, 1, lastIDEndDate), null) as NewStartDate
,IIF(Category is null and lastCategory = 'S' and nextCategory = 'M', RowNum, null) as NewStartRowNum
,t3.*
from cte t1
cross apply
(
select top 1 --find the last/ending row of the range
t2.IDEndDate as NewEndDate
,t2.RowNum as NewEndRowNum
from cte t2
where t1.ID = t2.ID
and t1.RowNum < t2.RowNum
and (t2.nextCategory ='S' or t2.nextCategory is null)
order by t1.ID, t1.RowNum
) t3
where Category is null and lastCategory = 'S' and nextCategory = 'M'
)
update t4
set StartDate = NewStartDate
,EndDate = NewEndDate
from cte t4
inner join cte2 t5
on t4.ID = t5.ID
and t4.RowNum Between NewStartRowNum and NewEndRowNum
select * from test

Fill the gaps, event based

I am trying to calculate churn of customers based on activity they could have done, opposed to churn by date that is the normal thing. We have events that is connected to a specific host, in my example all events are hosted by Alice but it could be different hosts.
All the people that follow a specific event should be placed in a category (new, active, churned and resurrected).
New: First time a person follow an event from the specific host.
Active: Follow again (and last event from specific host was also followed).
Churned: Follower had a chance to follow but didn't.
Resurrected: Follower that has churned has started to follow a previously followed host.
declare #events table (event varchar(50), host varchar(50), date date)
declare #eventFollows table (event varchar(50), follower varchar(50))
insert into #events values ('e_1', 'Alice', GETDATE())
insert into #events values ('e_2', 'Alice', GETDATE())
insert into #events values ('e_3', 'Alice', GETDATE())
insert into #events values ('e_4', 'Alice', GETDATE())
insert into #events values ('e_5', 'Alice', GETDATE())
insert into #eventFollows values ('e_1', 'Bob') --new
insert into #eventFollows values ('e_2', 'Bob') --active
--Bob churned
insert into #eventFollows values ('e_4', 'Megan') --new
insert into #eventFollows values ('e_5', 'Bob') --resurrected
insert into #eventFollows values ('e_5', 'Megan') --active
select * from #events
select * from #eventFollows
The expected outcome should be something like this
select 'e_1', 1 as New, 0 as resurrected, 0 as active, 0 as churned --First time Bob follows Alice event
union all
select 'e_2', 0 as New, 0 as resurrected, 1 as active, 0 as churned --Bob follows the next event that Alice host (considered as Active)
union all
select 'e_3', 0 as New, 0 as resurrected, 0 as active, 1 as churned --Bob churns since he does not follow the next event
union all
select 'e_4', 1 as New, 0 as resurrected, 0 as active, 0 as churned --First time Megan follows Alice event
union all
select 'e_5', 0 as New, 1 as resurrected, 1 as active, 0 as churned --Second time (active) for Megan and Bob is resurrected
I started with a query of something like below, but the problem is that I don't get all the events that the followers did not follow (but could have followed).
select a.event, follower, date,
LAG (a.event,1) over (partition by a.host, ma.follower order by date) as lag,
LEAD (a.event,1) over (partition by a.host, ma.follower order by date) as lead,
LAG (a.event,1) over (partition by a.host order by date) as lagP,
LEAD (a.event,1) over (partition by a.host order by date) as leadP
from #events a left join #eventFollows ma on ma.event = a.event order by host, follower, date
Any ideas?
This may seem a bit of an indirect approach, but it's possible to detect islands by checking for gaps in the numbers:
;with nrsE as
(
select *, ROW_NUMBER() over (order by event) rnrE from #events
), nrs as
(
select f.*,host, rnrE, ROW_NUMBER() over (partition by f.follower, e.host order by f.event ) rnrF
from nrsE e
join #eventFollows f on f.event = e.event
), f as
(
select host, follower, min(rnrE) FirstE, max(rnrE) LastE, ROW_NUMBER() over (partition by follower, host order by rnrE - rnrF) SeqNr
from nrs
group by host, follower, rnrE - rnrF --difference between rnr-Event and rnr-Follower to detect gaps
), stat as --from the result above on there are several options. this example uses getting a 'status' and pivoting on it
(
select e.event, e.host, case when f.FirstE is null then 'No participants' when f.LastE = e.rnrE - 1 then 'Churned' when rnrE = f.FirstE then case when SeqNr = 1 then 'New' else 'Resurrected' end else 'Active' end Status
from nrsE e
left join f on e.rnrE between f.FirstE and f.LastE + 1 and e.host = f.host
)
select p.* from stat pivot(count(Status) for Status in ([New], [Resurrected], [Active], [Churned])) p
The last 2 steps could be simplified, but getting the 'Status' this way might be reusable for other scenarios
This matches your desired result
SELECT
X.event, X.host, X.date,
IsNew = SUM(CASE WHEN X.FirstFollowerEvent = X.event THEN 1 ELSE 0 END),
IsActive = SUM(CASE WHEN X.lagFollowerEvent = X.lagEvent THEN 1 ELSE 0 END),
IsChurned = SUM(CASE WHEN X.follower IS NULL THEN 1 ELSE 0 END),
IsResurrected = SUM(CASE WHEN X.lagFollowerEvent <> X.lagEvent AND X.FirstFollowerEvent IS NOT NULL THEN 1 ELSE 0 END)
FROM
(
select
a.event, a.host, ma.follower, a.date,
FIRST_VALUE(a.event) over (partition by a.host, ma.follower order by a.date, a.event) as FirstFollowerEvent,
LAG (a.event,1) over (partition by a.host, ma.follower order by a.date, a.event) as lagFollowerEvent,
LAG (a.event,1) over (partition by a.host order by a.date, a.event) as lagEvent
FROM
#events a
LEFT join
#eventFollows ma on a.event = ma.event
) X
GROUP BY
X.event, X.host, X.date
ORDER by
X.event, X.host, X.date

Complex SQL Query advise : Reservation allotment logic

I am trying to do a complex query (at least, it is complex for me) on SQL Server 2008 and so far I can come this far. Here is the code;
DECLARE #Hotels AS TABLE(
HotelID INT,
HotelName NVARCHAR(100)
);
DECLARE #HotelAllotments AS TABLE(
HotelID INT,
StartDate DATETIME,
EndDate DATETIME,
Allotment INT
);
DECLARE #Reservations AS TABLE(
ReservationID INT,
HotelID INT,
CheckIn DATETIME,
CheckOut DATETIME,
IsCanceled BIT
);
INSERT #Hotels VALUES(1,'Foo Hotel');
INSERT #Hotels VALUES(2,'Poo Hotel');
INSERT #HotelAllotments VALUES(1,'2011-01-01', '2011-02-01', 10);
INSERT #HotelAllotments VALUES(1,'2011-02-02', '2011-02-18', 7);
INSERT #HotelAllotments VALUES(1,'2011-02-19', '2011-05-18', 19);
INSERT #HotelAllotments VALUES(1,'2011-05-19', '2011-10-18', 30);
INSERT #HotelAllotments VALUES(2,'2011-05-19', '2011-10-18', 30);
INSERT #Reservations VALUES(100, 1, '2011-05-10','2011-05-24',0);
INSERT #Reservations VALUES(101, 1, '2011-05-18','2011-05-28',0);
INSERT #Reservations VALUES(102, 1, '2011-03-07','2011-03-19',0);
INSERT #Reservations VALUES(103, 1, '2011-08-29','2011-09-07',0);
INSERT #Reservations VALUES(104, 1, '2011-09-01','2011-09-07',1);
INSERT #Reservations VALUES(105, 1, '2011-09-01','2011-09-07',1);
with e as(
SELECT ReservationID as resid1, CheckIn as chin1, 1 as lvl
FROM #Reservations res1
WHERE res1.HotelID = 1
UNION ALL
SELECT ReservationID as resid2, DATEADD(DAY,1,stall.chin1) as chin2, 1
FROM #Reservations res2
INNER JOIN e stall ON stall.chin1 < res2.CheckOut
WHERE stall.resid1 = res2.ReservationID
)
SELECT tb.chin1, SUM(lvl)
FROM e tb
GROUP BY tb.chin1
ORDER BY tb.chin1 DESC
On #HotelAllotments section, there are start and end dates as you can see. The allotment is for daily basis. I mean if row is like below;
INSERT #HotelAllotments VALUES(1,'2011-01-01', '2011-01-03', 10);
It means this;
The Hotel whose id is 1 has 10 allotment on 2011-01-01
The Hotel whose id is 1 has 10 allotment on 2011-01-02
The Hotel whose id is 1 has 10 allotment on 2011-01-03
Then, after that if we receive a reservation between 2011-01-01 and 2011-01-03, like below;
INSERT #Reservations VALUES(106, 1, '2011-01-01','2011-01-03',0);
The situation will be as below;
The Hotel whose id is 1 has 9 allotment left after the reservation on 2011-01-01
The Hotel whose id is 1 has 9 allotment left after the reservation on 2011-01-02
The Hotel whose id is 1 has 10 allotment left after the reservation on 2011-01-03
Above, I have created some temp tables and inserted some fake values and I tried a query. It gets me somewhere (I don't know how to call it. So if you have a
chance to run the query, you would see where it has gotten me so far) but not the place I need. What I need here is that;
I need to list all the dates which a hotel has an agreement and its left allotments after received reservations. here is an example;
HotelID Date Allotment
------- ---------- ---------
1 2011-01-01 9
1 2011-01-02 9
1 2011-01-03 10
1 2011-01-04 10
1 2011-01-05 10
So how can I achieve this?
EDIT
Some them should wonder why an allotment is taken away for the first two days of the reservation, but not the last one. It is because the guest wouldn't be staying all day at the hotel at the last day. S/he should empty the room until 12:00 am. So there won't be any allotment usage on the last date.
;WITH expanded AS (
SELECT
a.HotelID,
Date = DATEADD(DAY, v.number, a.StartDate),
a.Allotment
FROM #HotelAllotments a
INNER JOIN master..spt_values v ON v.type = 'P'
AND v.number BETWEEN 0 AND DATEDIFF(DAY, a.StartDate, a.EndDate)
),
filtered AS (
SELECT
e.HotelID,
e.Date,
Allotment = e.Allotment - COUNT(r.ReservationID)
FROM expanded e
LEFT JOIN #Reservations r ON e.HotelID = r.HotelID
AND e.Date >= r.CheckIn AND e.Date < r.CheckOut
AND r.IsCanceled = 0
GROUP BY e.HotelID, e.Date, e.Allotment
)
SELECT *
FROM filtered;
This solution uses a system table, master..spt_values, as a tally table to obtain the lists of dates instead of the date ranges. Next, the expanded allotment list is joined with the #Resevations table. For every date in the list, the correpsonding allotment is decreased by the number of reservations whose ranges match the given date.
I was a bit hasty on writing my where clause. I didnt know if you wanted to sort out the blank days. here is what i came up with after setting the where clause. The reason i have the datejumps is to compensate for the limitation of 100 recusive calls in sql. So I join with 10 rows from a system table make better use of the 100 recusive, that way i can get 1000 rows instead of 100.
WITH cte(HOTELID, STARTDATE, ENDDATE, Allotment)
as
(
SELECT H.HOTELID, A.STARTDATE + RN STARTDATE, (SELECT MAX(ENDDATE) FROM #HotelAllotments) ENDDATE, (select Allotment from #HotelAllotments where A.STARTDATE + RN between StartDate and enddate and H.HOTELID = HOTELID) Allotment
FROM (
SELECT MIN(STARTDATE) STARTDATE from #HotelAllotments c
) A,
(SELECT TOP 10 rn = ROW_NUMBER() OVER (ORDER BY (SELECT 1))-1 FROM INFORMATION_SCHEMA.COLUMNS) B,
#Hotels H
UNION ALL
SELECT ch.HOTELID, ch.STARTDATE + 10, ENDDATE, (select Allotment from #HotelAllotments where CH.STARTDATE + 10 between StartDate and enddate and CH.HOTELID = HOTELID)
FROM cte ch
WHERE CH.STARTDATE< ENDDATE
AND CH.HOTELID = HOTELID
)
SELECT HotelID, StartDate Date , Allotment - (select count(*) from #Reservations where cte.STARTDATE between CheckIn and CheckOut and cte.HOTELID = HOTELID) Allotment
FROM CTE where allotment is not null
ORDER BY STARTDATE, HOTELID

Resources