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.


Friday, February 7, 2014

Using Linux's Logrotate to Manage Alert and Listener Log Files

If you are running Oracle on Linux, there is a handy little utility called logrotate that can be used to manage those unwieldy alert logs and listener logs.

I recently worked on a client's database, and their alert log had information going back four years. When trying to examine the log with vi, it would take several minutes to open the file due to it's enormity. To help them out, I used this simple little tool to keep the files in check.

As root, I simply created a new file in /etc/logrotate.d on each RAC node called ora_cleanup. The example below is from node 2. This will copy the alert log with a numerical extension and truncate the existing file. It performs this monthly and keeps 13 months worth of log information before deleting old files. The listener log grows much faster, so it is configured to rotate weekly and keep 53 weeks of files. The copytruncate parameter is important for the listener log because the listener process holds an open handle on the existing file.

# alert log
/u01/app/oracle/diag/rdbms/orcl/orcl2/trace/alert_orcl2.log {
monthly
rotate 13
notifempty
missingok
copytruncate
nocreate
}

# listener log
/u01/app/asm/diag/tnslsnr/db102/listener_db102/trace/listener_db102.log {
weekly
rotate 53
notifempty
missingok
copytruncate
nocreate
}

After a few months, this is what the managed files look like in their directories.

$ cd /u01/app/oracle/diag/rdbms/orcl/orcl2/trace
$ ls -arltp alert_orcl2*
-rw-rw---- 1 oracle oinstall 50051579 Oct 10 15:17 alert_orcl2.log.5
-rw-rw---- 1 oracle oinstall   993219 Nov  1 04:02 alert_orcl2.log.4
-rw-rw---- 1 oracle oinstall  1124244 Dec  1 04:02 alert_orcl2.log.3
-rw-rw---- 1 oracle oinstall  1088332 Jan  1 04:02 alert_orcl2.log.2
-rw-rw---- 1 oracle oinstall  2163268 Feb  1 04:02 alert_orcl2.log.1
-rw-rw---- 1 oracle oinstall   279484 Feb  7 09:34 alert_orcl2.log

$ cd /u01/app/asm/diag/tnslsnr/db102/listener_db102/trace
$ ls -arltp listener_db102*
-rw-rw---- 1 oracle oinstall 3857048716 Oct 10 15:28 listener_db102.log.18
-rw-rw---- 1 oracle oinstall  196362891 Oct 18 13:13 listener_db102.log.17
-rw-rw---- 1 oracle oinstall   40298571 Oct 20 04:02 listener_db102.log.16
-rw-rw---- 1 oracle oinstall  178408740 Oct 27 04:02 listener_db102.log.15
-rw-rw---- 1 oracle oinstall  188301497 Nov  3 04:02 listener_db102.log.14
-rw-rw---- 1 oracle oinstall  185073120 Nov 10 04:02 listener_db102.log.13
-rw-rw---- 1 oracle oinstall  178345071 Nov 17 04:02 listener_db102.log.12
-rw-rw---- 1 oracle oinstall  178945914 Nov 24 04:02 listener_db102.log.11
-rw-rw---- 1 oracle oinstall  165829858 Dec  1 04:02 listener_db102.log.10
-rw-rw---- 1 oracle oinstall  179395363 Dec  8 04:02 listener_db102.log.9
-rw-rw---- 1 oracle oinstall  175671704 Dec 15 04:02 listener_db102.log.8
-rw-rw---- 1 oracle oinstall  195136727 Dec 22 04:02 listener_db102.log.7
-rw-rw---- 1 oracle oinstall  195512012 Dec 29 04:02 listener_db102.log.6
-rw-rw---- 1 oracle oinstall  201759600 Jan  5 04:02 listener_db102.log.5
-rw-rw---- 1 oracle oinstall  204589312 Jan 12 04:02 listener_db102.log.4
-rw-rw---- 1 oracle oinstall  213276652 Jan 19 04:02 listener_db102.log.3
-rw-rw---- 1 oracle oinstall  212229296 Jan 26 04:02 listener_db102.log.2
-rw-rw---- 1 oracle oinstall  211244326 Feb  2 04:02 listener_db102.log.1
-rw-rw---- 1 oracle oinstall  159275570 Feb  7 09:35 listener_db102.log