You are on page 1of 75

Oracle Database Performance Tips

Prepared By
Ayman Mohammed EL Moniary

1
Contents
Oracle Database Performance Tips ................................................................................................................1
Partitioning ...............................................................................................................................................................5
Partitioning Key.................................................................................................................................................................................... 5
Partitioned Tables ................................................................................................................................................................................. 5
When to Partition a Table ..................................................................................................................................................................... 6
When to Partition an Index ................................................................................................................................................................... 6
Benefits of Partitioning ......................................................................................................................................................................... 6
Partitioning Strategies........................................................................................................................................................................... 6
Single-Level Partitioning ...................................................................................................................................................................... 7
Range Partitioning ............................................................................................................................................................................ 7
Hash Partitioning .............................................................................................................................................................................. 7
List Partitioning ................................................................................................................................................................................ 7
Composite Partitioning ......................................................................................................................................................................... 8
Composite Range-Range Partitioning .............................................................................................................................................. 8
Composite Range-Hash Partitioning ................................................................................................................................................ 8
Composite Range-List Partitioning .................................................................................................................................................. 8
Composite List-Range Partitioning .................................................................................................................................................. 8
Composite List-Hash Partitioning .................................................................................................................................................... 8
Composite List-List Partitioning ...................................................................................................................................................... 8
Interval Partitioning .............................................................................................................................................................................. 9
Overview of Partitioned Indexes .......................................................................................................................................................... 9
Local Partitioned Indexes ................................................................................................................................................................. 9
Global Partitioned Indexes ............................................................................................................................................................. 10

Oracle Bulk Operations..................................................................................................................................... 12


FORALL Statement ............................................................................................................................................................................ 12
Example DELETE Statement in FORALL Statement ................................................................................................................... 12
Time Difference for INSERT Statement in FOR LOOP and FORALL Statement ........................................................................ 12
Example Handling FORALL Exceptions after FORALL Statement Completes ........................................................................... 13
Example Showing Number of Rows Affected by Each DELETE in FORALL ............................................................................. 14
Example Showing Number of Rows Affected by Each INSERT SELECT in FORALL .............................................................. 14
APPEND_VALUES Hint ................................................................................................................................................................... 15
During conventional INSERT operations ...................................................................................................................................... 17
During direct-path INSERT operations .......................................................................................................................................... 17
How Direct-Path INSERT Works ...................................................................................................................................................... 17
What is "high water mark"? ................................................................................................................................................................ 19

2
Deallocating Unused Space ............................................................................................................................................................ 20
Dropping Unused Object Storage................................................................................................................................................... 20
BULK COLLECT Clause .................................................................................................................................................................. 21

Chaining Pipelined Table Functions for Multiple Transformations .............................................. 24


Overview of Table Functions ............................................................................................................................................................. 24
Creating Pipelined Table Functions .................................................................................................................................................... 25
Cardinality ...................................................................................................................................................................................... 26
Implicit (Shadow) Types ................................................................................................................................................................ 29
PARALLEL_ENABLE Option (Recommended) .......................................................................................................................... 30
Transformation Pipelines ............................................................................................................................................................... 35
AUTONOMOUS_TRANSACTION Pragma ................................................................................................................................ 39
DETERMINISTIC Option (Recommended) .................................................................................................................................. 39
RETURN Data Type ...................................................................................................................................................................... 40
PIPE ROW Statement .................................................................................................................................................................... 41
RETURN Statement ....................................................................................................................................................................... 41

Cross-Session PL/SQL Function Result Cache (11g) ........................................................................... 41


SQL Hints ................................................................................................................................................................ 42
Using Global Hints ............................................................................................................................................................................. 44

Some Important Database parameters and hidden parameters .................................................... 45


DB file Multiblock read count ............................................................................................................................................................ 45

Oracle Direct I/O ................................................................................................................................................. 47


Pga aggregate target and pga max size ...................................................................................................... 47
Oracle Multiple blocksize................................................................................................................................. 48
The benefits of a larger blocksize ....................................................................................................................................................... 49
Reducing data buffer waste ................................................................................................................................................................ 49
Reducing logical I/O ........................................................................................................................................................................... 51
Improving data buffer efficiency ........................................................................................................................................................ 53
Improving SQL execution plans ......................................................................................................................................................... 53
Real World Applications of multiple blocksizes ................................................................................................................................ 54
Largest Benefit: .............................................................................................................................................................................. 54
Smallest benefit: ............................................................................................................................................................................. 54
Important Tips: ............................................................................................................................................................................... 54

Native PL/SQL compilation ............................................................................................................................ 55


Stop Making the Same Performance Mistakes ....................................................................................... 57
Stop using PL/SQL when you could use SQL .................................................................................................................................... 57

3
Stop avoiding bulk binds .................................................................................................................................................................... 57
Stop using pass-by-value (NOCOPY) .................................................................................................................................................. 58
Stop using the wrong data types ......................................................................................................................................................... 58
Quick Points ....................................................................................................................................................................................... 58

Performance of Numeric Data Types in PL/SQL ................................................................................... 58


NUMBER Data Type ......................................................................................................................................................................... 59
Integer Data Types ............................................................................................................................................................................. 59
Real Number Data Types ................................................................................................................................................................... 60

Real Time Materialized views ........................................................................................................................ 62


Setup ................................................................................................................................................................................................... 63
Materialized View Logs ..................................................................................................................................................................... 63
Materialized View .............................................................................................................................................................................. 64
Basic Rewrite...................................................................................................................................................................................... 64
Rewrite Plus Real-Time Refresh ........................................................................................................................................................ 65
Direct Query of Materialized View (FRESH_MV Hint) .................................................................................................................... 68

Authid current_user ........................................................................................................................................... 72


Passing Large Data Structures with PL/SQL NOCOPY ........................................................................ 73
Open Discussion Subjects ................................................................................................................................ 75

4
Partitioning
Partitioning allows a table, index, or index-organized table to be subdivided into smaller pieces, where each piece of such a database
object is called a partition. Each partition has its own name, and may optionally have its own storage characteristics.
From the perspective of a database administrator, a partitioned object has multiple pieces that can be managed either collectively or
individually. This gives the administrator considerable flexibility in managing partitioned objects. However, from the perspective of
the application, a partitioned table is identical to a non-partitioned table; no modifications are necessary when accessing a partitioned
table using SQL queries and DML statements.
The following picture offers a graphical view of how partitioned tables differ from non-partitioned tables.

Note:
All partitions of a partitioned object must reside in tablespaces of a single block size.

Partitioning Key
Each row in a partitioned table is unambiguously assigned to a single partition. The partitioning key is comprised of one or more
columns that determine the partition where each row will be stored. Oracle automatically directs insert, update, and delete operations
to the appropriate partition through the use of the partitioning key

Partitioned Tables
Any table can be partitioned into a million separate partitions except those tables containing columns with LONG or LONG RAW
datatypes. You can, however, use tables containing columns with CLOB or BLOB datatypes.

Note:
To reduce disk usage and memory usage (specifically, the buffer cache), you can store tables and partitions of a partitioned table in a
compressed format inside the database. This often leads to a better scale up for read-only operations. Table compression can also
speed up query execution. There is, however, a slight cost in CPU overhead.

5
Table compression should be used with highly redundant data, such as tables with many foreign keys. You should avoid compressing
tables with much update or other DML activity. Although compressed tables or partitions are updatable, there is some overhead in
updating these tables, and high update activity may work against compression by causing some space to be wasted.

When to Partition a Table


Here are some suggestions for when to partition a table:
Tables greater than 2 GB should always be considered as candidates for partitioning.
Tables containing historical data, in which new data is added into the newest partition. A typical example is a historical table where
only the current month's data is updatable and the other 11 months are read only.
When the contents of a table need to be distributed across different types of storage devices.

When to Partition an Index


Here are some suggestions for when to consider partitioning an index:
Avoid rebuilding the entire index when data is removed.
Perform maintenance on parts of the data without invalidating the entire index.
Reduce the impact of index skew caused by an index on a column with a monotonically increasing value.

Benefits of Partitioning
Partitioning can provide tremendous benefit to a wide variety of applications by improving performance, manageability, and
availability. It is not unusual for partitioning to improve the performance of certain queries or maintenance operations by an order of
magnitude. Moreover, partitioning can greatly simplify common administration tasks.
Partitioning also enables database designers and administrators to tackle some of the toughest problems posed by cutting-edge
applications. Partitioning is a key tool for building multi-terabyte systems or systems with extremely high availability requirements.

Partitioning Strategies
Oracle Partitioning offers three fundamental data distribution methods as basic partitioning strategies that control how data is placed
into individual partitions:
 Range
 Hash
 List
Using these data distribution methods, a table can either be partitioned as a single list or as a composite partitioned table:
 Single-Level Partitioning
 Composite Partitioning
Each partitioning strategy has different advantages and design considerations. Thus, each strategy is more appropriate for a particular
situation.

6
Single-Level Partitioning
A table is defined by specifying one of the following data distribution methodologies, using one or more columns as the partitioning
key:
 Range Partitioning
 Hash Partitioning
 List Partitioning

Range Partitioning
Range partitioning maps data to partitions based on ranges of values of the partitioning key that you establish for each partition. It is
the most common type of partitioning and is often used with dates. For a table with a date column as the partitioning key, the January-
2005 partition would contain rows with partitioning key values from 01-Jan-2005 to 31-Jan-2005.
Each partition has a VALUES LESS THAN clause, which specifies a non-inclusive upper bound for the partitions. Any values of the
partitioning key equal to or higher than this literal are added to the next higher partition. All partitions, except the first, have an
implicit lower bound specified by the VALUES LESS THAN clause of the previous partition.
A MAXVALUE literal can be defined for the highest partition. MAXVALUE represents a virtual infinite value that sorts higher than
any other possible value for the partitioning key, including the NULL value.

Hash Partitioning
Hash partitioning maps data to partitions based on a hashing algorithm that Oracle applies to the partitioning key that you identify.
The hashing algorithm evenly distributes rows among partitions, giving partitions approximately the same size.
Hash partitioning is the ideal method for distributing data evenly across devices. Hash partitioning is also an easy-to-use alternative to
range partitioning, especially when the data to be partitioned is not historical or has no obvious partitioning key.

List Partitioning
List partitioning enables you to explicitly control how rows map to partitions by specifying a list of discrete values for the partitioning
key in the description for each partition. The advantage of list partitioning is that you can group and organize unordered and unrelated
sets of data in a natural way. For a table with a region column as the partitioning key, the North America partition might contain
values Canada, USA, and Mexico.
The DEFAULT partition enables you to avoid specifying all possible values for a list-partitioned table by using a default partition, so
that all rows that do not map to any other partition do not generate an error.

7
Composite Partitioning
Composite partitioning is a combination of the basic data distribution methods; a table is partitioned by one data distribution method
and then each partition is further subdivided into subpartitions using a second data distribution method. All subpartitions for a given
partition together represent a logical subset of the data.
Composite partitioning supports historical operations, such as adding new range partitions, but also provides higher degrees of
potential partition pruning and finer granularity of data placement through subpartitioning

Composite Range-Range Partitioning


Composite range-range partitioning enables logical range partitioning along two dimensions; for example, partition by order_date and
range subpartition by shipping_date.

Composite Range-Hash Partitioning


Composite range-hash partitioning partitions data using the range method, and within each partition, subpartitions it using the hash
method. Composite range-hash partitioning provides the improved manageability of range partitioning and the data placement,
striping, and parallelism advantages of hash partitioning.

Composite Range-List Partitioning


Composite range-list partitioning partitions data using the range method, and within each partition, subpartitions it using the list
method. Composite range-list partitioning provides the manageability of range partitioning and the explicit control of list partitioning
for the subpartitions.

Composite List-Range Partitioning


Composite list-range partitioning enables logical range subpartitioning within a given list partitioning strategy; for example, list
partition by country_id and range subpartition by order_date.

Composite List-Hash Partitioning


Composite list-hash partitioning enables hash subpartitioning of a list-partitioned object; for example, to enable partition-wise joins.

Composite List-List Partitioning


Composite list-list partitioning enables logical list partitioning along two dimensions; for example, list partition by country_id and list
subpartition by sales_channel.

8
Interval Partitioning
Interval partitioning is an extension of range partitioning which instructs the database to automatically create partitions of a specified
interval when data inserted into the table exceeds all of the existing range partitions. You must specify at least one range partition.
The range partitioning key value determines the high value of the range partitions, which is called the transition point, and the
database creates interval partitions for data beyond that transition point. The lower boundary of every interval partition is the non-
inclusive upper boundary of the previous range or interval partition.
When using interval partitioning, consider the following restrictions:
 You can only specify one partitioning key column, and it must be of NUMBER or DATE type.
 Interval partitioning is not supported for index-organized tables

Example code for interval partitioning by month:


PARTITION BY RANGE (Date column) INTERVAL (NUMTOYMINTERVAL (1, 'MONTH'))

Overview of Partitioned Indexes


Just like partitioned tables, partitioned indexes improve manageability, availability, performance, and scalability. They can either be
partitioned independently (global indexes) or automatically linked to a table's partitioning method (local indexes). In general, you
should use global indexes for OLTP applications and local indexes for data warehousing or DSS applications. Also, whenever
possible, you should try to use local indexes because they are easier to manage. When deciding what kind of partitioned index to use,
you should consider the following guidelines in order:
If the table partitioning column is a subset of the index keys, use a local index. If this is the case, you are finished. If this is not the
case, continue to guideline 2.
If the index is unique and does not include the partitioning key columns, then use a global index. If this is the case, then you are
finished. Otherwise, continue to guideline 3.
If your priority is manageability, use a local index. If this is the case, you are finished. If this is not the case, continue to guideline 4.
If the application is an OLTP one and users need quick response times, use a global index. If the application is a DSS one and users
are more interested in throughput, use a local index.

Local Partitioned Indexes


Local partitioned indexes are easier to manage than other types of partitioned indexes. They also offer greater availability and are
common in DSS environments. The reason for this is equipartitioning: each partition of a local index is associated with exactly one
partition of the table. This enables Oracle to automatically keep the index partitions in sync with the table partitions, and makes each
table-index pair independent. Any actions that make one partition's data invalid or unavailable only affect a single partition.
You cannot explicitly add a partition to a local index. Instead, new partitions are added to local indexes only when you add a partition
to the underlying table. Likewise, you cannot explicitly drop a partition from a local index. Instead, local index partitions are dropped
only when you drop a partition from the underlying table.
A local index can be unique. However, in order for a local index to be unique, the partitioning key of the table must be part of the
index's key columns.

9
Global Partitioned Indexes
Oracle offers two types of global partitioned indexes: range partitioned and hash partitioned.

Global Range Partitioned Indexes


Global range partitioned indexes are flexible in that the degree of partitioning and the partitioning key are independent from the table's
partitioning method.
The highest partition of a global index must have a partition bound, all of whose values are MAXVALUE. This ensures that all rows
in the underlying table can be represented in the index. Global prefixed indexes can be unique or nonunique.
You cannot add a partition to a global index because the highest partition always has a partition bound of MAXVALUE. If you wish
to add a new highest partition, use the ALTER INDEX SPLIT PARTITION statement. If a global index partition is empty, you can
explicitly drop it by issuing the ALTER INDEX DROP PARTITION statement. If a global index partition contains data, dropping the
partition causes the next highest partition to be marked unusable. You cannot drop the highest partition in a global index.

Global Hash Partitioned Indexes


Global hash partitioned indexes improve performance by spreading out contention when the index is monotonically growing. In other
words, most of the index insertions occur only on the right edge of an index.
These indexes can be maintained by appending the clause UPDATE INDEXES to the SQL statements for the operation. The two
advantages to maintaining global indexes:
The index remains available and online throughout the operation. Hence no other applications are affected by this operation.
The index doesn't have to be rebuilt after the operation

10
Global Partitioned Indexes

Global Non-Partitioned Indexes


Global non-partitioned indexes behave just like a non-partitioned index.

11
Oracle Bulk Operations
Without the bulk bind, PL/SQL sends a SQL statement to the SQL engine for each record that is inserted, updated, or deleted leading
to context switches that hurt performance

FORALL Statement
The FORALL statement runs one DML statement multiple times, with different values in the VALUES and WHERE clauses. The
different values come from existing, populated collections or host arrays. The FORALL statement is usually much faster than an
equivalent FOR LOOP statement.

Note:
You can use the FORALL statement only in server programs, not in client programs.
Example DELETE Statement in FORALL Statement
DECLARE
TYPE NumList IS VARRAY(20) OF NUMBER;
depts NumList := NumList(10, 30, 70); -- department numbers
BEGIN
FORALL i IN depts.FIRST..depts.LAST
DELETE FROM employees_temp
WHERE department_id = depts(i);
END;

Time Difference for INSERT Statement in FOR LOOP and FORALL Statement
DROP TABLE parts1;
CREATE TABLE parts1 (
pnum INTEGER,
pname VARCHAR2(15)
);
DROP TABLE parts2;
CREATE TABLE parts2 (
pnum INTEGER,
pname VARCHAR2(15)
);
DECLARE
TYPE NumTab IS TABLE OF parts1.pnum%TYPE INDEX BY PLS_INTEGER;
TYPE NameTab IS TABLE OF parts1.pname%TYPE INDEX BY PLS_INTEGER;
pnums NumTab;
pnames NameTab;
iterations CONSTANT PLS_INTEGER := 50000;
t1 INTEGER;
t2 INTEGER;
t3 INTEGER;
BEGIN
FOR j IN 1..iterations LOOP -- populate collections
pnums(j) := j;
pnames(j) := 'Part No. ' || TO_CHAR(j);
END LOOP;
t1 := DBMS_UTILITY.get_time;
FOR i IN 1..iterations LOOP
INSERT INTO parts1 (pnum, pname)

