Make balance report with previous - sql

I have to make daily report from an SQL table which contains ID (user id), Timestamp, Balance Transactions.
My quest: Every transactions have been stored in the table. I have to know the summary all user's balance on every day.
For examle:
27/06/2016 8:10 User1 50$
27/06/2016 10:22 User1 75$
27/06/2016 11:32 User2 10$
28/06/2016 09:22 User3 40$
28/06/2016 17:35 User1 22$
In this case the results have to be the following:
27/06/2016: 85$ (75+10) because last user1's balance 75 and user2 10
28/06/2016: 72$ (22+10+40) because last user1's balance 22 and user2 10 (it was modified on yesterday but I have to count it!!!) and user3 22$
Please help.
Thanks
My solution but it is not correct: Only provide result if transaction was on the day and does not add previous day result.
Here are the requests I tried so far :
Request 1 :
USE DB1;
GO
WITH cte (bin, currency, id, currentbalance, currentledgerbalance, dt) as ( SELECT bin, w.currency, w.id,t.currentbalance, t.currentledgerbalance, t.dt
FROM [DB1].[Tb1] b
inner join [tb2] c
on b.[id]=c.[id]
inner join tb3 w
on c.id=w.id and w.currency=b.currency
inner join [DB1].[tb4] t
on t.walletid=w.id )
, CTE2 (bin,currency,id,currentbalance,currentledgerbalance,dt) as (
select *
from cte
where dt in (select MAX(dt) FROM cte GROUP BY currency,id,DAY(dt), MONTH(dt), YEAR(dt))
)
Request 2 :
select currency
,cast(dt as date) as stat_day
,sum(currentbalance) as currentbalance
from CTE2
GROUP BY currency,cast(dt as date)
order by stat_day go

I am not sure what other tables involved in the solution. Just giving you a generic solution based on the query given:
---Creating a test table
create table usertrans (tid int identity, tdate date, uname varchar(30),balance int);
insert into usertrans values ('06/27/2017','user1',50);
insert into usertrans values ('06/27/2017','user1',75);
insert into usertrans values ('06/27/2017','user2',10);
insert into usertrans values ('06/28/2017','user3',40);
insert into usertrans values ('06/28/2017','user1',22);
select * from usertrans
-- Retrieving (2017-06-28) balance
with UMaxTrans(UName,TID)
AS(
select uname, max(tid) AS TID from usertrans
WHERE TDate < = '2017-06-28'
group by uname)
select CAST(GETDATE() AS DATE) AS 'Today' , sum(Balance) FROM UserTrans UT
INNER JOIN UMaxTrans UMT ON UT.TID = UMT.TID;
-- Retrieving (2017-06-27) balance
with UMaxTrans(UName,TID)
AS(
select uname, max(tid) AS TID from usertrans
WHERE TDate < = '2017-06-27'
group by uname)
select CAST(GETDATE() AS DATE) AS 'Today' , sum(Balance) FROM UserTrans UT
INNER JOIN UMaxTrans UMT ON UT.TID = UMT.TID;
Logic:
We have users and the balance might get change any number of times in a given day but while calculating the total balance from all users we have to consider the latest transaction from the user. Thats what we are doing in the query. We are getting the maximum transaction ID for a given user and thats the latest balance.

Related

How to display data on separate rows in one row

