Tuesday, February 18, 2014

SQL Tuning: Using Oracle's Global Hints With Multiple Query Blocks

There's nothing much more frustrating than knowing how to tune a query, but being unable to due to one or more third party views that cannot be modified.  A great way to work around this is by using Oracle's global hints in the main query block.  This is accomplished by referring to the view name and the table name with this format /*+ hint(view.table) */.

According to Oracle's documentation, however, the optimizer ignores global hints in this format that refer to multiple query blocks.  So if you wanted to use the "leading" hint with more than one view like this /*+ leading(view1.table1 view2.table2) */, Oracle will not utilize your input, and you will be left banging your head against the wall.

Luckily, there is another format for referencing these query blocks, but it takes a little digging to get it right.  In the following example, I will create a table with a view that references it along with another view on the ever famous "dual" table.  These two views will be joined in a query and we will see what options there are for manipulating the execution plan without changing the views.

Here are the table and view statements to set up the example.

CREATE TABLE orders AS
SELECT
        LEVEL order_id,
        SYSDATE + DBMS_RANDOM.VALUE(-1000, 1000) order_date,
        DBMS_RANDOM.STRING('A', 20) comments
FROM dual
CONNECT BY LEVEL <= 100000;


CREATE INDEX order_id_ix ON orders ( order_id );

CREATE INDEX order_dt_id_ix ON orders ( order_date, order_id );


CREATE VIEW vw_orders AS
SELECT * FROM orders;

CREATE VIEW vw_dual AS
SELECT * FROM dual;

Here is a basic query which joins the two views

SELECT /*+ gather_plan_statistics */
        order_id,
        order_date,
        comments
FROM vw_orders,
        vw_dual
WHERE TRUNC(order_date) = TO_DATE('12-JAN-13','DD-MON-YY')
AND order_id = 1;


After running the query, we can take a look at the execution plan chosen by the optimizer with the following script.

set pages 999
set lines 200

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR
        (
        null,
        null,
        'allstats'
        ));


The plan shows that DUAL is the driving table and a nested loop is utilized to access the ORDERS table via an index.

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  dh3p87qwt5njy, child number 0
-------------------------------------
SELECT /*+ gather_plan_statistics */ order_id,  order_date,
comments FROM vw_orders,  vw_dual WHERE TRUNC(order_date) =
TO_DATE('12-JAN-13','DD-MON-YY') AND order_id = 1

Plan hash value: 1760841149

------------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name        | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |             |      1 |        |      1 |00:00:00.01 |       4 |
|   1 |  NESTED LOOPS                |             |      1 |      9 |      1 |00:00:00.01 |       4 |
|   2 |   FAST DUAL                  |             |      1 |      1 |      1 |00:00:00.01 |       0 |
|*  3 |   TABLE ACCESS BY INDEX ROWID| ORDERS      |      1 |      9 |      1 |00:00:00.01 |       4 |
|*  4 |    INDEX RANGE SCAN          | ORDER_ID_IX |      1 |      9 |      1 |00:00:00.01 |       3 |
------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - filter(TRUNC(INTERNAL_FUNCTION("ORDER_DATE"))=TO_DATE('12-JAN-13','DD-MON-YY'))
   4 - access("ORDER_ID"=1)

Note
-----
   - dynamic sampling used for this statement (level=2)


34 rows selected.


Now, if we want to force the optimizer to use a full table scan on the ORDERS table, we can throw in a global hint that references the view and the table.  As you can see from the execution plan, the optimizer follows the directive and a full table scan is performed.

SELECT /*+ gather_plan_statistics full(vw_orders.orders) */
        order_id,
        order_date,
        comments
FROM vw_orders,
        vw_dual
WHERE TRUNC(order_date) = TO_DATE('12-JAN-13','DD-MON-YY')
AND order_id = 1;


PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  9xby7nxuz82tc, child number 0
-------------------------------------
SELECT /*+ gather_plan_statistics full(vw_orders.orders) */
order_id,  order_date,  comments FROM vw_orders,  vw_dual WHERE
TRUNC(order_date) = TO_DATE('12-JAN-13','DD-MON-YY') AND order_id = 1

Plan hash value: 2691672058

