3

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
  • There's a similarly structured q/a [here](http://stackoverflow.com/questions/36847556/is-there-a-more-efficient-elegant-way-to-write-this-code-i-have/36875820#36875820) – Clay Jun 13 '16 at 13:54
  • 1
    You can use the functions [lag](https://msdn.microsoft.com/en-GB/library/hh231256.aspx) and [lead](https://msdn.microsoft.com/en-GB/library/hh213125.aspx) to compare values from previous and subsequent rows. Supported in SQL Server 2012 or higher. – David Rushton Jun 13 '16 at 14:07

2 Answers2

0

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.stack.imgur.com/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.stack.imgur.com/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.

Rubens Farias
  • 57,174
  • 8
  • 131
  • 162
  • Thanks. I don't know if this would help me because I have these Statuses : Assigned,Pending,In Progress,Resolved,Closed. And also my first row can start with Pending or Resolved for instance.If that is the case then I should compute the difference between Pending and In Progress and so on. – Szekely Zsolt Karoly Jun 13 '16 at 13:43
  • So you just need compare the first status with the second one, regardless the status itself? Take a look on the changed version. – Rubens Farias Jun 13 '16 at 13:57
  • Yes, and it needs to be ordered ascending by Incident_Sequence otherwise the calculations are not correct between the difs.But how do you order in a CTE? – Szekely Zsolt Karoly Jun 13 '16 at 14:25
  • 1
    i forgot that I can use order by in the row_number function.And I just added order by Incident_Sequence ASC. I am now veryfing the data consistency. – Szekely Zsolt Karoly Jun 13 '16 at 14:50
  • The changed version also doesn't help me :(. – Szekely Zsolt Karoly Jun 14 '16 at 09:56
  • What I need is to jump until the status is In Progress for example and when I found it then go to the next status and calculate the time difference in between them. – Szekely Zsolt Karoly Jun 14 '16 at 10:10
  • You're still thinking cursors, @SzekelyZsoltKaroly. Add a long, complex and concrete sample data and I'll tweak this query for you. – Rubens Farias Jun 14 '16 at 10:23
  • 1
    You can do this 'jumping' with the `DENSE_RANK()` function by using the correct partition (a partition tells it when to reset the count). Basically choose the correct arguments and get a number sequence that tells you when the status changes – Nick.Mc Jun 14 '16 at 11:45
  • @Rubens Farias I uploaded here a script with sample data :https://www.dropbox.com/sh/vc9wwt93k3dh3sz/AAC221SbbEqofogph-on7N3Da?dl=0 – Szekely Zsolt Karoly Jun 14 '16 at 12:31
  • So when Assigned group is like hotline and status is like In Progress or Pending on Assigned then datediff the last_modified_date from the next column. That is what I want to calculate.Then I can add it to a SP and create parameteres for filtering by Service,Assigned Group,Location and so on. – Szekely Zsolt Karoly Jun 14 '16 at 12:34
  • This is almost what I would need but I want to start calculating only when In Progress status is found until the very next row(doesn't matter which status).So if we take as example the Incident from above then : Starting from Sequence 13 calculate diff between seq 13 and 15.Then do not calculate anything and start again with seq 25 and calc the diff between 25 and 27.Then diff between 34 and 36.I hope that now I explained correctly.I have to do the exact same thing for the status :Pending and also for Assigned.For the assigned part I think I can use the current query that you posted since I ne – Szekely Zsolt Karoly Jun 14 '16 at 20:01
  • Since I need to calculate the difference until the Status <> Assigned. – Szekely Zsolt Karoly Jun 14 '16 at 20:02
  • I'm not following you. Can you please update your question and add a print with the calculations example, just like I did? – Rubens Farias Jun 14 '16 at 21:01
  • @Rubens Farias I am currently not in front of a computer but taking your excel as example I only need : F4-F3 ,F9-F8 and F13-F12. – Szekely Zsolt Karoly Jun 15 '16 at 03:34
  • @RubensFarias Did you compare the execution plan of your query with the execution plan produced by cursor? – Vahid Farahmandian Jun 15 '16 at 08:15
  • Exactly what I wanted.Thanks – Szekely Zsolt Karoly Jun 15 '16 at 11:48
-1

You can rewrite Cursor by WHILE(But not recommended in every situation). In order to speed up your cursor, you can define your cursors as FAST_FORWARD instead of getting rid of cursor.

DECLARE cur1 CURSOR FAST_FORWARD

Read more about rewriting Cursor by WHILE HERE

Vahid Farahmandian
  • 6,081
  • 7
  • 42
  • 62