I have a table with the following setup
ID InOut_Status InOut_Datetime
1 IN 9/12/2017 8:00
2 IN 9/12/2017 10:00
1 OUT 9/12/2017 1:00
2 OUT 9/12/2017 3:00
I want to be able to see both status and date on the same row vs separate rows for example
ID In_Status In_Datetime Out_Status Out_Datetime
1 IN 9/12/2017 8:00 OUT 9/12/2017 1:00
2 IN 9/12/2017 10:00 OUT 9/12/2017 3:00
I would like to return all columns. I just provided a few for example. I also would like to show only the most recent Datetime for each ID and if the user hasn't checked out, I would like for the Out_Datetime to be blank.
Any assistance would be greatly appreciated. Thanks.
You can use self join:
SELECT *
FROM
(
SELECT ins.id
, ins.InOut_Datetime as in_time
, outs.InOut_Datetime as out_time
, row_number() over (partition by ins.id order by ins.InOut_Datetime desc) as ranking
FROM table ins
LEFT JOIN table outs
ON ins.id = outs.id
AND outs.InOut_Status = 'OUT'
AND outs.InOut_Datetime > ins.InOut_Datetime
WHERE ins.InOut_Status = 'IN'
and ins.InOut_Datetime > DATEADD(day, -1, GETDATE())
) t
WHERE t.ranking = 1
Updated query to :
get logins within last 24 hours
get the latest login of a user only
show out time only if it's later than in time
You need to left join, however, you want to limit the join to the first record returned in descending order using a sub query.
SELECT * FROM
(
SELECT
ID,InOut_Status,InOutDateTime,
CheckOutInstanceDescending = ROW_NUMBER() OVER(PARTITION BY ClockOut.ID ORDER BY ClockOut.InOutDateTime DESC)
FROM
MyTable ClockIn
LEFT OUTER JOIN MyTable ClockOut ON ClockOut.ID=ClockIn.ID
WHERE
ClockIn.InOut_Status='IN'
)AS Combined
WHERE
Combined.CheckOutInstanceDescending=1
A simple pivot would fix this the easiest:
https://technet.microsoft.com/en-us/library/ms177410(v=sql.105).aspx. You can run this in SSMS 2008 or higher(I wrote it in SQL 2016).
DECLARE #Temp TABLE (Id INT, InOut_Status VARCHAR(8), InOut_Datetime DATETIME)
INSERT INTO #Temp (Id, InOut_Status, InOut_Datetime) VALUES (1, 'IN', '9-12-2017 8:00'), (2, 'IN', '9-12-2017 10:00'),(1, 'OUT', '9-12-2017 13:00'),(2, 'OUT', '9-12-2017 16:00'),(3, 'IN', '9-12-2017 06:00')
SELECT
pvt.Id
, 'IN' AS In_Status
, pvt.[In]
, 'OUT' AS Out_Status
, pvt.OUT
From #Temp
PIVOT(MAX(InOut_Datetime) FOR InOut_Status IN ([In], [OUT])) AS pvt

Count number of transactions for first 30 days of account creation for all accounts

I want to count the number of transactions for the first 30 days from an account's creation for all accounts. The issue is not all accounts were created at the same time.
Example: [Acct_createdTable]
Acct Created_date
909099 01/02/2015
878787 02/03/2003
676767 09/03/2013
I can't Declare a datetime variable since it can only take one datetime.
and I can't do :
Select acctnumber,min,count(*)
from transaction_table
where transactiondate between (
select Created_date from Acct_createdTable where Acct = 909099)
and (
select Created_date from Acct_createdTable where Acct = 909099)+30
Since then it'll only count the number of transaction for only one acct.
What I want for my output is.
Acct First_30_days_count
909099 23
878787 190
676767 23
I think what you're looking for is a basic GROUP BY query.
SELECT
ac.acctnumber,
COUNT(td.id)
FROM Acct_createdTable ac
LEFT JOIN transactiondate td ON
td.acct = ac.acctnumber
AND
td.transaction_date BETWEEN ac.create_date AND DATEADD(30, DAY, ac.create_date)
GROUP BY
ac.acctnumber
This should return number of transactions within first 30 days for each account. This of course is pseudocode as you didn't state your database platform. The left join will ensure that accounts with no transactions in that period will get displayed.
An alternative solution would be to use outer apply like this:
select a.acct, o.First_30_days_count
from acct_createdtable a
outer apply (
select count(*) First_30_days_count
from transaction_table
where acctnumber = a.acct
and transactiondate between a.created_date and dateadd(day, 30, a.created_date)
) o;

Getting ranges that are not in database

