﻿<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/">
  <channel>
    <title>Steven Feuerstein's Blog</title>
    <description>&lt;table cellspacing="1" cellpadding="1"&gt;
        &lt;tr&gt;
            &lt;td valign="top"&gt;&lt;img height="183" alt="" width="139" src="/Portals/0/Blog/blog-steven-feuerstein.png" /&gt;&lt;/td&gt;
            &lt;td valign="top"&gt;Steven Feuerstein is considered one of the world's leading experts on the Oracle PL/SQL language, having written ten books on PL/SQL (all published by O'Reilly Media, including Oracle PL/SQL Programming. Steven has been developing software since 1980, spent five years with Oracle (1987-1992) and has served as PL/SQL Evangelist for Quest Software since January 2001. He is also an Oracle ACE Director. He writes regularly for Oracle Magazine, which named him the PL/SQL Developer of the Year in both 2002 and 2006.
            &lt;p&gt;&amp;#160;Steven's blog provides advice and code that you can put to immediate use in your world of programming.&amp;#160;&lt;/p&gt;
            &lt;p&gt;&lt;font color="#003366" size="3"&gt;&lt;strong&gt;Recent postings on his  PL/SQL Obsession:&lt;/strong&gt;&lt;/font&gt;&lt;/p&gt;
            &lt;/td&gt;
        &lt;/tr&gt;
&lt;/table&gt;</description>
    <link>http://www.toadworld.com/BLOGS/tabid/67/BlogId/13/Default.aspx</link>
    <language>en-US</language>
    <managingEditor>Steven Feuerstein</managingEditor>
    <webMaster>webmaster@toadworld.com</webMaster>
    <pubDate>Tue, 16 Mar 2010 19:24:00 GMT</pubDate>
    <lastBuildDate>Tue, 16 Mar 2010 19:24:00 GMT</lastBuildDate>
    <docs>http://backend.userland.com/rss</docs>
    <generator>Blog RSS Generator Version 3.2.0.15477</generator>
    <item>
      <title>Always Bulk Collect</title>
      <description>&lt;div&gt;You learn something new every day, right? Well, I certainly do (more or less). Even about PL/SQL, about which I am sure many people think I already know &lt;em&gt;everything&lt;/em&gt;. Far from it.&lt;br /&gt;
 &lt;/div&gt;
&lt;div&gt;In fact, I learned just last week from the PL/SQL Product Manager, Bryn Llewellyn, that his recommendation regarding cursor FOR loops and bulk collect is different from mine – and for a very good reason.&lt;br /&gt;
 &lt;/div&gt;
&lt;div&gt;Several years ago, he informed me (and provided a script to verify the statement: 10g_optimize_cfl.sql in the &lt;a href="http://www.toadworld.com/LinkClick.aspx?fileticket=WdgBQ7kABao%3d&amp;tabid=67"&gt;demo.zip&lt;/a&gt;) that in Oracle10g, the compiler now optimizes every cursor FOR loop to execute at a level of performance similar to BULK COLLECT. &lt;br /&gt;
 &lt;/div&gt;
&lt;div&gt;This is a wonderful and non-trivial optimization. Rather than have to go through a manual rewrite of all cursor FOR loops, you can leave them in place and reap the benefits of the BULK COLLECT (sort of).&lt;br /&gt;
 &lt;/div&gt;
&lt;div&gt;Well, that's not quite true – and it's even less true than I thought.&lt;br /&gt;
 &lt;/div&gt;
&lt;div&gt;First of all, if your cursor FOR loop contains any DML statements, you will still want to explicitly convert to BULK COLLECT. The reason is that while the optimizer will improve the performance of the query step, it will &lt;em&gt;not&lt;/em&gt; optimize the DML statements to execute like FORALLs (the bulk processing analogue for updates, inserts and deletes).&lt;br /&gt;
 &lt;/div&gt;