12
VALUES (pnums(i), pnames(i));
END LOOP;
t2 := DBMS_UTILITY.get_time;
FORALL i IN 1..iterations
INSERT INTO parts2 (pnum, pname)
VALUES (pnums(i), pnames(i));
t3 := DBMS_UTILITY.get_time;
DBMS_OUTPUT.PUT_LINE('Execution Time (secs)');
DBMS_OUTPUT.PUT_LINE('---------------------');
DBMS_OUTPUT.PUT_LINE('FOR LOOP: ' || TO_CHAR((t2 - t1)/100));
DBMS_OUTPUT.PUT_LINE('FORALL: ' || TO_CHAR((t3 - t2)/100));
COMMIT;
END;
/
Result is similar to:
Execution Time (secs)
---------------------
FOR LOOP: 5.97
FORALL: .07
PL/SQL procedure successfully completed.

Example Handling FORALL Exceptions after FORALL Statement Completes


CREATE OR REPLACE PROCEDURE p AUTHID DEFINER AS
TYPE NumList IS TABLE OF NUMBER;
depts NumList := NumList(10, 20, 30);
error_message VARCHAR2(100);
bad_stmt_no PLS_INTEGER;
bad_deptno emp_temp.deptno%TYPE;
bad_job emp_temp.job%TYPE;
dml_errors EXCEPTION;
PRAGMA EXCEPTION_INIT(dml_errors, -24381);
BEGIN
-- Populate table:
INSERT INTO emp_temp (deptno, job) VALUES (10, 'Clerk');
INSERT INTO emp_temp (deptno, job) VALUES (20, 'Bookkeeper');
INSERT INTO emp_temp (deptno, job) VALUES (30, 'Analyst');
COMMIT;
-- Append 9-character string to each job:
FORALL j IN depts.FIRST..depts.LAST SAVE EXCEPTIONS
UPDATE emp_temp SET job = job || ' (Senior)'
WHERE deptno = depts(j);
EXCEPTION
WHEN dml_errors THEN
FOR i IN 1..SQL%BULK_EXCEPTIONS.COUNT LOOP
error_message := SQLERRM(-(SQL%BULK_EXCEPTIONS(i).ERROR_CODE));
DBMS_OUTPUT.PUT_LINE (error_message);
bad_stmt_no := SQL%BULK_EXCEPTIONS(i).ERROR_INDEX;
DBMS_OUTPUT.PUT_LINE('Bad statement #: ' || bad_stmt_no);
bad_deptno := depts(bad_stmt_no);
DBMS_OUTPUT.PUT_LINE('Bad department #: ' || bad_deptno);
SELECT job INTO bad_job FROM emp_temp WHERE deptno = bad_deptno;
DBMS_OUTPUT.PUT_LINE('Bad job: ' || bad_job);

13
END LOOP;
COMMIT; -- Commit results of successful updates
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Unrecognized error.');
RAISE;
END;
/

Example Showing Number of Rows Affected by Each DELETE in FORALL


DROP TABLE emp_temp;
CREATE TABLE emp_temp AS SELECT * FROM employees;
DECLARE
TYPE NumList IS TABLE OF NUMBER;
depts NumList := NumList(30, 50, 60);
BEGIN
FORALL j IN depts.FIRST..depts.LAST
DELETE FROM emp_temp WHERE department_id = depts(j);
FOR i IN depts.FIRST..depts.LAST LOOP
DBMS_OUTPUT.PUT_LINE (
'Statement #' || i || ' deleted ' ||
SQL%BULK_ROWCOUNT(i) || ' rows.'
);
END LOOP;
DBMS_OUTPUT.PUT_LINE('Total rows deleted: ' || SQL%ROWCOUNT);
END;
/
Result:
Statement #1 deleted 6 rows.
Statement #2 deleted 45 rows.
Statement #3 deleted 5 rows.
Total rows deleted: 56

Example Showing Number of Rows Affected by Each INSERT SELECT in


FORALL
DROP TABLE emp_by_dept;
CREATE TABLE emp_by_dept AS
SELECT employee_id, department_id
FROM employees
WHERE 1 = 0;
DECLARE
TYPE dept_tab IS TABLE OF departments.department_id%TYPE;
deptnums dept_tab;
BEGIN
SELECT department_id BULK COLLECT INTO deptnums FROM departments;
FORALL i IN 1..deptnums.COUNT
INSERT INTO emp_by_dept (employee_id, department_id)
SELECT employee_id, department_id
FROM employees
WHERE department_id = deptnums(i)
ORDER BY department_id, employee_id;
FOR i IN 1..deptnums.COUNT LOOP

14
-- Count how many rows were inserted for each department; that is,
-- how many employees are in each department.
DBMS_OUTPUT.PUT_LINE (
'Dept '||deptnums(i)||': inserted '||
SQL%BULK_ROWCOUNT(i)||' records'
);
END LOOP;
DBMS_OUTPUT.PUT_LINE('Total records inserted: ' || SQL%ROWCOUNT);
END;
/
Result:
Dept 10: inserted 1 records
Dept 20: inserted 2 records
Dept 30: inserted 6 records
Dept 40: inserted 1 records
Dept 50: inserted 45 records
Dept 60: inserted 5 records
Dept 70: inserted 1 records
Dept 80: inserted 34 records
Dept 90: inserted 3 records
Dept 100: inserted 6 records
Dept 110: inserted 2 records
Dept 120: inserted 0 records
Dept 130: inserted 0 records
Dept 140: inserted 0 records
Dept 150: inserted 0 records
Dept 160: inserted 0 records
Dept 170: inserted 0 records
Dept 180: inserted 0 records
Dept 190: inserted 0 records
Dept 200: inserted 0 records
Dept 210: inserted 0 records
Dept 220: inserted 0 records
Dept 230: inserted 0 records
Dept 240: inserted 0 records
Dept 250: inserted 0 records
Dept 260: inserted 0 records
Dept 270: inserted 0 records
Dept 280: inserted 0 records
Total records inserted: 106

APPEND_VALUES Hint
We have been able take advantage of the performance benefits of direct-path inserts in "INSERT ... SELECT" operations for a long
time using the APPEND Hint.
INSERT /*+ APPEND */ INTO dest_tab SELECT * FROM source_tab;
The APPEND_VALUES hint in Oracle 11g Release 2 now allows us to take advantage of direct-path inserts when insert statements
include a VALUES clause. Typically we would only want to do this when the insert statement is part of bulk operation using the
FORALL statement. We will use the following table to demonstrate the effect of the hint.
CREATE TABLE forall_test (
id NUMBER(10),

15
code VARCHAR2(10),
description VARCHAR2(50)
);
ALTER TABLE forall_test ADD (CONSTRAINT forall_test_pk PRIMARY KEY (id));
ALTER TABLE forall_test ADD (CONSTRAINT forall_test_uk UNIQUE (code));
The following code populates the base table then deletes half of the rows before performing each test. This is because during a regular
(conventional-path) insert, Oracle tries to use up any free space currently allocated to the table, including space left from previous
delete operations. In contrast direct-path inserts ignore existing free space and append the data to the end of the table. After preparing
the base table we time how long it takes to perform conventional-path insert as part of the FORALL statement. Next, we repeat the
same test, but this time use a the APPEND_VALUES hint to give us direct-path inserts.
SET SERVEROUTPUT ON
DECLARE
TYPE t_forall_test_tab IS TABLE OF forall_test%ROWTYPE;
l_tab t_forall_test_tab := t_forall_test_tab();
l_start NUMBER;
l_size NUMBER := 1000000;
PROCEDURE prepare_table AS
BEGIN
EXECUTE IMMEDIATE 'TRUNCATE TABLE forall_test';
INSERT /*+ APPEND */ INTO forall_test
SELECT level, TO_CHAR(level), 'Description: ' || TO_CHAR(level)
FROM dual
CONNECT BY level <= l_size;
COMMIT;
DELETE FROM forall_test WHERE MOD(id, 2) = 0;
COMMIT;
END prepare_table;
BEGIN
-- Populate collection.
FOR i IN 1 .. (l_size/2) LOOP
l_tab.extend;
l_tab(l_tab.last).id := i*2;
l_tab(l_tab.last).code := TO_CHAR(i*2);
l_tab(l_tab.last).description := 'Description: ' || TO_CHAR(i*2);
END LOOP;
prepare_table;
-- ----------------------------------------------------------------
-- Test 1: Time bulk inserts.
l_start := DBMS_UTILITY.get_time;
FORALL i IN l_tab.first .. l_tab.last
INSERT INTO forall_test VALUES l_tab(i);
DBMS_OUTPUT.put_line('Bulk Inserts : ' ||
(DBMS_UTILITY.get_time - l_start));
-- ----------------------------------------------------------------
ROLLBACK;
prepare_table;
-- ----------------------------------------------------------------
-- Test 2: Time bulk inserts using the APPEND_VALUES hint.
l_start := DBMS_UTILITY.get_time;
FORALL i IN l_tab.first .. l_tab.last
INSERT /*+ APPEND_VALUES */ INTO forall_test VALUES l_tab(i);
DBMS_OUTPUT.put_line('Bulk Inserts /*+ APPEND_VALUES */ : ' ||

16
(DBMS_UTILITY.get_time - l_start));
-- ----------------------------------------------------------------
ROLLBACK;
END;
/
Bulk Inserts : 394
Bulk Inserts /*+ APPEND_VALUES */ : 267
PL/SQL procedure successfully completed.
SQL>
We can see that the APPEND_VALUES hint gives us better performance by allowing us to use direct-path inserts within the
FORALL statement. Remember there are factors other than performance to consider before deciding to use direct-path inserts. Also,
this hint does not currently work with the SAVE EXCEPTIONS clause. If you try to use them together you will get the following
error.
ORA-38910: BATCH ERROR mode is not supported for this operation
Oracle Database inserts data into a table in one of two ways:

During conventional INSERT operations


The database reuses free space in the table, interleaving newly inserted data with existing data. During such operations, the database
also maintains referential integrity constraints. Reused, and referential integrity constraints are ignored. Direct-path insert can perform
significantly better than conventional

During direct-path INSERT operations


The database appends the inserted data after existing data in the table. Data is written directly into datafiles, bypassing the buffer
cache. Free space in the table is not insert.

How Direct-Path INSERT Works


You can use direct-path INSERT on both partitioned and non-partitioned tables.
Serial Direct-Path INSERT into Partitioned or Non-partitioned Tables
The single process inserts data beyond the current high water mark of the table segment or of each partition segment. (The high-water
mark is the level at which blocks have never been formatted to receive data.) When a COMMIT runs, the high-water mark is updated
to the new value, making the data visible to users.
Parallel Direct-Path INSERT into Partitioned Tables
This situation is analogous to serial direct-path INSERT. Each parallel execution server is assigned one or more partitions, with no
more than one process working on a single partition. Each parallel execution server inserts data beyond the current high-water mark of
its assigned partition segment(s). When a COMMIT runs, the high-water mark of each partition segment is updated to its new value,
making the data visible to users.

Parallel Direct-Path INSERT into Non-partitioned Tables


Each parallel execution server allocates a new temporary segment and inserts data into that temporary segment. When a COMMIT
runs, the parallel execution coordinator merges the new temporary segments into the primary table segment, where it is visible to
users.

Loading Data with Direct-Path INSERT


You can load data with direct-path INSERT by using direct-path INSERT SQL statements, inserting data in parallel mode, or by
using the Oracle SQL*Loader utility in direct-path mode. Direct-path inserts can be done in either serial or parallel mode.

17
Serial Mode Inserts with SQL Statements
You can activate direct-path insert in serial mode with SQL in the following ways:
If you are performing an INSERT with a subquery, specify the APPEND hint in each INSERT statement, either immediately after the
INSERT keyword, or immediately after the SELECT keyword in the subquery of the INSERT statement.
If you are performing an INSERT with the VALUES clause, specify the APPEND_VALUES hint in each INSERT statement
immediately after the INSERT keyword. Direct-path insert with the VALUES clause is best used when there are hundreds of
thousands or millions of rows to load. The typical usage scenario is for array inserts using OCI. Another usage scenario might be
inserts in a FORALL loop in PL/SQL.
If you specify the APPEND hint (as opposed to the APPEND_VALUES hint) in an INSERT statement with a VALUES clause, the
APPEND hint is ignored and a conventional insert is performed.

Parallel Mode Inserts with SQL Statements


When you are inserting in parallel mode, direct-path INSERT is the default. In order to run in parallel DML mode, the following
requirements must be met:
You must have Oracle Enterprise Edition installed.
You must enable parallel DML in your session. To do this, submit the following statement:
ALTER SESSION { ENABLE | FORCE } PARALLEL DML;
You must specify the parallel attribute for the target table, either at create time or subsequently, or you must specify the PARALLEL
hint for each insert operation.
To disable direct-path INSERT, specify the NOAPPEND hint in each INSERT statement. Doing so overrides parallel DML mode.

Note:
You cannot query or modify direct-path inserted data immediately after the insert is complete. If you attempt to do so, an ORA-12838
error is generated. You must first issue a COMMIT statement before attempting to read or modify the newly-inserted data.

Specifying the Logging Mode for Direct-Path INSERT


Direct-path INSERT lets you choose whether to log redo and undo information during the insert operation.
You can specify logging mode for a table, partition, index, or LOB storage at create time (in a CREATE statement) or subsequently
(in an ALTER statement).
If you do not specify either LOGGING or NOLOGGING at these times:
The logging attribute of a partition defaults to the logging attribute of its table.
The logging attribute of a table or index defaults to the logging attribute of the tablespace in which it resides.
The logging attribute of LOB storage defaults to LOGGING if you specify CACHE for LOB storage. If you do not specify CACHE,
then the logging attributes defaults to that of the tablespace in which the LOB values resides.
You set the logging attribute of a tablespace in a CREATE TABLESPACE or ALTER TABLESPACE statements.

Note:
If the database or tablespace is in FORCE LOGGING mode, then direct path INSERT always logs, regardless of the logging setting.

Direct-Path INSERT with Logging


In this mode, Oracle Database performs full redo logging for instance and media recovery. If the database is in ARCHIVELOG mode,
then you can archive redo logs to tape? If the database is in NOARCHIVELOG mode, then you can recover instance crashes but not
disk failures.
Direct-Path INSERT without Logging
In this mode, Oracle Database inserts data without redo or undo logging. Instead, the database logs a small number of block range
invalidation redo records and periodically updates the control file with information about the most recent direct write.
Direct-path insert without logging improves performance. However, if you subsequently must perform media recovery, the
invalidation redo records mark a range of blocks as logically corrupt, because no redo data was logged for them. Therefore, it is
important that you back up the data after such an insert operation.

18
Beginning with release 11.2.0.2 of Oracle Database, you can significantly improve the performance of unrecoverable direct path
inserts by disabling the periodic update of the controlfiles. You do so by setting the initialization parameter
DB_UNRECOVERABLE_SCN_TRACKING to FALSE. However, if you perform an unrecoverable direct path insert with these
controlfile updates disabled, you will no longer be able to accurately query the database to determine if any datafiles are currently
unrecoverable.
Additional Considerations for Direct-Path INSERT
The following are some additional considerations when using direct-path INSERT.
Compressed Tables
If a table is created with the basic compression, then you must use direct-path INSERT to compress table data as it is loaded. If a table
is created with OLTP, warehouse, or online archival compression, then best compression ratios are achieved with direct-path insert.
Index Maintenance with Direct-Path INSERT
Oracle Database performs index maintenance at the end of direct-path INSERT operations on tables (partitioned or non-partitioned)
that have indexes. This index maintenance is performed by the parallel execution servers for parallel direct-path INSERT or by the
single process for serial direct-path INSERT. You can avoid the performance impact of index maintenance by making the index
unusable before the INSERT operation and then rebuilding it afterward.
Space Considerations with Direct-Path INSERT
Direct-path INSERT requires more space than conventional-path INSERT.
All serial direct-path INSERT operations, as well as parallel direct-path INSERT into partitioned tables, insert data above the high-
water mark of the affected segment. This requires some additional space.
Parallel direct-path INSERT into non-partitioned tables requires even more space, because it creates a temporary segment for each
degree of parallelism. If the non-partitioned table is not in a locally managed tablespace in automatic segment-space management
mode, you can modify the values of the NEXT and PCTINCREASE storage parameter and MINIMUM EXTENT tablespace
parameter to provide sufficient (but not excess) storage for the temporary segments. Choose values for these parameters so that:
The size of each extent is not too small (no less than 1 MB). This setting affects the total number of extents in the object.
The size of each extent is not so large that the parallel INSERT results in wasted space on segments that are larger than necessary.
After the direct-path INSERT operation is complete, you can reset these parameters to settings more appropriate for serial operations.
Locking Considerations with Direct-Path INSERT
During direct-path INSERT, the database obtains exclusive locks on the table (or on all partitions of a partitioned table). As a result,
users cannot perform any concurrent insert, update, or delete operations on the table, and concurrent index creation and build
operations are not permitted. Concurrent queries, however, are supported, but the query will return only the information before the
insert operation.

What is "high water mark"?


All Oracle segments have an upper boundary containing the data within the segment. This upper boundary is called the "high water
mark" or HWM. The high water mark is an indicator that marks blocks that are allocated to a segment, but are not used yet. This high
water mark typically bumps up at 5 data blocks at a time.
Determining HWM, You can use *_tables for BLOCKS, EMPTY_BLOCKS - some information for high water mark.
BLOCKS: Number blocks that has been formatted to recieve data
EMPTY_BLOCKS: Among the allocated blocks, the blocks that were never used
SQL> SELECT blocks, empty_blocks, num_rows FROM user_tables WHERE table_name ='TB_DATA1';

BLOCKS EMPTY_BLOCKS NUM_ROWS


---------- ------------ ----------
3194 6 1000
You can shrink space in a table, index-organized table, index, partition, subpartition, materialized view, or materialized view log. You
do this using ALTER TABLE, ALTER INDEX, ALTER MATERIALIZED VIEW, or ALTER MATERIALIZED VIEW LOG
statement with the SHRINK SPACE clause.
Two optional clauses let you control how the shrink operation proceeds:
The COMPACT clause lets you divide the shrink segment operation into two phases. When you specify COMPACT, Oracle Database
defragments the segment space and compacts the table rows but postpones the resetting of the high water mark and the deallocation of

