Skip to main content

FORALLs and COMMITs

Received this note today from Debbie B:

I attended your Turbo Charge PL/SQL seminar today. I still have a question about where to put COMMIT. I’m using 10g. See (pseudo) code below.

Say I have 50,000 records and the LIMIT is 100. If an exception is thrown:
  • Do I need a COMMIT in the WHEN clauses so successful DML gets committed, then loop processing will continue?
  • I was thinking each DML needed it’s own BEGIN EXCEPTION END block so I would know if the error happened due to the insert or the update and could log the appropriate error. Is this wrong?

  OPEN v_cursor;
  LOOP
    FETCH v_cursor BULK COLLECT INTO data_array LIMIT i_limit;
    EXIT WHEN data_array.COUNT = 0;

    BEGIN
      FORALL i IN 1.. data_array.COUNT SAVE EXCEPTIONS
        INSERT INTO some_table VALUES data_array (i);
        COMMIT;
    EXCEPTION
        WHEN dml_errors THEN
             log_error;
             COMMIT;
             /* Don't RAISE, so execution will continue */
        WHEN OTHERS
             log_error;
             COMMIT;
             /* Don't RAISE, so execution will continue */
    END;

    BEGIN
      FORALL i IN 1.. data_array.COUNT SAVE EXCEPTIONS
        UPDATE some_table SET…
        COMMIT;
    EXCEPTION
        WHEN dml_errors THEN
             log_error;
             COMMIT;
             /* Don't RAISE, so execution will continue */
        WHEN OTHERS
             log_error;
             COMMIT;
             /* Don't RAISE, so execution will continue */
    END
  END LOOP;
  COMMIT;
  CLOSE v_cursor;

So many commits, so little time. 

Before addressing the specifics of this code, let's consider a more general question:

When and where should I put commits in my code?

Answer: How the heck should I know? Or, perhaps a little more politely: What is your transaction?

When you commit, you are really saying: I have finished making all changes needed for this transaction. Now it's time to save them, all together, all at once.

When we write back-end code that acts as an API to front end (user-driven) code, we usually don't have any commits. That's because we leave it up to the user to decide when they are done and ready to save their changes.

When we write code implementing backend processes not controlled by the user, then we do usually need to explicitly commit (and rollback). But again the primary question is very application-specific: what constitutes a transaction? 

Of course, the real world is messy - even when we clean it up, shear off rough edges, and make that real world fit into our cyber world.

For example, sometimes we have to do "incremental commits" - save changes to N rows at a time, to avoid rollback segment errors.

That may be the reason that Debbie has put COMMIT statements after each FORALL and after each logging of an error. 

But do you need a COMMIT in that exception section to ensure that successfully executed statements are not rolled back? NO! The failure of a SQL statement in your block will not force the rollback of previously completed statements.

As for putting each FORALL inside its own block, sure, that makes a lot of sense - if you want to make sure that both FORALLs always execute for each iteration of the loop. I would, however, put each inside its own nested subprogram (see my rewritten version of your code below).

I probably would not, however, use the same exceptional handling strategy for ORA-24381 (which your code implies was associated with the dml_errors exception) and for WHEN OTHERS. If something else went wrong in FORALL execution that was not caught by SAVE EXCEPTIONS, you should consider it "catastrophic" and stop the processing.

And, by the way, so far as I can tell, that final COMMIT before the CLOSE statement? Totally unnecessary - if you keep all those other COMMITs. You've already covered every path out of the loop!

OK, here's my offering of an alternative implementation. It's still kinda pseudo-codish because I lack the full details of your requirements, but:
  • Just one COMMIT at the end
  • Nested subprograms for each FORALL
  • No handling of unanticipated errors.

