Search code examples
sqlsql-servert-sqlssms-2012

How to get rid of cursors?


I have written a SQL query that uses cursors and I realized that it eats up too many resources on the server and it is also slow.

So, this is the table:

Source

I need to calculate the time difference between Status = Assigned and Status = In Progress where Assigned_group is or prev_assigned_group is like "%Hotline%" .

I also have a sequence so I select distinct the Incident id and order the rows ascending by Incident_Sequence because I have to start with the smallest sequence number.

So far so good.

I start with the first row and jump to the next one until I find that the Status is "In Progress".When status is found I calculate the difference between the first assigned row and the row where the status In progress was found.

So the big question is: Can I do this without cursors? If yes, then how ?

SET NOCOUNT ON
DECLARE @day DATE

SET @day = '20160606'

CREATE TABLE #result(
[Assigned_Group] NVARCHAR(100),
[ProgressTime] INTEGER,
[Tickets] INTEGER,
[Avarage] FLOAT
)
INSERT INTO #result(Assigned_Group,ProgressTime,Tickets,Avarage)

SELECT DISTINCT Assigned_Group,0,0,0.0

       FROM [grs_dwh].[smt].[Aht]

   WHERE (Assigned_Group like 'CI-Hotline%' OR Prev_assigned_Group like 'CI-Hotline%')
      and CONVERT(DATE,Last_Modified_Date, 104) = @day

-- raw
SELECT [Incident_Sequence]
      ,[Incident_Id]
      ,[Assigned_Group]
      ,[Prev_assigned_Group] 
      ,[Status]
      ,[Last_Modified_Date]    
      ,[Service]

         INTO #rawData

  FROM [grs_dwh].[smt].[Aht]

WHERE (Assigned_Group like 'CI-Hotline%' OR Prev_assigned_Group like 'CI-Hotline%')
  and CONVERT(DATE,Last_Modified_Date, 104) = @day

  ORDER BY Incident_Sequence asc

  --CREATE TABLE #orderList(


  --)
  SELECT DISTINCT[Incident_id] INTO #incidentList FROM #rawData

  DECLARE cur0 CURSOR FOR SELECT incident_Id FROM #incidentList
  DECLARE @currentIncident NVARCHAR(15)

  OPEN cur0 

  FETCH next from cur0 INTO @currentIncident

  WHILE @@FETCH_STATUS = 0

       BEGIN
 -- PRINT @currentIncident
       SELECT * INTO #tmpTable FROM #rawData WHERE Incident_Id = @currentIncident ORDER BY Incident_Sequence

       DECLARE cur1 CURSOR FOR SELECT * FROM #tmpTable ORDER BY Incident_Sequence ASC

       DECLARE @incident_Sequence INTEGER
    DECLARE @incident_Id NVARCHAR(50)
    DECLARE @assigned_Group NVARCHAR(100)
       DECLARE @previous_Assiggned NVARCHAR(100)
    DECLARE @status NVARCHAR(50)
    DECLARE @last_Modified_Date DATETIME
    DECLARE @service NVARCHAR(50)

       DECLARE @progressFound BIT
       DECLARE @startProgressDate DATETIME
       DECLARE @ticketProgressTime INTEGER
       DECLARE @resultGroup NVARCHAR(100)
       SET @progressFound = 0

       OPEN cur1
       FETCH next from cur1

    INTO @incident_Sequence, @incident_Id, @assigned_Group, @previous_Assiggned, @status, @last_Modified_Date, @service

       WHILE @@FETCH_STATUS = 0

             BEGIN

                    IF @progressFound = 0 AND @status <> 'In Progress'
                    BEGIN

                    FETCH next from cur1 INTO @incident_Sequence, @incident_Id, @assigned_Group, @previous_Assiggned, @status, @last_Modified_Date, @service
                    CONTINUE
                    END

                    IF @progressFound = 0
                    BEGIN
                    SET @startProgressDate = @last_Modified_Date
                    SET @resultGroup = @assigned_Group
                    SET @progressFound = 1
                    FETCH next from cur1 INTO @incident_Sequence, @incident_Id, @assigned_Group, @previous_Assiggned, @status, @last_Modified_Date, @service
                    CONTINUE
                    END
                    ELSE
                    BEGIN
                           SET @ticketProgressTime = DATEDIFF(SECOND,  @startProgressDate,  @last_Modified_Date)


                           UPDATE #result SET ProgressTime = ProgressTime + @ticketProgressTime, Tickets = Tickets+1 WHERE Assigned_Group = @resultGroup
                           SET @ticketProgressTime = 0
                           SET @progressFound = 0
                    END
             FETCH next from cur1
             INTO @incident_Sequence, @incident_Id, @assigned_Group, @previous_Assiggned, @status, @last_Modified_Date, @service
             END
             CLOSE cur1
             DEALLOCATE cur1
             --IF @incident_Id = 'INC000010047798'
             --SELECT * FROM #tmpTable ORDER BY Incident_Sequence ASC
             DROP TABLE #tmpTable
             FETCH next from cur0 INTO @currentIncident
       END
  CLOSE cur0
  DEALLOCATE cur0
  SET NUMERIC_ROUNDABORT OFF
  UPDATE #result SET Avarage = CAST(ProgressTime AS float) / CASE WHEN Tickets = 0 THEN 1 ELSE CAST(Tickets AS float) END
   SELECT * FROM #result
   ORDER BY 1 asc
   DROP TABLE #result
   DROP TABLE #rawData
   DROP TABLE #incidentList