19
the space until a future time. This option is useful if you have long-running queries that might span the operation and attempt to read
from blocks that have been reclaimed. The defragmentation and compaction results are saved to disk, so the data movement does not
have to be redone during the second phase. You can reissue the SHRINK SPACE clause without the COMPACT clause during off-
peak hours to complete the second phase.
The CASCADE clause extends the segment shrink operation to all dependent segments of the object. For example, if you specify
CASCADE when shrinking a table segment, all indexes of the table will also be shrunk. (You need not specify CASCADE to shrink
the partitions of a partitioned table.) To see a list of dependent segments of a given object, you can run the
OBJECT_DEPENDENT_SEGMENTS procedure of the DBMS_SPACE package.
As with other DDL operations, segment shrink causes subsequent SQL statements to be reparsed because of invalidation of cursors
unless you specify the COMPACT clause.
Examples
Shrink a table and all of its dependent segments (including BASICFILE LOB segments):
ALTER TABLE employees SHRINK SPACE CASCADE;
Shrink a BASICFILE LOB segment only:
ALTER TABLE employees MODIFY LOB (perf_review) (SHRINK SPACE);
Shrink a single partition of a partitioned table:
ALTER TABLE customers MODIFY PARTITION cust_P1 SHRINK SPACE;
Shrink an IOT index segment and the overflow segment:
ALTER TABLE cities SHRINK SPACE CASCADE;
Shrink an IOT overflow segment only:
ALTER TABLE cities OVERFLOW SHRINK SPACE

Deallocating Unused Space


When you deallocate unused space, the database frees the unused space at the unused (high water mark) end of the database segment
and makes the space available for other segments in the tablespace.
Before deallocation, you can run the UNUSED_SPACE procedure of the DBMS_SPACE package, which returns information about
the position of the high water mark and the amount of unused space in a segment. For segments in locally managed tablespaces with
automatic segment space management, use the SPACE_USAGE procedure for more accurate information on unused space.
The following statements deallocate unused space in a segment (table, index or cluster):
ALTER TABLE table DEALLOCATE UNUSED KEEP integer;
ALTER INDEX index DEALLOCATE UNUSED KEEP integer;
ALTER CLUSTER cluster DEALLOCATE UNUSED KEEP integer;
The KEEP clause is optional and lets you specify the amount of space retained in the segment. You can verify that the deallocated
space is freed by examining the DBA_FREE_SPACE view
Dropping Unused Object Storage
The DBMS_SPACE_ADMIN package includes the DROP_EMPTY_SEGMENTS procedure, which enables you to drop segments
for empty tables and partitions that have been migrated from previous releases. This includes segments of dependent objects of the
table, such as index segments, where possible.
The following example drops empty segments from every table in the database.
BEGIN
DBMS_SPACE_ADMIN.DROP_EMPTY_SEGMENTS();
END;
The following drops empty segments from the HR.EMPLOYEES table, including dependent objects.
BEGIN
DBMS_SPACE_ADMIN.DROP_EMPTY_SEGMENTS(
schema_name => 'HR',
table_name => 'EMPLOYEES');
END;

20
BULK COLLECT Clause
The BULK COLLECT clause, a feature of bulk SQL, returns results from SQL to PL/SQL in batches rather than one at a time. The
BULK COLLECT clause can appear in:
SELECT INTO statement
FETCH statement
RETURNING INTO clause of:
DELETE statement
INSERT statement
UPDATE statement
EXECUTE IMMEDIATE statement
With the BULK COLLECT clause, each of the preceding statements retrieves an entire result set and stores it in one or more
collection variables in a single operation (which is more efficient than using a loop statement to retrieve one result row at a time).

Note:
PL/SQL processes the BULK COLLECT clause similar to the way it processes a FETCH statement inside a LOOP statement.
PL/SQL does not raise an exception when a statement with a BULK COLLECT clause returns no rows. You must check the target
collections for emptiness

Example Bulk-Selecting Two Database Columns into Two Nested Tables


DECLARE
TYPE NumTab IS TABLE OF employees.employee_id%TYPE;
TYPE NameTab IS TABLE OF employees.last_name%TYPE;
enums NumTab;
names NameTab;
PROCEDURE print_first_n (n POSITIVE) IS
BEGIN
IF enums.COUNT = 0 THEN
DBMS_OUTPUT.PUT_LINE ('Collections are empty.');
ELSE
DBMS_OUTPUT.PUT_LINE ('First ' || n || ' employees:');
FOR i IN 1 .. n LOOP
DBMS_OUTPUT.PUT_LINE (
' Employee #' || enums(i) || ': ' || names(i));
END LOOP;
END IF;
END;
BEGIN
SELECT employee_id, last_name
BULK COLLECT INTO enums, names
FROM employees
ORDER BY employee_id;
print_first_n(3);
print_first_n(6);
END;
/
Result:
First 3 employees:
Employee #100: King
Employee #101: Kochhar
Employee #102: De Haan
First 6 employees:

21
Employee #100: King
Employee #101: Kochhar
Employee #102: De Haan
Employee #103: Hunold
Employee #104: Ernst
Employee #105: Austin

Cursor Workaround Example


CREATE OR REPLACE TYPE numbers_type IS
TABLE OF INTEGER;
CREATE OR REPLACE PROCEDURE p (i IN INTEGER) AUTHID DEFINER IS
numbers1 numbers_type := numbers_type(1,2,3,4,5);
CURSOR c IS
SELECT a.COLUMN_VALUE
FROM TABLE(numbers1) a
WHERE a.COLUMN_VALUE > p.i
ORDER BY a.COLUMN_VALUE;
BEGIN
DBMS_OUTPUT.PUT_LINE('Before FETCH statement');
DBMS_OUTPUT.PUT_LINE('numbers1.COUNT() = ' || numbers1.COUNT());
FOR j IN 1..numbers1.COUNT() LOOP
DBMS_OUTPUT.PUT_LINE('numbers1(' || j || ') = ' || numbers1(j));
END LOOP;
OPEN c;
FETCH c BULK COLLECT INTO numbers1;
CLOSE c;
DBMS_OUTPUT.PUT_LINE('After FETCH statement');
DBMS_OUTPUT.PUT_LINE('numbers1.COUNT() = ' || numbers1.COUNT());
IF numbers1.COUNT() > 0 THEN
FOR j IN 1..numbers1.COUNT() LOOP
DBMS_OUTPUT.PUT_LINE('numbers1(' || j || ') = ' || numbers1(j));
END LOOP;
END IF;
END p;

Example Limiting Bulk Selection with ROWNUM, SAMPLE, and FETCH FIRST
DECLARE
TYPE SalList IS TABLE OF employees.salary%TYPE;
sals SalList;
BEGIN
SELECT salary BULK COLLECT INTO sals FROM employees
WHERE ROWNUM <= 50;
SELECT salary BULK COLLECT INTO sals FROM employees
SAMPLE (10);
SELECT salary BULK COLLECT INTO sals FROM employees
FETCH FIRST 50 ROWS ONLY;
END;

22
Example Limiting Bulk FETCH with LIMIT
DECLARE
TYPE numtab IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
CURSOR c1 IS
SELECT employee_id
FROM employees
WHERE department_id = 80
ORDER BY employee_id;
empids numtab;
BEGIN
OPEN c1;
LOOP -- Fetch 10 rows or fewer in each iteration
FETCH c1 BULK COLLECT INTO empids LIMIT 10;
DBMS_OUTPUT.PUT_LINE ('------- Results from One Bulk Fetch --------');
FOR i IN 1..empids.COUNT LOOP
DBMS_OUTPUT.PUT_LINE ('Employee Id: ' || empids(i));
END LOOP;
EXIT WHEN c1%NOTFOUND;
END LOOP;
CLOSE c1;
END;

Example Returning Deleted Rows in Two Nested Tables


DROP TABLE emp_temp;
CREATE TABLE emp_temp AS
SELECT * FROM employees
ORDER BY employee_id;
DECLARE
TYPE NumList IS TABLE OF employees.employee_id%TYPE;
enums NumList;
TYPE NameList IS TABLE OF employees.last_name%TYPE;
names NameList;
BEGIN
DELETE FROM emp_temp
WHERE department_id = 30
RETURNING employee_id, last_name
BULK COLLECT INTO enums, names;
DBMS_OUTPUT.PUT_LINE ('Deleted ' || SQL%ROWCOUNT || ' rows:');
FOR i IN enums.FIRST .. enums.LAST
LOOP
DBMS_OUTPUT.PUT_LINE ('Employee #' || enums(i) || ': ' || names(i));
END LOOP;
END;

Example DELETE with RETURN BULK COLLECT INTO in FORALL Statement


DROP TABLE emp_temp;
CREATE TABLE emp_temp AS
SELECT * FROM employees
ORDER BY employee_id, department_id;
DECLARE
TYPE NumList IS TABLE OF NUMBER;

23
depts NumList := NumList(10,20,30);

TYPE enum_t IS TABLE OF employees.employee_id%TYPE;


e_ids enum_t;
TYPE dept_t IS TABLE OF employees.department_id%TYPE;
d_ids dept_t;
BEGIN
FORALL j IN depts.FIRST..depts.LAST
DELETE FROM emp_temp
WHERE department_id = depts(j)
RETURNING employee_id, department_id
BULK COLLECT INTO e_ids, d_ids;
DBMS_OUTPUT.PUT_LINE ('Deleted ' || SQL%ROWCOUNT || ' rows:');
FOR i IN e_ids.FIRST .. e_ids.LAST
LOOP
DBMS_OUTPUT.PUT_LINE (
'Employee #' || e_ids(i) || ' from dept #' || d_ids(i)
);
END LOOP;
END;
/

Chaining Pipelined Table Functions for Multiple


Transformations
Chaining pipelined table functions is an efficient way to perform multiple transformations on data.

Note:
You cannot run a pipelined table function over a database link. The reason is that the return type of a pipelined table function is a SQL
user-defined type, which can be used only in a single database. Although the return type of a pipelined table function might appear to
be a PL/SQL type, the database actually converts that PL/SQL type to a corresponding SQL user-defined type.

Overview of Table Functions


A table function is a user-defined PL/SQL function that returns a collection of rows. You can select from this collection as if it were a
database table by invoking the table function inside the TABLE clause in a SELECT statement or using the name of the collection
direct (starting from 12c only). For example:
SELECT * FROM TABLE(table_function_name(parameter_list))
Oracle Database 12c Example:

Create or replace package TEST as


Type pls_integer_rec is record
(id pls_integer);
Type pls_integer_type is table of pls_integer_rec index by pls_integer;
pls_tab pls_integer_type;
Function get_even_count(v_from pls_integer, v_to pls_integer) return number;
END;

Create or replace package Body TEST as

Function get_even_count (v_from pls_integer, v_to pls_integer) return number is

24
Result pls_integer;
BEGIN
For I in v_from..v_to LOOP
pls_tab(I):=I;
END LOOP:

SELECT count(1)
Into Result
From TABLE(TEST.pls_tab) t
Where mod(t.id,2) = 0;
Return Result;
END;

END:

SQL> select Test.get_even_count(10,20) from dual;

To improve the performance of a table function, you can enable the function for parallel execution with the PARALLEL_ENABLE
option.
Functions enabled for parallel execution can run concurrently.
Pipeline the function results, with the PIPELINED option.
A pipelined table function returns a row to its invoker immediately after processing that row and continues to process rows. Response
time improves because the entire collection need not be constructed and returned to the server before the query can return a single
result row. (Also, the function needs less memory, because the object cache need not materialize the entire collection.)
Caution:
A pipelined table function always references the current state of the data. If the data in the collection changes after the cursor opens
for the collection, then the cursor reflects the changes. PL/SQL variables are private to a session and are not transactional. Therefore,
read consistency, well known for its applicability to table data, does not apply to PL/SQL collection variables.

Creating Pipelined Table Functions


A pipelined table function must be either a standalone function or a package function.
PIPELINED Option (Required)
For a standalone function, specify the PIPELINED option in the CREATE FUNCTION statement for a package function, specify the
PIPELINED option in both the function declaration and function definition
Memory Usage Comparison
The following function returns the current value for a specified statistic. It will allow us to compare the memory used by regular and
pipelined table functions.
CREATE OR REPLACE FUNCTION get_stat (p_stat IN VARCHAR2) RETURN NUMBER AS
l_return NUMBER;
BEGIN
SELECT ms.value
INTO l_return
FROM v$mystat ms,
v$statname sn
WHERE ms.statistic# = sn.statistic#
AND sn.name = p_stat;
RETURN l_return;
END get_stat;
/

25
First we test the regular table function by creating a new connection and querying a large collection. Checking the PGA memory
allocation before and after the test allows us to see how much memory was allocated as a result of the test.
-- Create a new session.
CONN test/test
-- Test table function.
SET SERVEROUTPUT ON
DECLARE
l_start NUMBER;
BEGIN
l_start := get_stat('session pga memory');
FOR cur_rec IN (SELECT *
FROM TABLE(get_tab_tf(100000)))
LOOP
NULL;
END LOOP;
DBMS_OUTPUT.put_line('Regular table function : ' ||
(get_stat('session pga memory') - l_start));
END;
/
Regular table function : 22872064

PL/SQL procedure successfully completed.

SQL>
Next, we repeat the test for the pipelined table function.
-- Create a new session.
CONN test/test

-- Test pipelined table function.


SET SERVEROUTPUT ON
DECLARE
l_start NUMBER;
BEGIN
l_start := get_stat('session pga memory');

FOR cur_rec IN (SELECT *


FROM TABLE(get_tab_ptf(100000)))
LOOP
NULL;
END LOOP;

DBMS_OUTPUT.put_line('Pipelined table function : ' ||


(get_stat('session pga memory') - l_start));
END;
/
Pipelined table function : 65536

PL/SQL procedure successfully completed.

Cardinality
Oracle estimates the cardinality of a pipelined table function based on the database block size. When using the default block size, the
optimizer will always assume the cardinality is 8168 rows.

26
SET AUTOTRACE TRACE EXPLAIN

-- Return 10 rows.
SELECT *
FROM TABLE(get_tab_ptf(10));

Execution Plan
----------------------------------------------------------
Plan hash value: 822655197

-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 8168 | 16336 | 8 (0)| 00:02:19 |
| 1 | COLLECTION ITERATOR PICKLER FETCH| GET_TAB_PTF | 8168 | 16336 | 8 (0)| 00:02:19 |
-------------------------------------------------------------------------------------------------

SET AUTOTRACE OFF


This is fine if you are just querying the pipelined table function, but if you plan to use it in a join it can adversely affect the execution
plan.
There are 4 ways to correct the cardinality estimate for pipelined table functions:
 CARDINALITY hint (9i+): Undocumented
 OPT_ESTIMATE hint (10g+): Undocumented
 DYNAMIC_SAMPLING hint (11gR1+): Causes a full scan of the pipelined table function to estimate the cardinality before
running it in the query itself. This is very wasteful.
 Extensible Optimizer (9i+): The extensible optimizer feature allows us to tell the optimizer what the cardinality should be in
a supported manner.
Cardinality Feedback: In 11gR2 the optimizer notices if the actual cardinality from a query against a table function differs from the
expected cardinality. Subsequent queries will have their cardinality adjusted based on this feedback. If the statement is aged out of the
shared pool, or the instance is restarted, the cardinality feedback is lost. In 12c, cardinality feedback is persisted in the SYSAUX
tablespace.
To use the extensible optimizer we need to add a parameter to the pipelined table functions, which will be used to manually tell the
optimizer what cardinalty to use.
CREATE OR REPLACE FUNCTION get_tab_ptf (p_cardinality IN INTEGER DEFAULT 1)
RETURN t_tf_tab PIPELINED AS
BEGIN
FOR i IN 1 .. 10 LOOP
PIPE ROW (t_tf_row(i, 'Description for ' || i));
END LOOP;

RETURN;
END;
/
Notice the p_cardinality parameter isn't used anywhere in the function itself you can use or not it is optional.
Next, we build a type and type body to set the cardinality manually. Notice the reference to the p_cardinality parameter in the type.
CREATE OR REPLACE TYPE t_ptf_stats AS OBJECT (
dummy INTEGER,

STATIC FUNCTION ODCIGetInterfaces (


p_interfaces OUT SYS.ODCIObjectList
) RETURN NUMBER,

27
STATIC FUNCTION ODCIStatsTableFunction (
p_function IN SYS.ODCIFuncInfo,
p_stats OUT SYS.ODCITabFuncStats,
p_args IN SYS.ODCIArgDescList,
p_cardinality IN INTEGER
) RETURN NUMBER
);
/

CREATE OR REPLACE TYPE BODY t_ptf_stats AS


STATIC FUNCTION ODCIGetInterfaces (
p_interfaces OUT SYS.ODCIObjectList
) RETURN NUMBER IS
BEGIN
p_interfaces := SYS.ODCIObjectList(
SYS.ODCIObject ('SYS', 'ODCISTATS2')
);
RETURN ODCIConst.success;
END ODCIGetInterfaces;

STATIC FUNCTION ODCIStatsTableFunction (


p_function IN SYS.ODCIFuncInfo,
p_stats OUT SYS.ODCITabFuncStats,
p_args IN SYS.ODCIArgDescList,
p_cardinality IN INTEGER
) RETURN NUMBER IS
BEGIN
p_stats := SYS.ODCITabFuncStats(NULL);
p_stats.num_rows := p_cardinality;
RETURN ODCIConst.success;
END ODCIStatsTableFunction;
END;
/
This type can be associated with any pipelined table function using the following command.
ASSOCIATE STATISTICS WITH FUNCTIONS get_tab_ptf USING t_ptf_stats;
We know the function returns 10 rows, but the optimizer doesn't. Regardless of the number of rows returned by the function, the
optimizer uses the value of the p_cardinality parameter as the cardinality estimate.
SET AUTOTRACE TRACE EXPLAIN

SELECT *
FROM TABLE(get_tab_ptf(p_cardinality => 10));

Execution Plan
----------------------------------------------------------
Plan hash value: 822655197

-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 20 | 8 (0)| 00:02:19 |
| 1 | COLLECTION ITERATOR PICKLER FETCH| GET_TAB_PTF | 10 | 20 | 8 (0)| 00:02:19 |
-------------------------------------------------------------------------------------------------