---------------------------------------------------------------------------------------
| Id  | Operation          | Name   | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
---------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |        |      1 |        |      1 |00:00:00.01 |     545 |
|   1 |  NESTED LOOPS      |        |      1 |      9 |      1 |00:00:00.01 |     545 |
|   2 |   FAST DUAL        |        |      1 |      1 |      1 |00:00:00.01 |       0 |
|*  3 |   TABLE ACCESS FULL| ORDERS |      1 |      9 |      1 |00:00:00.01 |     545 |
---------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - filter(("ORDER_ID"=1 AND TRUNC(INTERNAL_FUNCTION("ORDER_DATE"))=TO_DATE(
              '12-JAN-13','DD-MON-YY')))

Note
-----
   - dynamic sampling used for this statement (level=2)


32 rows selected.



If we try the same approach with the "leading" hint which references more than one query block, Oracle is not so cooperative.  After running this example, we see that the "leading" hint is ignored and DUAL is still the driving table.

SELECT /*+ gather_plan_statistics full(vw_orders) leading(vw_orders.orders vw_dual.dual) */
        order_id,
        order_date,
        comments
FROM vw_orders,
        vw_dual
WHERE TRUNC(order_date) = TO_DATE('12-JAN-13','DD-MON-YY')
AND order_id = 1;



PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  cbp5hxyg5mjkx, child number 0
-------------------------------------
SELECT /*+ gather_plan_statistics full(vw_orders)
leading(vw_orders.orders vw_dual.dual) */ order_id,  order_date,
comments FROM vw_orders,  vw_dual WHERE TRUNC(order_date) =
TO_DATE('12-JAN-13','DD-MON-YY') AND order_id = 1

Plan hash value: 2691672058

---------------------------------------------------------------------------------------
| Id  | Operation          | Name   | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
---------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |        |      1 |        |      1 |00:00:00.01 |     545 |
|   1 |  NESTED LOOPS      |        |      1 |      9 |      1 |00:00:00.01 |     545 |
|   2 |   FAST DUAL        |        |      1 |      1 |      1 |00:00:00.01 |       0 |
|*  3 |   TABLE ACCESS FULL| ORDERS |      1 |      9 |      1 |00:00:00.01 |     545 |
---------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - filter(("ORDER_ID"=1 AND TRUNC(INTERNAL_FUNCTION("ORDER_DATE"))=TO_DATE(
              '12-JAN-13','DD-MON-YY')))

Note
-----
   - dynamic sampling used for this statement (level=2)


33 rows selected.



In order to give the optimizer the information it needs in this situation, we need to change our "DBMS_XPLAN" script so it will include the "alias" and the "outline" information.  In the "DBMS_XPLAN" script below, I have also eliminated the "predicate" and "note" data.


SELECT /*+ gather_plan_statistics */
        order_id,
        order_date,
        comments
FROM vw_orders,
        vw_dual
WHERE TRUNC(order_date) = TO_DATE('12-JAN-13','DD-MON-YY')
AND order_id = 1;


set pages 999
set lines 200

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR
        (
        null,
        null,
        'allstats +alias +outline -note -predicate'
        ));


PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  12b497umk9ytc, child number 0
-------------------------------------
SELECT /*+ gather_plan_statistics */ order_id,  order_date,
comments FROM vw_orders,  vw_dual WHERE TRUNC(order_date) =
TO_DATE('12-JAN-13','DD-MON-YY') AND order_id = 1

Plan hash value: 1760841149

------------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name        | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |             |      1 |        |      1 |00:00:00.01 |       4 |
|   1 |  NESTED LOOPS                |             |      1 |      9 |      1 |00:00:00.01 |       4 |
|   2 |   FAST DUAL                  |             |      1 |      1 |      1 |00:00:00.01 |       0 |
|   3 |   TABLE ACCESS BY INDEX ROWID| ORDERS      |      1 |      9 |      1 |00:00:00.01 |       4 |
|   4 |    INDEX RANGE SCAN          | ORDER_ID_IX |      1 |      9 |      1 |00:00:00.01 |       3 |
------------------------------------------------------------------------------------------------------

Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------

   1 - SEL$5428C7F1
   2 - SEL$5428C7F1 / DUAL@SEL$3
   3 - SEL$5428C7F1 / ORDERS@SEL$2
   4 - SEL$5428C7F1 / ORDERS@SEL$2

