0

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.

tugberk
  • 57,477
  • 67
  • 243
  • 335
  • 1
    I hope just typos, 2011-01-01 -> 2011-03-01 is 2 months and one day, not three days. In some spots in the question you seem to be transposing yyyy-mm-dd (which everyone expects) and yyyy-dd-mm (which very few will). – Aaron Bertrand Jun 27 '11 at 14:14
  • @Aaron very good catch. that was just typos, corrected it. – tugberk Jun 27 '11 at 14:43
  • 1
    I think there is still a typo in the next section "Then, after that if we receive..." the first and third bullets indicate different allotments for the same date (and none of those dates match the dates in the previous section). Also, it would be good to explain why an allotment is taken away for the first two days of the reservation, but not the last one. Is it because there are more days in that same reservation? So an allotment is subtracted from every day except the last day in a reservation? What if another reservation starts on the very next day? – Aaron Bertrand Jun 27 '11 at 14:45
  • @Aaron yes, there was a typo again. I inserted wrong dates as following : `INSERT @HotelAllotments VALUES(1,'2011-01-01', '2011-01-02', 10);` and now it is corrected. I am going to explain why an allotment is taken away for the first two days of the reservation, but not the last one on my question as an edit. – tugberk Jun 27 '11 at 14:50
  • @Aaron done, edited my question. Have a look. – tugberk Jun 27 '11 at 14:52
  • 1
    @tugberk: There still were some discrepancies in your post which I took the liberty of fixing. It would be great if you verified my changes. – Andriy M Jun 27 '11 at 15:50
  • @Andriy perfect. thanks! I looked at them and they are all right in place now. I am thinking that creating the question is this hard, I am guessing how hard creating the actual logic will be :) – tugberk Jun 27 '11 at 15:56
  • Still not quite clear on why the first insert (2011-01-01 -> 2011-02-01) yields allotments of 9 for Jan 1, 9 for Jan 2, and 10 for Jan 3. There are no other reservations that affect January, so what is special about January 3? Maybe your example and description should match your sample data instead of confusingly having different data... – Aaron Bertrand Jun 27 '11 at 15:58
  • @Aaron I updated the qu, hope that it is clear now. – tugberk Jun 27 '11 at 16:05
  • 1
    @tugberk: It's only partially related to the question but why you use the `DATETIME` dataype when in SQL-SErver 2008 there is `DATE` datatype? – ypercubeᵀᴹ Jun 27 '11 at 16:39
  • @ypercube hmm, never know it is there. **DATE** doesn't issue time, right? – tugberk Jun 27 '11 at 16:51
  • Yes: http://msdn.microsoft.com/en-us/library/bb630352%28v=SQL.100%29.aspx – ypercubeᵀᴹ Jun 27 '11 at 17:06

2 Answers2

4
;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.

Community
  • 1
  • 1
Andriy M
  • 76,112
  • 17
  • 94
  • 154
  • the same thing, I am gonna check that at home later today and let you know. – tugberk Jun 27 '11 at 16:52
  • I have not tested your script, but it seems correct and just for solving a question this hard you deserve points, normally people are sticky with points for hard questions. So it is well deserved and you deserve more. The question was posted for several hours before anyone came up with a solution. – t-clausen.dk Jun 28 '11 at 05:14
  • your script worked like a charm. thanks for all of your efforts. When I am going through your code, one thing I am having trouble with the logic of using `master..spt_values`. what is its role here exactly. (I know you explained it and gave a link as well but I am still having trouble to sort that out) – tugberk Jun 28 '11 at 07:39
  • 1
    @tugberk: For this solution I needed a number (tally) table, i.e. a table that contains a sequence of numbers (integers). Rather than creating one by myself, I used `master..spt_values`. I've now added a link to an SO question about that table. (Don't know why I didn't think of it initially.) Basically, it's a system table used for internal purposes by SQL Server's system SPs. One of the subsets of this table, the one where `type` has the value of `'P'`, gives you a list of integers from 0 to 2047, just what I needed for my query. … – Andriy M Jun 28 '11 at 09:17
  • 1
    @tugberk: As for the logic behind using `spt_values` as a number table, well, it's quite simple. With the help of the number table I am duplicating every row of `@HotelAllotments` so many times how many days the corresponding range spans, by using the condition `where number between 0 and (EndDate - StartDate)`, basically. When retrieving the values, I am simply adding the `number` of days to `StartDate`, and that's how I obtain the list of dates in place of the date range. Please let me know if anything remains unclear. – Andriy M Jun 28 '11 at 09:23
2

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
t-clausen.dk
  • 43,517
  • 12
  • 59
  • 92
  • I am gonna check that at home later today and let you know. – tugberk Jun 27 '11 at 16:51
  • 2
    When I tested this, it returned more rows than mine (wrongly, I think), also you didn't account for `IsCanceled` (you have now) and when comparing to reservation ranges, you included the end date which you shouldn't, as the OP has specifically requested. On the other point, your query outperformed mine considerably (again!), and that amazes me most. I mean, how? :) However, I've already got a couple of thoughts as to what could be improved about my solution. I'll probably need to wait until tomorrow when I can verify my thoughts. – Andriy M Jun 28 '11 at 09:51
  • *Again, it beat my solution in the lack of any indices. But this time I didn't check the difference with indices. (I'll possibly do that tomorrow.) – Andriy M Jun 28 '11 at 09:54