28
SELECT *
FROM TABLE(get_tab_ptf(p_cardinality => 10000));

Execution Plan
----------------------------------------------------------
Plan hash value: 822655197

-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10000 | 20000 | 8 (0)| 00:02:19 |
| 1 | COLLECTION ITERATOR PICKLER FETCH| GET_TAB_PTF | 10000 | 20000 | 8 (0)| 00:02:19 |
-------------------------------------------------------------------------------------------------

SET AUTOTRACE OFF

Implicit (Shadow) Types


Unlike regular table functions, pipelined table functions can be defined using record and table types defined in a package
specification.
-- Drop the previously created objects.
DROP FUNCTION get_tab_tf;
DROP FUNCTION get_tab_ptf;
DROP TYPE t_tf_tab;
DROP TYPE t_tf_row;

-- Build package containing record and table types internally.


CREATE OR REPLACE PACKAGE ptf_api AS
TYPE t_ptf_row IS RECORD (
id NUMBER,
description VARCHAR2(50)
);

TYPE t_ptf_tab IS TABLE OF t_ptf_row;

FUNCTION get_tab_ptf (p_rows IN NUMBER) RETURN t_ptf_tab PIPELINED;


END;
/

CREATE OR REPLACE PACKAGE BODY ptf_api AS

FUNCTION get_tab_ptf (p_rows IN NUMBER) RETURN t_ptf_tab PIPELINED IS


l_row t_ptf_row;
BEGIN
FOR i IN 1 .. p_rows LOOP
l_row.id := i;
l_row.description := 'Description for ' || i;
PIPE ROW (l_row);
END LOOP;

RETURN;
END;

29
END;
/

SELECT *
FROM TABLE(ptf_api.get_tab_ptf(10))
ORDER BY id DESC;

ID DESCRIPTION
---------- --------------------------------------------------
10 Description for 10
9 Description for 9
8 Description for 8
7 Description for 7
6 Description for 6
5 Description for 5
4 Description for 4
3 Description for 3
2 Description for 2
1 Description for 1

10 rows selected.

SQL>
This seems like a better solution than having to build all the database types manually, but behind the scenes Oracle is building the
shadow object types implicitly and it sometimes make some bugs when calling the function or select from it.
COLUMN object_name FORMAT A30

SELECT object_name, object_type


FROM user_objects;

OBJECT_NAME OBJECT_TYPE
------------------------------ -------------------
PTF_API PACKAGE BODY
SYS_PLSQL_82554_9_1 TYPE
SYS_PLSQL_82554_DUMMY_1 TYPE
SYS_PLSQL_82554_24_1 TYPE
PTF_API PACKAGE

5 rows selected.

SQL>
As you can see, Oracle has actually created three shadow object types with system generated names to support the types required by
the pipelined table function. For this reason I always build named database object types, rather than relying on the implicit types.

PARALLEL_ENABLE Option (Recommended)


To improve its performance, enable the pipelined table function for parallel execution by specifying the PARALLEL_ENABLE
option.
To parallel enable a pipelined table function the following conditions must be met.
The PARALLEL_ENABLE clause must be included.
It must have one or more REF CURSOR input parameters.
It must have a PARTITION BY clause to define a partitioning method for the workload. Weakly typed ref cursors can only use the
PARTITION BY ANY clause, which randomly partitions the workload.

30
The basic syntax is shown below.
CREATE FUNCTION function-name(parameter-name ref-cursor-type)
RETURN rec_tab_type PIPELINED
PARALLEL_ENABLE(PARTITION parameter-name BY [{HASH | RANGE} (column-list) | ANY ]) IS
BEGIN
...
END;
To see it in action, first we must create and populate a test table.
CREATE TABLE parallel_test (
id NUMBER(10),
country_code VARCHAR2(5),
description VARCHAR2(50)
);

INSERT /*+ APPEND */ INTO parallel_test


SELECT level AS id,
(CASE TRUNC(MOD(level, 4))
WHEN 1 THEN 'IN'
WHEN 2 THEN 'UK'
ELSE 'US'
END) AS country_code,
'Description or ' || level AS description
FROM dual
CONNECT BY level <= 100000;
COMMIT;

-- Check data.
SELECT country_code, count(*) FROM parallel_test GROUP BY country_code;

COUNT COUNT(*)
----- ----------
US 50000
IN 25000
UK 25000

3 rows selected.

SQL>
The following package defines parallel enabled pipelined table functions that accept ref cursors based on a query from the test table
and return the same rows, along with the SID of the session that processed them. We could use a weakly typed ref cursor, like
SYS_REFCURSOR, but this would restrict us to only the ANY partitioning type. The three functions represent the three partitioning
methods.
CREATE OR REPLACE PACKAGE parallel_ptf_api AS

TYPE t_parallel_test_row IS RECORD (


id NUMBER(10),
country_code VARCHAR2(5),
description VARCHAR2(50),
sid NUMBER
);

31
TYPE t_parallel_test_tab IS TABLE OF t_parallel_test_row;

TYPE t_parallel_test_ref_cursor IS REF CURSOR RETURN parallel_test%ROWTYPE;

FUNCTION test_ptf_any (p_cursor IN t_parallel_test_ref_cursor)


RETURN t_parallel_test_tab PIPELINED
PARALLEL_ENABLE(PARTITION p_cursor BY ANY);

FUNCTION test_ptf_hash (p_cursor IN t_parallel_test_ref_cursor)


RETURN t_parallel_test_tab PIPELINED
PARALLEL_ENABLE(PARTITION p_cursor BY HASH (country_code));

FUNCTION test_ptf_range (p_cursor IN t_parallel_test_ref_cursor)


RETURN t_parallel_test_tab PIPELINED
PARALLEL_ENABLE(PARTITION p_cursor BY RANGE (country_code));

END parallel_ptf_api;
/

CREATE OR REPLACE PACKAGE BODY parallel_ptf_api AS

FUNCTION test_ptf_any (p_cursor IN t_parallel_test_ref_cursor)


RETURN t_parallel_test_tab PIPELINED
PARALLEL_ENABLE(PARTITION p_cursor BY ANY)
IS
l_row t_parallel_test_row;
BEGIN
LOOP
FETCH p_cursor
INTO l_row.id,
l_row.country_code,
l_row.description;
EXIT WHEN p_cursor%NOTFOUND;

SELECT sid
INTO l_row.sid
FROM v$mystat
WHERE rownum = 1;

PIPE ROW (l_row);


END LOOP;
RETURN;
END test_ptf_any;

FUNCTION test_ptf_hash (p_cursor IN t_parallel_test_ref_cursor)


RETURN t_parallel_test_tab PIPELINED
PARALLEL_ENABLE(PARTITION p_cursor BY HASH (country_code))
IS
l_row t_parallel_test_row;
BEGIN
LOOP
FETCH p_cursor

32
INTO l_row.id,
l_row.country_code,
l_row.description;
EXIT WHEN p_cursor%NOTFOUND;

SELECT sid
INTO l_row.sid
FROM v$mystat
WHERE rownum = 1;

PIPE ROW (l_row);


END LOOP;
RETURN;
END test_ptf_hash;

FUNCTION test_ptf_range (p_cursor IN t_parallel_test_ref_cursor)


RETURN t_parallel_test_tab PIPELINED
PARALLEL_ENABLE(PARTITION p_cursor BY RANGE (country_code))
IS
l_row t_parallel_test_row;
BEGIN
LOOP
FETCH p_cursor
INTO l_row.id,
l_row.country_code,
l_row.description;
EXIT WHEN p_cursor%NOTFOUND;

SELECT sid
INTO l_row.sid
FROM v$mystat
WHERE rownum = 1;

PIPE ROW (l_row);


END LOOP;
RETURN;
END test_ptf_range;

END parallel_ptf_api;
/
The following query uses the CURSOR function to convert a query against the test table into a ref cursor that is past to the table
function as a parameter. The results are grouped by the SID of the session that processed the row. Notice all the rows were processed
by the same session. Why? Because although the function is parallel enabled, we didn't tell it to run in parallel.
SELECT sid, count(*)
FROM TABLE(parallel_ptf_api.test_ptf_any(CURSOR(SELECT * FROM parallel_test t1))) t2
GROUP BY sid;

SID COUNT(*)
---------- ----------
31 100000

1 row selected.

33
SQL>
The following queries include a parallel hint and call each of the functions.
SELECT country_code, sid, count(*)
FROM TABLE(parallel_ptf_api.test_ptf_any(CURSOR(SELECT /*+ parallel(t1, 5) */ * FROM parallel_test t1))) t2
GROUP BY country_code,sid
ORDER BY country_code,sid;

COUNT SID COUNT(*)


----- ---------- ----------
IN 23 4906
IN 26 5219
IN 41 4847
IN 42 4827
IN 43 5201
UK 23 4906
UK 26 5218
UK 41 4848
UK 42 4826
UK 43 5202
US 23 9811
US 26 10437
US 41 9695
US 42 9655
US 43 10402

15 rows selected.

SQL>

SELECT country_code, sid, count(*)


FROM TABLE(parallel_ptf_api.test_ptf_hash(CURSOR(SELECT /*+ parallel(t1, 5) */ * FROM parallel_test t1))) t2
GROUP BY country_code,sid
ORDER BY country_code,sid;

COUNT SID COUNT(*)


----- ---------- ----------
IN 29 25000
UK 38 25000
US 40 50000

3 rows selected.

SQL>

SELECT country_code, sid, count(*)


FROM TABLE(parallel_ptf_api.test_ptf_range(CURSOR(SELECT /*+ parallel(t1, 5) */ * FROM parallel_test t1))) t2
GROUP BY country_code,sid
ORDER BY country_code,sid;

COUNT SID COUNT(*)


----- ---------- ----------

34
IN 40 25000
UK 23 25000
US 41 50000

3 rows selected.

SQL>
The degree of parallelism (DOP) may be lower than that requested in the hint.
An optional streaming clause can be used to order or cluster the data inside the server process based on a column list. This may be
necessary if data has dependencies, for example you wish to partition by a specific column, but also want the rows processed in a
specific order within that partition. The extended syntax is shown below.
CREATE FUNCTION function-name(parameter-name ref-cursor-type)
RETURN rec_tab_type PIPELINED
PARALLEL_ENABLE(PARTITION parameter-name BY [{HASH | RANGE} (column-list) | ANY ])
[ORDER | CLUSTER] parameter-name BY (column-list) IS
BEGIN
...
END;
You may wish to do something like the following for example.
FUNCTION test_ptf_hash (p_cursor IN t_parallel_test_ref_cursor)
RETURN t_parallel_test_tab PIPELINED
PARALLEL_ENABLE(PARTITION p_cursor BY HASH (country_code))
ORDER p_cursor BY (country_code, created_date);

FUNCTION test_ptf_hash (p_cursor IN t_parallel_test_ref_cursor)


RETURN t_parallel_test_tab PIPELINED
PARALLEL_ENABLE(PARTITION p_cursor BY HASH (country_code))
CLUSTER p_cursor BY (country_code, created_date);

Transformation Pipelines
In traditional Extract Transform Load (ETL) processes you may be required to load data into a staging area, then make several passes
over it to transform it into a state where it can be loaded into your destination schema. Passing the data through staging tables can
represent a significant amount of disk I/O for both the data and the redo generated. An alternative is to perform the transformations in
pipelined table functions so data can be read from an external table and inserted directly into the destination table, removing much of
the disk I/O.
In this section we will see and example of this using the techniques discussed previously to build a transformation pipeline.
First, generate some test data as a flat file by spooling it out to the database server's file system.
SET PAGESIZE 0
SET FEEDBACK OFF
SET LINESIZE 1000
SET TRIMSPOOL ON
SPOOL /tmp/tp_test.txt
SELECT owner || ',' || object_name || ',' || object_type || ',' || status
FROM all_objects;
SPOOL OFF
SET FEEDBACK ON
SET PAGESIZE 24
Create a directory object pointing to the location of the file, create an external table to read the file and create a destination table.
-- Create a directory object pointing to the flat file.
CONN / AS SYSDBA

35
CREATE OR REPLACE DIRECTORY data_load_dir AS '/tmp/';
GRANT READ, WRITE ON DIRECTORY data_load_dir TO test;

CONN test/test
-- Create an external table.
DROP TABLE tp_test_ext;
CREATE TABLE tp_test_ext (
owner VARCHAR2(30),
object_name VARCHAR2(30),
object_type VARCHAR2(19),
status VARCHAR2(7)
)
ORGANIZATION EXTERNAL
(
TYPE ORACLE_LOADER
DEFAULT DIRECTORY data_load_dir
ACCESS PARAMETERS
(
RECORDS DELIMITED BY NEWLINE
BADFILE data_load_dir:'tp_test_%a_%p.bad'
LOGFILE data_load_dir:'tp_test_%a_%p.log'
FIELDS TERMINATED BY ','
MISSING FIELD VALUES ARE NULL
(
owner CHAR(30),
object_name CHAR(30),
object_type CHAR(19),
status CHAR(7)
)
)
LOCATION ('tp_test.txt')
)
PARALLEL 10
REJECT LIMIT UNLIMITED
/

-- Create a table as the final destination for the data.


CREATE TABLE tp_test (
owner VARCHAR2(30),
object_name VARCHAR2(30),
object_type VARCHAR2(19),
status VARCHAR2(7),
extra_1 NUMBER,
extra_2 NUMBER
);
Notice the destination table has two extra columns compared to the external table. Each of these columns represents a transformation
step. The actual transformations in this example are trivial, but imagine they were so complex they could not be done in SQL alone,
hence the need for the table functions.
The package below defines the two steps of the transformation process and a procedure to initiate it.
CREATE OR REPLACE PACKAGE tp_api AS

TYPE t_step_1_in_rc IS REF CURSOR RETURN tp_test_ext%ROWTYPE;

36
TYPE t_step_1_out_row IS RECORD (
owner VARCHAR2(30),
object_name VARCHAR2(30),
object_type VARCHAR2(19),
status VARCHAR2(7),
extra_1 NUMBER
);

TYPE t_step_1_out_tab IS TABLE OF t_step_1_out_row;

TYPE t_step_2_in_rc IS REF CURSOR RETURN t_step_1_out_row;

TYPE t_step_2_out_tab IS TABLE OF tp_test%ROWTYPE;

FUNCTION step_1 (p_cursor IN t_step_1_in_rc)


RETURN t_step_1_out_tab PIPELINED
PARALLEL_ENABLE(PARTITION p_cursor BY ANY);

FUNCTION step_2 (p_cursor IN t_step_2_in_rc)


RETURN t_step_2_out_tab PIPELINED
PARALLEL_ENABLE(PARTITION p_cursor BY ANY);

PROCEDURE load_data;

END tp_api;
/

CREATE OR REPLACE PACKAGE BODY tp_api AS

FUNCTION step_1 (p_cursor IN t_step_1_in_rc)


RETURN t_step_1_out_tab PIPELINED
PARALLEL_ENABLE(PARTITION p_cursor BY ANY)
IS
l_row t_step_1_out_row;
BEGIN
LOOP
FETCH p_cursor
INTO l_row.owner,
l_row.object_name,
l_row.object_type,
l_row.status;
EXIT WHEN p_cursor%NOTFOUND;

-- Do some work here.


l_row.extra_1 := p_cursor%ROWCOUNT;
PIPE ROW (l_row);
END LOOP;
RETURN;
END step_1;

37
FUNCTION step_2 (p_cursor IN t_step_2_in_rc)
RETURN t_step_2_out_tab PIPELINED
PARALLEL_ENABLE(PARTITION p_cursor BY ANY)
IS
l_row tp_test%ROWTYPE;
BEGIN
LOOP
FETCH p_cursor
INTO l_row.owner,
l_row.object_name,
l_row.object_type,
l_row.status,
l_row.extra_1;
EXIT WHEN p_cursor%NOTFOUND;

-- Do some work here.


l_row.extra_2 := p_cursor%ROWCOUNT;
PIPE ROW (l_row);
END LOOP;
RETURN;
END step_2;

PROCEDURE load_data IS
BEGIN
EXECUTE IMMEDIATE 'ALTER SESSION ENABLE PARALLEL DML';
EXECUTE IMMEDIATE 'TRUNCATE TABLE tp_test';

INSERT /*+ APPEND PARALLEL(t4, 5) */ INTO tp_test t4


SELECT /*+ PARALLEL(t3, 5) */ *
FROM TABLE(step_2(CURSOR(SELECT /*+ PARALLEL(t2, 5) */ *
FROM TABLE(step_1(CURSOR(SELECT /*+ PARALLEL(t1, 5) */ *
FROM tp_test_ext t1
)
)
) t2
)
)
) t3;
COMMIT;
END load_data;

END tp_api;
/
The insert inside the LOAD_DATA procedure represents the whole data load including the transformations. The statement looks
quite complicated, but it is made up of the following simple steps.
The rows are queried from the external table.
These are converted into the ref cursor using the CURSOR function.
This ref cursor is passed to the first stage of the transformation (STEP_1).
The return collection from STEP_1 is queried using the TABLE function.
The output of this query is converted to a ref cursor using the CURSOR function.

38
This ref cursor is passed to the second stage of the transformation (STEP_2).
The return collection from STEP_2 is queried using the TABLE function.
This query is used to drive the insert into the destination table.
By calling the LOAD_DATA procedure we can transform and load the data.
EXEC tp_api.load_data;

PL/SQL procedure successfully completed.


SQL>
-- Check the rows in the external table.
SELECT COUNT(*) FROM tp_test_ext;

COUNT(*)
----------
56059
1 row selected.
SQL>
-- Compare to the destination table.
SELECT COUNT(*) FROM tp_test;

COUNT(*)
----------
56059

1 row selected.

SQL>

AUTONOMOUS_TRANSACTION Pragma
If the pipelined table function runs DML statements, then make it autonomous, with the AUTONOMOUS_TRANSACTION pragma
then, during parallel execution, each instance of the function creates an independent transaction.

DETERMINISTIC Option (Recommended)