Solution

  • You can use the ROW_NUMBER function to keep track of the sequence and do a self join to compute those values:

    ;WITH   Data    AS
    (       -- Sample data (https://i.sstatic.net/TfzL7.png)
            SELECT  Id, Incident_Id Incident, Incident_Sequence Sequence,
                    Prev_Assigned_Group Previous, Assigned_Group GroupName,
                    Status, CAST(Last_Modified_Date AS DATETIME) Modified,
                    ROW_NUMBER() OVER (PARTITION BY Incident_Id ORDER BY Id) RowNumber  -- Start over the count for every Incident_Id
            FROM    (   VALUES
                        (164293, 05, 'INC000010047798', 'Eastern Europe1'   , 'CI-Hotline North America', 'Assigned'   , '2016-06-04 12:28:46'),
                        (171241, 07, 'INC000010047798', 'CI-Hotline'        , 'Eastern Europe1'         , 'Assigned'   , '2016-06-06 06:42:16'),
                        (171919, 09, 'INC000010047798', 'CI-Hotline'        , 'Eastern Europe1'         , 'In Progress', '2016-06-06 06:46:19'),
                        (172138, 10, 'INC000010047798', 'CI-Hotline Romania', 'CI-Hotline'              , 'Assigned'   , '2016-06-06 06:46:35'),
                        (172483, 12, 'INC000010047798', 'CI-Hotline Romania', 'CI-Hotline'              , 'In Progress', '2016-06-06 07:11:53'),
                        (173003, 15, 'INC000010047798', 'Austria Adria3'    , 'CI-Hotline Romania'      , 'Assigned'   , '2016-06-06 07:15:36'),
                        (208011, 17, 'INC000010047798', 'Austria Adria3'    , 'CI-Hotline Romania'      , 'Resolved'   , '2016-06-10 12:14:05')
                    )   AS X(Id, Incident_Sequence, Incident_Id, Assigned_Group, Prev_Assigned_Group, Status, Last_Modified_Date)
            WHERE   Assigned_Group LIKE '%HOTLINE%' OR 
                    Prev_Assigned_Group LIKE '%HOTLINE%'
    )
    SELECT  Assigned.Incident,
            Assigned.Status + ' » ' + InProgress.Status,
            DATEDIFF(second, InProgress.Modified, Assigned.Modified) / 60.0 / 60.0 Hours
            --,Assigned.*, InProgress.*
    FROM    Data Assigned
    JOIN    Data InProgress
        ON  Assigned.Incident  = InProgress.Incident AND
            Assigned.RowNumber = InProgress.RowNumber + 1
    

    Next time, please, send your sample data in text format ;-)

    EDIT: To calculate the time difference between any status different than 'In Progress' up to the next 'In Progress' status (or the last status available), use this code:

    ;WITH   Data    AS
    (       -- Sample data (https://i.sstatic.net/TfzL7.png)
            SELECT  Id, Incident_Id Incident, Incident_Sequence Sequence,
                    Prev_Assigned_Group Previous, Assigned_Group GroupName,
                    Status, CAST(Last_Modified_Date AS DATETIME) Modified,
                    ROW_NUMBER() OVER
                    (   PARTITION BY Incident_Id
                        ORDER BY Incident_Sequence
                    )   RowNumber       -- Start over the count for every Incident_Id
            FROM    [aht_data_one_day]
            WHERE   -- Incident_Id IN ('INC000010164572') AND
                    (Assigned_Group LIKE '%HOTLINE%' OR Prev_Assigned_Group LIKE '%HOTLINE%')
    )
    SELECT      Assigned.Id, Assigned.Incident,
                CAST(Assigned.Sequence AS VARCHAR(5)) + ' » ' + 
                CAST(InProgress.Sequence AS VARCHAR(5)) Transition,
                DATEDIFF(second, Assigned.Modified, InProgress.Modified) TotalSeconds
                --, Assigned.*, InProgress.*
    FROM        Data Assigned
    JOIN        Data InProgress
            ON  Assigned.Status NOT IN ('In Progress') AND
                InProgress.Id = ISNULL(
                    (   -- Try to locate the next 'In Progress' status
                        SELECT  MIN(Id)
                        FROM    Data
                        WHERE   Status IN ('In Progress') AND
                                Data.Incident = Assigned.Incident AND
                                Data.RowNumber > Assigned.RowNumber -- That's the trick
                    ),
                    (   -- If not found, get the latest status
                        SELECT  MAX(Id)
                        FROM    Data
                        WHERE   Data.Incident = Assigned.Incident
                    ))
    ORDER BY    Assigned.Incident, Assigned.Id
    

    EDIT: On your data update, I keep my previous code with minor changes. Here follows the logic validation on a large incident and the data returned by the query:

    enter image description here

    Id          Incident        Transition    TotalSeconds
    ----------- --------------- ------------- ------------
    172090      INC000010164572 10 » 13       1877
    172939      INC000010164572 15 » 25       6578
    173241      INC000010164572 17 » 25       4045
    173597      INC000010164572 20 » 25       3616
    173949      INC000010164572 23 » 25       1125
    174298      INC000010164572 27 » 34       981
    174468      INC000010164572 30 » 34       287
    174647      INC000010164572 33 » 34       100
    174773      INC000010164572 36 » 36       0
    

    EDIT: Last try

    SELECT      InProgress.Id, InProgress.Incident,
                CAST(InProgress.Sequence AS VARCHAR(5)) + ' » ' + 
                CAST(NextStatus.Sequence AS VARCHAR(5)) Transition,
                DATEDIFF(second, InProgress.Modified, NextStatus.Modified) TotalSeconds
                -- , InProgress.*, NextStatus.*
    FROM        Data InProgress
    JOIN        Data NextStatus
            ON  InProgress.Status IN ('In Progress') AND
                InProgress.Incident = NextStatus.Incident AND
                NextStatus.Id = ISNULL(
                    (   -- Try to locate the next status different than 'In Progress'
                        SELECT  MIN(Id)
                        FROM    Data
                        WHERE   Status NOT IN ('In Progress') AND
                                Data.Incident = InProgress.Incident AND
                                Data.RowNumber > InProgress.RowNumber -- That's the trick
                    ),
                    (   -- If not found, get the latest status
                        SELECT  MAX(Id)
                        FROM    Data
                        WHERE   Data.Incident = InProgress.Incident
                    ))
    ORDER BY    InProgress.Incident, InProgress.Id
    

    Output:

    Id          Incident        Transition    TotalSeconds
    ----------- --------------- ------------- ------------
    172564      INC000010164572 13 » 15       236
    174123      INC000010164572 25 » 27       688
    174689      INC000010164572 34 » 36       77
    

    Good luck.