&lt;div&gt;So if you have code that looks like (cfl_to_bulk_0.sql in &lt;em&gt;&lt;u&gt;&lt;a href="http://www.toadworld.comhttp://www.toadworld.com/LinkClick.aspx?fileticket=WdgBQ7kABao%3d&amp;tabid=67"&gt;demo.zip&lt;/a&gt;&lt;/u&gt;&lt;/em&gt;):&lt;br /&gt;
 &lt;/div&gt;
&lt;div&gt;&lt;font face="courier new" size="2"&gt;PROCEDURE upd_for_dept (&lt;br /&gt;
   dept_in     IN   employees.department_id%TYPE&lt;br /&gt;
 , newsal_in   IN   employees.salary%TYPE&lt;br /&gt;
)&lt;br /&gt;
IS&lt;br /&gt;
   CURSOR emp_cur&lt;br /&gt;
   IS&lt;br /&gt;
      SELECT employee_id, salary, hire_date&lt;br /&gt;
        FROM employees&lt;br /&gt;
       WHERE department_id = dept_in;&lt;br /&gt;
BEGIN&lt;br /&gt;
   FOR rec IN emp_cur&lt;br /&gt;
   LOOP&lt;br /&gt;
      BEGIN&lt;br /&gt;
         INSERT INTO employee_history&lt;br /&gt;
                     (employee_id, salary, hire_date&lt;br /&gt;
                     )&lt;br /&gt;
              VALUES (rec.employee_id, rec.salary, rec.hire_date&lt;br /&gt;
                     );&lt;br /&gt;
 &lt;br /&gt;
         adjust_compensation (rec.employee_id, rec.salary);&lt;br /&gt;
 &lt;br /&gt;
         UPDATE employees&lt;br /&gt;
            SET salary = newsal_in&lt;br /&gt;
          WHERE employee_id = rec.employee_id;&lt;br /&gt;
      EXCEPTION&lt;br /&gt;
         WHEN OTHERS&lt;br /&gt;
         THEN&lt;br /&gt;
            log_error;&lt;br /&gt;
      END;&lt;br /&gt;
   END LOOP;&lt;br /&gt;
END upd_for_dept;&lt;br /&gt;
&lt;/font&gt;&lt;font size="2"&gt; &lt;/font&gt;&lt;/div&gt;
&lt;div&gt;You will need to change the cursor FOR loop to BULK COLLECT so that you can fill up collections with data from the query, which can then be used in the FORALL statement. You will, sadly (due to increased complexity and program length) end up with something like this (cfl_to_bulk_5.sql in &lt;em&gt;&lt;u&gt;&lt;a href="http://www.toadworld.comhttp://www.toadworld.com/LinkClick.aspx?fileticket=WdgBQ7kABao%3d&amp;tabid=67"&gt;demo.zip&lt;/a&gt;&lt;/u&gt;&lt;/em&gt;):&lt;br /&gt;
 &lt;/div&gt;
