A Case FOR Cursors...

  • Hugo Kornelis (6/2/2015)


    Alan.B (6/1/2015)


    Until someone, anyone, can post an example where a cursor or other iterative method was faster than a well-written set-based solution I will remain convinced that the set-based solution is the way to go.

    A few years ago, I published a set of blog posts about the bin packing problem. I presented a few cursor-based solutions, a fully set-based solution, and then my own hybrid "iteration + set-based" solution. The fully set-based solution was far slower then the cursor-based solutions. This was before windowing functions were introduced so I cannot 100% certain say that this is still correct, but I personally do not know of any way to employ these to create a better set-based bin packing query.

    Link: http://sqlblog.com/blogs/hugo_kornelis/archive/tags/Bin+Packing/default.aspx

    Note that this series of blog posts was inspired by a real world problem (not from my own clients, but from a question I answered on internet).

    I need to do variations of bin packing and read your blog posts some time back, they come up first when I do a search for (good stuff). I noticed that in one of the threads, Dwain Camps said that he had a purely set-based way of handling bin packing that performed well but cannot find an article about that. I plan on revisiting your article and playing around with solutions using Window and 2012 framing functions.

    "I cant stress enough the importance of switching from a sequential files mindset to set-based thinking. After you make the switch, you can spend your time tuning and optimizing your queries instead of maintaining lengthy, poor-performing code."

    -- Itzik Ben-Gan 2001

  • fundpc (6/2/2015)


    ...and yes I am old enough to have been there when our IBM RD281 disk pack, read heads driven with hydraulic rams, blew a packing and saturated the innards with hydraulic fluid. YUCK...

    Old school memory leak! :w00t:

  • Andy DBA (6/2/2015)


    fundpc (6/2/2015)


    ...and yes I am old enough to have been there when our IBM RD281 disk pack, read heads driven with hydraulic rams, blew a packing and saturated the innards with hydraulic fluid. YUCK...

    Old school memory leak! :w00t:

    Nice!

    "I cant stress enough the importance of switching from a sequential files mindset to set-based thinking. After you make the switch, you can spend your time tuning and optimizing your queries instead of maintaining lengthy, poor-performing code."

    -- Itzik Ben-Gan 2001

  • There are people who think that avoiding cursor doesn't mean to avoid row by row processing, so they are going to substitute cursor with while loops. I guess that microsft is to blame for that misunderstanding and that Always set operations should be preferred more then anything else even if sometimes are somewhat more cryptic. So totally agree with this topic.

  • Regarding the GOTO statement, are people saying it has no place in TSQL either?

    I know VB and remember on my Amstrad 64 having to use GOTO statements constantly so in code yeah I can understand the sentiment but in a stored proc with an error handler is anyone saying the GOTO statement should be redundant?

    Here is an example stored proc with some GOTO statements to control basic error handling and also use an ext sections.

    This is a VERY SIMPLIFIED version of the stored proc with missing chunks of Insert/Update statements I have just put comments in

    SET ANSI_NULLS ON

    GO

    SET QUOTED_IDENTIFIER ON

    GO

    ALTER PROCEDURE [dbo].[usp_asp_save_job]

    @SitePK int = 0,

    @ClientPK int = 0,

    @JobPK int = 0, -- 0 = NEW JOB, > 0 EDIT JOb

    @JobTitle nvarchar(255) = NULL

    @JobDescription nvarchar(max) = NULL,

    @ApplicationFolders bit = 0

    AS

    SET NOCOUNT ON

    SET DATEFORMAT YMD

    DECLARE @success bit,

    @message nvarchar(255),

    @errors int,

    @rowcount int,

    @ChangeCode tinyint,

    @Action nvarchar(100)

    -- ensure we have some KEY data - application does real validation this is just a double check to ensure direct calls to the proc don't insert blank jobs

    IF ISNULL(@JobTitle,'') != '' OR ISNULL(@JobDescription,'') != '' OR @SitePK = 0 OR @ClientPK = 0

    BEGIN

    -- get appropriate error message for this site

    SELECT @message= dbo.udf_GET_MESSAGE(@SitePK, 700) -- e.g "no data supplied "

    -- jump to the error handler

    GOTO HANDLE_ERROR

    END

    -- check for existing job posted with same details within last 60 millsecond e.g double post

    IF EXISTS(SELECT jobPK FROM JOBS WHERE sitefk = @sitepk and ClientFK = @ClientPK and jobTitle = @JobTitle and jobDescription = @JobDescription AND DATEDDIFF(mi,livedate,getdate())<60)

    BEGIN

    -- get appropriate error message for this error from list of messages using a function and the code for the error 500 - message could be in any lanugage so pass sitePK to get relevant site message

    SELECT @message= dbo.udf_GET_MESSAGE(@SitePK, 500)

    -- jump to error handler

    GOTO HANDLE_ERROR

    END

    -- before updating work out what has changed for job history log table

    SELECT@Details = dbo.udf_GET_JOB_CHANGE_DETAILS(@SitePK, @JobPK)

    -- Do update as we have jobID

    IF @JobPk != 0

    BEGIN

    -- Get Edit Job message for site in appropriate language / wording for site

    SELECT @ChangeCode = 2 ,@Action = dbo.udf_GET_MESSAGE(@SitePK, 456)

    BEGIN TRAN

    UPDATEJOBS

    SETClientFK= @ClientPK,

    JobTitle= @JobTitle,

    JobDescription= @JobDescription

    -- others of course in real proc

    WHEREJobPK = @JobPK

    SELECT @errors = @@error , @rowcount = @@rowcount

    IF @errors != 0 OR @rowcount <> 1

    BEGIN

    -- get correct update error message

    SELECT @message = dbo.udf_GET_ERROR_MESSAGE(@SitePK, 101)

    -- go to error handler

    GOTO HANDLE_ERROR

    END

    -- if application folders are enabled for the site update group name in case it's changed (these are folders people can put jobs in e.g "best jobs", "IT jobs"

    IF @ApplicationFolders = 1

    BEGIN

    -- check if group already exists and if it does ensure the group name is updated to the job title in case its changed

    IF EXISTS(SELECT GroupPK FROM GROUPS WHERE clientFK = @ClientPK AND JobFK = @JobPK)

    BEGIN

    UPDATEGROUPS

    SETGroupName = @JobTitle

    WHEREClientFK = @ClientPK AND

    JobFK = @JobPK

    END

    ELSE

    BEGIN

    -- create new group for all applications to this job to go into by default

    INSERT INTO GROUPS

    (GroupName, ClientFK, GroupType, JobFK)

    VALUES

    (@JobTitle, @ClientPk, 'DJA', @JobPK)

    END

    -- ensure no errors and set message if there was a group error

    SELECT @errors = @@error , @rowcount = @@rowcount

    IF @errors != 0 OR @rowcount <> 1

    BEGIN

    -- get correct group inset error message

    SELECT @message = dbo.udf_GET_ERROR_MESSAGE(@SitePK, 220)

    -- go to error handler

    GOTO HANDLE_ERROR

    END

    END

    END

    ELSE

    BEGIN

    -- Get Add Job message for site in appropriate language / wording for site

    SELECT @ChangeCode = 1 , @Action = dbo.udf_GET_MESSAGE(@SitePK, 455)

    BEGIN TRAN

    -- short example of a new job

    INSERT INTO JOBS

    (SiteFK, ClientFK,JobTitle,JobDescription)

    VALUES

    (@SitePK, @ClientPK, @JobTitle, @JobDescription)

    -- get new jobID

    SELECT @errors = @@error, @rowcount = @@rowcount, @JobPK = SCOPE_IDENTITY()

    -- if error

    IF @errors != 0 OR @rowcount<>1

    BEGIN

    -- get correct update error message

    SELECT @message = dbo.udf_GET_ERROR_MESSAGE(@SitePK, 102)

    -- go to error handler

    GOTO HANDLE_ERROR

    END

    -- if application folders are on for the site create a new group

    IF @ApplicationFolders = 1

    BEGIN

    -- ensure group is clean without a check as that just wastes time IF it does exist already, obviously if it doesn't then it won't do anthing but the fields are indexed

    DELETE

    FROMGROUPS

    WHEREJobFK = @JobPK

    AND GroupType = 'DJA'

    AND ClientFK = @ClientPK

    -- doesn't matter if nothing was deleted

    INSERT INTO GROUPS

    (GroupName, ClientFK, GroupType, JobFK)

    VALUES

    (@JobTitle, @ClientPk, 'DJA', @JobPK)

    -- ensure no errors and set message if there was a group error

    SELECT @errors = @@error, @rowcount = @@rowcount

    IF @errors != 0 OR @rowcount <> 1

    BEGIN

    -- get correct group inset error message

    SELECT @message = dbo.udf_GET_ERROR_MESSAGE(@SitePK, 220)

    -- go to error handler

    GOTO HANDLE_ERROR

    END

    END

    END

    -- other work related to multiple linked sites

    -- other code related to handle PENDING (non live) jobs

    -- if we get here NO ERRORS so COMMIT the transaction

    IF @@TRANCOUNT = 0

    COMMIT TRAN

    -- set return values including a success message and the return code for the application

    SELECT @success = 1, @message = dbo.udf_GET_MESSAGE(@SitePK, 103)

    -- skip over error handler and exit proc

    GOTO EXIT_PROC

    END

    -- error handler code - cannot be in here if NO ERROR which is why GOTO jumps OVER it

    HANDLE_ERROR:

    -- if we have an open transaction close it

    IF @@TRANCOUNT > 0

    ROLLBACK TRAN

    -- if we don't have an error message get a default on e.g "Job failed to save please try back later"

    IF ISNULL(@message,'') = ''

    SELECT @message = dbo.udf_GET_ERROR_MESSAGE(@SitePK, 100)

    -- we failed so succes is 0

    SELECT @success = 0

    -- exit the stored proc

    EXIT_PROC:

    -- before we exit the proc we log any changes to the table and any error messages if thee were any

    IF INSULL(@Details,'') != ''

    BEGIN

    EXEC [dbo].[usp_sql_add_job_change_record] @SitePK, @JobPK, @ChangeCode, @Action, @Details, @Success, @Message

    END

    -- return to caller

    SELECT cast(@Success as int) as Success, isnull(@JobPK,0) as JobPK, @message as [message], @CheckMode as checkmode

    -- use success as return code in case we are using an ADO stored proce that relies on return codes

    RETURN @Success

    Also if I had a stored proc that saved MULTIPLE jobs at the same time e.g @JobID was not ONE integer but a comma delimited list of integers to save in one go, how could I not use a cursor to loop through them all when for EACH JOB I have statements to check for duplicate submits, finding out what has changed in the job for reporting, plus logging that information with another stored proc call that is used across the site for logging info regarding changes to important records.

    Cursors have their time and place especially if you have to do procedural type procedure like the one above. I have seen people just try and replace cursors with temporary tables with identity fields and then WHILE loops selecting the next ID record before doing the middle content of a proc like the one above before incrementing the loop counter. This is just creating a cursor in another manner and if you have to do a procedural type proc then a FAST_FORWARD LOCAL CURSOR is designed for looping from start to end so why try and replace it.

    If anyone can show me how I can replace all the contents of the proc above without GOTO statements and why the format used is bad I would like to know.

    Also if you can imagine having multiple jobs to handle in the proc rather than one, so @JobIDs varchar(1000) (e.g 12,13,14), @JobTitle nvarchar(max) 'Test 1','Test 2','Test 3' (Same for Job Desc) e.g position N of each comma delimited list relates to position N of the other values I would be interested to see.

    Each job needs the details of the changes done to it logged, the related groups checked and inserted/updated, plus the changes all logged with the stored proc currently residing in the EXIT_PROC part of the proc. In a cursor loop this would be at the bottom of the loop iteration.

    And I don't use cursors if I can do it in a set based by the way.

    Just when the data logic is more complicated than a single SELECT/UPDATE/INSERT statement and requires calls to other functions / procedures.

    Thanks

    Rob

  • Lenochka (6/1/2015)


    Thank you for writing this. I used to ask a cursor question when interviewing candidates and I got the same answer every time “you shouldn’t use cursors, they are bad”. I found it to be extremely frustrating. Developers use that as an excuse to never learn how to write cursors or when to use them. I have seen people write very un-maintainable, convoluted code (that might not always work correctly), or resort to manual processing in places where a cursor is an easy, clean an elegant solution.

    Cursors don't kill performance.

    People (using cursors inappropriately) kill performance.

  • Andy DBA (6/2/2015)


    fundpc (6/2/2015)


    ...and yes I am old enough to have been there when our IBM RD281 disk pack, read heads driven with hydraulic rams, blew a packing and saturated the innards with hydraulic fluid. YUCK...

    Old school memory leak! :w00t:

    BWAAAA-HAAAA!!!!! Way too funny! 😛

    --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)

  • Lenochka (6/1/2015)


    Thank you for writing this. I used to ask a cursor question when interviewing candidates and I got the same answer every time “you shouldn’t use cursors, they are bad”. I found it to be extremely frustrating. Developers use that as an excuse to never learn how to write cursors or when to use them. I have seen people write very un-maintainable, convoluted code (that might not always work correctly), or resort to manual processing in places where a cursor is an easy, clean an elegant solution.

    Not being combative, here. I'd really like to know. Can you explain what some of those "elegant" solutions that actually required a cursor were?

    --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)

  • Rob Reid-246754 (6/3/2015)


    Regarding the GOTO statement, are people saying it has no place in TSQL either?

    I know VB and remember on my Amstrad 64 having to use GOTO statements constantly so in code yeah I can understand the sentiment but in a stored proc with an error handler is anyone saying the GOTO statement should be redundant?

    Here is an example stored proc with some GOTO statements to control basic error handling and also use an ext sections.

    This is a VERY SIMPLIFIED version of the stored proc with missing chunks of Insert/Update statements I have just put comments in

    SET ANSI_NULLS ON

    GO

    SET QUOTED_IDENTIFIER ON

    GO

    ALTER PROCEDURE [dbo].[usp_asp_save_job]

    @SitePK int = 0,

    @ClientPK int = 0,

    @JobPK int = 0, -- 0 = NEW JOB, > 0 EDIT JOb

    @JobTitle nvarchar(255) = NULL

    @JobDescription nvarchar(max) = NULL,

    @ApplicationFolders bit = 0

    AS

    SET NOCOUNT ON

    SET DATEFORMAT YMD

    DECLARE @success bit,

    @message nvarchar(255),

    @errors int,

    @rowcount int,

    @ChangeCode tinyint,

    @Action nvarchar(100)

    -- ensure we have some KEY data - application does real validation this is just a double check to ensure direct calls to the proc don't insert blank jobs

    IF ISNULL(@JobTitle,'') != '' OR ISNULL(@JobDescription,'') != '' OR @SitePK = 0 OR @ClientPK = 0

    BEGIN

    -- get appropriate error message for this site

    SELECT @message= dbo.udf_GET_MESSAGE(@SitePK, 700) -- e.g "no data supplied "

    -- jump to the error handler

    GOTO HANDLE_ERROR

    END

    -- check for existing job posted with same details within last 60 millsecond e.g double post

    IF EXISTS(SELECT jobPK FROM JOBS WHERE sitefk = @sitepk and ClientFK = @ClientPK and jobTitle = @JobTitle and jobDescription = @JobDescription AND DATEDDIFF(mi,livedate,getdate())<60)

    BEGIN

    -- get appropriate error message for this error from list of messages using a function and the code for the error 500 - message could be in any lanugage so pass sitePK to get relevant site message

    SELECT @message= dbo.udf_GET_MESSAGE(@SitePK, 500)

    -- jump to error handler

    GOTO HANDLE_ERROR

    END

    -- before updating work out what has changed for job history log table

    SELECT@Details = dbo.udf_GET_JOB_CHANGE_DETAILS(@SitePK, @JobPK)

    -- Do update as we have jobID

    IF @JobPk != 0

    BEGIN

    -- Get Edit Job message for site in appropriate language / wording for site

    SELECT @ChangeCode = 2 ,@Action = dbo.udf_GET_MESSAGE(@SitePK, 456)

    BEGIN TRAN

    UPDATEJOBS

    SETClientFK= @ClientPK,

    JobTitle= @JobTitle,

    JobDescription= @JobDescription

    -- others of course in real proc

    WHEREJobPK = @JobPK

    SELECT @errors = @@error , @rowcount = @@rowcount

    IF @errors != 0 OR @rowcount <> 1

    BEGIN

    -- get correct update error message

    SELECT @message = dbo.udf_GET_ERROR_MESSAGE(@SitePK, 101)

    -- go to error handler

    GOTO HANDLE_ERROR

    END

    -- if application folders are enabled for the site update group name in case it's changed (these are folders people can put jobs in e.g "best jobs", "IT jobs"

    IF @ApplicationFolders = 1

    BEGIN

    -- check if group already exists and if it does ensure the group name is updated to the job title in case its changed

    IF EXISTS(SELECT GroupPK FROM GROUPS WHERE clientFK = @ClientPK AND JobFK = @JobPK)

    BEGIN

    UPDATEGROUPS

    SETGroupName = @JobTitle

    WHEREClientFK = @ClientPK AND

    JobFK = @JobPK

    END

    ELSE

    BEGIN

    -- create new group for all applications to this job to go into by default

    INSERT INTO GROUPS

    (GroupName, ClientFK, GroupType, JobFK)

    VALUES

    (@JobTitle, @ClientPk, 'DJA', @JobPK)

    END

    -- ensure no errors and set message if there was a group error

    SELECT @errors = @@error , @rowcount = @@rowcount

    IF @errors != 0 OR @rowcount <> 1

    BEGIN

    -- get correct group inset error message

    SELECT @message = dbo.udf_GET_ERROR_MESSAGE(@SitePK, 220)

    -- go to error handler

    GOTO HANDLE_ERROR

    END

    END

    END

    ELSE

    BEGIN

    -- Get Add Job message for site in appropriate language / wording for site

    SELECT @ChangeCode = 1 , @Action = dbo.udf_GET_MESSAGE(@SitePK, 455)

    BEGIN TRAN

    -- short example of a new job

    INSERT INTO JOBS

    (SiteFK, ClientFK,JobTitle,JobDescription)

    VALUES

    (@SitePK, @ClientPK, @JobTitle, @JobDescription)

    -- get new jobID

    SELECT @errors = @@error, @rowcount = @@rowcount, @JobPK = SCOPE_IDENTITY()

    -- if error

    IF @errors != 0 OR @rowcount<>1

    BEGIN

    -- get correct update error message

    SELECT @message = dbo.udf_GET_ERROR_MESSAGE(@SitePK, 102)

    -- go to error handler

    GOTO HANDLE_ERROR

    END

    -- if application folders are on for the site create a new group

    IF @ApplicationFolders = 1

    BEGIN

    -- ensure group is clean without a check as that just wastes time IF it does exist already, obviously if it doesn't then it won't do anthing but the fields are indexed

    DELETE

    FROMGROUPS

    WHEREJobFK = @JobPK

    AND GroupType = 'DJA'

    AND ClientFK = @ClientPK

    -- doesn't matter if nothing was deleted

    INSERT INTO GROUPS

    (GroupName, ClientFK, GroupType, JobFK)

    VALUES

    (@JobTitle, @ClientPk, 'DJA', @JobPK)

    -- ensure no errors and set message if there was a group error

    SELECT @errors = @@error, @rowcount = @@rowcount

    IF @errors != 0 OR @rowcount <> 1

    BEGIN

    -- get correct group inset error message

    SELECT @message = dbo.udf_GET_ERROR_MESSAGE(@SitePK, 220)

    -- go to error handler

    GOTO HANDLE_ERROR

    END

    END

    END

    -- other work related to multiple linked sites

    -- other code related to handle PENDING (non live) jobs

    -- if we get here NO ERRORS so COMMIT the transaction

    IF @@TRANCOUNT = 0

    COMMIT TRAN

    -- set return values including a success message and the return code for the application

    SELECT @success = 1, @message = dbo.udf_GET_MESSAGE(@SitePK, 103)

    -- skip over error handler and exit proc

    GOTO EXIT_PROC

    END

    -- error handler code - cannot be in here if NO ERROR which is why GOTO jumps OVER it

    HANDLE_ERROR:

    -- if we have an open transaction close it

    IF @@TRANCOUNT > 0

    ROLLBACK TRAN

    -- if we don't have an error message get a default on e.g "Job failed to save please try back later"

    IF ISNULL(@message,'') = ''

    SELECT @message = dbo.udf_GET_ERROR_MESSAGE(@SitePK, 100)

    -- we failed so succes is 0

    SELECT @success = 0

    -- exit the stored proc

    EXIT_PROC:

    -- before we exit the proc we log any changes to the table and any error messages if thee were any

    IF INSULL(@Details,'') != ''

    BEGIN

    EXEC [dbo].[usp_sql_add_job_change_record] @SitePK, @JobPK, @ChangeCode, @Action, @Details, @Success, @Message

    END

    -- return to caller

    SELECT cast(@Success as int) as Success, isnull(@JobPK,0) as JobPK, @message as [message], @CheckMode as checkmode

    -- use success as return code in case we are using an ADO stored proce that relies on return codes

    RETURN @Success

    Also if I had a stored proc that saved MULTIPLE jobs at the same time e.g @JobID was not ONE integer but a comma delimited list of integers to save in one go, how could I not use a cursor to loop through them all when for EACH JOB I have statements to check for duplicate submits, finding out what has changed in the job for reporting, plus logging that information with another stored proc call that is used across the site for logging info regarding changes to important records.

    Cursors have their time and place especially if you have to do procedural type procedure like the one above. I have seen people just try and replace cursors with temporary tables with identity fields and then WHILE loops selecting the next ID record before doing the middle content of a proc like the one above before incrementing the loop counter. This is just creating a cursor in another manner and if you have to do a procedural type proc then a FAST_FORWARD LOCAL CURSOR is designed for looping from start to end so why try and replace it.

    If anyone can show me how I can replace all the contents of the proc above without GOTO statements and why the format used is bad I would like to know.

    Also if you can imagine having multiple jobs to handle in the proc rather than one, so @JobIDs varchar(1000) (e.g 12,13,14), @JobTitle nvarchar(max) 'Test 1','Test 2','Test 3' (Same for Job Desc) e.g position N of each comma delimited list relates to position N of the other values I would be interested to see.

    Each job needs the details of the changes done to it logged, the related groups checked and inserted/updated, plus the changes all logged with the stored proc currently residing in the EXIT_PROC part of the proc. In a cursor loop this would be at the bottom of the loop iteration.

    And I don't use cursors if I can do it in a set based by the way.

    Just when the data logic is more complicated than a single SELECT/UPDATE/INSERT statement and requires calls to other functions / procedures.

    Thanks

    Rob

    Hey Rob, you could pretty easily rewrite this code using a TRY/CATCH block instead. the CATCH would automatically handle any errors that occur within the TRY block (as long as the severity is high enough). Therefore eliminating a lot of redundant code where you continuously check for @@ERROR.

    I may be wrong here also, but in your proc above, specific errors that occur at the statement level (such as divide-by-zero) might halt immediately and never enter into the HANDLE_ERROR block. A Try/Catch would not have this issue. (I may be wrong here because it sometimes depends on instance-level settings, such as the default values that connections are given for XACT_ABORT/etc...).

    I personally have not found a need for GOTO, where TRY/CATCH could not satisfy the same needs. Interested to hear if others feel differently?

  • Rob Reid-246754 (6/3/2015)


    Regarding the GOTO statement, are people saying it has no place in TSQL either?

    Yes that is my sentiment exactly. The GOTO has no relevance in t-sql since 2005 when the try/catch was introduced. Just like other programming languages when the try/catch is introduced the GOTO needs to stop being used.

    _______________________________________________________________

    Need help? Help us help you.

    Read the article at http://www.sqlservercentral.com/articles/Best+Practices/61537/ for best practices on asking questions.

    Need to split a string? Try Jeff Modens splitter http://www.sqlservercentral.com/articles/Tally+Table/72993/.

    Cross Tabs and Pivots, Part 1 – Converting Rows to Columns - http://www.sqlservercentral.com/articles/T-SQL/63681/
    Cross Tabs and Pivots, Part 2 - Dynamic Cross Tabs - http://www.sqlservercentral.com/articles/Crosstab/65048/
    Understanding and Using APPLY (Part 1) - http://www.sqlservercentral.com/articles/APPLY/69953/
    Understanding and Using APPLY (Part 2) - http://www.sqlservercentral.com/articles/APPLY/69954/

  • IMO, cursors are there to be used for admin scripts to be executed manually. They can be a time saver.

    The philosophy I attempt to maintain in my SQL Server environment is, if a T-SQL script (or a sproc) is used either by an application or is being automated (scheduler, sql job) and for SOME reason (good or bad) it must use a cursor, then I make my case to pull that code out of the SQL Server environment and have another language (C#, Java, Powershell, etc) do that work. The way I see it, loopie code should not be written with SQL in a relational database. Database is designed to store and retrieve data fast. So use SQL for that. Want to do fast loopie stuff, do it outside of SQL Server. Much faster and less load on the db. Yes, more work. But, if the loopie code is becoming a repeatable process, then it should be outside of the database. Just pull the data out that you need, do the work and stuff back the changes. If that is not good enough, then a service broker/messaging solution should be considered. I suppose, all I'm trying to say is use the proper technology for the task. SQL was not designed to do procedural programming, so don't use it for that. Yes, IF and WHILE statements and cursors are there, but they are extensions to SQL provided for administrative purposes. This is also a good reason to keep business logic outside of the database. When IF and WHILE statements are in a sproc, business logic begins to creep in. Those are the things I keep a close eye on to make sure that doesn't happen.

    Note: I'm using loopie code to mean RBAR.

  • Ok, I totally get that as I do use TRY CATCH and all the ERROR_MESSAGE() ERROR_LINE() ERROR_NUMBER() ERROR_PROCEDURE() functions to get detailed errors in TRY CATCH statements and log the errors into a table before a ROLLBACK but then that doesn't still stop the need for a "get out" statement as the errors could be NON SQL Errors e.g DATA/BUSINESS LOGIC such as the company who is placing the job has a setting to only allow one job per job reference or unique job titles etc.

    These are not TSQL errors but would need TSQL to check the logic and then still I can see a need if there is an error like that to jump to a HANDLE_ERROR statement and an EXIT_PROC statement to get out the proc.

    A TRY/CATCH statement would be no use in this case as the errors are not SQL ones.

    So what would you do there?

    Just RETURN from the proc at the point where you find the logic error even if it means duplicate code?

    OR have a single HANDLE_ERROR section where you handle the return settings and return error messag

    -- some logic that raises a NON SQL error

    -- check for dupe JobRef

    IF EXISTS(SELECT JobREF FROM JOBS WHERE SiteFK = @SitePK AND ClientFK = @ClientPK)

    BEGIN

    -- dupe job ref

    SELECT @ErrorMessage = 'There is already a job with this job ref in the system'

    GOTO HANDLE_ERROR

    END

    -- do error handling in TRY CATCH

    BEGIN TRY

    BEGIN TRAN

    -- code to save job

    -- other code

    END TRY

    BEGIN CATCH

    -- log error before rollback

    EXEC usp_sql_log_error ERROR_MESSAGE(), ERROR_PROCEDURE(), ERROR_NUMBER(), ERROR_LINE(), @SitePK

    IF @@TRANCOUNT > 0

    ROLLBACK TRAN

    END CATCH

    IF @@TRANCOUNT > 0

    COMMIT TRAN

    -- everything good so skip over error handle section

    GOTO EXIT PROC:

    HANDLE_ERROR:

    SELECT @Success = 0, @ErrorMessage = ISNULL(@DefaultErrorMessage)

    EXIT_PROC:

    SELECT @Success = 1, @Message = 'Job Saved'

    What would you do in this scenario?

  • Rob Reid-246754 (6/3/2015)


    Ok, I totally get that as I do use TRY CATCH and all the ERROR_MESSAGE() ERROR_LINE() ERROR_NUMBER() ERROR_PROCEDURE() functions to get detailed errors in TRY CATCH statements and log the errors into a table before a ROLLBACK but then that doesn't still stop the need for a "get out" statement as the errors could be NON SQL Errors e.g DATA/BUSINESS LOGIC such as the company who is placing the job has a setting to only allow one job per job reference or unique job titles etc.

    These are not TSQL errors but would need TSQL to check the logic and then still I can see a need if there is an error like that to jump to a HANDLE_ERROR statement and an EXIT_PROC statement to get out the proc.

    A TRY/CATCH statement would be no use in this case as the errors are not SQL ones.

    So what would you do there?

    Just RETURN from the proc at the point where you find the logic error even if it means duplicate code?

    OR have a single HANDLE_ERROR section where you handle the return settings and return error messag

    -- some logic that raises a NON SQL error

    -- check for dupe JobRef

    IF EXISTS(SELECT JobREF FROM JOBS WHERE SiteFK = @SitePK AND ClientFK = @ClientPK)

    BEGIN

    -- dupe job ref

    SELECT @ErrorMessage = 'There is already a job with this job ref in the system'

    GOTO HANDLE_ERROR

    END

    -- do error handling in TRY CATCH

    BEGIN TRY

    BEGIN TRAN

    -- code to save job

    -- other code

    END TRY

    BEGIN CATCH

    -- log error before rollback

    EXEC usp_sql_log_error ERROR_MESSAGE(), ERROR_PROCEDURE(), ERROR_NUMBER(), ERROR_LINE(), @SitePK

    IF @@TRANCOUNT > 0

    ROLLBACK TRAN

    END CATCH

    IF @@TRANCOUNT > 0

    COMMIT TRAN

    -- everything good so skip over error handle section

    GOTO EXIT PROC:

    HANDLE_ERROR:

    SELECT @Success = 0, @ErrorMessage = ISNULL(@DefaultErrorMessage)

    EXIT_PROC:

    SELECT @Success = 1, @Message = 'Job Saved'

    What would you do in this scenario?

    Something like this....

    BEGIN TRY

    DECLARE @Sev INT = 16; -- change this value to 7 to raise exception but not drop into the CATCH block.

    DECLARE @Tst BIT = 1; -- change this value to 0 for unexpected exception

    IF @Tst = 'TRUE'

    RAISERROR('Raise an error', @Sev, 1); -- NOTE: Severity 11-19 will cause execution to jump to CATCH block

    ELSE

    SELECT 1/0; -- force unexpected exception

    SELECT 'This statement will only execute when severity is not 11-19. CATCH block bypassed.';

    END TRY

    BEGIN CATCH

    IF ERROR_NUMBER() >= 50000 -- Check for user defined messages (sp_addmessage).

    PRINT 'Expected exception';

    ELSE

    PRINT 'Unexpected exception';

    -- THROW; -- if needed

    END CATCH

  • To add to what qbrt is (I think rightly) pointing out, you can check ERROR_NUMBER() in your CATCH block and if it is 50000, you'll know that you raised it. If you want to differentiate how you handle "expected" errors that you raised and "unexpected" T-SQL errors.

  • Brian J. Parker (6/3/2015)


    To add to what qbrt is (I think rightly) pointing out, you can check ERROR_NUMBER() in your CATCH block and if it is 50000, you'll know that you raised it. If you want to differentiate how you handle "expected" errors that you raised and "unexpected" T-SQL errors.

    Correct. Actually, any number above 50,000 if defining user messages with sp_addmessage sproc.

    Thanks for the clarification, Brian. I also updated the code snippet to reflect the expected/unexpected message check.

Viewing 15 posts - 91 through 105 (of 215 total)

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