The DETERMINSTIC hint has been available since Oracle 8i, where it was first introduced to mark functions as deterministic to
allow them to be used in function-based indexes, but it is only in Oracle 10gR2 onward that it has some affect on how the function is
cached in SQL.
In the following example, labelling the function as deterministic does improve performance, but the caching is limited to a single
fetch, so it is affected by the array size. In this example, the array size is varied using the SQL*Plus "SET ARRAYSIZE n" command.
CREATE OR REPLACE FUNCTION slow_function (p_in IN NUMBER)
RETURN NUMBER
DETERMINISTIC
AS
BEGIN
DBMS_LOCK.sleep(1);
RETURN p_in;
END;
/

SET TIMING ON
SET ARRAYSIZE 15
SELECT slow_function(id)

39
FROM func_test;

SLOW_FUNCTION(ID)
-----------------
1
2
1
2
1
2
1
2
1
3

10 rows selected.

Elapsed: 00:00:04.04

SQL>

SET TIMING ON
SET ARRAYSIZE 2
SELECT slow_function(id)
FROM func_test;

SLOW_FUNCTION(ID)
-----------------
1
2
1
2
1
2
1
2
1
3

10 rows selected.

Elapsed: 00:00:10.01

SQL>
The difference in array size produced drastically different performance, showing that caching is only available for the lifetime of the
fetch. Subsequent queries (or fetches) have no access to the cached values of previous runs.
RETURN Data Type
The data type of the value that a pipelined table function returns must be a collection type defined either at schema level or inside a
package (therefore, it cannot be an associative array type). The elements of the collection type must be SQL data types, not data types
supported only by PL/SQL (such as PLS_INTEGER and BOOLEAN)

40
PIPE ROW Statement
Inside a pipelined table function, use the PIPE ROW statement to return a collection element to the invoker without returning control
to the invoke
RETURN Statement
As in every function, every execution path in a pipelined table function must lead to a RETURN statement, which returns control to
the invoker. However, in a pipelined table function, a RETURN statement need not return a value to the invoker

Cross-Session PL/SQL Function Result Cache (11g)


Oracle 11g introduced two new caching mechanisms:
Cross-Session PL/SQL Function Result Cache: Used for caching the results of function calls.
Query Result Cache: Used for caching the whole result set produced by a query.
We can use the first mechanism to cache the results of our slow function, allowing us to remove the need to rerun it for the same
parameter signature.
CREATE OR REPLACE FUNCTION slow_function (p_in IN NUMBER)
RETURN NUMBER
RESULT_CACHE
AS
BEGIN
DBMS_LOCK.sleep(1);
RETURN p_in;
END;
/

SET TIMING ON
SELECT slow_function(id)
FROM func_test;

SLOW_FUNCTION(ID)
-----------------
1
2
1
2
1
2
1
2
1
3

10 rows selected.

Elapsed: 00:00:03.09

SQL>
The advantage of this method is the cached information can be reused by any session and dependencies are managed automatically. If
we run the query again we get even better performance because we can used the cached values without calling the function at all.
SET TIMING ON
SELECT slow_function(id)

41
FROM func_test;

SLOW_FUNCTION(ID)
-----------------
1
2
1
2
1
2
1
2
1
3

10 rows selected.

Elapsed: 00:00:00.02

SQL>

SQL Hints
There are many Oracle hints available to the developer for use in tuning SQL statements that are embedded in PL/SQL.
You should first get the explain plan of your SQL and determine what changes can be done to make the code operate without using
hints if possible. However, Oracle hints such as ORDERED, LEADING, INDEX, FULL, and the various AJ and SJ Oracle hints can
take a wild optimizer and give you optimal performance.
Oracle hints are enclosed within comments to the SQL commands DELETE, SELECT or UPDATE or are designated by two dashes
and a plus sign. To show the format the SELECT statement only will be used, but the format is identical for all three commands.
SELECT /*+ hint --or-- text */ statement body
-- or --
SELECT --+ hint --or-- text statement body
Where:

 /*, */ - These are the comment delimiters for multi-line comments


 -- - This is the comment delimiter for a single line comment (not usually used for hints)
 + - This tells Oracle a hint follows, it must come immediately after the /*
 hint - This is one of the allowed hints
 text - This is the comment text

Oracle Hint Meaning


+ Must be immediately after comment indicator, tells Oracle this is a list of hints.
ALL_ROWS Use the cost based approach for best throughput.
CHOOSE Default, if statistics are available will use cost, if not, rule.
FIRST_ROWS Use the cost based approach for best response time.
RULE Use rules based approach; this cancels any other hints specified for this statement.
Access Method Oracle Hints:
CLUSTER(table) This tells Oracle to do a cluster scan to access the table.

42
FULL(table) This tells the optimizer to do a full scan of the specified table.
HASH(table) Tells Oracle to explicitly choose the hash access method for the table.
HASH_AJ(table) Transforms a NOT IN subquery to a hash anti-join.
ROWID(table) Forces a rowid scan of the specified table.
Forces an index scan of the specified table using the specified index(s). If a list of indexes is
INDEX(table [index]) specified, the optimizer chooses the one with the lowest cost. If no index is specified then the
optimizer chooses the available index for the table with the lowest cost.
Same as INDEX only performs an ascending search of the index chosen, this is functionally
INDEX_ASC (table [index])
identical to the INDEX statement.
Same as INDEX except performs a descending search. If more than one table is accessed, this is
INDEX_DESC(table [index])
ignored.
Combines the bitmapped indexes on the table if the cost shows that to do so would give better
INDEX_COMBINE(table index)
performance.
INDEX_FFS(table index) Perform a fast full index scan rather than a table scan.
MERGE_AJ (table) Transforms a NOT IN subquery into a merge anti-join.
AND_EQUAL(table index index
This hint causes a merge on several single column indexes. Two must be specified, five can be.
[index index index])
NL_AJ Transforms a NOT IN subquery into a NL anti-join (nested loop).
Inserted into the EXISTS subquery; This converts the subquery into a special type of hash join
HASH_SJ(t1, t2) between t1 and t2 that preserves the semantics of the subquery. That is, even if there is more
than one matching row in t2 for a row in t1, the row in t1 is returned only once.
Inserted into the EXISTS subquery; This converts the subquery into a special type of merge join
MERGE_SJ (t1, t2) between t1 and t2 that preserves the semantics of the subquery. That is, even if there is more
than one matching row in t2 for a row in t1, the row in t1 is returned only once.
Inserted into the EXISTS subquery; This converts the subquery into a special type of nested
NL_SJ loop join between t1 and t2 that preserves the semantics of the subquery. That is, even if there is
more than one matching row in t2 for a row in t1, the row in t1 is returned only once.
Oracle Hints for join orders and
transformations:
This hint forces tables to be joined in the order specified. If you know table X has fewer rows,
ORDERED
then ordering it first may speed execution in a join.
STAR Forces the largest table to be joined last using a nested loops join on the index.
STAR_TRANSFORMATION Makes the optimizer use the best plan in which a start transformation is used.
FACT(table) When performing a star transformation use the specified table as a fact table.
NO_FACT(table) When performing a star transformation do not use the specified table as a fact table.
This causes nonmerged subqueries to be evaluated at the earliest possible point in the execution
PUSH_SUBQ
plan.
If possible forces the query to use the specified materialized view, if no materialized view is
REWRITE(mview)
specified, the system chooses what it calculates is the appropriate view.
Turns off query rewrite for the statement, use it for when data returned must be concurrent and
NOREWRITE
can't come from a materialized view.
Forces combined OR conditions and IN processing in the WHERE clause to be transformed
USE_CONCAT
into a compound query using the UNION ALL set operator.
This causes Oracle to join each specified table with another row source without a sort-merge
NO_MERGE (table)
join.
NO_EXPAND Prevents OR and IN processing expansion.

43
Oracle Hints for Join Operations:
USE_HASH (table)
This causes Oracle to join each specified table with another row source with a hash join.

USE_NL(table) This operation forces a nested loop using the specified table as the controlling table.
USE_MERGE(table,[table, - ]) This operation forces a sort-merge-join operation of the specified tables.
The hint forces query execution to be done at a different site than that selected by Oracle. This
DRIVING_SITE
hint can be used with either rule-based or cost-based optimization.
LEADING(table) The hint causes Oracle to use the specified table as the first table in the join order.
Oracle Hints for Parallel
Operations:
This specifies that data is to be or not to be appended to the end of a file rather than into existing
[NO]APPEND
free space. Use only with INSERT commands.
NOPARALLEL (table This specifies the operation is not to be done in parallel.
PARALLEL(table, instances) This specifies the operation is to be done in parallel.
PARALLEL_INDEX Allows parallelization of a fast full index scan on any index.
Other Oracle Hints:
Specifies that the blocks retrieved for the table in the hint are placed at the most recently used
CACHE
end of the LRU list when the table is full table scanned.
Specifies that the blocks retrieved for the table in the hint are placed at the least recently used
NOCACHE
end of the LRU list when the table is full table scanned.
[NO]APPEND For insert operations will append (or not append) data at the HWM of table.
Turns on the UNNEST_SUBQUERY option for statement if UNNEST_SUBQUERY
UNNEST
parameter is set to FALSE.
Turns off the UNNEST_SUBQUERY option for statement if UNNEST_SUBQUERY
NO_UNNEST
parameter is set to TRUE.
PUSH_PRED Pushes the join predicate into the view.

As you can see, a dilemma with a stubborn index can be easily solved using FULL or NO_INDEX Oracle hints. You must know the
application to be tuned. The DBA can provide guidance to developers but in all but the smallest development projects, it will be
nearly impossible for a DBA to know everything about each application. It is clear that responsibility for application tuning rests
solely on the developer's shoulders with help and guidance from the DBA.

Using Global Hints


While Oracle hints normally refer to table in the query it is possible to specify a hint for a table within a view through the use of what
are known as Oracle GLOBAL HINTS. This is done using the Oracle global hint syntax. Any table hint can be transformed into an
Oracle global hint.
The syntax is:
/*+ hint(view_name.table_in_view) */
For example:
/*+ full(sales_totals_vw.s_customer)*/
If the view is an inline view, place an alias on it and then use the alias to reference the inline view in the Oracle global hint.

44
Some Important Database parameters and hidden
parameters
DB file Multiblock read count
Oracle notes that the cost of reading the blocks from disk into the buffer cache can be amortized by reading the blocks in large I/O
operations. The db_file_multiblock_read_count parameter controls the number of blocks that are pre-fetched into the buffer cache if a
cache miss is encountered for a particular block.

The value of this parameter can have a significant impact on the overall database performance and it is not easy for the administrator
to determine its most appropriate value. Oracle Database 10g Release 2 automatically selects the appropriate value for this parameter
depending on the operating system optimal I/O size and the size of the buffer cache

The Oracle database improves the performance of table scans by increasing the number of blocks read in a single database I/O
operation. If your SQL statement is going to read all of the rows in a table, it makes sense to return as many blocks as you can in a
single read. In releases prior to Oracle10G R2, administrators used the db_file_multiblock_read_count initialization parameter to tell
Oracle how many block to retrieve in the single I/O operation.

But setting the db_file_multiblock_read_count parameter too high can affect access path selection. Full table scans use multi-block
reads, so the cost of a full table scan depends on the number of multi-block reads required to read the entire table. The more blocks
retrieved in a single multi-block I/O execution, the more favorable a tablescan looks to the optimizer.

In releases prior to Oracle10G R2, the permitted values for db_file_multiblock_read_count were platform-dependent. The most
common settings ranged from 4 to 64 blocks per single multi-block I/O execution.

The DB_FILE_MULTIBLOCK_READ_COUNT parameter controls the number of blocks pre-fetched into the buffer cache during
scan operations, such as full table scan and index fast full scan.

Oracle Database 10g Release 2 automatically selects the appropriate value for this parameter depending on the operating system
optimal I/O size and the size of the buffer cache.

This is the default behavior in Oracle Database 10g Release 2, if you do not set any value for the db_file_multiblock_read_count
parameter (i.e. removing it from your spfile or init.ora file). If you explicitly set a value, then that value is used, and is consistent with
the previous behavior.

Remember, the parameter db_file_multiblock_read_count is only applicable for tables/indexes that are full scanned, but it also effects
the SQL optimizer in its calculation of the cost of a full-table scan.

According to Oracle, this is the formula for setting db_file_multiblock_read_count:

max I/O chunk size


db_file_multiblock_read_count = -------------------
db_block_size
But how do we know the value of the max I/O chunk size?
The maximum effective setting for db_file_multiblock_read_count is OS and disk dependant. There is script to assist you in setting an
appropriate level. This script conducts a test and sample actual I/O chunk sizes on your server to aid you in setting
db_file_multiblock_read_count:

45
-------------------------------------------------------------------------------
--
-- Script: multiblock_read_test.sql
-- Purpose: find largest actual multiblock read size
--
--
-- Description: This script prompts the user to enter the name of a table to
-- scan, and then does so with a large multiblock read count, and
-- with event 10046 enabled at level 8.
-- The trace file is then examined to find the largest multiblock
-- read actually performed.
--
-------------------------------------------------------------------------------
@save_sqlplus_settings

alter session set db_file_multiblock_read_count = 32768;


/
column value heading "Maximum possible multiblock read count"
select
value
from
sys.v_$parameter
where
name = 'db_file_multiblock_read_count'
/

prompt
@accept Table "Table to scan" SYS.SOURCE$
prompt Scanning ...
set termout off
alter session set events '10046 trace name context forever, level 8'
/
select /*+ full(t) noparallel(t) nocache(t) */ count(*) from &Table t
/
alter session set events '10046 trace name context off'
/

set termout on

@trace_file_name

prompt
prompt Maximum effective multiblock read count
prompt ----------------------------------------

46
Oracle Direct I/O
Many Oracle shops are plagued with slow I/O intensive databases, and this tip is for anyone whose STATSPACK top-5 timed events
shows disk I/O as a major event:
Top 5 Timed Events
% Total
Event Waits Time (s) Ela Time
--------------------------- ------------ ----------- -----------
db file sequential read 2,598 7,146 48.54
db file scattered read 25,519 3,246 22.04
library cache load lock 673 1,363 9.26
CPU time 2,154 934 7.83
log file parallel write 19,157 837 5.68

Oracle direct I/O should be verified for Solaris, HP/UX, and Linux AIX. This tip is important to you if you have reads waits in your
top-5 timed events. Remember, if disk I/O is not your bottleneck then making it faster WILL NOT improve performance.

Also, this is an OS-level solution, and often I/O-bound Oracle databases can be fixed by tuning the SQL to reduce unnecessary large-
table full-table scans. I monitor file I/O using the stats$filestatxs view

Oracle controls direct I/O with a parameter named filesystemio_options. According to the Oracle documentation the
filesystemio_options parameter must be set to "setall" (the preferred method, according to the Oracle documentation) or ”directio" in
order for Oracle to read data blocks directly from disk
Using direct I/O allows you to enhance I/O by bypassing the redundant OS block buffers, reading the data block directly into the
Oracle SGA. Using direct I/O also allow you to create multiple blocksized tablespaces to improve I/O performance

Pga aggregate target and pga max size


Almost every Oracle professionals agrees that the old-fashioned sort_area_size and hash_area_size parameters imposed a
cumbersome one-size-fits-all approach to sorting and hash joins. Different tasks require different RAM areas, and the trick has been
to allow? Enough? PGA RAM for sorting and hash joins without having any high-resource task? Hog? All of the PGA, to the
exclusion of other users.

The pga_aggregate_target parameters to fix this resource issue, and by-and-large, pga_aggregate_target works very well for most
systems. You can check your overall PGA usage with the v$pga_target_advice advisory utility or a STATSPACK or AWR
report. High values for multi-pass executions, high disk sorts, or low hash join invocation might indicate a low resource usage for
PGA regions.

For monitoring pga_aggregate_target Oracle provides a dictionary view called v$pgastat. The v$pgastat view shows the total amount
of RAM memory utilization for every RAM memory region within the database.

You can also increase your pga_aggregate_target above the default 200 megabyte setting by setting the hidden _pga_max_size
parameter.

 _pga_max_size = 1000m
 _smm_px_max_size = 333m

With pga_aggregate_target and _pga_max_size hidden parameter set to 1G we see a 5x improvement over the default for parallel
queries and sorts:

 A RAM sort or hash join may now have up to 50 megabytes (5% of pga_aggegate_target).

47
 Parallel queries may now have up to 330 megabytes of RAM (30% of pga_aggegate_target), such that a DEGREE=4
parallel query would have 83 megabytes (333 meg/4).

When an Oracle process requires an operation, such as a sort or a hash join, it goes to the shared RAM memory area within
pga_aggregate_target region and attempts to obtain enough contiguous RAM frames to perform the operation. If the process is able
to acquire these RAM frames immediately from pga_aggregate_target, it is marked as an "optimal" RAM access.

If the RAM acquisition requires a single pass through pga_aggregate_target, the RAM memory allocation is marked as one pass. If all
RAM is in use, Oracle may have to make multiple passes through pga_aggregate_target to acquire the RAM memory. This is called
multipass.

Remember, RAM memory is extremely fast, and most sorts or hash joins are completed in microseconds. Oracle allows a single
process to use up to 5 percent of the pga_aggregate_target, and parallel operations are allowed to consume up to 30 percent of the
PGA RAM pool.

Pga_aggregate_target "multipass" executions indicate a RAM shortage, and you should always allocate enough RAM to ensure that at
least 95 percent of connected tasks can acquire their RAM memory optimally.

You can also obtain information about workarea executions by querying the v$sysstat view shown here:

col c1 heading 'Workarea|Profile' format a35


col c2 heading 'Count' format 999,999,999
col c3 heading 'Percentage' format 99
select name c1,cnt c2,decode(total, 0, 0, round(cnt*100/total)) c3
from
(
select name,value cnt,(sum(value) over ()) total
from
v$sysstat
where
name like 'workarea exec%'
);

Note:

When oracle couldn’t find enough ram to sort then it will use TEMP tablespace that may cause lots of I/O and it will impact the
performance

Oracle Multiple blocksize