I want to get all times that an event is not taking place for each room. The start of the day is 9:00:00 and end is 22:00:00.
What my database looks like is this:
Event EventStart EventEnd Days Rooms DayStarts
CISC 3660 09:00:00 12:30:00 Monday 7-3 9/19/2014
MATH 2501 15:00:00 17:00:00 Monday:Wednesday 7-2 10/13/2014
CISC 1110 14:00:00 16:00:00 Monday 7-3 9/19/2014
I want to get the times that aren't in the database.
ex. For SelectedDate (9/19/2014) the table should return:
Room FreeTimeStart FreeTimeEnd
7-3 12:30:00 14:00:00
7-3 16:00:00 22:00:00
ex2. SelectedDate (10/13/2014):
Room FreeTimeStart FreeTimeEnd
7-2 9:00:00 15:00:00
7-2 17:00:00 22:00:00
What I have tried is something like this:
select * from Events where ________ NOT BETWEEN eventstart AND eventend;
But I do not know what to put in the place of the space.
This was a pretty complex request. SQL works best with sets, and not looking at line by line. Here is what I came up with. To make it easier to figure out, I wrote it as a series of CTE's so I could work through the problem a step at a time. I am not saying that this is the best possible way to do it, but it doesn't require the use of any cursors. You need the Events table and a table of the room names (otherwise, you don't see a room that doesn't have any bookings).
Here is the query and I will explain the methodology.
DECLARE #Events TABLE (Event varchar(20), EventStart Time, EventEnd Time, Days varchar(50), Rooms varchar(10), DayStarts date)
INSERT INTO #Events
SELECT 'CISC 3660', '09:00:00', '12:30:00', 'Monday', '7-3', '9/19/2014' UNION
SELECT 'MATH 2501', '15:00:00', '17:00:00', 'Monday:Wednesday', '7-2', '10/13/2014' UNION
SELECT 'CISC 1110', '14:00:00', '16:00:00', 'Monday', '7-3', '9/19/2014'
DECLARE #Rooms TABLE (RoomName varchar(10))
INSERT INTO #Rooms
SELECT '7-2' UNION
SELECT '7-3'
DECLARE #SelectedDate date = '9/19/2014'
DECLARE #MinTimeInterval int = 30 --smallest time unit room can be reserved for
;WITH
D1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
),
D2(N) AS (SELECT 1 FROM D1 a, D1 b),
D4(N) AS (SELECT 1 FROM D2 a, D2 b),
Numbers AS (SELECT TOP 3600 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1 AS Number FROM D4),
AllTimes AS
(SELECT CAST(DATEADD(n,Numbers.Number*#MinTimeInterval,'09:00:00') as time) AS m FROM Numbers
WHERE DATEADD(n,Numbers.Number*#MinTimeInterval,'09:00:00') <= '22:00:00'),
OccupiedTimes AS (
SELECT e.Rooms, ValidTimes.m
FROM #Events E
CROSS APPLY (SELECT m FROM AllTimes WHERE m BETWEEN CASE WHEN e.EventStart = '09:00:00' THEN e.EventStart ELSE DATEADD(n,1,e.EventStart) END and CASE WHEN e.EventEnd = '22:00:00' THEN e.EventEnd ELSE DATEADD(n,-1,e.EventEnd) END) ValidTimes
WHERE e.DayStarts = #SelectedDate
),
AllRoomsAllTimes AS (
SELECT * FROM #Rooms R CROSS JOIN AllTimes
), AllOpenTimes AS (
SELECT a.*, ROW_NUMBER() OVER( PARTITION BY (a.RoomName) ORDER BY a.m) AS pos
FROM AllRoomsAllTimes A
LEFT OUTER JOIN OccupiedTimes o ON a.RoomName = o.Rooms AND a.m = o.m
WHERE o.m IS NULL
), Finalize AS (
SELECT a1.RoomName,
CASE WHEN a3.m IS NULL OR DATEDIFF(n,a3.m, a1.m) > #MinTimeInterval THEN a1.m else NULL END AS FreeTimeStart,
CASE WHEN a2.m IS NULL OR DATEDIFF(n,a1.m,a2.m) > #MinTimeInterval THEN A1.m ELSE NULL END AS FreeTimeEnd,
ROW_NUMBER() OVER( ORDER BY a1.RoomName ) AS Pos
FROM AllOpenTimes A1
LEFT OUTER JOIN AllOpenTimes A2 ON a1.RoomName = a2.RoomName and a1.pos = a2.pos-1
LEFT OUTER JOIN AllOpenTimes A3 ON a1.RoomName = a3.RoomName and a1.pos = a3.pos+1
WHERE A2.m IS NULL OR DATEDIFF(n,a1.m,a2.m) > #MinTimeInterval
OR
A3.m IS NULL OR DATEDIFF(n,a3.m, a1.m) > #MinTimeInterval
)
SELECT F1.RoomName, f1.FreeTimeStart, f2.FreeTimeEnd FROM Finalize F1
LEFT OUTER JOIN Finalize F2 ON F1.Pos = F2.pos-1 AND f1.RoomName = f2.RoomName
WHERE f1.pos % 2 = 1
In the first several lines, I create temp variables to simulate your tables Events and Rooms.
The variable #MinTimeInterval determines what time interval the room schedules can be on (every 30 min, 15 min, etc - this number needs to divide evenly into 60).
Since SQL cannot query data that is missing, we need to create a table that holds all of the times that we want to check for. The first several lines in the WITH create a table called AllTimes which are all the possible time intervals in your day.
Next, we get a list of all of the times that are occupied (OccupiedTimes), and then LEFT OUTER JOIN this table to the AllTimes table which gives us all the available times. Since we only want the start and end of each free time, create the Finalize table which self joins each record to the previous and next record in the table. If the times in these rows are greater than #MinTimeInterval, then we know it is either a start or end of a free time.
Finally we self join this last table to put the start and end times in the same row and only look at every other row.
This will need to be adjusted if a single row in Events spans multiple days or multiple rooms.
Here's a solution that will return the "complete picture" including rooms that aren't booked at all for the day in question:
Declare #Date char(8) = '20141013'
;
WITH cte as
(
SELECT *
FROM -- use your table name instead of the VALUES construct
(VALUES
('09:00:00','12:30:00' ,'7-3', '20140919'),
('15:00:00','17:00:00' ,'7-2', '20141013'),
('14:00:00','16:00:00' ,'7-3', '20140919')) x(EventStart , EventEnd,Rooms, DayStarts)
), cte_Days_Rooms AS
-- get a cartesian product for the day specified and all rooms as well as the start and end time to compare against
(
SELECT y.EventStart,y.EventEnd, x.rooms,a.DayStarts FROM
(SELECT #Date DayStarts) a
CROSS JOIN
(SELECT DISTINCT Rooms FROM cte)x
CROSS JOIN
(SELECT '09:00:00' EventStart,'09:00:00' EventEnd UNION ALL
SELECT '22:00:00' EventStart,'22:00:00' EventEnd) y
), cte_1 AS
-- Merge the original data an the "base data"
(
SELECT * FROM cte WHERE DayStarts=#Date
UNION ALL
SELECT * FROM cte_Days_Rooms
), cte_2 as
-- use the ROW_NUMBER() approach to sort the data
(
SELECT *, ROW_NUMBER() OVER(PARTITION BY DayStarts, Rooms ORDER BY EventStart) as pos
FROM cte_1
)
-- final query: self join with an offest of one row, eliminating duplicate rows if a room is booked starting 9:00 or ending 22:00
SELECT c2a.DayStarts, c2a.Rooms , c2a.EventEnd, c2b.EventStart
FROM cte_2 c2a
INNER JOIN cte_2 c2b on c2a.DayStarts = c2b.DayStarts AND c2a.Rooms =c2b.Rooms AND c2a.pos = c2b.pos -1
WHERE c2a.EventEnd <> c2b.EventStart
ORDER BY c2a.DayStarts, c2a.Rooms

Find consecutive days of service without a day break in between

service table:
claimid, customerid, serv-start-date, service-end-date, charge
1, A1, 1-1-14 , 1-5-14 , $200
2, A1, 1-6-14 , 1-8-14 , $300
3, A1, 2-1-14 , 2-1-14 , $100
4, A2, 2-1-14 , 2-1-14 , $100
5, A2, 2-3-14 , 2-5-14 , $100
6, A2, 2-6-14 , 2-8-14 , $100
Problem:
Basically to see the maximum total consecutive days Service start date and end date.
for customer A1 it would be 8 days (1-5 plus 6-8) and customer A2 it would be 5 6 days (3-5 plus 6-8) ... (claimid is unique PK).
Dates are in m-d-yy notation.
This gets a little messy since you could possibly have customers without multiple records. This uses a common-table-expressions, along with the max aggregate and union all to determine your results:
with cte as (
select s.customerid,
s.servicestartdate,
s2.serviceenddate,
datediff(day,s.servicestartdate,s2.serviceenddate)+1 daysdiff
from service s
join service s2 on s.customerid = s2.customerid
and s2.servicestartdate in (s.serviceenddate, dateadd(day,1,s.serviceenddate))
)
select customerid, max(daysdiff) daysdiff
from cte
group by customerid
union all
select customerid, max(datediff(day, servicestartdate, serviceenddate))
from service s
where not exists (
select 1
from cte
where s.customerid = cte.customerid
)
group by customerid
SQL Fiddle Demo
The second query in the union statement is what determines those service records without multiple records with consecutive days.
Here ya go, I think it's the simplest way:
SELECT customerid, sum(datediff([serv-end-date],[serv-start-date]))
FROM [service]
GROUP BY customerid
You will have to decide if same day start/end records count as 1. If they do, then add one to the datediff function, e.g. sum(datediff([serv-end-date],[serv-start-date]) + 1)
If you don't want to count same day services but DO want to count start/end dates inclusively when you sum them up, you will need to add a function that does the +1 only when start and end dates are different. Let me know if you want ideas on how to do that.
The only way I could think to solve the issue described by Jonathan Leffler (in a comment on another answer) was to use a temp table to merge contiguous date ranges. This would be best accomplished in an SP - but failing that the following batch may produce the output you are looking for:-
select *, datediff(day,servicestartdate,serviceenddate)+1 as numberofdays
into #t
from service
while ##rowcount>0 begin
update t1 set
t1.serviceenddate=t2.serviceenddate,
t1.numberofdays=datediff(day,t1.servicestartdate,t2.serviceenddate)+1
from #t t1
join #t t2 on t2.customerid=t1.customerid
and t2.servicestartdate=dateadd(day,1,t1.serviceenddate)
end
select
customerid,
max(numberofdays) as maxconsecutivedays
from #t
group by customerid
The update to the temp table needs to be in a loop because the date range could (I assume) be spread over any number of records (1->n). Interesting problem.
I've made updates to the code so that the temp table ends up with an extra column that holds the number of days in the date range on each record. This allows the following:-
select x.customerid, x.maxconsecutivedays, max(x.serviceenddate) as serviceenddate
from (
select t1.customerid, t1.maxconsecutivedays, t2.serviceenddate
from (
select
customerid,
max(numberofdays) as maxconsecutivedays
from #t
group by customerid
) t1
join #t t2 on t2.customerid=t1.customerid and t2.numberofdays=t1.maxconsecutivedays
) x
group by x.customerid, x.maxconsecutivedays
To identify the longest block of consecutive days (or the latest/longest if there is a tie) for each customer. This would allow you to subsequently dive back into the temp table to pull out the rows related to that block - by searching on the customerid and the serviceenddate (not maxconsecutivedays). Not sure this fits with your use case - but it may help.
WITH chain_builder AS
(
SELECT ROW_NUMBER() OVER(ORDER BY s.customerid, s.CLAIMID) as chain_ID,
s.customerid,
s.serv-start-date, s.service-end-date, s.CLAIMID, 1 as chain_count
FROM services s
WHERE s.serv-start-date <> ALL
(
SELECT DATEADD(d, 1, s2.service-end-date)
FROM services s2
)
UNION ALL
SELECT chain_ID, s.customerid, s.serv-start-date, s.service-end-date,
s.CLAIMID, chain_count + 1
FROM services s
JOIN chain_builder as c
ON s.customerid = c.customerid AND
s.serv-start-date = DATEADD(d, 1, c.service-end-date)
),
chains AS
(
SELECT chain_ID, customerid, serv-start-date, service-end-date,
CLAIMID, chain_count
FROM chain_builder
),
diff AS
(
SELECT c.chain_ID, c.customerid, c.serv-start-date, c.service-end-date,
c.CLAIMID, c.chain_count,
datediff(day,c.serv-start-date,c.service-end-date)+1 daysdiff
FROM chains c
),
diff_sum AS
(
SELECT chain_ID, customerid, serv-start-date, service-end-date,
CLAIMID, chain_count,
SUM(daysdiff) OVER (PARTITION BY chain_ID) as total_diff
FROM diff
),
diff_comp AS
(
SELECT chain_ID, customerid,
MAX(total_diff) OVER (PARTITION BY customerid) as total_diff
FROM diff_sum
)
SELECT DISTINCT ds.CLAIMID, ds.customerid, ds.serv-start-date,
ds.service-end-date, ds.total_diff as total_days, ds.chain_count
FROM diff_sum ds
JOIN diff_comp dc
ON ds.chain_ID = dc.chain_ID AND ds.customerid = dc.customerid
AND ds.total_diff = dc.total_diff
ORDER BY customerid, chain_count
OPTION (maxrecursion 0)

Filter LEFT JOINed table with dates to display current event, else future, else past?

I have a table that lists vacation information for different users (username, vacation start, and vacation end dates) -- 4 users are listed below:
Username VacationStart DeploymentEnd
rsuarez 2014-03-10 2014-03-26
studd 2014-01-18 2014-01-29
studd 2014-02-11 2014-02-26
studd 2014-03-02 2014-03-04
ssteele 2014-03-11 2014-03-26
ssteele 2014-03-18 2014-03-28
atidball 2014-03-05 2014-03-20
atidball 2014-03-06 2014-03-26
atidball 2014-03-13 2014-03-20
atidball 2014-03-18 2014-03-31
For a new query, I want to display only 4 rows, with each user having only one set of vacation dates displayed, either current/in-progress vacation, future/next vacation (if no current exists) or most recent (if two above are false).
The end result should be following (assuming today is 3/9/2014):
Username VacationStart DeploymentEnd
rsuarez 2014-03-10 2014-03-26
studd 2014-03-02 2014-03-04
ssteele 2014-03-11 2014-03-26
atidball 2014-03-05 2014-03-20
Vacation dates are actually coming from another table (data_vacations), which I left join to data_users. I am trying to perform case selection inside left join statement.
Here is what I tried before, but my logic fails there, since I ended up to mix different vacation end dates to vacation start dates:
SELECT Username, VacationStart, VacationEnd
FROM data_users
LEFT JOIN
(
SELECT userGUID,
CASE WHEN MIN(CASE WHEN (VacationEnd < getdate()) THEN NULL ELSE VacationStart END) IS NULL THEN MAX(VacationStart)
ELSE MIN(VacationStart) END AS VacationStart,
CASE WHEN MIN(CASE WHEN (VacationEnd < getdate()) THEN NULL ELSE VacationEnd END) IS NULL THEN MAX(VacationEnd)
ELSE MIN(VacationEnd) END AS VacationEnd
FROM data_vacations
GROUP BY userGUID
) b ON(data_empl_master.userGUID= b.userGUID)
What am I doing wrong? How could I fix it?
Also.. on side note.. Do I perform this filtering in LEFT JOIN correctly? Since data_users is much bigger, having distinct user ids... and I would like to join the available vacation information based on example above, while still displaying all unique user ids.
Using a common table expression to rank by category (current = 1, future = 2, past = 3) and each category individually by start date/differene from GETDATE(), you can get the result you want by ranking the result using ROW_NUMBER();
DECLARE #DATE DATETIME = GETDATE()
;WITH cte AS (
SELECT *, 1 r, VacationStart s FROM data_users
WHERE #DATE BETWEEN VacationStart and DeploymentEnd
UNION ALL
SELECT *,2 r, VacationStart - #DATE s FROM data_users
WHERE VacationStart > #DATE
UNION ALL
SELECT *,3 r, #DATE - DeploymentEnd s FROM data_users
WHERE DeploymentEnd < #DATE
), cte2 AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY username ORDER BY r,s) rn FROM cte
)
SELECT Username, VacationStart, DeploymentEnd FROM cte2 WHERE rn=1;
An SQLfiddle to test with.
Getting the date as a variable is necessary to get a consistent GETDATE() value over the whole query, otherwise it may not be consistent if called multiple times.
select u.name,s.startdate,s.enddate
from users u
left join
(
select su.name,
max(su.start) as startdate,
max(su.end) as enddate from users su group by su.name
)s on u.name= s.name
group by u.name
Since you are asking two questions I will answer the one about getting the vacation dates and let you figure out the join.
I don't think you can get the desired vacations dates in one simple query. First you need to establish if the given date range is in past, present or future. Then you need to order those ranges by start/end dates to get the most recent or next upcoming. You need sort the past vacations in descending and upcoming in ascending order. Funny enough user atidball has two vacations in-progress, I sorted that in the same manner as future vacation. Finally apply your rules, I did that by sorting by state.
declare #currentDate date = '20140309'
;
with cte1 as
(
-- state: the lower number the higher priority
select Username, VacationStart, DeploymentEnd,
case
when VacationStart <= #currentDate and DeploymentEnd >= #currentDate
then 0 -- in progress
when VacationStart > #currentDate
then 1 -- future
when DeploymentEnd < #currentDate
then 2 -- past
else NULL
end as state
from data_vacations
)
, cte2 as
(
select *,
row_number() over(partition by username, state order by VacationStart, DeploymentEnd) as rn
from cte1
where state < 2 -- current or upcoming
union all
select *,
row_number() over(partition by username, state order by DeploymentEnd desc, VacationStart desc) as rn
from cte1
where state = 2 -- past
)
, cte3 as
(
-- apply the rules: find the record with highest priority
select Username, min(state) as minstate
from cte1
group by Username
)
select cte2.Username, cte2.VacationStart, cte2.DeploymentEnd
from cte2
inner join cte3
on cte2.Username = cte3.Username
and cte2.state = cte3.minstate
and cte2.rn = 1 -- most recent or next upcoming
See the SQLFiddle.