• Here's one possible answer...

    Assuming the "ACCESS_SOURCE" is a table, we need to first combine the Event_D and Event_T columns into a single, sortable, calculable column. This should have been done when the table was imported by SSIS instead of in the code below.

    We also take the opportunity to build a working table with some extra columns to make it easier to calculate and aggregate entry and exit times. Here's the code to build the working table. As always, the details are in the comments. The clustered index applied to this table is absolutely required for the next step to work correctly.

    --===== Set the date format for the given data

    SET DATEFORMAT DMY

    ;

    --===== Conditionally drop the temp table to make reruns in SSMS easier.

    IF OBJECT_ID('tempdb..#Corrected','U') IS NOT NULL

    DROP TABLE #Corrected

    ;

    --===== Create a table with combined dates/times.

    -- This should have been done during the SSIS load.

    SELECT Pass_M = CAST(PASS_M AS VARCHAR(10)),

    Event_DT = CAST(EVENT_D AS DATETIME)

    + CAST(EVENT_T AS DATETIME),

    Message_X = CAST(MESSAGE_X AS VARCHAR(6)),

    Entry_DT = CAST(NULL AS DATETIME),

    Exit_DT = CASE

    WHEN MESSAGE_X = 'Exit'

    THEN CAST(EVENT_D AS DATETIME)+CAST(EVENT_T AS DATETIME)

    ELSE CAST(NULL AS DATETIME)

    END

    INTO #Work

    FROM (--==== This whole section would just be FROM dbo.ACCESS_SOURCE

    SELECT 'Staff A','30/04/2012','18:10:00','Exit' UNION ALL

    SELECT 'Staff B','01/05/2012','08:30:10','Access' UNION ALL

    SELECT 'Staff B','01/05/2012','18:31:20','Exit' UNION ALL

    SELECT 'Staff C','30/04/2012','22:00:17','Access' UNION ALL

    SELECT 'Staff C','30/04/2012','22:01:28','Access' UNION ALL

    SELECT 'Staff C','01/05/2012','05:31:00','Exit' UNION ALL

    SELECT 'Staff D','30/04/2012','10:00:00','Access' UNION ALL

    SELECT 'Staff D','30/04/2012','16:00:00','Exit' UNION ALL

    SELECT 'Staff D','30/04/2012','16:03:00','Exit' UNION ALL

    SELECT 'Staff D','01/05/2012','19:50:00','Access' UNION ALL

    SELECT 'Staff D','02/05/2012','03:50:00','Exit' UNION ALL

    SELECT 'Staff D','02/05/2012','12:00:00','Access' UNION ALL

    SELECT 'Staff D','02/05/2012','18:00:00','Exit' UNION ALL

    SELECT 'Staff E','02/05/2012','19:00:00','Access'

    ) source (PASS_M, EVENT_D, EVENT_T, MESSAGE_X)

    ;

    --===== Add the quintessential clustered index for the Quirky Update

    CREATE UNIQUE CLUSTERED INDEX IX_#Work_QU

    ON #Work (Pass_M, Event_DT)

    ;

    I'm pretty sure that someone could figure this all out just using some prestidigitation with ROW_NUMBER() but I decided to do it a different way. This is known as the "Quirky Update" method and it emulates the same method that you would use if you were doing this in front-end code. It will blow the doors off of any recursive CTE, Cursor, or While loop method that you'll be able to come up with. I must warn you that the method is NOT supported by Microsoft but has been a viable method even in the earliest versions of SQL Server and continues to be viable even in SQL Server 2012. It does not support partitioned tables but the Temp Table takes care of that little problem.

    The table and option "hints" are absolutely required.

    --===== Declare some obviously named variables just like you would do in the front end

    DECLARE @PrevPass_M VARCHAR(10),

    @PrevEntry_DT DATETIME,

    @PrevMessage_X VARCHAR(6),

    @SafetyCounter INT

    ;

    --===== Assuming there's at least one row to be process,

    -- preset the safety counter to "1"

    SELECT @SafetyCounter = 1

    ;

    --===== "Smear" the "first" entry dates down to other rows.

    -- See the embedded comments on the CASE function for details.

    WITH

    cteSafetyRowNumber AS

    ( --=== Adds a "counter" to the rows in the expected processing order.

    SELECT RowNumber = ROW_NUMBER() OVER (ORDER BY Pass_M, Event_DT),

    Pass_M, Event_DT, Message_X, Entry_DT

    FROM #Work

    )

    UPDATE srn

    SET @PrevEntry_DT = CASE --This CASE does the safety check

    WHEN RowNumber = @SafetyCounter --Checks sequence of processing

    THEN

    CASE --This CASE does the data "smear"

    --== Finds first "Access" after "Exit" for same Pass_M

    WHEN Pass_M = @PrevPass_M

    AND @PrevMessage_X = 'Exit'

    AND Message_X = 'Access'

    THEN Event_DT

    --== Finds first "Access" for new Pass_M

    WHEN Pass_M <> @PrevPass_M

    AND Message_X = 'Access'

    THEN Event_DT

    --== "Data smear" the current date for everything else

    ELSE @PrevEntry_DT

    END

    ELSE 1/0 --Forces an error if out of sequence

    END,

    --== Setup for next "iteration"

    Entry_Dt = @PrevEntry_DT,

    @PrevPass_M = Pass_M,

    @PrevMessage_X = Message_X,

    @SafetyCounter = @SafetyCounter + 1

    FROM cteSafetyRowNumber AS srn WITH (TABLOCKX)

    OPTION (MAXDOP 1)

    ;

    --===== This will create the final required output

    WITH

    ctePreAggregate AS

    ( --=== Find the max exit date for every smeared PASS_M/entry date group.

    SELECT Pass_M,

    Entry_DT,

    Exit_DT = MAX(Exit_Dt)

    FROM #Work

    WHERE Entry_DT IS NOT NULL

    AND Exit_DT IS NOT NULL

    GROUP BY Pass_M, Entry_Dt

    ) --=== This simply calculates the columns that require calculations.

    SELECT Pass_M,

    Entry_Dt,

    Exit_Dt,

    Worked_Hrs = CAST(DATEDIFF(hh,0,Exit_DT-Entry_DT) AS VARCHAR(10))

    + ':'

    + SUBSTRING(CONVERT(CHAR(5),DATEADD(ss,30,Exit_DT-Entry_DT),108),4,2),

    Consecutive_D = ROW_NUMBER() OVER (PARTITION BY Pass_M ORDER BY Entry_DT)

    FROM ctePreAggregate

    ORDER BY Pass_M, Entry_DT

    ;

    That produces the requested output as follows.

    Pass_M Entry_Dt Exit_Dt Worked_Hrs Consecutive_D

    ------- ----------------------- ----------------------- ---------- -------------

    Staff B 2012-05-01 08:30:10.000 2012-05-01 18:31:20.000 10:01 1

    Staff C 2012-04-30 22:00:17.000 2012-05-01 05:31:00.000 7:31 1

    Staff D 2012-04-30 10:00:00.000 2012-04-30 16:03:00.000 6:03 1

    Staff D 2012-05-01 19:50:00.000 2012-05-02 03:50:00.000 8:00 2

    Staff D 2012-05-02 12:00:00.000 2012-05-02 18:00:00.000 6:00 3

    [font="Arial Black"]Again and as a reminder, if this is meant to be some form of timekeeping system, there are some serious faults with the logic of the original requirements. Unless door entry and exit can be guaranteed recorded events and that every entry will have one and only one exit, this data must NOT be used for keeping time especially for pay![/font]

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)