WARNING: Using multiple blocksizes effectively is not simple. It requires expert-level Oracle skills and an intimate knowledge of
your I/O landscape. While deploying multiple blocksizes can greatly reduce I/O and improve response time, it can also wreak havoc
in the hands of inexperienced DBA's. Using non-standard blocksizes is not recommended for beginners.
Databases with multiple blocksizes have been around for more than 20 years and were first introduced in the 1980?s as a method to
segregate and partition data buffers. Once Oracle adopted multiple blocksizes in Oraclein 2001, the database foundation for using
multiple blocksizes was already as well-tested and proven approach. Non-relational databases such as the CA IDMS/R network
database have been using multiple blocksizes for nearly two decades.

48
All else being equal, insert-intensive databases will perform less write I/O (via the DBWR process) with larger block sizes because
more "logical inserts" can take place within the data buffer before the block becomes full and requires writing it back to disk.
At first, beginners denounced multiple block sizes because they were invented to support transportable tablespaces. Fortunately,
Oracle has codified the benefits of multiple blocksizes, and the Oracle Performance Tuning Guide notes that multiple blocksizes are
indeed beneficial in large databases to eliminate superfluous I/O and isolate critical objects into a separate data buffer cache:
? With segments that have atypical access patterns, store blocks from those segments in two different buffer pools: the KEEP pool and
the RECYCLE pool.
A segment's access pattern may be atypical if it is constantly accessed (that is, hot) or infrequently accessed (for example, a large
segment accessed by a batch job only once a day).
Multiple buffer pools let you address these differences. You can use a KEEP buffer pool to maintain frequently accessed segments in
the buffer cache, and a RECYCLE buffer pool to prevent objects from consuming unnecessary space in the cache. . .
By allocating objects to appropriate buffer pools, you can:
 Reduce or eliminate I/Os
 Isolate or limit an object to a separate cache

For example:
When the 16k instance runs an 850,000 row update (nowhere clause), it finishes in 45 minutes. When the 4k instance runs an 850,000
row update (nowhere clause), it finishes in 2.2 minutes. The change in blocksize caused the job to run TWENTY TIMES FASTER

Let's review the benefits of using multiple block sizes.

The benefits of a larger blocksize