&lt;div&gt;&lt;font size="2"&gt;&lt;font face="courier new" size="2"&gt;PROCEDURE upd_for_dept (&lt;br /&gt;
   dept_in     IN   employees.department_id%TYPE&lt;br /&gt;
 , newsal_in   IN   employees.salary%TYPE&lt;br /&gt;
)&lt;br /&gt;
IS&lt;br /&gt;
   bulk_errors    EXCEPTION;&lt;br /&gt;
   PRAGMA EXCEPTION_INIT (bulk_errors, -24381);&lt;br /&gt;
 &lt;br /&gt;
   TYPE employee_tt IS TABLE OF employees.employee_id%TYPE&lt;br /&gt;
      INDEX BY BINARY_INTEGER;&lt;br /&gt;
 &lt;br /&gt;
   employee_ids   employee_tt;&lt;br /&gt;
 &lt;br /&gt;
   TYPE salary_tt IS TABLE OF employees.salary%TYPE&lt;br /&gt;
     INDEX BY BINARY_INTEGER;&lt;br /&gt;
 &lt;br /&gt;
   salaries       salary_tt;&lt;br /&gt;
 &lt;br /&gt;
   TYPE hire_date_tt IS TABLE OF employees.hire_date%TYPE&lt;br /&gt;
      INDEX BY BINARY_INTEGER;&lt;br /&gt;
 &lt;br /&gt;
   hire_dates     hire_date_tt;&lt;br /&gt;
 &lt;br /&gt;
   CURSOR employees_cur&lt;br /&gt;
   IS&lt;br /&gt;
      SELECT     employee_id, salary, hire_date&lt;br /&gt;
            FROM employees&lt;br /&gt;
           WHERE department_id = dept_in&lt;br /&gt;
      FOR UPDATE;&lt;br /&gt;
 &lt;br /&gt;
   PROCEDURE fetch_data_quickly (&lt;br /&gt;
      limit_in           IN       PLS_INTEGER&lt;br /&gt;
    , employee_ids_out   OUT      employee_tt&lt;br /&gt;
    , salaries_out       OUT      salary_tt&lt;br /&gt;
    , hire_dates_out     OUT      hire_date_tt&lt;br /&gt;
   )&lt;br /&gt;
   IS&lt;br /&gt;
   BEGIN&lt;br /&gt;
      FETCH employees_cur&lt;br /&gt;
      BULK COLLECT INTO employee_ids_out, salaries_out, hire_dates_out LIMIT limit_in;&lt;br /&gt;
   END fetch_data_quickly;&lt;br /&gt;
 &lt;br /&gt;
   PROCEDURE adj_comp_for_arrays (&lt;br /&gt;
      employee_ids_io   IN OUT   employee_tt&lt;br /&gt;
    , salaries_io       IN OUT   salary_tt&lt;br /&gt;
   )&lt;br /&gt;
   IS&lt;br /&gt;
   BEGIN&lt;br /&gt;
      FOR indx IN 1 .. employee_ids_io.COUNT&lt;br /&gt;
      LOOP&lt;br /&gt;
         adjust_compensation (employee_ids_io (indx), salaries_io (indx));&lt;br /&gt;
      END LOOP;&lt;br /&gt;
   END adj_comp_for_arrays;&lt;br /&gt;
 &lt;br /&gt;
   PROCEDURE insert_history&lt;br /&gt;
   IS&lt;br /&gt;
   BEGIN&lt;br /&gt;
      FORALL indx IN employee_ids.FIRST .. employee_ids.LAST SAVE EXCEPTIONS&lt;br /&gt;
         INSERT INTO employee_history&lt;br /&gt;
                     (employee_id, salary, hire_date&lt;br /&gt;
                     )&lt;br /&gt;
              VALUES (employee_ids (indx), salaries (indx), hire_dates (indx)&lt;br /&gt;
                     );&lt;br /&gt;
   EXCEPTION&lt;br /&gt;
      WHEN bulk_errors&lt;br /&gt;
      THEN&lt;br /&gt;
         FOR indx IN 1 .. SQL%BULK_EXCEPTIONS.COUNT&lt;br /&gt;
         LOOP&lt;br /&gt;
            -- Log the error&lt;br /&gt;
            log_error&lt;br /&gt;
                     (   'Unable to insert history row for employee '&lt;br /&gt;
                      || employee_ids&lt;br /&gt;
                                     (SQL%BULK_EXCEPTIONS (indx).ERROR_INDEX)&lt;br /&gt;
                    , SQL%BULK_EXCEPTIONS (indx).ERROR_CODE&lt;br /&gt;
                     );&lt;br /&gt;
            /*&lt;br /&gt;
            Communicate this failure to the update phase:&lt;br /&gt;
            Delete this row so that the update will not take place.&lt;br /&gt;
            */&lt;br /&gt;
            employee_ids.DELETE (SQL%BULK_EXCEPTIONS (indx).ERROR_INDEX);&lt;br /&gt;
         END LOOP;&lt;br /&gt;
   END insert_history;&lt;br /&gt;
 &lt;br /&gt;
   PROCEDURE update_employee&lt;br /&gt;
   IS&lt;br /&gt;
   BEGIN&lt;br /&gt;
      /*&lt;br /&gt;
        Use Oracle10g INDICES OF to avoid errors &lt;br /&gt;
        from a sparsely-populated employee_ids collection.&lt;br /&gt;
      */&lt;br /&gt;
      FORALL indx IN INDICES OF employee_ids SAVE EXCEPTIONS&lt;br /&gt;
         UPDATE employees&lt;br /&gt;
            SET salary = newsal_in&lt;br /&gt;
              , hire_date = hire_dates (indx)&lt;br /&gt;
          WHERE employee_id = employee_ids (indx);&lt;br /&gt;
   EXCEPTION&lt;br /&gt;
      WHEN bulk_errors&lt;br /&gt;
      THEN&lt;br /&gt;
         FOR indx IN 1 .. SQL%BULK_EXCEPTIONS.COUNT&lt;br /&gt;
         LOOP&lt;br /&gt;
            log_error&lt;br /&gt;
                     (   'Unable to update salary for employee '&lt;br /&gt;
                      || employee_ids&lt;br /&gt;
                                     (SQL%BULK_EXCEPTIONS (indx).ERROR_INDEX)&lt;br /&gt;
                    , SQL%BULK_EXCEPTIONS (indx).ERROR_CODE&lt;br /&gt;
                     );&lt;br /&gt;
         END LOOP;&lt;br /&gt;
   END update_employee;&lt;br /&gt;
BEGIN&lt;br /&gt;
   OPEN employees_cur;&lt;br /&gt;
 &lt;br /&gt;
   LOOP&lt;br /&gt;
      fetch_data_quickly (100, employee_ids, salaries, hire_dates);&lt;br /&gt;
      EXIT WHEN employee_ids.COUNT = 0;&lt;br /&gt;
      insert_history;&lt;br /&gt;
      adj_comp_for_arrays (employee_ids, salaries);&lt;br /&gt;
      update_employee;&lt;br /&gt;
   END LOOP;&lt;br /&gt;
END upd_for_dept;&lt;br /&gt;
&lt;/font&gt;&lt;/font&gt;&lt;font size="2"&gt; &lt;/font&gt;&lt;/div&gt;
&lt;div&gt;Sorry, I know that I should probably explain this code but to be brutally honest my dad had heart bypass surgery and I am currently at the hospital and so you get the abbreviated version ....&lt;br /&gt;
 &lt;/div&gt;
&lt;div&gt;Anyway, that is one scenario where you definitely need to convert from the cursor FOR loop. &lt;br /&gt;
 &lt;/div&gt;
&lt;div&gt;But Bryn, to my surprise, recommends that you &lt;em&gt;always&lt;/em&gt; convert explicitly to BULK COLLECT. Why is that? For performance reasons. Apparently, the cursor FOR loop optimization makes the code run faster, but not as quickly as BULK COLLECT.&lt;br /&gt;
 &lt;/div&gt;
&lt;div&gt;In my tests (which you can reproduce in cfl_vs_bulkcollect.sql), I found that BULK COLLECT ran 33% faster than the cursor FOR loop:&lt;/div&gt;
&lt;font size="2"&gt;
&lt;ul&gt;
    &lt;li&gt;Cursor For Loop Elapsed: 8.12 seconds. &lt;/li&gt;
    &lt;li&gt;Bulk Collect Elapsed: 5.46 seconds.  &lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;So if you really want the best performance, take the time and make the effort to switch over to BULK COLLECT.&lt;br /&gt;
 &lt;/div&gt;
&lt;/font&gt;</description>
      <link>http://www.toadworld.com/BLOGS/tabid/67/EntryID/239/Default.aspx</link>
      <author>Steven Feuerstein</author>
      <comments>http://www.toadworld.com/BLOGS/tabid/67/EntryID/239/Default.aspx#Comments</comments>
      <guid isPermaLink="true">http://www.toadworld.com/Default.aspx?tabid=67&amp;EntryID=239</guid>
      <pubDate>Mon, 23 Jun 2008 14:34:00 GMT</pubDate>
      <slash:comments>0</slash:comments>
      <trackback:ping>http://www.toadworld.com/DesktopModules/Blog/Trackback.aspx?id=239</trackback:ping>
    </item>
  </channel>
</rss>