IS
   failure_in_forall   EXCEPTION;
   PRAGMA EXCEPTION_INIT (failure_in_forall, -24381);

   PROCEDURE bulk_inserts (data_array_in your_type)
   IS
   BEGIN
      FORALL i IN 1 .. data_array_in.COUNT SAVE EXCEPTIONS
         INSERT INTO some_table
              VALUES data_array_in (i);
   EXCEPTION
      WHEN failure_in_forall
      THEN
         log_error;
   END;

   PROCEDURE bulk_updates (data_array_in your_type)
   IS
   BEGIN
      FORALL i IN 1 .. data_array.COUNT SAVE EXCEPTIONS
         UPDATE some_table
            SET x = data_array (i);
   EXCEPTION
      WHEN failure_in_forall
      THEN
         log_error;
   END;
BEGIN
   OPEN v_cursor;

   LOOP
      FETCH v_cursor BULK COLLECT INTO data_array LIMIT i_limit;

      EXIT WHEN data_array.COUNT = 0;

      bulk_inserts (data_array);
      bulk_updates (data_array);
   END LOOP;

   CLOSE v_cursor;

   COMMIT;
END;



Comments

  1. Hello Steven, a little error inside of the procedure 'bulk_updates' - I think it should be "SET x = data_array (i);" and not "SET x = data_array (1);".
    Kind regards, Niels

    ReplyDelete
  2. Thanks, Niels. Surely one of the biggest mistakes every made by font designers was to create a font in which the number ONE and the letter EL and the letter EYE could look so much alike.

    ReplyDelete

Post a Comment

Popular posts from this blog

Quick Guide to User-Defined Types in Oracle PL/SQL

A Twitter follower recently asked for more information on user-defined types in the PL/SQL language, and I figured the best way to answer is to offer up this blog post. PL/SQL is a strongly-typed language . Before you can work with a variable or constant, it must be declared with a type (yes, PL/SQL also supports lots of implicit conversions from one type to another, but still, everything must be declared with a type). PL/SQL offers a wide array of pre-defined data types , both in the language natively (such as VARCHAR2, PLS_INTEGER, BOOLEAN, etc.) and in a variety of supplied packages (e.g., the NUMBER_TABLE collection type in the DBMS_SQL package). Data types in PL/SQL can be scalars, such as strings and numbers, or composite (consisting of one or more scalars), such as record types, collection types and object types. You can't really declare your own "user-defined" scalars, though you can define subtypes  from those scalars, which can be very helpful from the p...

Three tips for getting started right with Oracle Database development

By "Oracle Database development", I mean, more or less, writing SQL and PL/SQL. I assume in this post that you have access to Oracle Database (which you can get via Cloud services, Docker , GitHub and OTN ). A. Use a powerful IDE, designed with database programming in mind. There are lots of editors out there, and many IDEs that work with Oracle Database. Sure, you could use Notepad, but OMG the productivity loss. You could also use a popular editor like Sublime, and then it get it working with Oracle. I suggest, however, that you  download  and install Oracle's own own, free, powerful IDE: SQL Developer . If you like to complement your graphical IDE with a command line tool (or OMG if you actually prefer  a command line tool to a graphical interface), you should also check out the relatively new and generating-lots-of-excitement SQLcl . B. Enable compile-time warnings and PL/Scope. The database has tons of useful functionality burned right into it, ready for...

Why DBMS_OUTPUT.PUT_LINE should not be in your application code

A database developer recently came across my  Bulletproof PL/SQL  presentation, which includes this slide. That first item in the list caught his attention: Never put calls to DBMS_OUTPUT.PUT_LINE in your application code. So he sent me an email asking why I would say that. Well, I suppose that is the problem with publishing slide decks. All the explanatory verbiage is missing. I suppose maybe I should do a video. :-) But in the meantime, allow me to explain. First, what does DBMS_OUTPUT.PUT_LINE do? It writes text out to a buffer, and when your current PL/SQL block terminates, the buffer is displayed on your screen. [Note: there can be more to it than that. For example, you could in your own code call DBMS_OUTPUT.GET_LINE(S) to get the contents of the buffer and do something with it, but I will keep things simple right now.] Second, if I am telling you not to use this built-in, how could text from your program be displayed on your screen? Not without a lot o...