The benefits of large blocksizes are demonstrated on this OTN thread where we see a demo showing 3x faster performance using a
larger block size:
SQL> r
1 select count(MYFIELD) from table_8K where ttime >to_date('27/09/2006','dd/mm/y
2* and ttime <to_date('06/10/2006','dd/mm/yyyy')

COUNT(MYFIELD)
-------------------
164864

Elapsed: 00:00:01.40
...
(This command is executed several times - the execution time was approximately the same ~
00:00:01.40)

And now the test with the same table, but created together with the index in 16k tablespace:

SQL> r
1 select count(MYFIELD) from table_16K where ttime >to_date('27/09/2006','dd/mm/
2* and ttime <to_date('06/10/2006','dd/mm/yyyy')

COUNT(MYFIELD)
-------------------
164864

Elapsed: 00:00:00.36

(Again, the command is executed several times, the new execution time is approximately the same ~

Reducing data buffer waste

49
By performing block reads of an appropriate size the DBA can significantly increase the efficiency of the data buffers. For example,
consider an OLTP database that randomly reads 80 byte customer rows. If you have a 16k db_block_size, Oracle must read all of the
16k into the data buffer to get your 80 bytes, a waste of data buffer resources. If we migrate this customer table into a 2k blocksize,
we now only need to read-in 2k to get the row data. This results in 8 times more available space for random block fetches.

50
Improvements in data buffer utilization

Reducing logical I/O


As more and more Oracle database become CPU-bound as a result of solid-state disks and 64-bit systems with large data buffer
caches, minimizing logical I/O consistent gets from the data buffer) has become an important way to reduce CPU consumption. This
can be illustrated with indexes. Oracle performs index range scams during many types of operations such as nested loop joins and
enforcing row order for result sets. In these cases, moving Oracle indexes into large blocksizes can reduce both the physical I/O (disk
reads) and the logical I/O (buffer gets).

Robin Schumacher has proven in his book Oracle Performance Troubleshooting (2003, Rampant TechPress) that Oracle b-tree
indexes are built in flatter structures in 32k blocksizes. We also see a huge reduction in logical I/O during index range scans and
sorting within the TEMP tablespace because adjacent rows are located inside the same data block.

51
We can also identify those indexes with the most index range scans with this simple AWR script.
col c1 heading ?Object|Name? format a30
col c2 heading ?Option? format a15
col c3 heading ?Index|Usage|Count? format 999,999

select
p.object_name c1,
p.options c2,
count(1) c3
from
dba_hist_sql_plan p,
dba_hist_sqlstat s
where
p.object_owner <> 'SYS'
and
p.options like '%RANGE SCAN%'
and
p.operation like ?%INDEX%?
and
p.sql_id = s.sql_id
group by
p.object_name,
p.operation,
p.options
order by
1,2,3;

Here is the output where we see overall total counts for each object and table access method.

52
Object Usage
Name Option Count
----------------------------- --------------- --------
CUSTOMER_CHECK RANGE SCAN 4,232
AVAILABILITY_PRIMARY_KEY RANGE SCAN 1,783
CON_UK RANGE SCAN 473
CURRENT_SEVERITY RANGE SCAN 323
CWM$CUBEDIMENSIONUSE_IDX RANGE SCAN 72
ORDERS_FK RANGE SCAN 20

Improving data buffer efficiency


One of the greatest problems if very large data buffers is the overhead of Oracle in cleaning-out ?direct blocks? that result from
truncate operations and high activity DML. This overhead can drive-up CPU consumption of databases that have large data buffers.

Dirty Block cleanup in a large vs. small data buffer

By segregating high activity tables into a separate, smaller data buffer, Oracle has far less RAM frames to scan for dirty block,
improving the throughout and also reducing CPU consumption. This is especially important for super-high update tables with more
than 100 row changes per second.

Improving SQL execution plans


It?s obvious that intelligent buffer segregation improves overall execution speed by reducing buffer gets, but there are also some other
important reasons to use multiple blocksizes.

In general, the Oracle cost-based optimizer is unaware of buffers details (except when you set the optimizer_index_caching
parameter), and using multiple data buffers will not impact SQL execution plans. When data using the new cpu_cost parameter in
Oracle10g, the Oracle SQL optimizer builds the SQL plan decision tree based on the execution plan that will have the lowest
estimated CPU cost.

For example, if you implement a 32k data buffer for your index tablespaces you can ensure that your indexes are caches for optimal
performance and minimal logical I/O in range scans.

For example, if my database has 50 gigabytes of index space, I can define a 60 gigabyte db_32k_cache_size and then set my
optimizer_index_caching parameter to 100, telling the SQL optimizer that all of my Oracle indexes reside in RAM. Remember when

53
Oracle makes the index vs. table scan decision, knowing that the index nodes are in RAM will greatly influence the optimizer because
the CBO knows that a logical I/O is often 100 times faster than a physical disk read.

In sum, moving Oracle indexes into a fully-cached 32k buffer will ensure that Oracle favors index access, reducing unnecessary full-
table scans and greatly reducing logical I/O because adjacent index nodes will reside within the larger, 32k block.

Real World Applications of multiple blocksizes


The use of multiple blocksizes is the most important for very-large database with thousands of updates per second and thousands of
concurrent user?s access terabytes of data. In these super-large databases, multiple blocksizes have proven to make a huge difference
in response time.

Largest Benefit:
We see the largest benefit of multiple blocksizes in these types of databases:

OLTP databases: Databases with a large amount of index access (first_rows optimizer_mode) and databases with random fetches of
small rows are ideal for buffer pool segregation.

64-bit Oracle databases: Oracle databases with 64-bit software can support very large data buffer caches and these are ideal for
caching frequently-referenced tables and indexes.

High-update databases: Databases where a small sub-set of the database receives large update activity (i.e. a single partition within a
table), will see a large reduction in CPU consumption when the high update objects are moved into a smaller buffer cache.

Smallest benefit:
However, there are specific types of databases that may not benefit from using multiple blocksizes:

Small node Oracle10g Grid systems: Because each data blade in an Oracle10g grid node has only 2 to 4 gigabytes of RAM, data
blade grid applications do not show a noticeable benefit from multiple block sizes.

Solid-state databases: Oracle databases using solid-state disks (RAM-SAN) perform fastest with super-small data buffer, just large
enough to hold the Oracle serialization locks and latches.

Decision Support Systems: Large Oracle data warehouses with parallel large-table full table scans do not benefit from multiple
blocksizes. Remember, parallel full-table scans bypass the data buffers and store the intermediate rows sets in the PGA region. As a
general rule, databases with the all_rows optimizer_mode may not benefit from multiple blocksizes.
Even though Oracle introduced multiple blocksizes for a innocuous reason, their power has become obvious in very large database
systems. The same divide-and-conquer approach that Oracle has used to support very large databases can also be used to divide and
conquer your Oracle data buffers.

Important Tips:
Objects that experience full-table scans should be placed in a larger block size, with db_file_multiblock_read_count set to the block
size of that tablespace

54
By defining large (i.e. 32k) blocksizes for the target table, you reduce I/O because more rows fit onto a block before a "block full"
condition (as set by PCTFREE) unlinks the block from the freelist.

Small rows in a large block perform worse under heavy DML than large rows in a small blocksize.

Heavy insert/update tables can see faster performance when segregated into another blocksize which is mapped to a small data buffer
cache. Smaller data buffer caches often see faster throughput performance.

RAC can perform far faster with smaller blocksizes, reducing cache fusion overhead

Moving random access small row tables to a smaller blocksize (with a corresponding small blocksize buffer) will reduce buffer waste
and improve the chance that other data blocks will remain in the cache.

Tables and indexes that require full scans can see faster performance when placed in a large blocksize

If you are not setting db_cache_size to the largest supported block size for your server, you should not use the db_recycle_cache_size
parameter. Instead, you will want to create a db_32k_cache_size (or whatever your max is), and assign all tables that experience
frequent large-table full-table scans to the largest buffer cache in your database.

Oracle recommend 2K blocksizes for bitmap indexes, to minimize redo generation during bitmap index rebuilds

For those databases that fetch small rows randomly from the disk, the Oracle DBA can segregate these types of tables into 2K
Tablespaces. We have to remember that while disk is becoming cheaper every day, we still don't want to waste any available RAM by
reading in more information to RAM than the number actually going be used by the query. Hence, many Oracle DBAs will use small
block size is in cases of tiny, random access record retrieval.

For those Oracle tables that contain raw, long raw, or in-line LOBs, moving the table rows to large block size will have an extremely
beneficial effect on disk I/O. Experienced DBAs will check dba_tables.avg_row_len to make sure that the blocksize is larger than the
average size. Row chaining will be reduced while at the same time the entire LOB can be read within a single disk I/O, thereby
avoiding the additional overhead of having Oracle to go out of read multiple blocks.

The block size for a tables' tablespace should always be greater than the average row length for the table (dba_tables.avg_row_len).
Not it is smaller than the average row length, rows chaining occurs and excessive disk I/O is incurred.

Native PL/SQL compilation


When PL/SQL is loaded into the server it is compiled to byte code before execution. The process of native compilation converts
PL/SQL stored procedures to native code shared libraries which are linked into the kernel resulting in performance increases for the
procedural code. The extent of the performance increase depends on the content of the PL/SQL. The compilation process does not
affect the speed of database calls, only the procedural logic around them such as loops and calculations.

The following test is an example of the performance gains available for pure procedural code.
ALTER SESSION SET plsql_compiler_flags = 'INTERPRETED';

CREATE OR REPLACE PROCEDURE test_speed AS


v_number NUMBER;
BEGIN
FOR i IN 1 .. 1000000 LOOP
v_number := i / 1000;
END LOOP;

55
END;
/

SET TIMING ON
EXEC test_speed;

PL/SQL procedure successfully completed.

Elapsed: 00:00:01.00

ALTER SESSION SET plsql_compiler_flags = 'NATIVE';


ALTER PROCEDURE test_speed COMPILE;

EXEC test_speed;

PL/SQL procedure successfully completed.

Elapsed: 00:00:00.05

From this you can see that a basic procedural operation can be speeded up significantly. In this example the natively compiled code
takes approximately 1/20 of the time to run.
The speed of database calls is unaffected by native compilation so the performance increases of most stored procedures will not be so
dramatic. This is illustrated by the following example.
ALTER SESSION SET plsql_compiler_flags = 'INTERPRETED';

CREATE OR REPLACE PROCEDURE test_speed AS


v_date DATE;
BEGIN
FOR i IN 1 .. 1000000 LOOP
SELECT SYSDATE
INTO v_date
FROM dual;
END LOOP;
END;
/

EXEC test_speed;

PL/SQL procedure successfully completed.

Elapsed: 00:02:21.04

ALTER SESSION SET plsql_compiler_flags = 'NATIVE';


ALTER PROCEDURE test_speed COMPILE;

EXEC test_speed;

PL/SQL procedure successfully completed.

Elapsed: 00:02:19.04

56
Stop Making the Same Performance Mistakes
PL/SQL is great, but like any programming language it is capable of being misused. This article highlights the common performance
mistakes made when developing in PL/SQL, turning what should be an elegant solution into a resource hog. This is very much an
overview, but each section includes links to the relevant articles on this site that discuss the topic in greater depth, including example
code, so think of this more like a check-list of things to avoid, that will help you get the best performance from your PL/SQL.

Stop using PL/SQL when you could use SQL


The first sentence in the first chapter of the PL/SQL documentation states the following.
"PL/SQL, the Oracle procedural extension of SQL, is a portable, high-performance transaction-processing language."
So PL/SQL is an extension to SQL, not a replacement for it. In the majority of cases, a pure SQL solution will perform better than one
made up of a combination of SQL and PL/SQL. Remember, databases are designed to work with sets of data. As soon as you start to
process data in a row-by-row (or slow-by-slow) manner, you are stopping the database from doing what it does best. With that in
mind, a PL/SQL programmer should aim to be an expert in SQL that knows a bit of PL/SQL, rather than an expert in PL/SQL that
knows a little bit of SQL.
SQL has evolved greatly over the last 20 years. The introduction of features like analytic functions and SQL/XML mean you can
perform very complex tasks directly from SQL. The following points describe some of the common situations where people use
PL/SQL when SQL would be more appropriate.
Stop using UTL_FILE to read text files if you can external tables. Using the UTL_FILE package to read data from flat files is very
inefficient. Since Oracle 7 people have been using SQL*Loader to improve performance, but since Oracle 9i the recommended way to
read data from flat files is to use external tables. Not only is is more efficient by default, but it is easy to read the data in parallel and
allows preprocessor commands to do tasks like unzipping files on the fly before reading them. In many cases, your PL/SQL load
process can be replaced by a single INSERT ... SELECT statement with the data sourced from an external table.
Stop writing PL/SQL merges if you can use the MERGE statement. Merging, or upserting, large amounts of data using PL/SQL is a
terrible waste of resources. Instead you should use the MERGE statement to perform the action in a single DML statement. Not only
is it quicker, but it looks simpler and is easily made to run in parallel.
Stop coding multitable inserts manually. Why send multiple DML statements to the server when an action can be performed in a
single multitable insert? Since Oracle 9i multitable inserts have provided a flexible way of reducing round-trips to the server.
Stop using bulk binds (FORALL) when you can use DML error logging (DBMS_ERRLOG) to trap failures in DML. In some
situations the most obvious solution to a problem is a DML statement (INSERT ... SELECT, UPDATE, DELETE), but you may
choose to avoid DML because of the way it reacts to exceptions. By default, if a single row in a DML statement raises an exception,
all the work done by that DML statement is rolled back. In the past this meant operations that were logically a single DML statements
affecting multiple rows had to be coded as a PL/SQL bulk operation using the FORALL ... SAVE EXCEPTIONS construct, for fear
that a single exception would trash the whole process. Oracle 10g Release 2 introduced DML error logging, allowing us to revert back
to using a single DML statement to replace the unnecessary bulk bind operation.
The thing to remember about all these points is they replace PL/SQL with DML. In addition to them being more efficient, provided
the server has enough resources to cope with it, it is very easy to make them even faster on large operations by running them in
parallel. Making PL/SQL run in parallel is considerably more difficult in comparison (see parallel-enabled pipelined table functions
and DBMS_PARALLEL_EXECUTE).

Stop avoiding bulk binds


Having just told you to avoid bulk binds in favor of single DML statements, I'm now going to tell you to stop avoiding bulk binds
where they are appropriate. If you are in a situation where a single DML statement is not possible and you need to process many rows
individually, you should use bulk binds as they can often provide an order of magnitude performance improvement over conventional
row-by-row processing in PL/SQL.
Bulk binds have been available since Oracle 8i, but it was the inclusion of record processing in bulk bind operations in Oracle 9i
Release 2 that made them significantly easier to work with.

57
The BULK COLLECT clause allows you to pull multiple rows back into a collection. The FORALL construct allows you to bind all
the data in a collection into a DML statement. In both cases, the performance improvements are achieved by reducing the number of
context switches between PL/SQL and SQL that are associated with row-by-row processing.

Stop using pass-by-value (NOCOPY)


As the Oracle database and PL/SQL have matured it has become increasingly common to work with large objects (LOBs), collections
and complex object types, such as XMLTYPE. When these large and complicated types are passed as OUT and IN OUT parameters
to procedures and functions, the default pass-by-value processing of these parameters can represent a significant performance
overhead.
The NOCOPY hint allows you to switch from the default pass-by-value to pass-by-reference, eliminating this overhead. In many
cases, this can represent a significant performance improvement with virtually no effort.

Stop using the wrong data types


When you use the wrong data types, Oracle is forced to do an implicit conversion during assignments and comparisons, which
represents an unnecessary overhead. In some cases this can lead to unexpected and dramatic issues, like preventing the optimizer from
using an index or resulting in incorrect date conversions.
Oracle provide a variety of data types, many of which have dramatically difference performance characteristics. Nowhere is this more
evident than with the performance of numeric data types.
Make sure you pick the appropriate data type for the job you are doing!

Quick Points
 Stop doing index scans when you can use ROWIDs.
 Stop using explicit cursors.
 Stop putting your code in the wrong order. Take advantage of performance gains associated with short-circuit evaluation and
logic/branch ordering in PL/SQL.
 Stop doing intensive processing immediately if it is more appropriate to decouple it.
 Stop calling PL/SQL functions in your SQL statements. If you must do it, make sure you use the most efficient manner
possible.
 Stop avoiding code instrumentation (DBMS_APPLICATION_INFO and DBMS_SESSION). It's a very quick way to
identify problems.
 Stop avoiding PL/SQL native compilation.
 Stop avoiding conditional compilation where it is appropriate. The easiest way to improve the speed of doing something is to
avoid doing it in the first place.
 Stop reinventing the wheel. Oracle has many built-in packages, procedures and functions that will probably do the job much
more efficiently than you will, so learn them and use them

Performance of Numeric Data Types in PL/SQL


Oracle provide a variety of numeric data types that have differing performance characteristics. This article demonstrates the
performance differences between the basic integer and real number datatypes.

 NUMBER Data Type


 Integer Data Types

58
 Real Number Data Types

NUMBER Data Type


The NUMBER data type is the supertype of all numeric datatypes. It is an internal type, meaning the Oracle program performs the
mathematical operations, making it slower, but extremely portable. All the tests in this article will compare the performance of other
types with the NUMBER data type.

Integer Data Types


The following code compares the performance of some integer data types with that of the NUMBER data type.

SET SERVEROUTPUT ON
DECLARE
l_number1 NUMBER := 1;
l_number2 NUMBER := 1;
l_integer1 INTEGER := 1;
l_integer2 INTEGER := 1;
l_pls_integer1 PLS_INTEGER := 1;
l_pls_integer2 PLS_INTEGER := 1;
l_binary_integer1 BINARY_INTEGER := 1;
l_binary_integer2 BINARY_INTEGER := 1;
l_simple_integer1 BINARY_INTEGER := 1;
l_simple_integer2 BINARY_INTEGER := 1;
l_loops NUMBER := 10000000;
l_start NUMBER;
BEGIN
-- Time NUMBER.
l_start := DBMS_UTILITY.get_time;

FOR i IN 1 .. l_loops LOOP


l_number1 := l_number1 + l_number2;
END LOOP;

DBMS_OUTPUT.put_line('NUMBER : ' ||
(DBMS_UTILITY.get_time - l_start) || ' hsecs');

-- Time INTEGER.
l_start := DBMS_UTILITY.get_time;

FOR i IN 1 .. l_loops LOOP


l_integer1 := l_integer1 + l_integer2;
END LOOP;

DBMS_OUTPUT.put_line('INTEGER : ' ||
(DBMS_UTILITY.get_time - l_start) || ' hsecs');

-- Time PLS_INTEGER.
l_start := DBMS_UTILITY.get_time;

FOR i IN 1 .. l_loops LOOP


l_pls_integer1 := l_pls_integer1 + l_pls_integer2;
END LOOP;

DBMS_OUTPUT.put_line('PLS_INTEGER : ' ||
(DBMS_UTILITY.get_time - l_start) || ' hsecs');

59
-- Time BINARY_INTEGER.
l_start := DBMS_UTILITY.get_time;

FOR i IN 1 .. l_loops LOOP


l_binary_integer1 := l_binary_integer1 + l_binary_integer2;
END LOOP;

DBMS_OUTPUT.put_line('BINARY_INTEGER : ' ||
(DBMS_UTILITY.get_time - l_start) || ' hsecs');

-- Time SIMPLE_INTEGER.
l_start := DBMS_UTILITY.get_time;

FOR i IN 1 .. l_loops LOOP


l_simple_integer1 := l_simple_integer1 + l_simple_integer2;
END LOOP;

DBMS_OUTPUT.put_line('SIMPLE_INTEGER : ' ||
(DBMS_UTILITY.get_time - l_start) || ' hsecs');
END;
/
NUMBER : 52 hsecs
INTEGER : 101 hsecs
PLS_INTEGER : 17 hsecs
BINARY_INTEGER : 17 hsecs
SIMPLE_INTEGER : 19 hsecs

PL/SQL procedure successfully completed.

SQL>

From this we can make a number of conclusions:


 NUMBER: As mentioned previously, since NUMBER is an internal datatype, it is not the fastest of the numeric types.
 INTEGER: Like NUMBER, the INTEGER type is an internal type, but the extra constraints on this datatype make it
substantially slower than NUMBER. If possible, you should avoid constrained internal datatypes.
 PLS_INTEGER: This type uses machine arithmetic, making it much faster than the internal datatypes. If in doubt, you
should use PLS_INTEGER for integer variables.
 BINARY_INTEGER: Prior to Oracle 10g, BINARY_INTEGER was an internal type with performance characteristics worse
than the INTEGER datatype. From Oracle 10g upward BINARY_INTEGER is identical to PLS_INTEGER.
 SIMPLE_INTEGER: This subtype of PLS_INTEGER was introduced in Oracle 11g. In interpreted mode it has similar
performance to BINARY_INTEGER and PLS_INTEGER, but in natively compiled code it is typically about twice as fast as
those types.

Real Number Data Types


Oracle 10g introduced the BINARY_FLOAT and BINARY_DOUBLE data types to handle real numbers. Both new types use
machine arithmetic, making them faster than the NUMBER data type, as shown in the following example.

SET SERVEROUTPUT ON
DECLARE
l_number1 NUMBER := 1.1;
l_number2 NUMBER := 1.1;
l_binary_float1 BINARY_FLOAT := 1.1;
l_binary_float2 BINARY_FLOAT := 1.1;
l_simple_float1 SIMPLE_FLOAT := 1.1;
l_simple_float2 SIMPLE_FLOAT := 1.1;
l_binary_double1 BINARY_DOUBLE := 1.1;

60
l_binary_double2 BINARY_DOUBLE := 1.1;
l_simple_double1 SIMPLE_DOUBLE := 1.1;
l_simple_double2 SIMPLE_DOUBLE := 1.1;
l_loops NUMBER := 10000000;
l_start NUMBER;
BEGIN
-- Time NUMBER.
l_start := DBMS_UTILITY.get_time;

FOR i IN 1 .. l_loops LOOP


l_number1 := l_number1 + l_number2;
END LOOP;

DBMS_OUTPUT.put_line('NUMBER : ' ||
(DBMS_UTILITY.get_time - l_start) || ' hsecs');

-- Time BINARY_FLOAT.
l_start := DBMS_UTILITY.get_time;

FOR i IN 1 .. l_loops LOOP


l_binary_float1 := l_binary_float1 + l_binary_float2;
END LOOP;

DBMS_OUTPUT.put_line('BINARY_FLOAT : ' ||
(DBMS_UTILITY.get_time - l_start) || ' hsecs');

-- Time SIMPLE_FLOAT.
l_start := DBMS_UTILITY.get_time;

FOR i IN 1 .. l_loops LOOP


l_simple_float1 := l_simple_float1 + l_simple_float2;
END LOOP;

DBMS_OUTPUT.put_line('SIMPLE_FLOAT : ' ||
(DBMS_UTILITY.get_time - l_start) || ' hsecs');

-- Time BINARY_DOUBLE.
l_start := DBMS_UTILITY.get_time;

FOR i IN 1 .. l_loops LOOP


l_binary_double1 := l_binary_double1 + l_binary_double2;
END LOOP;

DBMS_OUTPUT.put_line('BINARY_DOUBLE : ' ||
(DBMS_UTILITY.get_time - l_start) || ' hsecs');

-- Time SIMPLE_DOUBLE.
l_start := DBMS_UTILITY.get_time;

FOR i IN 1 .. l_loops LOOP


l_simple_double1 := l_simple_double1 + l_simple_double2;
END LOOP;

DBMS_OUTPUT.put_line('SIMPLE_DOUBLE : ' ||
(DBMS_UTILITY.get_time - l_start) || ' hsecs');
END;
/
NUMBER : 56 hsecs
BINARY_FLOAT : 25 hsecs
SIMPLE_FLOAT : 26 hsecs

61
BINARY_DOUBLE : 33 hsecs
SIMPLE_DOUBLE : 34 hsecs

PL/SQL procedure successfully completed.

SQL>

Both BINARY_FLOAT and BINARY_DOUBLE out-perform the NUMBER data type, but the fact they use machine
arithmetic can potentially make them less portable. The same mathematical operations performed on two different underlying
architectures may result in minor rounding errors. If portability is your primary concern, then you should use the NUMBER type,
otherwise you can take advantage of these types for increased performance.

Similar to SIMPLE_INTEGER, SIMPLE_FLOAT and SIMPLE_DOUBLE provide improved performance in natively


compiled code because of the removal of NULL checking.

Real Time Materialized views


Materialized views are a really useful performance feature, allowing you to pre-calcuate joins and aggregations,
which can make applications and reports feel more responsive. The complication comes from the lag between
the last refresh of the materialized view and subsequent DML changes to the base tables. Fast refreshes allow
you to run refreshes more often, and in some cases you can make use of refreshes triggered on commit of
changes to the base tables, but this can represent a significant overhead from a DML performance perspective.

Oracle 12.2 introduced the concept of real-time materialized views, which allow a statement-level wind-
forward of a stale materialised view, making the data appear fresh to the statement. This wind-forward is based
on changes computed using materialized view logs, similar to a conventional fast refresh, but the operation only
affect the current statement. The changes are not persisted in the materialized view, so a conventional refresh is
still required at some point.

The real-time materialized functionality has some restrictions associated with it including the following.

 It is only available if the QUERY_REWRITE_INTEGRITY parameter is set to ENFORCED (the default) or


TRUSTED. If the QUERY_REWRITE_INTEGRITY parameter is set to STALE_TOLERATED, Oracle will not
wind forward the data in a stale materialized view.
 This can't be used in conjunction with a materialized view using the REFRESH ... ON COMMIT option.

62
 The materialized view must be capable of a fast refresh, so all the typical fast refresh restrictions apply
here also.
 The materialized view can't use database links. I don't think this is a problem as I see this as a solution
for real-time reporting and dashboards, rather than part of a distributed environment.
 The materialized view must use the ENABLE ON QUERY COMPUTATION option.
 Queries making direct references to a materialized view will not use the real-time materialized view
functionality by default. To use this functionality the query much use the FRESH_MV hint.

The rest of this article provides some simple examples of real-time materialized views.

Setup
We need a table to act as the source of the materialized view. The following script creates and populates a test
table with random data.

CONN test/test@pdb1

DROP TABLE order_lines PURGE;

CREATE TABLE order_lines (


id NUMBER(10),
order_id NUMBER(10),
line_qty NUMBER(5),
total_value NUMBER(10,2),
created_date DATE,
CONSTRAINT orders_pk PRIMARY KEY (id)
);

INSERT /*+ APPEND */ INTO order_lines


SELECT level AS id,
TRUNC(DBMS_RANDOM.value(1,1000)) AS order_id,
TRUNC(DBMS_RANDOM.value(1,20)) AS line_qty,
ROUND(DBMS_RANDOM.value(1,1000),2) AS total_value,
TRUNC(SYSDATE - DBMS_RANDOM.value(0,366)) AS created_date
FROM dual CONNECT BY level <= 100000;
COMMIT;

EXEC DBMS_STATS.gather_table_stats(USER, 'order_lines');

Materialized View Logs


For real-time materialized views to work we must have materialised view logs on all the tables the materialized
view is based on.

DROP MATERIALIZED VIEW LOG ON order_lines;

CREATE MATERIALIZED VIEW LOG ON order_lines


WITH ROWID, SEQUENCE(order_id, line_qty, total_value)
INCLUDING NEW VALUES;

63
Materialized View
We can now create the materialized view. Notice the ENABLE ON QUERY COMPUTATION option, which is new to
Oracle 12.2.

DROP MATERIALIZED VIEW order_summary_rtmv;

CREATE MATERIALIZED VIEW order_summary_rtmv


REFRESH FAST ON DEMAND
ENABLE QUERY REWRITE
ENABLE ON QUERY COMPUTATION
AS
SELECT order_id,
SUM(line_qty) AS sum_line_qty,
SUM(total_value) AS sum_total_value,
COUNT(*) AS row_count
FROM order_lines
GROUP BY order_id;

EXEC DBMS_STATS.gather_table_stats(USER, 'order_summary_rtmv');

Basic Rewrite
As with previous versions of the database, if we run a query that could be serviced quicker by the materialized
view, Oracle will rewrite the query to use the materialized view.

SELECT order_id,
SUM(line_qty) AS sum_line_qty,
SUM(total_value) AS sum_total_value,
COUNT(*) AS row_count
FROM order_lines
WHERE order_id = 1
GROUP BY order_id;

ORDER_ID SUM_LINE_QTY SUM_TOTAL_VALUE ROW_COUNT


---------- ------------ --------------- ----------
1 880 44573.88 95

SQL>

SET LINESIZE 200 PAGESIZE 100


SELECT *
FROM dbms_xplan.display_cursor();

PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------
----------
SQL_ID 3rttkdd0ybtaw, child number 0
-------------------------------------
SELECT order_id, SUM(line_qty) AS sum_line_qty,
SUM(total_value) AS sum_total_value, COUNT(*) AS row_count FROM
order_lines WHERE order_id = 1 GROUP BY order_id

Plan hash value: 1165901663

64
-----------------------------------------------------------------------------------------
----------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
Time |
-----------------------------------------------------------------------------------------
----------
| 0 | SELECT STATEMENT | | | | 4 (100)| |
|* 1 | MAT_VIEW REWRITE ACCESS FULL| ORDER_SUMMARY_RTMV | 1 | 17 | 4 (0)|
00:00:01 |
-----------------------------------------------------------------------------------------
----------

Predicate Information (identified by operation id):


---------------------------------------------------

1 - filter("ORDER_SUMMARY_RTMV"."ORDER_ID"=1)

20 rows selected.

SQL>

We can see from the execution plan the materialized view was used, rather than accessing the base table. Notice
the row count value of 95.

Rewrite Plus Real-Time Refresh


We amend the data in the table, so the materialized view is now considered stale.

INSERT INTO order_lines VALUES (100001, 1, 30, 10000, SYSDATE);


COMMIT;

COLUMN mview_name FORMAT A30

SELECT mview_name,
staleness,
on_query_computation
FROM user_mviews;

MVIEW_NAME STALENESS O
------------------------------ ------------------- -
ORDER_SUMMARY_RTMV NEEDS_COMPILE Y

SQL>

A regular materialized view would no longer be considered for query rewrites unless we had the
QUERY_REWRITE_INTEGRITY parameter set to STALE_TOLERATED for the session. Since we have the ENABLE ON
QUERY COMPUTATION option on the materialized view it is still considered usable, as Oracle will dynamically
amend the values to reflect the changes in the materialized view logs.

SELECT order_id,
SUM(line_qty) AS sum_line_qty,
SUM(total_value) AS sum_total_value,
COUNT(*) AS row_count
FROM order_lines
WHERE order_id = 1
GROUP BY order_id;

65
ORDER_ID SUM_LINE_QTY SUM_TOTAL_VALUE ROW_COUNT
---------- ------------ --------------- ----------
1 910 54573.88 96

SQL>

SET LINESIZE 200 PAGESIZE 100


SELECT *
FROM dbms_xplan.display_cursor();

PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------
-------------------------
SQL_ID 3rttkdd0ybtaw, child number 1
-------------------------------------
SELECT order_id, SUM(line_qty) AS sum_line_qty,
SUM(total_value) AS sum_total_value, COUNT(*) AS row_count FROM
order_lines WHERE order_id = 1 GROUP BY order_id

Plan hash value: 1640379716

-----------------------------------------------------------------------------------------
-------------------------
| Id | Operation | Name | Rows | Bytes |
Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------
-------------------------
| 0 | SELECT STATEMENT | | | |
19 (100)| |
| 1 | VIEW | | 1001 | 52052 |
19 (27)| 00:00:01 |
| 2 | UNION-ALL | | | |
|* 3 | FILTER | | | |
|* 4 | HASH JOIN OUTER | | 1 | 33 |
9 (23)| 00:00:01 |
|* 5 | MAT_VIEW ACCESS FULL | ORDER_SUMMARY_RTMV | 1 | 17 |
4 (0)| 00:00:01 |
| 6 | VIEW | | 1 | 16 |
5 (40)| 00:00:01 |
| 7 | HASH GROUP BY | | 1 | 39 |
5 (40)| 00:00:01 |
| 8 | VIEW | | 1 | 39 |
4 (25)| 00:00:01 |
| 9 | RESULT CACHE | dyqrs00u554qffvsw6akf32p2p | | |
|* 10 | VIEW | | 1 | 103 |
4 (25)| 00:00:01 |
| 11 | WINDOW SORT | | 1 | 194 |
4 (25)| 00:00:01 |
|* 12 | TABLE ACCESS FULL | MLOG$_ORDER_LINES | 1 | 194 |
3 (0)| 00:00:01 |
| 13 | VIEW | | 1000 | 52000 |
10 (30)| 00:00:01 |
| 14 | UNION-ALL | | | |
|* 15 | FILTER | | | |
| 16 | NESTED LOOPS OUTER | | 999 | 94905 |
4 (25)| 00:00:01 |
| 17 | VIEW | | 1 | 78 |
4 (25)| 00:00:01 |
|* 18 | FILTER | | | |

66
| 19 | HASH GROUP BY | | 1 | 39 |
4 (25)| 00:00:01 |
|* 20 | VIEW | | 1 | 39 |
4 (25)| 00:00:01 |
| 21 | RESULT CACHE | dyqrs00u554qffvsw6akf32p2p | | |
|* 22 | VIEW | | 1 | 103 |
4 (25)| 00:00:01 |
| 23 | WINDOW SORT | | 1 | 194 |
4 (25)| 00:00:01 |
|* 24 | TABLE ACCESS FULL | MLOG$_ORDER_LINES | 1 | 194 |
3 (0)| 00:00:01 |
|* 25 | INDEX UNIQUE SCAN | I_SNAP$_ORDER_SUMMARY_RTMV | 999 | 16983 |
0 (0)| |
| 26 | NESTED LOOPS | | 1 | 98 |
6 (34)| 00:00:01 |
| 27 | VIEW | | 1 | 81 |
5 (40)| 00:00:01 |
| 28 | HASH GROUP BY | | 1 | 39 |
5 (40)| 00:00:01 |
| 29 | VIEW | | 1 | 39 |
4 (25)| 00:00:01 |
| 30 | RESULT CACHE | dyqrs00u554qffvsw6akf32p2p | | |
|* 31 | VIEW | | 1 | 103 |
4 (25)| 00:00:01 |
| 32 | WINDOW SORT | | 1 | 194 |
4 (25)| 00:00:01 |
|* 33 | TABLE ACCESS FULL | MLOG$_ORDER_LINES | 1 | 194 |
3 (0)| 00:00:01 |
|* 34 | MAT_VIEW ACCESS BY INDEX ROWID| ORDER_SUMMARY_RTMV | 1 | 17 |
1 (0)| 00:00:01 |
|* 35 | INDEX UNIQUE SCAN | I_SNAP$_ORDER_SUMMARY_RTMV | 1 | |
0 (0)| |
-----------------------------------------------------------------------------------------
-------------------------

Predicate Information (identified by operation id):


---------------------------------------------------

3 -
filter("AV$0"."OJ_MARK" IS NULL)
4 -
access(SYS_OP_MAP_NONNULL("ORDER_ID")=SYS_OP_MAP_NONNULL("AV$0"."GB0"))
5 -
filter("ORDER_SUMMARY_RTMV"."ORDER_ID"=1)
10 -
filter((("MAS$"."OLD_NEW$$"='N' AND "MAS$"."SEQ$$"="MAS$"."MAXSEQ$$") OR
(INTERNAL_FUNCTION("MAS$"."OLD_NEW$$") AND
"MAS$"."SEQ$$"="MAS$"."MINSEQ$$")))
12 - filter("MAS$"."SNAPTIME$$">TO_DATE(' 2017-07-16 19:11:21', 'syyyy-mm-dd
hh24:mi:ss'))
15 - filter(CASE WHEN ROWID IS NOT NULL THEN 1 ELSE NULL END IS NULL)
18 - filter(SUM(1)>0)
20 - filter("MAS$"."ORDER_ID"=1)
22 - filter((("MAS$"."OLD_NEW$$"='N' AND "MAS$"."SEQ$$"="MAS$"."MAXSEQ$$") OR
(INTERNAL_FUNCTION("MAS$"."OLD_NEW$$") AND
"MAS$"."SEQ$$"="MAS$"."MINSEQ$$")))
24 - filter("MAS$"."SNAPTIME$$">TO_DATE(' 2017-07-16 19:11:21', 'syyyy-mm-dd
hh24:mi:ss'))
25 - access("ORDER_SUMMARY_RTMV"."SYS_NC00005$"=SYS_OP_MAP_NONNULL("AV$0"."GB0"))
31 - filter((("MAS$"."OLD_NEW$$"='N' AND "MAS$"."SEQ$$"="MAS$"."MAXSEQ$$") OR
(INTERNAL_FUNCTION("MAS$"."OLD_NEW$$") AND
"MAS$"."SEQ$$"="MAS$"."MINSEQ$$")))
33 - filter("MAS$"."SNAPTIME$$">TO_DATE(' 2017-07-16 19:11:21', 'syyyy-mm-dd
hh24:mi:ss'))

67
34 - filter(("ORDER_SUMMARY_RTMV"."ORDER_ID"=1 AND
"ORDER_SUMMARY_RTMV"."ROW_COUNT"+"AV$0"."D0">0))
35 - access("ORDER_SUMMARY_RTMV"."SYS_NC00005$"=SYS_OP_MAP_NONNULL("AV$0"."GB0"))

Result Cache Information (identified by operation id):


------------------------------------------------------

9 -
21 -
30 -

Note
-----
- dynamic statistics used: dynamic sampling (level=2)
- this is an adaptive plan

83 rows selected.

SQL>

We can see the row count is now 96 and the execution plan includes additional work to complete the wind-
forward.

Direct Query of Materialized View (FRESH_MV Hint)


In addition to the query rewrites, we also have the ability to query materialized views directly. When we do this
we get the current contents of the materialized view by default.

SELECT order_id,
sum_line_qty,
sum_total_value,
row_count
FROM order_summary_rtmv
WHERE order_id = 1;

ORDER_ID SUM_LINE_QTY SUM_TOTAL_VALUE ROW_COUNT


---------- ------------ --------------- ----------
1 880 44573.88 95

SQL>

SET LINESIZE 200 PAGESIZE 100


SELECT *
FROM dbms_xplan.display_cursor();

PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------
--
SQL_ID 8tq6wzmccuzfm, child number 0
-------------------------------------
SELECT order_id, sum_line_qty, sum_total_value,
row_count FROM order_summary_rtmv WHERE order_id = 1

Plan hash value: 3344356712

68
-----------------------------------------------------------------------------------------
--
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------
--
| 0 | SELECT STATEMENT | | | | 4 (100)| |
|* 1 | MAT_VIEW ACCESS FULL| ORDER_SUMMARY_RTMV | 1 | 17 | 4 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------
--

Predicate Information (identified by operation id):


---------------------------------------------------

1 - filter("ORDER_ID"=1)

19 rows selected.

SQL>

The FRESH_VM hint tells Oracle we want to take advantage of the real-time functionality when doing a direct
query against the materialized view, which is why we see a row count of 96 again.

SELECT /*+ FRESH_MV */


order_id,
sum_line_qty,
sum_total_value,
row_count
FROM order_summary_rtmv
WHERE order_id = 1;

ORDER_ID SUM_LINE_QTY SUM_TOTAL_VALUE ROW_COUNT


---------- ------------ --------------- ----------
1 910 54573.88 96

SQL>

SET LINESIZE 200 PAGESIZE 100


SELECT *
FROM dbms_xplan.display_cursor();

PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------
-------------------------
SQL_ID 0pqhrf8c5kbgz, child number 0
-------------------------------------
SELECT /*+ FRESH_MV */ order_id, sum_line_qty,
sum_total_value, row_count FROM order_summary_rtmv WHERE
order_id = 1

Plan hash value: 1640379716

-----------------------------------------------------------------------------------------
-------------------------
| Id | Operation | Name | Rows | Bytes |
Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------
-------------------------

69
| 0 | SELECT STATEMENT | | | |
20 (100)| |
| 1 | VIEW | | 1001 | 52052 |
20 (30)| 00:00:01 |
| 2 | UNION-ALL | | | |
|* 3 | FILTER | | | |
|* 4 | HASH JOIN OUTER | | 1 | 33 |
9 (23)| 00:00:01 |
|* 5 | MAT_VIEW ACCESS FULL | ORDER_SUMMARY_RTMV | 1 | 17 |
4 (0)| 00:00:01 |
| 6 | VIEW | | 1 | 16 |
5 (40)| 00:00:01 |
| 7 | HASH GROUP BY | | 1 | 39 |
5 (40)| 00:00:01 |
| 8 | VIEW | | 1 | 39 |
4 (25)| 00:00:01 |
| 9 | RESULT CACHE | dyqrs00u554qffvsw6akf32p2p | | |
|* 10 | VIEW | | 1 | 103 |
4 (25)| 00:00:01 |
| 11 | WINDOW SORT | | 1 | 194 |
4 (25)| 00:00:01 |
|* 12 | TABLE ACCESS FULL | MLOG$_ORDER_LINES | 1 | 194 |
3 (0)| 00:00:01 |
| 13 | VIEW | | 1000 | 52000 |
11 (37)| 00:00:01 |
| 14 | UNION-ALL | | | |
|* 15 | FILTER | | | |
| 16 | NESTED LOOPS OUTER | | 999 | 94905 |
5 (40)| 00:00:01 |
| 17 | VIEW | | 1 | 78 |
5 (40)| 00:00:01 |
|* 18 | FILTER | | | |
| 19 | HASH GROUP BY | | 1 | 39 |
5 (40)| 00:00:01 |
|* 20 | VIEW | | 1 | 39 |
4 (25)| 00:00:01 |
| 21 | RESULT CACHE | dyqrs00u554qffvsw6akf32p2p | | |
|* 22 | VIEW | | 1 | 103 |
4 (25)| 00:00:01 |
| 23 | WINDOW SORT | | 1 | 194 |
4 (25)| 00:00:01 |
|* 24 | TABLE ACCESS FULL | MLOG$_ORDER_LINES | 1 | 194 |
3 (0)| 00:00:01 |
|* 25 | INDEX UNIQUE SCAN | I_SNAP$_ORDER_SUMMARY_RTMV | 999 | 16983 |
0 (0)| |
| 26 | NESTED LOOPS | | 1 | 98 |
6 (34)| 00:00:01 |
| 27 | VIEW | | 1 | 81 |
5 (40)| 00:00:01 |
| 28 | HASH GROUP BY | | 1 | 39 |
5 (40)| 00:00:01 |
| 29 | VIEW | | 1 | 39 |
4 (25)| 00:00:01 |
| 30 | RESULT CACHE | dyqrs00u554qffvsw6akf32p2p | | |
|* 31 | VIEW | | 1 | 103 |
4 (25)| 00:00:01 |
| 32 | WINDOW SORT | | 1 | 194 |
4 (25)| 00:00:01 |
|* 33 | TABLE ACCESS FULL | MLOG$_ORDER_LINES | 1 | 194 |
3 (0)| 00:00:01 |

70
|* 34 | MAT_VIEW ACCESS BY INDEX ROWID| ORDER_SUMMARY_RTMV | 1 | 17 |
1 (0)| 00:00:01 |
|* 35 | INDEX UNIQUE SCAN | I_SNAP$_ORDER_SUMMARY_RTMV | 1 | |
0 (0)| |
-----------------------------------------------------------------------------------------
-------------------------

Predicate Information (identified by operation id):


---------------------------------------------------

3 -
filter("AV$0"."OJ_MARK" IS NULL)
4 -
access(SYS_OP_MAP_NONNULL("ORDER_ID")=SYS_OP_MAP_NONNULL("AV$0"."GB0"))
5 -
filter("ORDER_SUMMARY_RTMV"."ORDER_ID"=1)
10 -
filter((("MAS$"."OLD_NEW$$"='N' AND "MAS$"."SEQ$$"="MAS$"."MAXSEQ$$") OR
(INTERNAL_FUNCTION("MAS$"."OLD_NEW$$") AND
"MAS$"."SEQ$$"="MAS$"."MINSEQ$$")))
12 - filter("MAS$"."SNAPTIME$$">TO_DATE(' 2017-07-16 19:11:21', 'syyyy-mm-dd
hh24:mi:ss'))
15 - filter(CASE WHEN ROWID IS NOT NULL THEN 1 ELSE NULL END IS NULL)
18 - filter(SUM(1)>0)
20 - filter("MAS$"."ORDER_ID"=1)
22 - filter((("MAS$"."OLD_NEW$$"='N' AND "MAS$"."SEQ$$"="MAS$"."MAXSEQ$$") OR
(INTERNAL_FUNCTION("MAS$"."OLD_NEW$$") AND
"MAS$"."SEQ$$"="MAS$"."MINSEQ$$")))
24 - filter("MAS$"."SNAPTIME$$">TO_DATE(' 2017-07-16 19:11:21', 'syyyy-mm-dd
hh24:mi:ss'))
25 - access("ORDER_SUMMARY_RTMV"."SYS_NC00005$"=SYS_OP_MAP_NONNULL("AV$0"."GB0"))
31 - filter((("MAS$"."OLD_NEW$$"='N' AND "MAS$"."SEQ$$"="MAS$"."MAXSEQ$$") OR
(INTERNAL_FUNCTION("MAS$"."OLD_NEW$$") AND
"MAS$"."SEQ$$"="MAS$"."MINSEQ$$")))
33 - filter("MAS$"."SNAPTIME$$">TO_DATE(' 2017-07-16 19:11:21', 'syyyy-mm-dd
hh24:mi:ss'))
34 - filter(("ORDER_SUMMARY_RTMV"."ORDER_ID"=1 AND
"ORDER_SUMMARY_RTMV"."ROW_COUNT"+"AV$0"."D0">0))
35 - access("ORDER_SUMMARY_RTMV"."SYS_NC00005$"=SYS_OP_MAP_NONNULL("AV$0"."GB0"))

Result Cache Information (identified by operation id):


------------------------------------------------------

9 -
21 -
30 -

Note
-----
- dynamic statistics used: dynamic sampling (level=2)
- this is an adaptive plan

83 rows selected.

SQL>

71
Authid current_user
The authid current_user is used when you want a piece of code (PL/SQL) to execute with the privileges of the
current user, and NOT the user ID that created the procedure. This is termed a "invoker rights", the opposite of
"definer rights".

The authid current_user is the opposite of authid definer.

In the same sense, the authid current_user is the reverse of the "grant execute" where the current user does not
matter, the privileges of the creating user are used.

PL/SQL, by default, run with the privileges of the schema within which they are created no matter who invokes
the procedure. In order for a PL/SQL package to run with invokers rights AUTHID CURRENT_USER has to
be explicitly written into the package.

To understand the authid current_user, consider this type definition:

CREATE TYPE address_t


AUTHID CURRENT_USER
AS OBJECT (
address_line1 varchar2(80),
address_line2 varchar2(80),
street_name varchar2(30),
street_number number,
city varchar2(30),
state_or_province varchar2(2),
zip number(5),
zip_4 number(4),
country_code varchar2(20));

The authid current_user clause tells the kernel that any methods that may be used in the type specification (in
the above example, none) should execute with the privilege of the executing user, not the owner.

The default option is authid definer, which would correspond to the behavior in pre-Oracle8i releases, where
the method would execute with the privileges of the user creating the type.

See this example of authid current_user to understand how this syntax causes invoker rights, which, in turn,
changes the behavior of the PL/SQL code.

WARNING: Writing PL/SQL code with the default authid definer, can facilitate SQL injection attacks, because
an intruder would get privileges that they would not get if they used authid current_user.

72
Passing Large Data Structures with PL/SQL
NOCOPY
The PL/SQL runtime engine has two different methods for passing parameter values between stored procedures
and functions, by value and by reference.

Also note this on using the nocopy hint.

When a parameter is passed by value the PL/SQL runtime engine copies the actual value of the parameter into
the formal parameter. Any changes made to the parameter inside the procedure has no effect on the values of
the variables that were passed to the procedure from outside.

When a parameter is passed by reference the runtime engine sets up the procedure call so that both the actual
and the formal parameters point (reference) the same memory location that holds the value of the parameter.

By default OUT and IN OUT parameters are passed by value and IN parameters are passed by reference. When
an OUT or IN OUT parameter is modified inside the procedure the procedure actually only modifies a copy of
the parameter value. Only when the procedure has finished without exception is the result value copied back to
the formal parameter.

Now, if you pass a large collection as an OUT or an IN OUT parameter then it will be passed by value, in other
words the entire collection will be copied to the formal parameter when entering the procedure and back again
when exiting the procedure. If the collection is large this can lead to unnecessary CPU and memory
consumption.

The NOCOPY hint alleviates this problem because you can use it to instruct the runtime engine to try to pass
OUT or IN OUT parameters by reference instead of by value. For example:

procedure get_customer_orders(
p_customer_id in number,
p_orders out nocopy orders_coll
);
theorders orders_coll;
get_customer_orders(124, theorders);

In the absence of the NOCOPY hint the entire orders collection would have been copied into the theorders
variable upon exit from the procedure. Instead the collection is now passed by reference.

Keep in mind, however, that there is a downside to using NOCOPY. When you pass parameters to a procedure
by reference then any modifications you perform on the parameters inside the procedure is done on the same
memory location as the actual parameter, so the modifications are visible. In other words, there is no way to
?undo? or ?rollback? these modifications, even when an exception is raised midway. So if an exception is
raised inside the procedure the value of the parameter is ?undefined? and cannot be trusted.

Consider our get_customer_orders example. If the p_orders parameter was half-filled with orders when an
exception was raised, then upon exit our theorders variable will also be half-filled because it points to the same
memory location as the p_orders parameter. This downside is most problematic for IN OUT parameters

73
because if an exception occurs midway then not only is the output garbage, but you?ve also made the input
garbage.

To sum up, a NOCOPY hint can offer a small performance boost, but you must be careful and know how it
affects program behavior, in particular exception handling.

NOCOPY Clause

The final point to cover in passing variables is the NOCOPY clause . When a parameter is passed as an IN
variable, it is passed by reference. Since it will not change, PL/SQL uses the passed variable in the
procedure/function. When variables are passed in OUT or INOUT mode, a new variable is define, and the value
is copied to the passed variable when the procedure ends. If the variable is a large structure such as a PL/SQL
table or an array, the application could see a performance degradation cause by copying this structure.

The NOCOPY clause tells to PL/SQL engine to pass the variable by reference, thus avoiding the cost of
copying the variable at the end of the procedure. The PL/SQL engine has requirements that must be met before
passing the variable by reference and if those requirements are not met, the NOCOPY clause will simply be
ignored by the PL/SQL engine.

Important note: If an OUT or INOUT variable is passed by reference (NOCOPY) and the procedure
terminates due to an unhandled exception (ends abnormally), the value of the referenced variable may no
longer be valid.

Both stored procedures and functions are passed variables, the only difference is that a function can only be
passed IN variables because a function returns a value.

74
Open Discussion Subjects
 Select from more than one partition with local index
 Tablespace for each index tip
 Tablespaces with NOLOGGING feature
 Update index fields tip
 Unusable index tip
 Parallel index
 Parallel hint tips
 Table Access BY USER ROWID
 Hash table joins VS nested Loops table joins
 Truncate table drop all storage
 SET Operators VS (in and not in) and (exists and not exists)
 Multiset with collection (Map PK)
 Object type Collections and speed and ram
 Forall Values of and indices of
 Regular expressions
 Dynamic plsql
 With clause
 Pin package tip
 Clear session memory tip
 DBMS_SCHEDULER
 Virtual Columns

75

You might also like