Sunday, September 30, 2012

Maintaining Optimizer Statistics on PeopleSoft on Oracle 11g (and beyond)

Update 20.4.2023: Nearly everything in this article also applies to later versions of Oracle too.
I have been considering how to collect optimizer statistics for a PeopleSoft system running on an Oracle 11g database.  Despite 11g being several years old, most of my current customers are still using 10g, though some are looking at the upgrade to 11g.  I believe a slightly different approach is required.

In 2009, I wrote a series of blog postings on the subject of collecting statistics.  However, these were all based on Oracle 10g.  I proposed a PL/SQL package that would use meta-data in a database table to determine how to collect statistics on a table, or deliberately suppress the collection of statistics.
I also recommended that statistics on tables created for use as temporary records in Application Engine programs should have their statistics deleted and locked to prevent system-wide jobs from refreshing their statistics. I proposed a package that collected statistics according to meta-data defined in a table.

IN 2011, Oracle published document 1322888.1 “pscbo_stats - Improving Statistics in Oracle RDBMS for PeopleSoft Enterprise”.  It takes a similar approach to my 10g package.  A PL/SQL package is used to collect statistics.  A number of tables control whether, when, and how statistics are collected on each record.  Oracle's package is also intended to be used to collect schema-wide statistics.

The psbo_stats package is an attempt to solve a genuine problem, and it has continued to evolve since its initial release.  However, I have a number of objections to it.
  • You are expected to replace the default automatic jobs that collect statistics with pscbo_stats, so it is a move away from standard default maintenance procedures.
  • pscbo_stats is still fundamentally a 10g solution. It does not use 11g table preferences.
  • It does use the Oracle automatic sample size in 11g if histograms are not to be collected.  Otherwise, it defaults to the previous behaviour of either using 100% sample size for when called by %UpdateStats with the ‘high’ sample size, or a variety of fixed sample sizes based on internal rules and the number of rows in the table.
  • The package contains a procedure that collects statistics on all objects in the schema that also refreshes statistics that are not stale but which have not been refreshed for a period of time determined by the size of the table. 
  • When it collects histograms it always sets the maximum bucket size of 254. This may not always be desirable for height-balanced histograms.
  • There is no support for collecting aggregated or incremental statistics on partitioned objects.
I think that the 11g table preferences offer better control over collection of statistics.

Oracle considerably enhanced the delivered DBMS_STATS package in 11g.  It became possible to specify default values for parameters in the dbms_stats.gather_table_stats program for each table.

The guiding principle in 11g, and one that is not specific to PeopleSoft, is that instead of calling dbms_stats.gather_table_stats with the desired parameters, we should set table preferences with the desired parameters and then just call dbms_stats without table-specific parameters. We can then just leave the default database and schema-wide procedures to get on with the job of collecting statistics.    

A document that describes the updated approach and the various scripts is available on the Go-Faster website at http://www2.go-faster.co.uk/docs.htm#Managing.Statistics.11g.

CAVEAT: This document represents some experimental work that is in progress.  It has not been tested against a customer system, let alone been used in production. I would welcome any feedback, and the opportunity to work with someone on a PeopleSoft system on Oracle 11g.

Update 4.2.2015: I have put this solution into PeopleSoft systems at two customer systems last year, so it is no longer just theory.

Update 5.4.2017: PeopleTools 8.55 set the PeopleSoft owner ID (usually SYSADM) in variable [DBNAME] in lowercase. Thus it is passed in lowercase to the PL/SQL package has been updated to handle this.

Thursday, September 20, 2012

Enabling Oracle Extended SQL Trace by Module and Action

I have written previously about the value of assigning meaningful values to the module and action attributes on a database session (see Using Oracle Enterprise Manager (Grid Control) with PeopleSoft). Oracle added instrumentation to PeopleTools 8.50 (see PeopleTools 8.50 uses DBMS_APPLICATION_INFO to Identify Database Sessions) that sets module and action for on-line and batch sessions.  However, I still use my own trigger to set these attributes for processes initiated by the Process Scheduler.

