Okay, lacking a clarification here is some TSQL that computes both:
- the total number of sessions active in each hour and
- the maximum number of concurrent sessions active in each hour.
EDIT: The sample data from the updated question has been used, the output of the last query which shows concurrent sessions now includes the session ids, and a bug in the prior optimization was corrected which greatly improves performance.
NB: These queries work best when the SessionId
values vary from row to row. Using a value of 1
for all rows will cause disappointing results. Hence the IDENTITY
property on the SessionId
column.
-- Parameters.
declare @Start as DateTime = '20120901 00:00:00'
declare @End as DateTime = '20120901 12:00:00'
declare @Interval as Time = '01:00:00.00' -- One hour.
select @Start as [Start], @End as [End], @Interval as [Interval]
-- Sample data.
declare @Sessions as Table ( SessionId Int Identity, SessionStart DateTime, SessionEnd DateTime )
insert into @Sessions ( SessionStart, SessionEnd ) values
( '20120901 00:00:00', '20120901 05:59:59' ), -- Several hours in a single session.
( '20120901 01:01:00', '20120901 01:01:30' ), -- An assortment of overlapping ...
( '20120901 01:02:00', '20120901 01:03:30' ), -- ... sessions during a single hour.
( '20120901 00:00:05.077', '20120901 00:04:02.280' ),
( '20120901 00:00:14.687', '20120901 00:06:05.947' ),
( '20120901 00:00:17.857', '20120901 00:07:34.757' ),
( '20120901 00:00:25.843', '20120901 00:07:38.720' ),
( '20120901 00:00:29.427', '20120901 00:01:58.180' ),
( '20120901 00:00:31.853', '20120901 00:05:10.733' ),
( '20120901 00:00:40.693', '20120901 00:00:44.237' ),
( '20120901 00:00:58.773', '20120901 00:06:14.667' ),
( '20120901 00:00:59.457', '20120901 00:01:01.310' ),
( '20120901 00:01:16.390', '20120901 00:11:18.383' )
select * from @Sessions
-- Summary of sessions active at any time during each hour.
; with SampleWindows as (
select @Start as WindowStart, @Start + @Interval as WindowEnd
union all
select SW.WindowStart + @Interval, SW.WindowEnd + @Interval
from SampleWindows as SW
where SW.WindowEnd < @End
)
select SW.WindowStart, Count( S.SessionStart ) as [Sessions]
from SampleWindows as SW left outer join
@Sessions as S on SW.WindowStart <= S.SessionEnd and S.SessionStart < SW.WindowEnd
group by SW.WindowStart
-- Summary of maximum concurrent sessions active during each hour.
; with SampleWindows as (
select 1 as SampleWindowId, @Start as WindowStart, @Start + @Interval as WindowEnd
union all
select SW.SampleWindowId + 1, SW.WindowStart + @Interval, SW.WindowEnd + @Interval
from SampleWindows as SW
where SW.WindowEnd < @End
),
ActiveSessionsDuringWindow as (
select SW.SampleWindowId, SW.WindowStart, SW.WindowEnd, S.SessionId, S.SessionStart, S.SessionEnd,
-- A "pane" is the more restrictive of the window and the session start/end times.
case when SW.WindowStart <= S.SessionStart then S.SessionStart else SW.WindowStart end as PaneStart,
case when SW.WindowEnd >= S.SessionEnd then S.SessionEnd else SW.WindowEnd end as PaneEnd
from SampleWindows as SW left outer join
@Sessions as S on SW.WindowStart <= S.SessionEnd and S.SessionStart < SW.WindowEnd
),
ConcurrentSearch as (
select SampleWindowId, WindowStart, WindowEnd, SessionId, SessionStart, SessionEnd, PaneStart, PaneEnd,
Cast( '|' + Right( Replicate( '0', 3 ) + Cast( SessionId as VarChar(4) ), 4 ) + '|' as VarChar(1024) ) as SessionIds,
Cast( case when SessionId is NULL then 0 else 1 end as Int ) as Sessions
from ActiveSessionsDuringWindow
union all
select CS.SampleWindowId, CS.WindowStart, CS.WindowEnd, ASDW.SessionId, CS.SessionStart, CS.SessionEnd,
case when CS.PaneStart <= ASDW.PaneStart then ASDW.PaneStart else CS.PaneStart end as PaneStart,
case when CS.PaneEnd >= ASDW.PaneEnd then ASDW.PaneEnd else CS.PaneEnd end as PaneEnd,
Cast( CS.SessionIds + Right( Replicate( '0', 3 ) + Cast( ASDW.SessionId as VarChar(4) ), 4 ) + '|' as VarChar(1024) ),
CS.Sessions + 1
from ConcurrentSearch as CS inner join
ActiveSessionsDuringWindow as ASDW on ASDW.SampleWindowId = CS.SampleWindowId and
-- We haven't visited this session along this path.
CS.SessionId < ASDW.SessionId and -- EDIT: Reduce the size of the search tree.
CharIndex( '|' + Right( Replicate( '0', 3 ) + Cast( ASDW.SessionId as VarChar(4) ), 4 ) + '|', CS.SessionIds ) = 0 and
-- The session's pane overlaps the concurrent search pane.
CS.PaneStart <= ASDW.PaneEnd and ASDW.PaneStart <= CS.PaneEnd
)
select WindowStart, Max( Sessions ) as Sessions,
( select top 1 SessionIds from ConcurrentSearch where Sessions = Max( CS.Sessions ) ) as SessionIds
from ConcurrentSearch as CS
group by WindowStart
Following is a variation on the last query which does not use row id values from the @Sessions
table. Instead it uses Row_Number()
to assign suitable values for the duration of the query. This also changes the assumption that the SessionId
values do not exceed four digits to an assumption that there are not more than 9,999 sessions active within any given hour.
-- Summary of maximum concurrent sessions active during each hour.
; with SampleWindows as (
select 1 as SampleWindowId, @Start as WindowStart, @Start + @Interval as WindowEnd
union all
select SW.SampleWindowId + 1, SW.WindowStart + @Interval, SW.WindowEnd + @Interval
from SampleWindows as SW
where SW.WindowEnd < @End
),
ActiveSessionsDuringWindow as (
select SW.SampleWindowId, SW.WindowStart, SW.WindowEnd, S.SessionStart, S.SessionEnd,
-- A "pane" is the more restrictive of the window and the session start/end times.
case when SW.WindowStart <= S.SessionStart then S.SessionStart else SW.WindowStart end as PaneStart,
case when SW.WindowEnd >= S.SessionEnd then S.SessionEnd else SW.WindowEnd end as PaneEnd,
Row_Number() over ( partition by SW.SampleWindowId order by S.SessionStart ) as SampleId
from SampleWindows as SW left outer join
@Sessions as S on SW.WindowStart <= S.SessionEnd and S.SessionStart < SW.WindowEnd
),
ConcurrentSearch as (
select SampleWindowId, WindowStart, WindowEnd, SampleId, SessionStart, SessionEnd, PaneStart, PaneEnd,
Cast( '|' + Right( Replicate( '0', 3 ) + Cast( SampleId as VarChar(4) ), 4 ) + '|' as VarChar(1024) ) as SampleIds,
Cast( case when SampleId is NULL then 0 else 1 end as Int ) as Sessions
from ActiveSessionsDuringWindow
union all
select CS.SampleWindowId, CS.WindowStart, CS.WindowEnd, ASDW.SampleId, CS.SessionStart, CS.SessionEnd,
case when CS.PaneStart <= ASDW.PaneStart then ASDW.PaneStart else CS.PaneStart end as PaneStart,
case when CS.PaneEnd >= ASDW.PaneEnd then ASDW.PaneEnd else CS.PaneEnd end as PaneEnd,
Cast( CS.SampleIds + Right( Replicate( '0', 3 ) + Cast( ASDW.SampleId as VarChar(4) ), 4 ) + '|' as VarChar(1024) ),
CS.Sessions + 1
from ConcurrentSearch as CS inner join
ActiveSessionsDuringWindow as ASDW on ASDW.SampleWindowId = CS.SampleWindowId and
-- We haven't visited this session along this path.
CS.SampleId < ASDW.SampleId and -- EDIT: Reduce the size of the search tree.
CharIndex( '|' + Right( Replicate( '0', 3 ) + Cast( ASDW.SampleId as VarChar(4) ), 4 ) + '|', CS.SampleIds ) = 0 and
-- The session's pane overlaps the concurrent search pane.
CS.PaneStart <= ASDW.PaneEnd and ASDW.PaneStart <= CS.PaneEnd
)
select WindowStart, Max( Sessions ) as Sessions
from ConcurrentSearch as CS
group by WindowStart
This should be easy to modify to run against an existing table. A single index on SessionStart
ascending, SessionEnd
ascending should improve performance.