Complex SQL Query advise : Reservation allotment logic - sql-server

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

Related

How do i select the free time between start time and end time in sql?

I want to select all the free time other that the mentioned ones for which the room is free.
eg table:
room starttime endtime date
1 | 12pm | 1pm | 2018-02-18 00:00:00.000
1 | 2pm | 3pm | 2018-02-18 00:00:00.000
1 | 3pm | 5pm | 2018-02-18 00:00:00.000
expected output eg:
freetime
6-12,5-10
Is this even possible?
Thanks in advance.
This is for a assignment where the room is booked for a class for particular starttime(date) and endtime(date) and date.
I need to find out the times when the class will be free.
Above free timings was just an example.
Possible solution:
set dateformat ymd
-- Sample data
declare #MyTable
table (room varchar(1), starttime varchar(4), endtime varchar(4), [date] datetime)
insert into #MyTable values ('1', '12pm', '1pm', '2018-02-18 00:00:00.000')
insert into #MyTable values ('1', '2pm', '3pm', '2018-02-18 00:00:00.000')
insert into #MyTable values ('1', '3pm', '5pm', '2018-02-18 00:00:00.000')
select d.[date], r.room, t.freehour
from
(select cast(dateadd(hour, number, 0) as time) as freehour
from master..spt_values where type = 'P' and number between 6 and 22) t,
(select distinct room from #MyTable) r,
(select distinct [date] as [date] from #MyTable) d
where not exists (
select 1
from #MyTable m
where m.[date] = d.[date]
and m.room = r.room
and t.freehour between cast(m.starttime as time)
and dateadd(hour,-1,cast(m.endtime as time))
)

How to get a count between to dates and write the count of rows into a table with T-SQL?

My Language is T-SQL and I am working with MS SQLServer 2008.
Well, I have a table with a lot of data with information concerning employees.
Every employee has a "startdate" (the time when he startet to work for the company) and and "enddate" (the time when he quit the job).
I would like to write into a table the same count of rows as the employee worked for the company in month. For example:
My basic table:
Employee Number | StartDate | EndDate
4711 20150101 20150523
This example shows that the employee worked for the company for 5 Month.
So I want to insert in the new table 5 rows with the following information:
New Table:
Employee Number | StartDate | EndDate
row1: 4711 20150101 20150523
row2: 4711 20150201 20150523
row3: 4711 20150301 20150523
row4: 4711 20150401 20150523
row5: 4711 20150501 20150523
I tried this to get the number of month between the dates. I guess I need to work with a cursor or something like that.
declare #start DATE = '2011-05-01'
declare #end DATE = '2011-08-01'
;with months (date)
AS
(
SELECT #start
UNION ALL
SELECT DATEADD(month,1,date)
from months
where DATEADD(month,1,date)<=#end
)
select Datename(month,date) from months
Hope you got the idea, I tried to be as specific as I can.
I think that you were on right path.
declare #start DATE = (select min(startdate) from dbo.employee)
declare #end DATE = cast(sysdatetime() as date)
set #start = DATEADD(day, - datepart(day, #start) + 1, #start)
;with months (date)
AS
(
SELECT #start
UNION ALL
SELECT DATEADD(month,1,date)
from months
where DATEADD(month,1,date)<=#end
)
select employee.EmployeeNumber, Year = datepart(year, date), Month = DATENAME(month, date), employee.StartDate, employee.EndDate
from months
inner join dbo.employee on month.date >= employee.startdate and (month.date <= employee.enddate or employee.enddate is null)
found your new question and want to show you another way:
You need a list of running numbers. In this example I create a very handsome function first. You will need this for sure in many scenarios...
CREATE FUNCTION [dbo].[GetRunningNumbers](#counter INT=10000000, #StartAt INT=0)
RETURNS TABLE
AS
RETURN
WITH E1(N) AS( -- 10 ^ 1 = 10 rows
SELECT 1 FROM(VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))t(N)
),
E2(N) AS(SELECT 1 FROM E1 a CROSS JOIN E1 b), -- 10 ^ 2 = 100 rows
E4(N) AS(SELECT 1 FROM E2 a CROSS JOIN E2 b), -- 10 ^ 4 = 10,000 rows
E8(N) AS(SELECT 1 FROM E4 a CROSS JOIN E4 b), -- 10 ^ 8 = 10,000,000 rows
CteTally AS(
SELECT TOP(ISNULL(#counter,1000000)) ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) -1 + ISNULL(#StartAt,0) As Nmbr
FROM E8
)
SELECT * FROM CteTally;
GO
Your problem is solved as a one-liner with CROSS APPLY. As you have different intervalls in each row you need a row based approach rather than a set based (what is the CTE).
DECLARE #tbl TABLE(id INT, someValue VARCHAR(10),StartDate DATETIME, EndDate DATETIME);
INSERT INTO #tbl VALUES(1,'test1',{d'2015-01-04'},{d'2015-01-06'})
,(2,'test2',{d'2015-01-02'},{d'2015-01-08'}) --overlapping
,(3,'test3',{d'2015-01-10'},{d'2015-01-13'});
SELECT *
,DATEADD(DAY,RuNmbr.Nmbr,StartDate) AS RunningDate
FROM #tbl AS tbl
CROSS APPLY dbo.GetRunningNumbers(DATEDIFF(DAY,StartDate,EndDate)+1,0) AS RuNmbr;

SQL - Filter on dates X number of days apart from the previous

I have a table containing orders. I would like to select those orders that are a certain number of days apart for a specific client. For example, in the table below I would like to select all of the orders for CustomerID = 10 that are at least 30 days apart from the previous instance. With the starting point to be the first occurrence (07/05/2014 in this data).
OrderID | CustomerID | OrderDate
==========================================
1 10 07/05/2014
2 10 07/15/2014
3 11 07/20/2014
4 11 08/20/2014
5 11 09/21/2014
6 10 09/23/2014
7 10 10/15/2014
8 10 10/30/2014
I would want to select OrderIDs (1,6,8) since they are 30 days apart from each other and all from CustomerID = 10. OrderIDs 2 and 7 would not be included as they are within 30 days of the previous order for that customer.
What confuses me is how to set the "checkpoint" to the last valid date. Here is a little "pseudo" SQL.
SELECT OrderID
FROM Orders
WHERE CusomerID = 10
AND OrderDate > LastValidOrderDate + 30
i came here and i saw #SveinFidjestøl already posted answer but i can't control my self after by long tried :
with the help of LAG and LEAD we can comparison between same column
and as per your Q you are looking 1,6,8. might be this is helpful
SQL SERVER 2012 and after
declare #temp table
(orderid int,
customerid int,
orderDate date
);
insert into #temp values (1, 10, '07/05/2014')
insert into #temp values (2, 10, '07/15/2014')
insert into #temp values (3, 11, '07/20/2014')
insert into #temp values (4, 11, '08/20/2014')
insert into #temp values (5, 11, '09/21/2014')
insert into #temp values (6, 10, '09/23/2014')
insert into #temp values (7, 10, '10/15/2014')
insert into #temp values (8, 10, '10/30/2014');
with cte as
(SELECT orderid,customerid,orderDate,
LAG(orderDate) OVER (ORDER BY orderid ) PreviousValue,
LEAD(orderDate) OVER (ORDER BY orderid) NextValue,
rownum = ROW_NUMBER() OVER (ORDER BY orderid)
FROM #temp
WHERE customerid = 10)
select orderid,customerid,orderDate from cte
where DATEDIFF ( day , PreviousValue , orderDate) > 30
or PreviousValue is null or NextValue is null
SQL SERVER 2005 and after
WITH CTE AS (
SELECT
rownum = ROW_NUMBER() OVER (ORDER BY p.orderid),
p.orderid,
p.customerid,
p.orderDate
FROM #temp p
where p.customerid = 10)
SELECT CTE.orderid,CTE.customerid,CTE.orderDate,
prev.orderDate PreviousValue,
nex.orderDate NextValue
FROM CTE
LEFT JOIN CTE prev ON prev.rownum = CTE.rownum - 1
LEFT JOIN CTE nex ON nex.rownum = CTE.rownum + 1
where CTE.customerid = 10
and
DATEDIFF ( day , prev.orderDate , CTE.orderDate) > 30
or prev.orderDate is null or nex.orderDate is null
GO
You can use the LAG() function, available in SQL Server 2012, together with a Common Table Expression. You calculate the days between the customer's current order and the customer's previous order and then query the Common Table Expression using the filter >= 30
with cte as
(select OrderId
,CustomerId
,datediff(d
,lag(orderdate) over (partition by CustomerId order by OrderDate)
,OrderDate) DaysSinceLastOrder
from Orders)
select OrderId, CustomerId, DaysSinceLastOrder
from cte
where DaysSinceLastOrder >= 30 or DaysSinceLastOrder is null
Results:
OrderId CustomerId DaysSinceLastOrder
1 10 NULL
6 10 70
3 11 NULL
4 11 31
5 11 32
(Note that 1970-01-01 is chosen arbitrarily, you may choose any date)
Update
A slighty more reliable way of doing it will involve a temporary table. But the original table tbl can be left unchanged. See here:
CREATE TABLE #tmp (id int); -- set-up temp table
INSERT INTO #tmp VALUES (1); -- plant "seed": first oid
WHILE (##ROWCOUNT>0)
INSERT INTO #tmp (id)
SELECT TOP 1 OrderId FROM tbl
WHERE OrderId>0 AND CustomerId=10
AND OrderDate>(SELECT max(OrderDate)+30 FROM tbl INNER JOIN #tmp ON id=OrderId)
ORDER BY OrderDate;
-- now list all found entries of tbl:
SELECT * FROM tbl WHERE EXISTS (SELECT 1 FROM #tmp WHERE id=OrderId)
#tinka shows how to use CTEs to do the trick, and the new windowed functions (for 2012 and later) are probably the best answer. There is also the option, assuming you do not have a very large data set, to use a recursive CTE.
Example:
declare #customerid int = 10;
declare #temp table
(orderid int,
customerid int,
orderDate date
);
insert into #temp values (1, 10, '07/05/2014')
insert into #temp values (2, 10, '07/15/2014')
insert into #temp values (3, 11, '07/20/2014')
insert into #temp values (4, 11, '08/20/2014')
insert into #temp values (5, 11, '09/21/2014')
insert into #temp values (6, 10, '09/23/2014')
insert into #temp values (7, 10, '10/15/2014')
insert into #temp values (8, 10, '10/30/2014');
with datefilter AS
(
SELECT row_number() OVER(PARTITION BY CustomerId ORDER BY OrderDate) as RowId,
OrderId,
CustomerId,
OrderDate,
DATEADD(day, 30, OrderDate) as FilterDate
from #temp
WHERE CustomerId = #customerid
)
, firstdate as
(
SELECT RowId, OrderId, CustomerId, OrderDate, FilterDate
FROM datefilter
WHERE rowId = 1
union all
SELECT datefilter.RowId, datefilter.OrderId, datefilter.CustomerId,
datefilter.OrderDate, datefilter.FilterDate
FROM datefilter
join firstdate
on datefilter.CustomerId = firstdate.CustomerId
and datefilter.OrderDate > firstdate.FilterDate
WHERE NOT EXISTS
(
SELECT 1 FROM datefilter betweens
WHERE betweens.CustomerId = firstdate.CustomerId
AND betweens.orderdate > firstdate.FilterDate
AND datefilter.orderdate > betweens.orderdate
)
)
SELECT * FROM firstdate

Rolling Prior13 months with Current Month Sales

Within a SQL Server 2012 database, I have a table with two columns customerid and date. I am interested in getting by year-month, a count of customers that have purchased in current month but not in prior 13 months. The table is extremely large so something efficient would be highly appreciated. Results table is shown after the input data. In essence, it is a count of customers that purchased in current month but not in prior 13 months (by year and month).
---input table-----
declare #Sales as Table ( customerid Int, date Date );
insert into #Sales ( customerid, date) values
( 1, '01/01/2012' ),
( 1, '04/01/2013' ),
( 1, '01/01/2014' ),
( 1, '01/01/2014' ),
( 1, '04/06/2014' ),
( 2, '04/01/2014' ),
( 3, '01/03/2012' ),
( 3, '01/03/2014' ),
( 4, '01/04/2012' ),
( 4, '04/04/2013' ),
( 5, '02/01/2010' ),
( 5, '02/01/2013' ),
( 5, '04/01/2014' )
select customerid, date
from #Sales;
---desired results ----
yearmth monthpurchasers monthpurchasernot13m
201002 1 1
201201 3 3
201302 1 1
201304 2 2
201401 2 1
201404 3 2
Thanks very much for looking at this!
Dev
You didn't provide the expected result, but I believe this is pretty close (at least logically):
;with g as (
select customerid, year(date)*100 + month(date) as mon
from #Sales
group by customerid, year(date)*100 + month(date)
),
x as (
select *,
count(*) over(partition by customerid order by mon
rows between 13 preceding and 1 preceding) as cnt
from g
),
y as (
select mon, count(*) as cnt from x
where cnt = 0
group by mon
)
select g.mon,
count(distinct(g.customerid)) as monthpurchasers,
isnull(y.cnt, 0) as cnt
from g
left join y on g.mon = y.mon
group by g.mon, y.cnt
order by g.mon
Tell me if this query helps. It extracts all the rows which meet your condition into a Table variable. Then, I use your query and join to this table.
declare #startDate datetime
declare #todayDate datetime
declare #tbl_Custs as Table(customerid int)
set #startDate = '04/01/2014' -- mm/dd/yyyy
set #todayDate = GETDATE()
insert into #tbl_Custs
-- purchased only this month
select customerid
from Sales
where ([date] >= #startDate and [date] <= #todayDate)
and customerid NOT in
(
-- purchased in past 13 months
select distinct customerid
from Sales
where ([date] >= DATEADD(MONTH,-13,[date])
and [date] < #startDate)
)
-- your query goes here
select year(date) as year
,month(date) as month
,count(distinct(c.customerid)) as monthpurchasers
from #tbl_Custs as c right join
Sales as s
on c.customerid = s.customerid
group by year(date) , month(date)
order by year(date) , month(date)
Below query will produce what you are looking for. I am not sure how performance will be on a big table (how big is your table?) but it is pretty straight forward so I think it will be ok. I simply calculate the 13 months earlier on CTE to find my sale window. Than join to the Sales table within that window / customer id and grouping records based on the unmatched records. You don't actually need 2 CTE's here you can do the DATEADD(mm,-13,date) on the join part of the second CTE but I thought it might be more clear this way.
P.S. If you need to change the time frame from 13 months to something else all you have to change is the DATEADD(mm,-13,date) this simply substracts 13 months from the date value.
Hope this helps or at least leads to a better solution
;WITH PurchaseWindow AS (
select customerid, date, DATEADD(mm,-13,date) minsaledate
FROM #Sales
), JoinBySaleWindow AS (
SELECT a.customerid, a.date,a.minsaledate,b.date earliersaledate
FROM PurchaseWindow a
LEFT JOIN #sales b ON a.customerid =b.customerid
--Find the sales for the customer within the last 13 months of original sale
AND b.date BETWEEN a.date AND a.minsaledate
)
SELECT DATEPART(yy,date) AS [year], DATEPART(mm, date) AS [month], COUNT(DISTINCT customerid) monthpurchases
FROM JoinBySaleWindow
--Exclude records where a sale within last 13 months occured
WHERE earliersaledate IS NULL
GROUP BY DATEPART(mm, date), DATEPART(yy,date)
Sorry about the typos they are fixed now.

TSQL for Repeating rows within table

I have single table with below fields.
id name startdate enddate
1 u1 2013-01-15 00:00:00.000 2013-01-17 00:00:00.000
2 u2 2013-01-22 00:00:00.000 2013-01-23 00:00:00.000
3 u3 2013-01-23 00:00:00.000 2013-01-23 00:00:00.000
Now, I want multiple rows depends on start and end dates. So as per above rows.. It returns with three rows for first record.. date 15 to 17 which returns 3 rows (3 days).
I am bit confused for query. Is there any better way or any sample to achieve?
Thanks.
You could use a CTE to solve that:
DECLARE #Id int
SELECT #Id = 1
;
WITH Multiple AS
(
SELECT 1 Sequence, Id, Name, StartDate, EndDate
FROM ( VALUES
(1, 'u1', '2013-01-15', '2013-01-17'),
(2, 'u2', '2013-01-22', '2013-01-23'),
(3, 'u3', '2013-01-23', '2013-01-23')
) AS Sample(Id, Name, StartDate, EndDate)
WHERE Id = #Id
UNION ALL
SELECT Sequence + 1, Id, Name, StartDate, EndDate
FROM Multiple
WHERE Id = #Id AND DATEADD(d, Sequence, StartDate) <= EndDate
)
SELECT *
FROM Multiple
If you have a 'Dates' table with a 'Date' column in there, just join your table to the 'Dates' on 'Dates.Date BETWEEN startdate AND enddate'.
I am not pretty sure but below is what I have tried..
SELECT c1.*
FROM master..spt_values p
INNER JOIN tempQuery c1
ON RIGHT(CAST(c1.stdate AS DATE),2) <= (CASE WHEN p.number = 0 THEN 1 ELSE p.number END)
WHERE TYPE='p'
AND p.number BETWEEN RIGHT(CAST(c1.stdate AS DATE),2) AND RIGHT(CAST(c1.endate AS DATE),2)

Resources