I originally became interested in module and action because it made it possible to analyse performance problems in specific processes with Active Session History (see Practical Use of ASH). However, since Oracle 10g there is also a very easy way to enable Oracle's extended session trace in the sessions relating to specific processes.

In the Oracle supplied PL/SQL package DBMS_MONITOR, there are two programs to specify combinations of module and action for which SQL trace will be enabled.  It is rather like setting a watchpoint in a debugger. SERV_MOD_ACT_TRACE_ENABLE is used to create a watchpoint, and SERV_MOD_ACT_TRACE_DISABLE removes it. Trace is then enabled in a session when the module and action is set to a value that matches a watchpoint and disabled when the module and/or action is changed to a value for which there is no watchpoint.

This screenshot from OEM shows that a query is being run within a PeopleSoft component RECV_PO, page PO_PICK_ORDERS.

It would be a simple matter to have Oracle trace the session for that component by setting a watchpoint.

BEGIN
 sys.dbms_monitor.serv_mod_act_trace_enable
 (service_name=>'P1PPP'
 ,module_name =>'RECV_PO'
 ,waits=>TRUE
 ,binds=>TRUE);
END;
/

Note that the service name must be specified as part of the watchpoint.  This will usually be the same as the PeopleSoft database name.  Thus, watchpoints copied to another database by cloning won't be effective because the service name will be different.

I could also have specified an action in the example above, but I want to trace all pages in the component and the search dialogue.

As with the other programs in dbms_monitor, information on wait events is included in traces by default.  In the above example, I have also requested information on bind variables to be included in the trace.

When setting a SQL trace for an on-line component it is possible that many different users could trigger tracing.  Trace will be enabled and disabled for each user. The result is that you will get a trace file for each application server process, and each trace file might contain the activity for more than one user if the application server process handled more than one service request.

Watchpoints can be removed in a similar way.
BEGIN
 sys.dbms_monitor.serv_mod_act_trace_disable
 (service_name=>'P1PPP'
 ,module_name =>'RECV_PO'
 ,action_name =>'PO_PICK_ORDERS');
END;
/

Watchpoints are held the table SYS.WRI$_TRACING_ENABLED.

SELECT * FROM sys.wri$_tracing_enabled
/

TRACE_TYPE PRIMARY_ID QUALIFIER_ID1 QUALIFIER_ID2   INSTANCE_NAME FLAGS
---------- ---------- ------------- --------------- ------------- -----
         5 P1PPP      RECV_PO       PO_PICK_ORDERS                   12

This technique is also effective for processes initiated by the Process Scheduler. The default behaviour from PeopleTools 8.50 is to set Action to the process name, my trigger sets Module to the process name.
If I specify only the Module in SERV_MOD_ACT_TRACE_ENABLE, then it trace will be enabled for that module and for any action.
BEGIN
 sys.dbms_monitor.serv_mod_act_trace_disable
 (service_name=>'AAAAAAA'
 ,module_name =>'GPPDPRUN');
END;
/
I choose to set the action to the Process Instance number because I find it useful to relate ASH data to a specific batch process. The above screenshot from OEM shows a streamed Global Payroll calculation running with many concurrent GPPDPRUN processes. The watchpoint would cause each and every process to trace.