Outline Data
-------------

  /*+
      BEGIN_OUTLINE_DATA
      IGNORE_OPTIM_EMBEDDED_HINTS
      OPTIMIZER_FEATURES_ENABLE('11.2.0.2')
      DB_VERSION('11.2.0.2')
      ALL_ROWS
      OUTLINE_LEAF(@"SEL$5428C7F1")
      MERGE(@"SEL$2")
      MERGE(@"SEL$3")
      OUTLINE(@"SEL$1")
      OUTLINE(@"SEL$2")
      OUTLINE(@"SEL$3")
      INDEX_RS_ASC(@"SEL$5428C7F1" "ORDERS"@"SEL$2" ("ORDERS"."ORDER_ID"))
      LEADING(@"SEL$5428C7F1" "DUAL"@"SEL$3" "ORDERS"@"SEL$2")
      USE_NL(@"SEL$5428C7F1" "ORDERS"@"SEL$2")
      END_OUTLINE_DATA
  */


53 rows selected.


In the output above, the operations are numbered in the execution plan.  These numbers correspond to the numbers identifying the query block and object alias.  These two query blocks and aliases are found in the outline section with a leading hint.  This leading hint can be modified by listing the ORDERS table first as shown below and using the ORDERS alias in the full table scan hint as well.  The optimizer now accepts the hints and executes the query as directed.

SELECT /*+ gather_plan_statistics full(@"SEL$5428C7F1" "ORDERS"@"SEL$2") leading(@"SEL$5428C7F1" "ORDERS"@"SEL$2" "DUAL"@"SEL$3") */
        order_id,
        order_date,
        comments
FROM vw_orders,
        vw_dual
WHERE TRUNC(order_date) = TO_DATE('12-JAN-13','DD-MON-YY')
AND order_id = 1;

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID bpy75y38s6phy, child number 0
-------------------------------------
SELECT /*+ gather_plan_statistics full(@"SEL$5428C7F1"
"ORDERS"@"SEL$2") leading(@"SEL$5428C7F1" "ORDERS"@"SEL$2"
"DUAL"@"SEL$3") */    order_id,      order_date,
comments FROM vw_orders,  vw_dual WHERE TRUNC(order_date) =
TO_DATE('12-JAN-13','DD-MON-YY') AND order_id = 1

Plan hash value: 795833099

-----------------------------------------------------------------------------------------
| Id  | Operation            | Name   | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |        |      1 |        |      0 |00:00:00.01 |     544 |
|   1 |  MERGE JOIN CARTESIAN|        |      1 |     10 |      0 |00:00:00.01 |     544 |
|   2 |   TABLE ACCESS FULL  | ORDERS |      1 |     10 |      0 |00:00:00.01 |     544 |
|   3 |   BUFFER SORT        |        |      0 |      1 |      0 |00:00:00.01 |       0 |
|   4 |    FAST DUAL         |        |      0 |      1 |      0 |00:00:00.01 |       0 |
-----------------------------------------------------------------------------------------

Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------

   1 - SEL$5428C7F1
   2 - SEL$5428C7F1 / ORDERS@SEL$2
   4 - SEL$5428C7F1 / DUAL@SEL$3

Outline Data
-------------

  /*+
      BEGIN_OUTLINE_DATA
      IGNORE_OPTIM_EMBEDDED_HINTS
      OPTIMIZER_FEATURES_ENABLE('12.1.0.1')
      DB_VERSION('12.1.0.1')
      ALL_ROWS
      OUTLINE_LEAF(@"SEL$5428C7F1")
      MERGE(@"SEL$2")
      MERGE(@"SEL$3")
      OUTLINE(@"SEL$1")
      OUTLINE(@"SEL$2")
      OUTLINE(@"SEL$3")
      FULL(@"SEL$5428C7F1" "ORDERS"@"SEL$2")
      LEADING(@"SEL$5428C7F1" "ORDERS"@"SEL$2" "DUAL"@"SEL$3")
      USE_MERGE_CARTESIAN(@"SEL$5428C7F1" "DUAL"@"SEL$3")
      END_OUTLINE_DATA
  */


48 rows selected.


1 comment: