{
  "date": "2015-03-03T18:52:30",
  "slug": "generic-query-for-multicriteria-search-part-ii-bindaware-adaptive-cursor-sharing",
  "link": "https://www.dbi-services.com/blog/generic-query-for-multicriteria-search-part-ii-bindaware-adaptive-cursor-sharing/",
  "title": {
    "rendered": "Generic query for multicriteria search &#8211; part II: BIND_AWARE (Adaptive Cursor Sharing)"
  },
  "content": {
    "rendered": "<h2>By Franck Pachot</h2>\n<p>.<br />\nIn the <a href=\"/generic-query-for-multicriteria-search-part-i-useconcat-or-expansion\">previous post</a> I explained the performance issue encountered when using a generic query to deal with optional search criteria on multiple columns. The statement was shared by all executions, was marked as bind sensitive, but never became bind aware. Let&#8217;s use the BIND_AWARE hint.</p>\n<h3>All binds null</h3>\n<p>I assign null for all of them &#8211; meaning that I don&#8217;t want to filter anything:</p>\n<pre><code>SQL&gt; exec :job_id:=null; :department_id:=null; :manager_id:=null; :employee_id:=null;\nPL/SQL procedure successfully completed.\n</code></pre>\n<p>and I run my generic query &#8211; but with the BIND_AWARE hint:</p>\n<pre><code>SQL&gt;\n     SELECT /*+ BIND_AWARE */\n     COUNT(*)\n     FROM\n       (SELECT 1\n       FROM employees\n       WHERE (job_id = NVL(:job_id, job_id))\n       AND (department_id = NVL(:department_id, department_id))\n       AND (manager_id = NVL(:manager_id, manager_id))\n       AND (employee_id = NVL(:employee_id, employee_id))\n       )\nSQL&gt; /\n\n  COUNT(*)\n----------\n       105\n</code></pre>\n<p>and here is the plan:</p>\n<pre><code>SQL&gt; select * from table(dbms_xplan.display_cursor(format=&gt;'allstats last +outline'));\n\nPLAN_TABLE_OUTPUT\n------------------------------------------------------------------------------------\nSQL_ID  fhpytfwk0y4r3, child number 0\n-------------------------------------\nSELECT /*+ BIND_AWARE */ COUNT(*) FROM   (SELECT 1   FROM employees\nWHERE (job_id = NVL(:job_id, job_id))   AND (department_id =\nNVL(:department_id, department_id))   AND (manager_id =\nNVL(:manager_id, manager_id))   AND (employee_id = NVL(:employee_id,\nemployee_id))   )\n\nPlan hash value: 3424141370\n\n------------------------------------------------------------------------------------\n| Id  | Operation                       | Name          | Starts | E-Rows | A-Rows |\n------------------------------------------------------------------------------------\n|   0 | SELECT STATEMENT                |               |      1 |        |      1 |\n|   1 |  SORT AGGREGATE                 |               |      1 |      1 |      1 |\n|   2 |   CONCATENATION                 |               |      1 |        |    105 |\n|*  3 |    FILTER                       |               |      1 |        |    105 |\n|*  4 |     TABLE ACCESS BY INDEX ROWID | EMPLOYEES     |      1 |    105 |    105 |\n|*  5 |      INDEX FULL SCAN            | EMP_EMP_ID_PK |      1 |    107 |    107 |\n|*  6 |    FILTER                       |               |      1 |        |      0 |\n|*  7 |     TABLE ACCESS BY INDEX ROWID | EMPLOYEES     |      0 |      1 |      0 |\n|*  8 |      INDEX UNIQUE SCAN          | EMP_EMP_ID_PK |      0 |      1 |      0 |\n------------------------------------------------------------------------------------\n\nOutline Data\n-------------\n\n  /*+\n      BEGIN_OUTLINE_DATA\n      IGNORE_OPTIM_EMBEDDED_HINTS\n      OPTIMIZER_FEATURES_ENABLE('12.1.0.2')\n      DB_VERSION('12.1.0.2')\n      ALL_ROWS\n      OUTLINE_LEAF(@\"SEL$F5BB74E1\")\n      MERGE(@\"SEL$2\")\n      OUTLINE_LEAF(@\"SEL$F5BB74E1_1\")\n      USE_CONCAT(@\"SEL$F5BB74E1\" 8 OR_PREDICATES(4))\n      OUTLINE_LEAF(@\"SEL$F5BB74E1_2\")\n      OUTLINE(@\"SEL$1\")\n      OUTLINE(@\"SEL$2\")\n      OUTLINE(@\"SEL$F5BB74E1\")\n      MERGE(@\"SEL$2\")\n      INDEX(@\"SEL$F5BB74E1_1\" \"EMPLOYEES\"@\"SEL$2\" (\"EMPLOYEES\".\"EMPLOYEE_ID\"))\n      BATCH_TABLE_ACCESS_BY_ROWID(@\"SEL$F5BB74E1_1\" \"EMPLOYEES\"@\"SEL$2\")\n      INDEX_RS_ASC(@\"SEL$F5BB74E1_2\" \"EMPLOYEES\"@\"SEL$F5BB74E1_2\" (\"EMPLOYEES\".\"EMPLOYEE_ID\"))\n      END_OUTLINE_DATA\n  */\n\nPredicate Information (identified by operation id):\n---------------------------------------------------\n\n   3 - filter(:EMPLOYEE_ID IS NULL)\n   4 - filter((\"DEPARTMENT_ID\"=NVL(:DEPARTMENT_ID,\"DEPARTMENT_ID\") AND\n              \"MANAGER_ID\"=NVL(:MANAGER_ID,\"MANAGER_ID\") AND \"JOB_ID\"=NVL(:JOB_ID,\"JOB_ID\")))\n   5 - filter(\"EMPLOYEE_ID\" IS NOT NULL)\n   6 - filter(:EMPLOYEE_ID IS NOT NULL)\n   7 - filter((\"DEPARTMENT_ID\"=NVL(:DEPARTMENT_ID,\"DEPARTMENT_ID\") AND\n              \"MANAGER_ID\"=NVL(:MANAGER_ID,\"MANAGER_ID\") AND \"JOB_ID\"=NVL(:JOB_ID,\"JOB_ID\")))\n   8 - access(\"EMPLOYEE_ID\"=:EMPLOYEE_ID)\n\n</code></pre>\n<p>It&#8217;s the same plan as before. FULL SCAN in the index on EMPLOYEE_ID because the CBO estimates it&#8217;s the fastest way to get non null EMPLOYEE_ID.</p>\n<h3>query for one EMPLOYEE_ID</h3>\n<p>And now running the same query, but for a specific EMPLOYEE_ID</p>\n<pre><code>SQL&gt; exec :job_id:=null; :department_id:=null; :manager_id:=null; :employee_id:=0;\nPL/SQL procedure successfully completed.\n</code></pre>\n<pre><code>SQL&gt; /\n\n  COUNT(*)\n----------\n         0</code></pre>\n<p>A new cursor has been created for it:</p>\n<pre><code>SQL&gt; select * from table(dbms_xplan.display_cursor(format=&gt;'allstats last +outline'));\n\nPLAN_TABLE_OUTPUT\n------------------------------------------------------------------------------------\nSQL_ID  fhpytfwk0y4r3, child number 1\n-------------------------------------\nSELECT /*+ BIND_AWARE */ COUNT(*) FROM   (SELECT 1   FROM employees\nWHERE (job_id = NVL(:job_id, job_id))   AND (department_id =\nNVL(:department_id, department_id))   AND (manager_id =\nNVL(:manager_id, manager_id))   AND (employee_id = NVL(:employee_id,\nemployee_id))   )\n\nPlan hash value: 1540312732\n\n-------------------------------------------------------------------------------\n| Id  | Operation                 | Name           | Starts | E-Rows | A-Rows |\n-------------------------------------------------------------------------------\n|   0 | SELECT STATEMENT          |                |      1 |        |      1 |\n|   1 |  SORT AGGREGATE           |                |      1 |      1 |      1 |\n|   2 |   CONCATENATION           |                |      1 |        |      0 |\n|*  3 |    FILTER                 |                |      1 |        |      0 |\n|*  4 |     TABLE ACCESS BY INDEX | EMPLOYEES      |      1 |      1 |      0 |\n|*  5 |      INDEX FULL SCAN      | EMP_MANAGER_IX |      1 |      1 |    106 |\n|*  6 |    FILTER                 |                |      1 |        |      0 |\n|*  7 |     TABLE ACCESS BY INDEX | EMPLOYEES      |      0 |      1 |      0 |\n|*  8 |      INDEX RANGE SCAN     | EMP_MANAGER_IX |      0 |      1 |      0 |\n-------------------------------------------------------------------------------\nPredicate Information (identified by operation id):\n---------------------------------------------------\n\n   3 - filter(:MANAGER_ID IS NULL)\n   4 - filter((\"EMPLOYEE_ID\"=NVL(:EMPLOYEE_ID,\"EMPLOYEE_ID\") AND\n              \"DEPARTMENT_ID\"=NVL(:DEPARTMENT_ID,\"DEPARTMENT_ID\") AND \"JOB_ID\"=NVL(:JOB_ID,\"JOB_ID\")))\n   5 - filter(\"MANAGER_ID\" IS NOT NULL)\n   6 - filter(:MANAGER_ID IS NOT NULL)\n   7 - filter((\"EMPLOYEE_ID\"=NVL(:EMPLOYEE_ID,\"EMPLOYEE_ID\") AND\n              \"DEPARTMENT_ID\"=NVL(:DEPARTMENT_ID,\"DEPARTMENT_ID\") AND \"JOB_ID\"=NVL(:JOB_ID,\"JOB_ID\")))\n   8 - access(\"MANAGER_ID\"=:MANAGER_ID)\n</code></pre>\n<p>Now, the optimizer has chosen to full scan the index on MANAGER_ID. Once again the goal is not check if it is the right choice or not. But the important point is that thanks to BIND_AWARE a new cursor has been created and the OR Expansion occured for another predicate:</p>\n<pre><code>Outline Data\n-------------\n\n  /*+\n      BEGIN_OUTLINE_DATA\n      IGNORE_OPTIM_EMBEDDED_HINTS\n      OPTIMIZER_FEATURES_ENABLE('12.1.0.2')\n      DB_VERSION('12.1.0.2')\n      ALL_ROWS\n      OUTLINE_LEAF(@\"SEL$F5BB74E1\")\n      MERGE(@\"SEL$2\")\n      OUTLINE_LEAF(@\"SEL$F5BB74E1_1\")\n      USE_CONCAT(@\"SEL$F5BB74E1\" 8 OR_PREDICATES(3))\n      OUTLINE_LEAF(@\"SEL$F5BB74E1_2\")\n      OUTLINE(@\"SEL$1\")\n      OUTLINE(@\"SEL$2\")\n      OUTLINE(@\"SEL$F5BB74E1\")\n      MERGE(@\"SEL$2\")\n      INDEX(@\"SEL$F5BB74E1_1\" \"EMPLOYEES\"@\"SEL$2\" (\"EMPLOYEES\".\"MANAGER_ID\"))\n      BATCH_TABLE_ACCESS_BY_ROWID(@\"SEL$F5BB74E1_1\" \"EMPLOYEES\"@\"SEL$2\")\n      INDEX_RS_ASC(@\"SEL$F5BB74E1_2\" \"EMPLOYEES\"@\"SEL$F5BB74E1_2\" (\"EMPLOYEES\".\"MANAGER_ID\"))\n      BATCH_TABLE_ACCESS_BY_ROWID(@\"SEL$F5BB74E1_2\" \"EMPLOYEES\"@\"SEL$F5BB74E1_2\")\n      END_OUTLINE_DATA\n  */\n</code></pre>\n<p>From the outline hints we can see that the 3rd predicate has been chosen, which is the one on MANAGER_ID. Note that the OR_PREDICATE part of the USE_CONCAT is not documented, and can become complex to control when other transformations change the order of predicates.</p>\n<h3>All combinations</h3>\n<p>I&#8217;ve run it with all combinations and it seems that the OR Expansion occured for the 4 predicate possibilities:</p>\n<pre><code>SQL&gt; select distinct plan_table_output from table(dbms_xplan.display_cursor(sql_id=&gt;'fhpytfwk0y4r3',cursor_child_no=&gt;null,format=&gt;'basic +outline +peeked_binds')) where plan_table_output like '%USE_CONCAT%';\n\nPLAN_TABLE_OUTPUT\n--------------------------------------------------------------------------------\n      USE_CONCAT(@\"SEL$F5BB74E1\" 8 OR_PREDICATES(4))\n      USE_CONCAT(@\"SEL$F5BB74E1\" 8 OR_PREDICATES(2))\n      USE_CONCAT(@\"SEL$F5BB74E1\" 8 OR_PREDICATES(3))\n      USE_CONCAT(@\"SEL$F5BB74E1\" 8 OR_PREDICATES(1))\n</code></pre>\n<p>And I&#8217;ve several cursors:</p>\n<pre><code>SQL&gt; select child_number,predicate,range_id,low,high from v$sql_cs_selectivity where sql_id in ('a9taz8xhfu2kc','fhpytfwk0y4r3') order by sql_id,child_number;\n\nCHILD_NUMBER PREDICATE                                  RANGE_ID LOW        HIGH\n------------ ---------------------------------------- ---------- ---------- ----------\n           0 =EMPLOYEE_I                                       0 0.000000   0.000000\n           1 =MANAGER_ID                                       0 0.000000   0.000000\n           2 =DEPARTMENT                                       0 0.000000   0.000000\n           3 =JOB_ID                                           0 0.000000   0.000000\n           4 =EMPLOYEE_I                                       0 0.000000   0.005140\n</code></pre>\n<p>Is that sufficient? probably not. A dynamic query will probably find a better plan for a specific combination. But at least we have the possibility have several cursors and get a plan that has an efficient index access.</p>\n<p>You want more plans without having to do dynamic sampling? You can do that by changing any session parameter that causes an &#8216;optimizer mismatch&#8217;. For example, have a different one for each combination of fields that are null or not. Which optimizer parameter? It would be nice to have a dummy one so that it does not have any side effects. If you have an idea, please post.</p>\n<p>Of course there is this &#8220;_optimizer_random_plan&#8221; but do you want to play with an undocumented that has such a name? Maybe that will be for part III&#8230;</p>\n",
    "protected": false
  }
}