However, it has been suggested that if the Action was set to the process run control ID then the watchpoint could be set to the specific run control value used by a user or scheduled job. It would be a simple matter of changing the trigger (I'll leave that as an exercise).  Then, a user could be told to use a specific value for run control ID that would then invoke trace.
BEGIN
 sys.dbms_monitor.serv_mod_act_trace_disable
 (service_name=>'AAAAAAA'
 ,action_name =>'TRACEME');
END;
/

Update 24.11.2012 This technique will not work for Application Engine from PeopleTools 8.52 because it now sets MODULE to a value that includes a session number and so cannot be predicted.

Thursday, July 19, 2012

Gathering Aggregated Cost-Based Optimiser Statistics on Partitioned Objects

Recently, I have been looking into how to gather cost-based optimizer statistics on composite partitioned objects. 

Database partitioning is not used by default in PeopleSoft because it is database specific.  Not all databases support partitioning, and where they do it is done in a platform specific manner.  Application Designer has no capability to create partitioned objects (although from PeopleTools 8.51 it will preserve partitioning in tables and index that are already partitioned in an Oracle database). It is therefore no surprise that it is not used widely by PeopleSoft customers.  However, partitioning is essential in Global Payroll systems that use 'streamed'  processing (many concurrent payroll processes).  By definition, these are the payroll systems that have large volumes of data where collecting statistics on the results tables can quickly become a challenge.  Partitioning can also have application in General Ledger reporting in Financials, particularly where nVision is in use.

It is important that statistics on partitioned tables are both up to date and accurate.  However, collecting global statistics on a large partitioned object can be a time-consuming and resource intensive business as Oracle samples all the physical partitions or sub-partitions. Briefly, if you do not collect global statistics on a partitioned table, Oracle will aggegrate the statistics on the physical partitons or sub-partitions to calculate statistics on the logical table and partition segments.

Oracle 10g makes a number of mistakes in its calculation of these aggregated statistics.  In particular the number of distinct values on columns by which the table is partitioned have impossibly low values.  This is can affect cardinality calculations and so lead the optimizer to choose the wrong execution plan.

I have now published the second of two documents on my website that examine aspects of statistics on partitioned, and in particular composite partitioned tables. The first document examines the problems in 10g, and proposes a procedure to 'correct' the aggregated statistics to at least minimum possible values.
The second document looks at the same issue in 11g and shows that while most of the issues are fixed, one problem remains.

Gathering Aggregated Cost-Based Optimiser Statistics on Partitioned Objects

Recently, I have been looking into how to gather cost-based optimizer statistics on composite partitioned objects.  Collecting global statistics on a large partitioned object can be a time-consuming and resource intensive business as Oracle samples all the physical partitions or sub-partitions. Briefly, if you do not collect global statistics on a partitioned table, Oracle will aggegrate the statistics on the physical partitons or sub-partitions to calculate statistics on the logical table and partition segments.

Oracle 10g makes a number of mistakes in its calculation of these aggregated statistics.  In particular, the number of distinct values on columns by which the table is partitioned have impossibly low values.  This is can affect cardinality calculations and so lead the optimizer to choose the wrong execution plan.

I have now published the second of two documents on my website that examine aspects of statistics on partitioned, and in particular composite partitioned tables. The first document examines the problems in 10g, and proposes a procedure to 'correct' the aggregated statistics to at least minimum possible values.
The second document looks at the same issue in 11g and shows that while most of the issues are fixed, one problem remains.







Friday, March 02, 2012

Editing Hints in Stored Outlines

Introduction

This note describes changing a hint in a SQL Outline by updating the OUTLN.OL$ and OUTLN.OL$HINTS tables directly. It shows that in the limited test case that it does affect the resulting execution plan.  
CAVEAT: This is a hack, so this is more of an oddity than something that I would recommend for use in production. I am only interested in it for the limited purpose of adding a NO_EXPAND hint to an outline, that would not otherwise be included if the outline was generated in the conventional manner.

Why Outlines?

I am using outlines because I am working with Oracle 10g, so I don't have SQL Plan Management until Oracle 11g. I am not using SQL Profiles because I am working with PeopleSoft Global Payroll. I want complete stability of execution plans rather than having the optimizer produce 'better plans' with adjusted costing. Otherwise, a single employee payroll calculation can put a different plan into the library cache which can then be used for a company-wide payroll calculation. I want to guarantee just one execution plan.

Preparation

I'll start by creating a table to use.
DROP TABLE t PURGE
/
CREATE TABLE t
(a NUMBER NOT NULL
,b NUMBER NOT NULL
,c VARCHAR2(100)
)
/
TRUNCATE TABLE t
/
INSERT INTO t (a,b,c)
SELECT row_number() over (order by level)
,      row_number() over (order by level desc)
,      TO_CHAR(TO_DATE(rownum,'J'),'Jsp')
FROM DUAL
CONNECT BY LEVEL <= 10000
/
BEGIN
 dbms_stats.gather_table_stats
 (ownname=>user
 ,tabname=>'T'
 ,cascade=>TRUE
 );
end;
/
Table T has 10000 rows. Column A is numbered 1 to 10000, B is numbered 10000 to 1, and there is a third column to provide some padding.
         A          B C
---------- ---------- ----------------------------------------
         1      10000 One
         2       9999 Two
         3       9998 Three
…
      9998          3 Nine Thousand Nine Hundred Ninety-Eight
      9999          2 Nine Thousand Nine Hundred Ninety-Nine
     10000          1 Ten Thousand

Collecting Outlines

So, the table doesn't have any indexes. I will disable autotrace in SQL*Plus, and dynamic sampling to prevent the collection of additional SQL outlines.
DROP INDEX t1;
DROP INDEX t2;

SET AUTOTRACE OFF
CLEAR SCREEN
ALTER SESSION SET optimizer_dynamic_sampling=0;
ALTER SYSTEM FLUSH SHARED_POOL;
ALTER SESSION SET create_stored_outlines=C1;

SELECT *
FROM t
WHERE a=42
OR    b=42
/

ALTER SESSION SET create_stored_outlines=FALSE;
ALTER SESSION SET use_stored_outlines=FALSE;
Without any indexes, Oracle can only do a full scan on the table.

Plan hash value: 1601196873
--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      |     2 |    86 |    22   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| T    |     2 |    86 |    22   (0)| 00:00:01 |
--------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("A"=42 OR "B"=42)
I will now add the indexes and create another stored outline for the same statement but in a different category.
CREATE INDEX t1 ON t (a,b);
CREATE INDEX t2 ON t (b,a);
SET AUTOTRACE OFF 
ALTER SYSTEM FLUSH SHARED_POOL;
ALTER SESSION SET create_stored_outlines=C2;

SELECT *
FROM t
WHERE a=42
OR    b=42
/
ALTER SESSION SET create_stored_outlines=FALSE;
ALTER SESSION SET use_stored_outlines=FALSE;
With the indexes, the execution plan includes a concatenation of queries on each index.
Plan hash value: 277049827
-------------------------------------------------------------------------------------
| Id  | Operation                    | Name | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |      |     2 |    86 |     6   (0)| 00:00:01 |
|   1 |  CONCATENATION               |      |       |       |            |          |
|   2 |   TABLE ACCESS BY INDEX ROWID| T    |     1 |    43 |     3   (0)| 00:00:01 |
|*  3 |    INDEX RANGE SCAN          | T2   |     1 |       |     2   (0)| 00:00:01 |
|   4 |   TABLE ACCESS BY INDEX ROWID| T    |     1 |    43 |     3   (0)| 00:00:01 |
|*  5 |    INDEX RANGE SCAN          | T1   |     1 |       |     2   (0)| 00:00:01 |
-------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   3 - access("B"=42)
   5 - access("A"=42)
       filter(LNNVL("B"=42))
I will create a third stored outline in a third category so that I have two copies of the outline for comparison.
ALTER SYSTEM FLUSH SHARED_POOL;
ALTER SESSION SET create_stored_outlines=C3;

SELECT *
FROM   t
WHERE  a=42
OR     b=42
/

ALTER SESSION SET create_stored_outlines=FALSE;
I am going to rename the outlines to match the category to make it easier to work in the rest of this example.
BEGIN
 FOR i IN (
  SELECT DISTINCT c1.category, c1.name name
  FROM user_outlines c1
  , user_outlines c2
  WHERE c1.category != c2.category 
  AND c1.signature = c2.signature
  AND c1.category != c1.name
 ) LOOP
  EXECUTE IMMEDIATE 'ALTER OUTLINE '||i.name||' RENAME TO '||i.category;
 END LOOP;
END;
/

Looking at Outlines

We now have three outlines
SET AUTOTRACE OFF PAGES 100 LINES 120
BREAK ON name SKIP 1
SELECT * FROM user_outlines ORDER BY 1
/

NAME       CATEGORY   USED   TIMESTAMP           VERSION
---------- ---------- ------ ------------------- ----------
SQL_TEXT
--------------------------------------------------------------------------------
SIGNATURE                        COMPATIBLE ENABLED  FORMAT     MIGRATED
-------------------------------- ---------- -------- ---------- ------------
C1         C1         UNUSED 14:49:10 01/03/2012 11.2.0.2.0
SELECT *
FROM t
WHERE a=42
OR    b=42
260D04C39979D97078EDCE9B6727C4A3 COMPATIBLE ENABLED  NORMAL     NOT-MIGRATED


C2         C2         UNUSED 14:49:10 01/03/2012 11.2.0.2.0
SELECT *
FROM t
WHERE a=42
OR    b=42
260D04C39979D97078EDCE9B6727C4A3 COMPATIBLE ENABLED  NORMAL     NOT-MIGRATED


C3         C3         UNUSED 14:48:33 01/03/2012 11.2.0.2.0
SELECT *
FROM t
WHERE a=42
OR    b=42
260D04C39979D97078EDCE9B6727C4A3 COMPATIBLE ENABLED  NORMAL     NOT-MIGRATED
And these are the hints in the outlines
SELECT  *
FROM user_outline_hints
ORDER BY 1,2,3
/

NAME        NODE  STAGE  JOIN_POS HINT
---------- ----- ------ --------- ----------------------------------------
C1             1      1         1 FULL(@"SEL$1" "T"@"SEL$1")
               1      1         0 OUTLINE_LEAF(@"SEL$1")
               1      1         0 ALL_ROWS
               1      1         0 OPT_PARAM('optimizer_dynamic_sampling' 0)
               1      1         0 DB_VERSION('11.2.0.2')
               1      1         0 OPTIMIZER_FEATURES_ENABLE('11.2.0.2')
               1      1         0 IGNORE_OPTIM_EMBEDDED_HINTS

C2             1      1         1 INDEX_RS_ASC(@"SEL$1_2" "T"@"SEL$1_2" ("T"."A" "T"."B"))
               1      1         1 INDEX_RS_ASC(@"SEL$1_1" "T"@"SEL$1" ("T"."B" "T"."A"))
               1      1         0 OUTLINE(@"SEL$1")
               1      1         0 OUTLINE_LEAF(@"SEL$1_2")
               1      1         0 USE_CONCAT(@"SEL$1" 8 OR_PREDICATES(1))
               1      1         0 OUTLINE_LEAF(@"SEL$1_1")
               1      1         0 OUTLINE_LEAF(@"SEL$1")
               1      1         0 ALL_ROWS
               1      1         0 OPT_PARAM('optimizer_dynamic_sampling' 0)
               1      1         0 DB_VERSION('11.2.0.2')
               1      1         0 OPTIMIZER_FEATURES_ENABLE('11.2.0.2')
               1      1         0 IGNORE_OPTIM_EMBEDDED_HINTS

C3             1      1         1 INDEX_RS_ASC(@"SEL$1_2" "T"@"SEL$1_2" ("T"."A" "T"."B"))
               1      1         1 INDEX_RS_ASC(@"SEL$1_1" "T"@"SEL$1" ("T"."B" "T"."A"))
               1      1         0 OUTLINE(@"SEL$1")
               1      1         0 OUTLINE_LEAF(@"SEL$1_2")
               1      1         0 USE_CONCAT(@"SEL$1" 8 OR_PREDICATES(1))
               1      1         0 OUTLINE_LEAF(@"SEL$1_1")
               1      1         0 OUTLINE_LEAF(@"SEL$1")
               1      1         0 ALL_ROWS
               1      1         0 OPT_PARAM('optimizer_dynamic_sampling' 0)
               1      1         0 DB_VERSION('11.2.0.2')
               1      1         0 OPTIMIZER_FEATURES_ENABLE('11.2.0.2')
               1      1         0 IGNORE_OPTIM_EMBEDDED_HINTS
Or, I queried from OUTLN.OL$HINTS
SET LINES 110 PAGES 999
BREAK ON ol_name SKIP 1
SELECT ol_name,hint#,category,hint_type,hint_text
,stage#,node#,table_name,table_tin,table_pos
,ref_id,user_table_name,cost,cardinality,bytes
,hint_textoff,hint_textlen
FROM outln.ol$hints
WHERE category IN('C1','C2')
ORDER BY 1,2,3,4,5
/

OL_NAME    HINT# CATEGORY  HINT_TYPE HINT_TEXT                                                    STAGE# NODE#
---------- ----- -------- ---------- ------------------------------------------------------------ ------ -----
TABLE_NAME  TABLE_TIN  TABLE_POS     REF_ID USER_TABLE       COST CARDINALITY      BYTES HINT_TEXTOFF HINT_TEXTLEN
---------- ---------- ---------- ---------- ---------- ---------- ----------- ---------- ------------ ------------
C1             1 C1                2 FULL(@"SEL$1" "T"@"SEL$1")                                        1     1
T                   1          1          0 SYSADM.T   22.1083368           2         86           15            1

               2 C1             1011 OUTLINE_LEAF(@"SEL$1")                                            1     1
                    0          0          0                     0           0          0            0        0

               3 C1             1013 ALL_ROWS                                                          1     1
                    0          0          0                     0           0          0            0        0

               4 C1             1012 OPT_PARAM('optimizer_dynamic_sampling' 0)                         1     1
                    0          0          0                     0           0          0            0        0

               5 C1               54 DB_VERSION('11.2.0.2')                                            1     1
                    0          0          0                     0           0          0            0        0

               6 C1             1009 OPTIMIZER_FEATURES_ENABLE('11.2.0.2')                             1     1
                    0          0          0                     0           0          0            0        0

               7 C1             1008 IGNORE_OPTIM_EMBEDDED_HINTS                                       1     1
                    0          0          0                     0           0          0            0        0

C2             1 C2               51 INDEX_RS_ASC(@"SEL$1_2" "T"@"SEL$1_2" ("T"."A" "T"."B"))          1     1
T                   1          1          0 SYSADM.T   3.00073364           1         43           15            1

               2 C2               51 INDEX_RS_ASC(@"SEL$1_1" "T"@"SEL$1" ("T"."B" "T"."A"))            1     1
                    1          1          0 SYSADM.T   3.00073196           1         43           15            1

               3 C2             1010 OUTLINE(@"SEL$1")                                                 1     1
                    0          0          0                     0           0          0            0        0

               4 C2             1011 OUTLINE_LEAF(@"SEL$1_2")                                          1     1
                    0          0          0                     0           0          0            0        0

               5 C2               38 USE_CONCAT(@"SEL$1" 8 OR_PREDICATES(1))                           1     1
                    0          0          0                     0           0          0            0        0

               6 C2             1011 OUTLINE_LEAF(@"SEL$1_1")                                          1     1
                    0          0          0                     0           0          0            0        0

               7 C2             1011 OUTLINE_LEAF(@"SEL$1")                                            1     1
                    0          0          0                     0           0          0            0        0

               8 C2             1013 ALL_ROWS                                                          1     1
                    0          0          0                     0           0          0            0        0

               9 C2             1012 OPT_PARAM('optimizer_dynamic_sampling' 0)                         1     1
                    0          0          0                     0           0          0            0        0

              10 C2               54 DB_VERSION('11.2.0.2')                                            1     1
                    0          0          0                     0           0          0            0        0

              11 C2             1009 OPTIMIZER_FEATURES_ENABLE('11.2.0.2')                             1     1
                    0          0          0                     0           0          0            0        0

              12 C2             1008 IGNORE_OPTIM_EMBEDDED_HINTS                                       1     1
                    0          0          0                     0           0          0            0        0

Editing Outlines 

Adding a Hint 

I can add a new hint, thus
REM Insert hint at positition 0
INSERT INTO outln.ol$hints
(ol_name,hint#,category,hint_type,hint_text
,stage#,node#,table_name,table_tin,table_pos
,ref_id,user_table_name,cost,cardinality,bytes
,hint_textoff,hint_textlen)
VALUES
('C2',0,'C2',42,'NO_EXPAND'
,1,1,NULL,0,0
,0,NULL,0,0,0
,0,0)
/
REM increment hint numbers if there is a hint at position 0
UPDATE outln.ol$hints x
SET hint#=hint#+1
WHERE EXISTS (select 'x'
 FROM outln.ol$hints y
 WHERE y.ol_name = x.ol_name
 AND y.hint#=0)
AND ol_name = 'C2'
/
REM update the hint count on the parent record
UPDATE outln.ol$ x
set hintcount = (
 SELECT count(*)
 FROM outln.ol$hints h
 where h.ol_name = h.ol_name)
where ol_name = 'C3'
/

Changing a Hint 

However, in this case, I want to change an existing hint from USE_CONCAT to NO_EXPAND.
UPDATE outln.ol$hints
SET    hint_text = 'NO EXPAND'
WHERE  ol_name = 'C3'
AND    hint_text like 'USE_CONCAT(%)'
/

Testing Outlines 

Original Outline 

CLEAR SCREEN
SET AUTOTRACE OFF
ALTER SESSION SET statistics_level = ALL;
ALTER SYSTEM FLUSH SHARED_POOL;
ALTER SESSION SET use_stored_outlines=C2;
SELECT *
FROM t
WHERE a=42
OR    b=42
/
SELECT * FROM table(dbms_xplan.display_cursor(null,null,'ADVANCED'))
/
The unchanged outline C2 still produces the concatenation
Plan hash value: 277049827
-------------------------------------------------------------------------------------
| Id  | Operation                    | Name | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |      |     2 |    86 |     6   (0)| 00:00:01 |
|   1 |  CONCATENATION               |      |       |       |            |          |
|   2 |   TABLE ACCESS BY INDEX ROWID| T    |     1 |    43 |     3   (0)| 00:00:01 |
|*  3 |    INDEX RANGE SCAN          | T2   |     1 |       |     2   (0)| 00:00:01 |
|   4 |   TABLE ACCESS BY INDEX ROWID| T    |     1 |    43 |     3   (0)| 00:00:01 |
|*  5 |    INDEX RANGE SCAN          | T1   |     1 |       |     2   (0)| 00:00:01 |
-------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   3 - access("B"=42)
   5 - access("A"=42)
       filter(LNNVL("B"=42))

Edited Outline 

Now, let's try the outline that I updated directly.
ALTER SESSION SET use_stored_outlines=C3;
SELECT *
FROM t
WHERE a=42
OR    b=42
/
SELECT * FROM table(dbms_xplan.display_cursor(null,null,'ADVANCED'))
/
ALTER SESSION SET use_stored_outlines=FALSE;
I get a different execution plan that doesn't do CONCATENATION. Note that the outline still doesn't contain the NO_EXPAND hint that I put into the outline.
Plan hash value: 4269684720
-----------------------------------------------------------------------------------------
| Id  | Operation                        | Name | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                 |      |       |       |     6 (100)|          |
|   1 |  TABLE ACCESS BY INDEX ROWID     | T    |     2 |    86 |     6  (34)| 00:00:01 |
|   2 |   BITMAP CONVERSION TO ROWIDS    |      |       |       |            |          |
|   3 |    BITMAP OR                     |      |       |       |            |          |
|   4 |     BITMAP CONVERSION FROM ROWIDS|      |       |       |            |          |
|   5 |      SORT ORDER BY               |      |       |       |            |          |
|*  6 |       INDEX RANGE SCAN           | T1   |       |       |     2   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------

Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------
   1 - SEL$1 / T@SEL$1

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

  /*+
      BEGIN_OUTLINE_DATA
      IGNORE_OPTIM_EMBEDDED_HINTS
      OPTIMIZER_FEATURES_ENABLE('11.2.0.2')
      DB_VERSION('11.2.0.2')
      OPT_PARAM('optimizer_dynamic_sampling' 0)
      ALL_ROWS
      OUTLINE_LEAF(@"SEL$1")
      BITMAP_TREE(@"SEL$1" "T"@"SEL$1" OR(1 1 ("T"."A" "T"."B")))
      END_OUTLINE_DATA
  */

Predicate Information (identified by operation id):
---------------------------------------------------
   6 - access("A"=42)
       filter("A"=42)