CURSOR v WHILE for beginners

  • Hi all,

    Hoping for a bit of help please. I've been working in SQL for a few years now, but I'm entirely self-taught so I’m pretty certain that a lot of things that I do could be done better… the way I do them is just because that’s how I’ve learnt. I generally get results, but I’m getting to a point now where optimisation is becoming an increasing focus so I’m trying to unlearn bad habits – not easy!!

    The problem I have here is that I have quite a few processes to move data between tables… and to do this I’ve always used cursors (please stop screaming!). I’ve recently become aware that While loops are much better than cursors, but I need a bit of help turning one of my standard cursors in to a while loop… once I’ve got the basics I should be able to use the principles in my other queries.

    The example below is something I run on a regular basis, but it’s performance is horrible – it takes upwards of ½ an hour to process about 12k records… so I’m thinking there must be a better way of doing things. I’d appreciate it if I could get some guidance on both general tips that people can observe looking at the way I’m doing things as well as converting the provided cursor in to a while loop.

    BEGIN

    SET NOCOUNT ON;

    DECLARE @PHONEID INT

    DECLARE @PERSONID INT

    DECLARE @PERSLEGACYACCOUNTID INT

    DECLARE @PHONETYPE VARCHAR(40)

    DECLARE @PHONENUMBER VARCHAR(20)

    DECLARE MY_CURSOR CURSOR FOR

    SELECT PHON_TYPE,

    LEFT(PHON_NUMBER,20),

    PHON_PERSLEGACYACCOUNTID

    FROM ZZIMPPHONE

    WHERE PHON_PERSLEGACYACCOUNTID IN (SELECT DISTINCT PERS_LEGACYACCOUNTID FROM PERSON)

    OPEN MY_CURSOR

    FETCH NEXT FROM MY_CURSOR INTO

    @PHONETYPE,

    @PHONENUMBER,

    @PERSLEGACYACCOUNTID

    WHILE @@FETCH_STATUS = 0

    BEGIN

    EXEC @PHONEID = eware_get_identity_id 'Phone'

    SET @PERSONID = (SELECT TOP 1 PERS_PERSONID FROM PERSON WHERE PERS_LEGACYACCOUNTID = @PERSLEGACYACCOUNTID)

    INSERT INTO Phone

    (Phon_PhoneId,

    Phon_PersonID,

    Phon_Type,

    Phon_Number,

    Phon_CreatedBy,

    Phon_CreatedDate,

    Phon_UpdatedBy,

    Phon_UpdatedDate,

    Phon_TimeStamp)

    VALUES

    (@PHONEID,

    @PERSONID,

    @PHONETYPE,

    @PHONENUMBER,

    1,

    GETDATE(),

    1,

    GETDATE(),

    GETDATE()

    )

    FETCH NEXT FROM MY_CURSOR INTO

    @PHONETYPE,

    @PHONENUMBER,

    @PERSLEGACYACCOUNTID

    END

    CLOSE MY_CURSOR

    DEALLOCATE MY_CURSOR

    END

    Thanks in advance

    Brett

  • What does eware_get_identity_id do?

    Can this be turned into a function?

    If it's not too long, can you post it's definition?

    I ask because I'm thinking that this can be done in one INSERT statement. 😀

    ______________________________________________________________________

    Personal Motto: Why push the envelope when you can just open it?

    If you follow the direction given HERE[/url] you'll likely increase the number and quality of responses you get to your question.

    Jason L. Selburg
  • No yelling here....but let's see if we can do away with that cursor....It's got nasty overhead, and performance is usually why we get rid of them....

    Could you post what eware_get_identity_id does? Assuming it doesn't do anything outlandish - we should be able to get that down to a few seconds.....

    ----------------------------------------------------------------------------------
    Your lack of planning does not constitute an emergency on my part...unless you're my manager...or a director and above...or a really loud-spoken end-user..All right - what was my emergency again?

  • Matt Miller (12/17/2007)


    No yelling here....but let's see if we can do away with that cursor....It's got nasty overhead, and performance is usually why we get rid of them....

    Could you post what eware_get_identity_id does? Assuming it doesn't do anything outlandish - we should be able to get that down to a few seconds.....

    Copy cat ... LOL :w00t:

    Get out of my head Matt! :hehe:

    ______________________________________________________________________

    Personal Motto: Why push the envelope when you can just open it?

    If you follow the direction given HERE[/url] you'll likely increase the number and quality of responses you get to your question.

    Jason L. Selburg
  • Jason Selburg (12/17/2007)


    What does eware_get_identity_id do?

    Can this be turned into a function?

    Basically it is a proprietary SP provided by a SQL based web application for the purpose of generating unique integer based row IDs. It does several things including calling other SP’s for record locking, remote client id range updates as well index updates… so I don’t think so, but here is the code nonetheless:

    SET ANSI_NULLS ON

    GO

    SET QUOTED_IDENTIFIER OFF

    GO

    ALTER PROCEDURE [dbo].[eware_get_identity_id]

    @table_name NVARCHAR(80)

    AS

    DECLARE @table_id INT

    DECLARE @errmsg NVARCHAR(250)

    SELECT @errmsg =''

    DECLARE @timeout INTeger

    DECLARE @start_lock datetime

    DECLARE @id_inc INT

    DECLARE @sql NVARCHAR(200)

    DECLARE @r_start INT

    DECLARE @r_end INT

    DECLARE @r_nextstart INT

    DECLARE @r_nextend INT

    DECLARE @r_nextcontrol INT

    DECLARE @range_limit INT

    DECLARE @new_id INT

    SELECT @new_id = 0

    DECLARE @no_lock INT

    SELECT @range_limit = 10000

    SELECT @timeout = 5 -- try for 5 seconds to get the lock

    SET NOCOUNT ON

    -- Get the Table Id from the table name

    SELECT @table_id = Bord_TableId FROM Custom_Tables NOLOCK WHERE Bord_Name = @table_name

    IF (@@ERROR <> 0) OR @table_id = 0 BEGIN

    RAISERROR('crm_new_id: No Table Found %s',16,-1,@table_name) WITH LOG

    RETURN 0

    END

    BEGIN TRANSACTION T1

    -- keep trying until we get the lock

    -- must put a timeout here and RETURN ERROR IF cannot get lock

    SELECT @start_lock = getdate();

    SELECT @no_lock = 0

    WHILE (@no_lock = 0) AND (Getdate() < DateAdd(second,@timeout,@start_lock) ) BEGIN

    INSERT INTO Locks (Lock_SessionId, Lock_TableId,Lock_RecordId,Lock_CreatedBy,Lock_CreatedDate)

    VALUES(0,@table_id,-99,0,getDate())

    IF (@@ERROR = 0) BEGIN

    SELECT @no_lock = 1

    END

    END

    IF @no_lock = 0 BEGIN

    ROLLBACK TRANSACTION T1

    RAISERROR('crm_new_id: Timeout trying to get lock (%s)',16,-1,@table_name) WITH LOG

    RETURN 0

    END

    ELSE BEGIN

    COMMIT TRANSACTION T1

    END

    BEGIN TRANSACTION T2

    -- check out the range values for this table

    SELECT @r_start = Range_RangeStart, @r_end = Range_RangeEnd,

    @r_nextstart = Range_NextRangeStart, @r_nextend = Range_NextRangeEnd,

    @r_nextcontrol = Range_Control_NextRange FROM Rep_Ranges NOLOCK WHERE Range_TableId = @table_id

    -- get the next id for this table

    EXEC @new_id = crm_next_id @table_id = @table_id

    -- is the id within the range?

    IF (@new_id 0) BEGIN

    -- have used up all the range so create a new range

    UPDATE Rep_Ranges SET

    Range_RangeStart = @r_nextstart,

    Range_RangeEnd = @r_nextend,

    Range_NextRangeStart = @r_nextcontrol,

    Range_NextRangeEnd = @r_nextcontrol + @range_limit - 1,

    Range_Control_NextRange = @r_nextcontrol + @range_limit

    WHERE Range_TableId = @table_id

    IF @@ERROR <> 0 BEGIN

    SELECT @new_id = 0

    SELECT @errmsg = 'crm_new_id: Update New Range Failed (%s)'

    GOTO END_TRANS

    END

    -- result is first id FROM new range

    SELECT @new_id = @r_nextstart

    -- apply the new range so eware_get_next_id will RETURN next id FROM within new range

    UPDATE Sql_Identity SET Id_NextId = @r_nextstart+1 WHERE Id_TableId = @table_id

    IF @@ERROR <> 0 BEGIN

    SELECT @new_id = 0

    SELECT @errmsg = 'crm_new_id: Apply New Range Failed (%s)'

    GOTO END_TRANS

    END

    END

    END_TRANS: -- commit or rollback the transaction

    IF @new_id = 0 BEGIN

    ROLLBACK TRANSACTION T2

    IF @errmsg = ''

    SELECT @errmsg = 'crm_new_id: Get Next Id Failed (%s)'

    END

    ELSE BEGIN

    COMMIT TRANSACTION T2

    END

    --Finished so remove the lock

    BEGIN TRANSACTION T3

    SELECT @start_lock = getdate();

    SELECT @no_lock = 0

    WHILE (@no_lock = 0) AND (Getdate() < DateAdd(second,@timeout,@start_lock) ) BEGIN

    DELETE FROM Locks WHERE Lock_TableId=@table_id and Lock_RecordId = -99

    IF (@@ERROR = 0) BEGIN

    SELECT @no_lock = 1

    END

    END

    IF @no_lock = 0 BEGIN

    ROLLBACK TRANSACTION T3

    RAISERROR('crm_new_id: Error Deleting Lock (%s)',16,-1,@table_name) WITH LOG

    RETURN 0

    END

    ELSE BEGIN

    COMMIT TRANSACTION T3

    END

    -- Make sure RaisError is the last call, so that @@ERROR is set correctly

    IF (@errmsg <> '') BEGIN

    RAISERROR(@errmsg,16,-1,@table_name) WITH LOG

    RETURN 0

    END

    SET NOCOUNT OFF

    RETURN @new_id

  • Depending on what the procedure eware_get_identity_id does, I think this is what you're looking for.

    INSERT Phone

    SELECT

    dbo.eware_get_identity_id('Phone')

    ,p.PERS_PERSONID

    ,z.PHON_TYPE

    ,LEFT(z.PHON_NUMBER,20)

    ,1,GETDATE(),1,GETDATE(),GETDATE()

    FROM

    ZZIMPPHONE z

    INNER JOIN PERSON p

    ON p.PERS_LEGACYACCOUNTID = z.PERS_LEGACYACCOUNTID

    What do you think Matt?

    ______________________________________________________________________

    Personal Motto: Why push the envelope when you can just open it?

    If you follow the direction given HERE[/url] you'll likely increase the number and quality of responses you get to your question.

    Jason L. Selburg
  • For a while loop you could do something like....

    SET NOCOUNT ON;

    DECLARE @PHONEID INT

    DECLARE @PERSONID INT

    DECLARE @PERSLEGACYACCOUNTID INT

    DECLARE @PHONETYPE VARCHAR(40)

    DECLARE @PHONENUMBER VARCHAR(20)

    INSERT INTO #TempPhone ( LoopId int IDENTITY(1,1) NOT NULL,

    PHON_TYPE varchar(40),

    PHON_NUMBER varchar(20),

    PERSLEGACYACCOUNTID int )

    INSERT INTO #TempPhone

    SELECT PHON_TYPE,

    LEFT(PHON_NUMBER,20),

    PHON_PERSLEGACYACCOUNTID

    FROM ZZIMPPHONE

    WHERE PHON_PERSLEGACYACCOUNTID IN (SELECT DISTINCT PERS_LEGACYACCOUNTID FROM PERSON)

    DECLARE @LoopCounter int, @Iterations int

    SET @LoopCounter = 1

    SELECT @Iterations = MAX(LoopId) FROM #TempPhone

    WHILE @LoopCounter <= ISNULL(@Iterations,0)

    BEGIN

    SELECT @PHONETYPE,

    @PHONENUMBER,

    @PERSLEGACYACCOUNTID

    FROM #TempPhone

    WHERE LoopId = @LoopCounter

    EXEC @PHONEID = eware_get_identity_id 'Phone'

    SET @PERSONID = (SELECT TOP 1 PERS_PERSONID FROM PERSON WHERE PERS_LEGACYACCOUNTID = @PERSLEGACYACCOUNTID)

    INSERT INTO Phone

    (Phon_PhoneId,

    Phon_PersonID,

    Phon_Type,

    Phon_Number,

    Phon_CreatedBy,

    Phon_CreatedDate,

    Phon_UpdatedBy,

    Phon_UpdatedDate,

    Phon_TimeStamp)

    VALUES

    (@PHONEID,

    @PERSONID,

    @PHONETYPE,

    @PHONENUMBER,

    1,

    GETDATE(),

    1,

    GETDATE(),

    GETDATE()

    )

    SET @LoopCounter = @LoopCounter + 1

    END

    However, if you can turn eware_get_identity_id into a function or show us what it does then you could do a set based approach such as ...

    INSERT INTO Phone

    (Phon_PhoneId,

    Phon_PersonID,

    Phon_Type,

    Phon_Number,

    Phon_CreatedBy,

    Phon_CreatedDate,

    Phon_UpdatedBy,

    Phon_UpdatedDate,

    Phon_TimeStamp)

    SELECT

    dbo.fn_eware_get_identity_id ( 'Phone'),

    P.PERS_PERSONID,

    A.PHON_TYPE,

    LEFT(A.PHON_NUMBER,20),

    1,

    GETDATE(),

    1,

    GETDATE(),

    GETDATE()

    FROM ZZIMPPHONE A

    JOIN ( SELECT

    PERS_LEGACYACCOUNTID,

    MIN(PERS_PERSONID) as PERS_PERSONID

    FROM PERSON

    GROUP BY PERS_LEGACYACCOUNTID )P

    ON A.PHON_PERSLEGACYACCOUNTID = P.PERS_LEGACYACCOUNTID

  • Jason Selburg (12/17/2007)


    Depending on what the procedure eware_get_identity_id does, I think this is what you're looking for.

    INSERT Phone

    SELECT

    dbo.eware_get_identity_id('Phone')

    ,p.PERS_PERSONID

    ,z.PHON_TYPE

    ,LEFT(z.PHON_NUMBER,20)

    ,1,GETDATE(),1,GETDATE(),GETDATE()

    FROM

    ZZIMPPHONE z

    INNER JOIN PERSON p

    ON p.PERS_LEGACYACCOUNTID = z.PERS_LEGACYACCOUNTID

    What do you think Matt?

    I think you type faster than I do:)

    ----------------------------------------------------------------------------------
    Your lack of planning does not constitute an emergency on my part...unless you're my manager...or a director and above...or a really loud-spoken end-user..All right - what was my emergency again?

  • Being that your procedure does all of that, IMHO you are stuck with looping of some sort.

    You can always use a temp table and loop through an IDENTITY column in it.

    ... as below

    DECLARE @PHONEID INT

    CREATE TABLE #myTable

    (nDex INT IDENTITY(1,1)

    ,PHONETYPE VARCHAR(50) -- or what ever it should be

    ,PHONENUMBER VARCHAR(20)

    ,PERSLEGACYACCOUNTID INT)

    INSERT #myTable

    SELECT PHON_TYPE,LEFT(PHON_NUMBER,20),PHON_PERSLEGACYACCOUNTID

    FROM ZZIMPPHONE

    WHERE PHON_PERSLEGACYACCOUNTID IN (SELECT DISTINCT PERS_LEGACYACCOUNTID FROM PERSON)

    DECLARE @i INT

    SET @i = 1

    WHILE @i <= (SELECT MAX(nDex) FROM @myTable)

    BEGIN

    EXEC @PHONEID = eware_get_identity_id 'Phone'

    INSERT Phone

    SELECT

    @PHONEID

    ,p.PERS_PERSONID

    ,z.PHON_TYPE

    ,LEFT(z.PHON_NUMBER,20)

    ,1,GETDATE(),1,GETDATE(),GETDATE()

    FROM

    #myTable z

    INNER JOIN PERSON p

    ON p.PERS_LEGACYACCOUNTID = z.PERS_LEGACYACCOUNTID

    WHERE

    nDex = @i

    SELECT @i = @i + 1

    END

    DROP TABLE #myTable

    at least I think this will work. 😛

    ______________________________________________________________________

    Personal Motto: Why push the envelope when you can just open it?

    If you follow the direction given HERE[/url] you'll likely increase the number and quality of responses you get to your question.

    Jason L. Selburg
  • All right - now THAT's ugly.... Tell me - what does get_next_CRM_ID do?

    Using that proprietary thing right there is the reason you're jammed up. Looks to me that someone needs to be flogged... repeatedly....for that. It assumes you will only ever need one ID at a time, which for SQL Server is invariably a bad assumption

    We're going to need to enlist a Tally table for this, but let's get to the bottom of this numbering scheme first

    What gets inserted into Rep_ranges? (a sample of what the data looks like)... just curious.

    ----------------------------------------------------------------------------------
    Your lack of planning does not constitute an emergency on my part...unless you're my manager...or a director and above...or a really loud-spoken end-user..All right - what was my emergency again?

  • Matt Miller (12/17/2007)


    All right - now THAT's ugly.... Tell me - what does get_next_CRM_ID do?

    Using that proprietary thing right there is the reason you're jammed up. Looks to me that someone needs to be flogged... repeatedly....for that. It assumes you will only ever need one ID at a time, which for SQL Server is invariably a bad assumption

    We're going to need to enlist a Tally table for this, but let's get to the bottom of this numbering scheme first

    What gets inserted into Rep_ranges? (a sample of what the data looks like)... just curious.

    On that note, it appears Matt is willing to dig far deeper into this particular problem. So I respectfully bow out of this one... LOL;)

    ______________________________________________________________________

    Personal Motto: Why push the envelope when you can just open it?

    If you follow the direction given HERE[/url] you'll likely increase the number and quality of responses you get to your question.

    Jason L. Selburg
  • Matt Miller (12/17/2007)


    Tell me - what does get_next_CRM_ID do?

    Basically it gets the current valid ID from a tracking table, then updates the table with the current value +1 ready for the next execution.

    What gets inserted into Rep_ranges? (a sample of what the data looks like)... just curious.

    Here is a sample (Excuse the formatting):

    Range_TableId Range_RangeStart Range_RangeEnd Range_NextRangeStart Range_NextRangeEnd Range_Control_NextRange

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

    10148 6001 56000 56001 106000 106001

    10149 6001 56000 56001 106000 106001

    10150 6000 15999 16000 25999 26000

    10151 6000 15999 16000 25999 26000

  • Oh no... a "sequence" table. :hehe: Third party app we got used one... cause an average of 640 deadlocks per day because of the way they wrote the increment code.

    --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)
    Intro to Tally Tables and Functions

  • (rolling sleeves up)

    Thanks for the update...I need the DDL of the tracking table it's updating. If there is a loop in the end result, it will be one updating 10K records at a time. One at a time it, well, crazy....

    The thought here to fix this:

    - create a temp table with the candidate records you need to update

    - get the open range. If it covers the record count you need, then assign IDs (in a set) to all records. Otherwise - use all of the remaining numbers in the sequence to update as many as you can.

    - if you still have rows needing an id, request another range (another 10K if I see the SP correctly), and do the above step again on all of those with no ID.

    - do # 2 and 3 until you give ID's to all that need one.

    - insert them all into final table.

    The million $ question is whether you are ALLOWED to "reverse engineer" the sequence assigning process (or in this case - leave the existing process alone, but build another, more efficient one next to it) without your mgmt screeching/a vendor crying/ support being yanked for doing soemthing "not supported. If you think you might, then stick with Jason's solution, although I doubt you will see much in terms of gains. While is a LITTLE more efficient than a cursor, but it probably won't be enough to write home to mom about it.... On the other hand - the process I'm talking about should finish quite a bit faster.

    Otherwise - get some table definitions out... For one - all of the tables mentioned in the code above, and the one in get_next_CRM_ID.

    ----------------------------------------------------------------------------------
    Your lack of planning does not constitute an emergency on my part...unless you're my manager...or a director and above...or a really loud-spoken end-user..All right - what was my emergency again?

  • Matt said: "The million $ question is whether you are ALLOWED to "reverse engineer" the sequence assigning process (or in this case - leave the existing process alone, but build another, more efficient one next to it) without your mgmt screeching/a vendor crying/ support being yanked for doing soemthing "not supported."

    You are so right, there! The vendor might just get petty about it if you change their code. However one thing you can do is create the parallel process that works much better then submit it to them as a suggestion for a product enhancement. (Use the word "fix", even though that's what it is, and they might just dismiss it out of hand.)

    A lot depends on the wording of your EULA. It probably specifies that you are not to touch the code but there may not be anything in there that prohibits your looking at it. If they are smart they will appreciate suggestions for improving their product and everyone wins - you get a product that runs better and it makes them look good to their other clients. :w00t:

    Cheers,

    Don

Viewing 15 posts - 1 through 15 (of 25 total)

You must be logged in to reply to this topic. Login to reply