Quick Hint: Using LOBS with PL/SQL – Performance Problems

Published in Database Tips, PL/SQL, Quick Tips, Tuning by gpike Monday May 12, 2008

I just recently authored a PL/SQL Server Page designed to funnel large amounts of data via the htp.p function to a web application. Because the volume of data could be very large, I wanted to minimize the individual calls to the htp.p function (which can only take a maximum of 32767 characters per call).

The answer was obviously CLOBS! I proceeded to accumulate the full multi-megabyte response in a CLOB by concatenating the individual VARCHAR2 results line by line as they came out of the database.

Now I’ll be the first to admit that my experience with LOBs in Oracle has been very limited. And with the onset of SecureFiles in 11g, maybe I will never really become very proficient with LOBs. But in my LOB naivety, I assumed that if it COULD be done, it would probably work fine. You would think that after 15 years of Oracle development, I would know better…nope.

DECLARE
    bigLob CLOB;
    CURSOR getVarchars2 IS
         SELECT singleColumn FROM table;
BEGIN
FOR v_rec IN getVarchars2 LOOP
    bigClob := bigClob||v_rec.singleRow||linebreak;
END LOOP;
    doStuffWithTheCLOB(bigCLOB);
END;

It looks so innocent. Append the VARCHAR2 to the end of the CLOB. Let the database do the data type conversion from VARCHAR2 to CLOB. No problema! 10 rows, 100 rows, 1000 rows, all working just fine. Then came the request for 44K rows of data. The database became very angry and would not cooperate (“Stop asking for 44k rows!”). The user community became very angry and would not cooperate (“We need our 44k rows!”).

The solution is of course found in an obscure part of the Oracle documentation.

Here are the relevant entries:
• When Possible, Read/Write Large Data Chunks at a Time: Because LOBs are big, you can obtain the best performance by reading and writing large pieces of a LOB value at a time. This helps in several respects:
    o Writing to the LOB creates a new version of the LOB chunk. Therefore, writing small amounts at a time will incur the cost of a new version for each small write. If logging is on, then the chunk is also stored in the redo log.

• If you use the newly provided enhanced SQL semantics functionality in your applications, then there will be many more temporary LOBs created silently in SQL and PL/SQL than before. Ensure that temporary tablespace for storing these temporary LOBs is large enough for your applications. In particular, these temporary LOBs are silently created when you use the following:
     o SQL functions on LOBs
     o PL/SQL built-in character functions on LOBs
     o Variable assignments from VARCHAR2/RAW to CLOBs/BLOBs, respectively.
     o Perform a LONG-to-LOB migration

I highly recommend looking at this chapter in the LOB documentation prior to working with LOBs in SQL and PL/SQL.


Greg Pike

Greg Pike
PIOCON Technologies Website

Write your own messages to the alert log

Published in Quick Tips by cdawes Tuesday May 22, 2007

Sometimes while managing your databases, you find that it would be really slick to write a custom message to the alert log, but how?

The standard built-in package dbms_system allows you to write you own messages to either a trace file or to the alert log.

Wait! you say. I can’t find dbms_system in the PL/SQL Packages and Types Reference.
Well, No you can’t. But you can find it in your $ORACLE_HOME/rbms/admin directory.
The dbmsutil.sql file tell us it was moved to prvtutl.plb many years ago to give it more obscurity.
When we describe this package in SQLPlus, we see the function ksdwrt takes 2 parameters:

PROCEDURE KSDWRT
 Argument Name                  Type                    In/Out Default?
 ------------------------------ ----------------------- ------ --------
 DEST                           BINARY_INTEGER          IN
 TST                            VARCHAR2                IN

The DEST tells Oracle where to write the message 1=trace file, 2=alert log, 3=both.
The TST is the message to write.

The closely related function ksdddt writes a timestamp in regular alert log fashion to your output location.
So, here’s the scenario where we want to use this bit of cleverness.
We have a program that wants to record a custom message in the alert log so our monitor picks it up and deals with it (email’s the DBA, logs it in the database trouble log, what ever you need to do…)
Use it like this:

begin
dbms_system.ksdddt;
dbms_system.ksdwrt(2,'ORA-20212 My custom message is THIS!');
end;

Now let’s look for it in our alert log:

select row_num ,start_row ,alert_date ,alert_text
from alert_log
where alert_date > sysdate -1/24

 Line number Timestamp            Text
------------ -------------------- ----------------------------------------
        6951 22-May-2007 21:57:22 Tue May 22 21:57:22 2007
        6952 22-May-2007 21:57:22 ORA-20212 My custom message is THIS!

If you missed that trick of reading the alert log with SQL, check out my article from February on reading the alert log.
http://www.singlequery.com/?p=23

Lastly, execute privileges on this undocumented gem are not granted to anyone by default, so connect as SYS and
grant execute on dbms_system to system;
or to whomever you need to.

Happy coding,
Chip Dawes

Generating Multiple Artificial Rows in Oracle 10g Using a Query

Published in Database Tips, Quick Tips, SQL by jweicher Tuesday April 24, 2007

Have you ever been in a situation that required you to generate a set of multiple “artificial rows”? Perhaps you needed it for a report. Typically, you would need to do this in a PL/SQL loop of some kind. How else does one generate a >1 set of rows dynamically, from nothing? With a single query of course!

Our Scenario:

For a report of some kind, or perhaps for use in a much larger query, you need to create a rowset that consists of the dates of the 365 days previous to today, each along with a random number between 1 and 20 (pointless I agree, but useful as an example).

There are a couple ways to tackle this.

 

Method 1 - Using DUAL and a CONNECT BY
This technique actually came to us late in the drafting of this topic by way of one of our readers, Rose. But it is so simple, elegant and ingenious that it immediately became our chosen solution for most situations. Our hat tip to Rose.

Many people I’m sure are already familiar with Oracle’s CONNECT BY clause, which allows one to construct queries that select rows from a table in a hierarchical ordering that may be present in the data, by specifying the conditions that identify a parent-child relationship between rows. But for those that aren’t, a brief example is in order, though greater detail will be omitted.

Consider the following Employee table:

emp_id emp_name mgr_id
1 M. Vranicar null
2 J. Simmons 1
3 J. Weicher 2
4 R. Rolek 2

The CONNECT BY clause (and its associated optional keywords) allow us to cleverly query the table, ordering the rows hierarchically, using a relationship we know is defined through columns in the table itself, in this case, an employee to his manager:


> SELECT  LEVEL  – oracle keyword available in CONNECT BY queries
          LPAD(’ ‘, LEVEL*2) || emp_name name
  FROM employee
  START WITH mgr_id is null
  CONNECT BY PRIOR emp_id = mgr_id

Connect By Example

Without going into greater detail, Oracle retrieves the records using the CONNECT BY PRIOR condition to in essence “filter” the records to determine the next appropriate “child” of the record last retrieved, when determining the retrieval order.

Returning to our example scenario, we can use this functionality to generate the 365 row result set that we are looking for by omitting the PRIOR keyword, and using only a condition based on the LEVEL. This is doable as the CONNECT BY PRIOR clause really just acts as a filter, determining which record is the next child (or in this case, simply when to stop selecting more “child” rows!):


> SELECT  sysdate - LEVEL thedate,
          mod(abs(dbms_random.random), 20) + 1 thevalue
  FROM dual
  CONNECT BY LEVEL <= 365;

THEDATE     THEVALUE
--------- ----------
23-APR-07         20
22-APR-07         19
21-APR-07         10
20-APR-07          5
19-APR-07         13
18-APR-07          9
17-APR-07         12
16-APR-07         14
15-APR-07         19
14-APR-07         14
...
28-APR-06          9
27-APR-06          7
26-APR-06          4
25-APR-06         10
24-APR-06          8

365 rows selected.


What is happening?
This technique works because the CONNECT BY clause causes Oracle to execute the query by taking the following steps:

1) Select the first date and random number using the single record from dual.

2) Next, “connect back” to select the next appropriate “child” record, again from dual, using the CONNECT BY condition as a “filter”. Dual only has one record, and the only “filter” is that LEVEL be <= 365. The result is the single record being selected from dual again, though this time LEVEL is automatically incremented by 1.

3) Step 2 will be continually repeated, as the CONNECT BY condition/filter will continue to find the single eligible record in dual with every "connect back", until one is retrieved where LEVEL has been incremented beyond 365!

Kudos again to Rose for this one.

 

Method 2 - Using DUAL and a WITH Clause
Another technique is available, which also uses DUAL, but relies on a WITH clause instead of CONNECT BY. It should be considered more of a “brute force” method. The goal is to build an “empty” rowset of size 2n that is equal or greater than our target rowset size.

We begin by selecting anything from DUAL twice, and using a UNION ALL to preserve the duplication:


> SELECT 1 FROM DUAL UNION ALL SELECT 1 FROM DUAL;

         1
----------
         1
         1

Next, let’s use this query in a WITH clause for efficiency, and select from it multiple times without a join condition- a Cartesian Product!

> WITH tworows AS
( SELECT 1 FROM dual UNION ALL SELECT 1 FROM dual)
SELECT	*
FROM	tworows	t1, /* produces 2 rows */
	tworows t2, /* produces 4 rows */
	tworows t3, /* produces 8 rows */
	tworows t4, /* produces 16 rows */
	tworows t5, /* produces 32 rows */
	tworows t6, /* produces 64 rows */
	tworows t7, /* produces 128 rows */
	tworows t8, /* produces 256 rows */
	tworows t9  /* produces 512 rows */

What we see produced is a result set of 512 rows and 9 columns:


         1          1          1          1          1          1
---------- ---------- ---------- ---------- ---------- ---------- ...
         1          1          1          1          1          1
         1          1          1          1          1          1
         1          1          1          1          1          1
         1          1          1          1          1          1
         1          1          1          1          1          1
         1          1          1          1          1          1
         1          1          1          1          1          1
...
         1          1          1          1          1          1
         1          1          1          1          1          1
         1          1          1          1          1          1

512 rows selected.

Next, we can extend our query by actually selecting the data we are interested in by utilizing ROWNUM: the past 365 days and a random number:


> WITH tworows AS
( SELECT 1 FROM dual union all SELECT 1 FROM dual)
SELECT	sysdate - ROWNUM thedate,
	mod(abs(dbms_random.random), 20) + 1 thevalue
FROM	tworows	t1, /* produces 2 rows */
	tworows t2, /* produces 4 rows */
	tworows t3, /* produces 8 rows */
	tworows t4, /* produces 16 rows */
	tworows t5, /* produces 32 rows */
	tworows t6, /* produces 64 rows */
	tworows t7, /* produces 128 rows */
	tworows t8, /* produces 256 rows */
	tworows t9  /* produces 512 rows */

And again, we see that this produces a rowset of 512 rows, this time with only values we are interested in:


THEDATE     THEVALUE
--------- ----------
12-APR-07          2
11-APR-07         18
10-APR-07         12
09-APR-07         18
08-APR-07         19
07-APR-07          2
...
22-NOV-05          9
21-NOV-05         17
20-NOV-05          5
19-NOV-05          1
18-NOV-05         19
17-NOV-05          8

512 rows selected.

Finally, we can limit the number of rows down to our target size with a filter on “rownum”:


> WITH tworows AS
( SELECT 1 FROM dual union all SELECT 1 FROM dual)
SELECT	sysdate - rownum thedate,
	mod(abs(dbms_random.random), 20) + 1 thevalue
FROM	tworows	t1, /* produces 2 rows */
	tworows t2, /* produces 4 rows */
	tworows t3, /* produces 8 rows */
	tworows t4, /* produces 16 rows */
	tworows t5, /* produces 32 rows */
	tworows t6, /* produces 64 rows */
	tworows t7, /* produces 128 rows */
	tworows t8, /* produces 256 rows */
	tworows t9  /* produces 512 rows */
WHERE	rownum <= 365

And we now have the result set we were looking for:


THEDATE     THEVALUE
--------- ----------
12-APR-07         16
11-APR-07         20
10-APR-07         12
09-APR-07         12
08-APR-07         11
07-APR-07         17
06-APR-07          5
...
17-APR-06         13
16-APR-06          7
15-APR-06          7
14-APR-06          2
13-APR-06         18

365 rows selected.

 

So Which is Better?
You guessed it: it depends. In our testing, we’ve found that while Method 2 is a bit less efficient in terms of database activities (gets, sorts, etc.), the timing differences are negligible at worst when dealing with smaller result set sizes. However, this does not remain the case should you be generating significantly larger numbers of records. With large rowsets, Method 2 wins out in the end.

For purposes of illustration, consider a rowset of ~1.7 million records (224).

If we run our query using Method 1, we see that the execution time is a little over 10 seconds (~10.3):


> SELECT count(*) from (
    SELECT  sysdate - rownum thedate,
	    mod(abs(dbms_random.random), 20) + 1 thevalue
    FROM dual
    CONNECT BY LEVEL <= 16777216
  )

  COUNT(*)
----------
  16777216

Elapsed: 00:00:10.29

Running this query using Method 2 however, takes about 7 seconds (~6.9), a savings of about 3 seconds:


> WITH tworows AS
( SELECT 1 FROM dual union all SELECT 1 FROM dual)
select count(*) from (
SELECT	sysdate - rownum thedate,
	mod(abs(dbms_random.random), 20) + 1 thevalue
FROM	tworows	t1,  /* produces 2 rows */
	tworows t2,  /* produces 4 rows */
	tworows t3,  /* produces 8 rows */
	tworows t4,  /* produces 16 rows */
	tworows t5,  /* produces 32 rows */
	tworows t6,  /* produces 64 rows */
	tworows t7,  /* produces 128 rows */
	tworows t8,  /* produces 256 rows */
	tworows t9,  /* produces 512 rows */
	tworows t10,  /* produces 1024 rows */
	tworows t11,  /* produces 2048 rows */
	tworows t12,  /* produces 4096 rows */
	tworows t13,  /* produces 8192 rows */
	tworows t14,  /* ... */
	tworows t15,
	tworows t16,
	tworows t17,
	tworows t18,
	tworows t19,
	tworows t20,
	tworows t21,
	tworows t22,
	tworows t23,
	tworows t24   /* produces 16777216 rows */
)

  COUNT(*)
----------
  16777216

Elapsed: 00:00:06.90

The real savings for large results found with Method 2 however, comes from the ability to “tune” it. Specifically, there is nothing stopping us from increasing the the number of records initially produced in the WITH clause subquery from 2, to say 4. Due to Oracle’s efficiency in MERGE JOIN CARTESIANS, the dominant database operation resulting from Method 2, the savings can be significant:


> WITH fourrows AS
( SELECT 1 FROM dual
  union all
  SELECT 1 FROM dual
  union all
  SELECT 1 FROM dual
  union all
  SELECT 1 FROM dual
)
select count(*) from (
SELECT	sysdate - rownum thedate,
	mod(abs(dbms_random.random), 20) + 1 thevalue
FROM	fourrows t1,  /* produces 4 rows */
	fourrows t2,  /* produces 16 rows */
	fourrows t3,  /* produces 64 rows */
	fourrows t4,  /* produces 256 rows */
	fourrows t5,  /* produces 1024 rows */
	fourrows t6,  /* produces 4096 rows */
	fourrows t7,  /* produces 16384 rows */
	fourrows t8,  /* produces 65536 rows */
	fourrows t9,  /* produces 262144 rows */
	fourrows t10, /* produces 1048576 rows */
	fourrows t11, /* produces 4194304 rows */
	fourrows t12  /* produces 16777216 rows */
)

  COUNT(*)
----------
  16777216

Elapsed: 00:00:03.54

 

Conclusion
For all but the largest record sets, Method 1 is ideally suited for dynamically generating artificial rowsets using a query.

 
Direct E-mail John Weicher
PIOCON Technologies Logo

Dealing with Oracle 10g’s PARTITION BY and Bitmap Indexes Bug

Published in Database Tips, Quick Tips, SQL by jweicher Thursday March 29, 2007

While being an incredibly powerful and efficient way of indexing certain types of data, it is now relatively well accepted that Oracle’s Bitmap Indexes still have some kinks and sometimes cause erratic behavior in queries, particularly when dealing with parallel queries.

Unfortunately, this erratic behavior can even crop up when parallelism has been set to zero both on a given table, and in a query itself, if you are trying to execute even simple queries that contain PARTITION BY clauses. Unfortunately, there often appears to be no rhyme or reason as to when queries will exhibit this behavior when querying one bitmap-indexed table versus another.

Nonetheless, you may have found yourself faced with the following situation, and simply need a workaround.

Consider:

A fact table, ORDER_ITEM_F of the following general structure:

ORDER_ITEM_F
record_pk NUMBER NOT NULL
loc_id NUMBER NOT NULL
prod_id NUMBER NOT NULL
quantity NUMBER NOT NULL
cost NUMBER NOT NULL

This table has numerous records already in it:


> SELECT count(*) FROM order_item_f;

  COUNT(*)
----------
    657802

Additionally, among others, a bitmap index has been created on the location ID field (loc_id).

> CREATE BITMAP INDEX order_item_f_b_idx_11 ON order_item_f (loc_id)

Next, for some internal reporting you would like to query this table to find counts of order items per sales location, while attaching a total for the set to each row (perhaps you are using this query in an Oracle Report and doing some nice formatting).

A simple query could be as follows:


> SELECT  distinct loc_id,
          count(*) over (partition by loc_id) loc_total,
          count(*) over () tbl_total
  FROM    order_item_f
  ORDER
  BY      loc_id;

What you expect to receive from such a query is a row for each LOC_ID value, which includes a count of records having that LOC_ID, and an extra column containing the total number of records in the table (this value will be the same for each row returned).

Unfortunately, what you receive instead falls into one of the two scenarios below:

Scenario 1: Inaccurate counts based on full table count query (example above):


    LOC_ID  LOC_TOTAL  TBL_TOTAL
---------- ---------- ----------
        17         12         89
        18         12         89
        19          8         89
        20          9         89
        21          1         89
        22          3         89
        23          4         89
        24          6         89
        25          6         89
        26          1         89
        27         11         89
        28          6         89
        29          4         89
        30          3         89
        31          1         89
        44          1         89
        45          1         89

Scenario 2: ORA-0600 - The Dread Internal Error


ERROR at line 1:
ORA-00600: internal error code, arguments: [kxfqupp_bad_cvl], [7215], [4],
 [0], [], [], [], []

Enabling autotrace (>SET AUTOTRACE ON) and re-running the query reveals that the database attempted to retrieve your results using the bitmap index:


Execution Plan
----------------------------------------------------------
Plan hash value: 2270429775

-------------------------------------------------------------------------
| Id  | Operation                      | Name                  | Rows
-------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |                       |    16
|   1 |  SORT UNIQUE                   |                       |    16
|   2 |   WINDOW SORT                  |                       |    16
|   3 |    WINDOW BUFFER               |                       |    16
|   4 |     BITMAP INDEX FAST FULL SCAN| ORDER_ITEM_F_B_IDX_11 |   657K
-------------------------------------------------------------------------

An inspection of the plan shows that ~657K records are in fact accounted for by the fast full scan of the bitmap index. Yet in the case of Scenario 1 above, the counts coming back clearly do not reflect this total (if they come back at all!).

The PARALLEL/BITMAP bug strikes!

While a much more in-depth discussion of the cause for this bug could follow, sometimes you just need a practical fix:

The Fix: Full Table Scan Hint

To quickly resolve your problem, a simple hint can be added to your SELECT query, to force a full table scan instead in place of the bitmap index scan:


> SELECT  /*+ FULL(order_item_f) */
          distinct loc_id,
          count(*) over (PARTITION by loc_id) loc_total,
          count(*) over () tbl_total
  FROM    order_item_f
  ORDER
  BY      loc_id;

    LOC_ID  LOC_TOTAL  TBL_TOTAL
---------- ---------- ----------
        17     127674     657802
        18     136815     657802
        19      49540     657802
        20      51782     657802
        21       3908     657802
        22      11463     657802
        23      33415     657802
        24      29190     657802
        25      34141     657802
        26       1901     657802
        27      97483     657802
        28      31405     657802
        29      32619     657802
        30      15248     657802
        31          1     657802
        44       1090     657802
        45        127     657802

17 rows selected.

 
Direct E-mail John Weicher
PIOCON Technologies Logo

Sliding Windows for Great Views of Data - Using PARTITION BY

Published in Quick Tips, SQL by mmetrik Monday March 12, 2007

Those who have been involved with developing and deploying Business Intelligence solutions at an organization have probably experienced something that is close to Sir Isaac Newton’s first law; BI users have their habits and tend to stick to their habits unless shown a better way. One of the most common user habits I have observed over the last decade is the immediate and sometimes aggressive need to export data from new reports that have been developed and work with the data in their beloved spreadsheet program. For the BI developer, this need is often met with a good mix of astonishment and disappointment, as the value of their very flashy and complexly organized report is seemingly diminished as the data is then viewed column by column, row by row. Consuming BI data in that way is sometimes counter-productive and could seriously undermine the efforts of a BI project. But, as this is SingleQuery.com, I will turn the focus of this post to ways of possibly heading off that movement of data by using a single SQL query to solve a common user request.

Users will quite often wish to compare one result of data to the entire set of data it belongs. This is usually as described as a “Percent of Total” calculation. While a very simple and straightforward concept that both business users and BI developers all understand, those calculations are often missing in reports. This omission then causes motivated users to download the data into a spreadsheet and develop the formulas themselves to then easily see how the sales of blue shirts compares to the sales of shirts in total. Ranking is also a common driver of moving data into a spreadsheet. In order to rank products by number sold, all results must first be evaluated and then correctly ordered.

I have seen this occur in BI tools that do not handle multiple passes through data very well. This lack of functionality then causes the developer to make serious choices as to whether the inclusion of the calculation in the report is worth the extra effort needed to develop a custom solution in the report to deliver the “Percent of Total” or ranking calculations. Using Oracle’s Analytic Functions can make this worry immediately go away. They allow for results in each row to use sub-totals and totals of data to then use in Percent of Total calculations. So, the step that users perform to create a spreadsheet and formulas that divide the value in each row by the total of all values in the column is rendered moot and can easily be accomplished using one single SQL query.

The beauty of the Analytic Function is their ability to use “PARTITION BY” and “ORDER BY” clauses to create windows of data to perform functions on. The PARTITION BY drives the window or sub-set of data that the query is looking to evaluate. The ORDER BY allows for the rows to be in the desired order within a PARTITION BY argument.

Let’s look at a set of data that tracks sales of different clothing products over years:

Metrik - Table 1

The first step is determining a Percent of Total Sales value within each year is for each row to contain the total for all products within the year. Using the PARTITION BY clause the query will create windows for each year and then a sum on (units) will result in desired total.

select
year,
product,
units,
sum(units) over(partition by year)
from sales

SQL 2

Once the total for all units sold in a year appears on every row in the data set, you can see you have all the components to perform a “percent of total” calculation.

select
year,
product,
units,
sum(units) over(partition by year),
(units/(sum(units) over(partition by year))) Percent_of_Year
from sales

SQL 3

So, there you have it. Using a single query, Oracle can evaluate a specified set of rows in a result set and perform sub-total type calculations that can then be used for analytical calculations. This is all done in “one pass” of the data by the SQL query and could possibly go a long way and heading off issues with Business Intelligence reports in the future. And you didn’t need to get hit on the head by an apple to figure this out.

Metro

Discoverer Slow? Resist the CBO urge…use /*+ no_merge */

Published in Hints, Quick Tips by gpike Thursday March 1, 2007

Although not a wide-spread problem, Oracle sometimes produces less than optimal execution plans when the Discoverer-generated queries are poorly conceived. I recently encountered this problem in an Apps 11i environment on Oracle 9i using Discoverer 4i. Even though the database server contained ample memory and hardware resources (20+ processors and lots o’ RAM), some queries simply refused to complete in a reasonable amount of time. What is a reasonable amount of time you ask? In this case, less than the 20-40 estimated hours as seen in V$SESSION_LONGOPS!

The problem manifests itself from a poorly-conceived EUL combined with the 9i Cost Based Optimizer’s (CBO) desire to use Cartesian Merge Joins when it feels this join method is appropriate. However, in an Oracle Apps environment, the tables may contain 100+ columns and often every one of these is exposed in the EUL. If the EUL developer added all of the flex fields from the base table into the EUL, watch out! A Cartesian Merge Join with this many columns consumes system resources quickly and forces the joins off to physical disk. All of a sudden, the Cartesian Merge Join is going to take 17000 seconds. If you are experiencing agonizingly slow queries from Discoverer and see a Cartesian Merge Join, this may be the culprit. Short of reworking the Discoverer EUL or the individual workbooks, forcing the CBO to change the execution plan may be solution.

In my situation, strategic placement of the /*+ no_merge */ hint in selected, complex folders of the EUL provided a workable, but hardly optimal, solution. The /*+ no_merge */ hint kindly asks the CBO to use an alternate method for joining intermediate result sets as the query is executed. While the new plan eliminates Cartesian Merge Joins and may result in a 99% time reduction, you still may be faced with a 24 minute query that should probably take a minute or less. Although there is most definitely a better solution, /*+ no_merge */ offers a quick and dirty way to get your Discoverer queries back under control until a more suitable long-term solution can be applied.

By the way, the real long-term solution is to eliminate all unused columns from the EUL (especially the VARCHAR2(150) flex fields) and then remove the /*+ no_merge */ hints.

Caching The Results in Oracle 11g with /*+ result_cache */

Published in Hints, Oracle 11g, Quick Tips by jsimmons Wednesday February 21, 2007

select /*+ result_cache */ * from my_table

New for Oracle 11g, the result_cache hint caches the result set of a select statement. This is similar to:

alter table my_table cache

But as you can imagine adding columns or predicates makes /*+ result_cache */ considerably more powerful by caching a subset of larger tables and common queries.

select /*+ result_cache */ colA, colB, colC
from my_table where colA := myBindVar

Cartesian Products: Don’t Fear this Reaper

Published in Quick Tips, SQL by gpike Saturday February 3, 2007

Myth: From the Oracle® Database 10g SQL Reference “A Cartesian product always generates many rows and is rarely useful.”

Fact: The Cartesian product is a powerful tool that can help in situations where it is known in advance that every combination of every row in two or more tables is needed.

The Cartesian product (named for the French philosopher and mathematician Rene Descartes, but there appears to be evidence that he is not the actual inventor) is generated when two tables in a query contain no join condition. The Cartesian product produces a result set with all possible ordered pairs from the two (or more) tables. For example, a Cartesian product with two tables containing 10 rows each yields a result set of 100 rows.

Typically, when a Cartesian product pops up in a query, it’s a mistake that produces those irritating 50M row result sets. Oooops! But there are also very valid situations that can benefit from the strategically-placed Cartesian product. Use Cartesian products where sparse data requires an entry (usually a 0 or a NULL). These situations may arise in Business Intelligence or other applications where the results are rendered graphically.

Data warehouse example:

TABLE DATES_D
    date_id    NUMBER
    full_date  DATE

TABLE PRODUCT_D
    product_id    NUMBER
    product_name  VARCHAR2

Table ORDER_F:
    date_id      NUMBER
    product_id   NUMBER
    total_orders NUMBER

To ensure that any query against the ORDERS table will produce a row for every date and product combination, a Cartesian product is used. Please note that the Cartesian product is placed in an in-line view purely for emphasis.

SELECT full_date,
       product_name,
       nvl(total_orders,0) -- Make the "manufactured"
                           -- rows zeros
FROM
       (
        SELECT date_id,
               full_date,
               product_id,
               product_name
        FROM   dates_d d,  -- Woah, a Cartesian product!
               product_d p -- Now we have a complete
                           -- set of every possible product
                           -- and every possible day.
       ) temp,
       order_f o
WHERE  o.date_id    = temp.date_id(+)
AND    o.product_id = temp.product_id(+)
-- The outer joins ensure a row even for products that
-- do not have any sales on a given day

There are endless situations where a Cartesian product can be a valuable tool. Of course, it is important to remember that creating a Cartesian product may not be in your database’s best interest. If the DATE_D table in the above example contained every day since the Pyramids were constructed and the PRODUCT_D table listed every item found in your local grocery store, you might find this Cartesian to be a very, very bad idea. But in situations where the cardinality is known, don’t fear the much-maligned Cartesian product.

Driving Site

Published in Hints, Quick Tips by jsimmons Wednesday January 10, 2007

/*+ DRIVING_SITE([table]) */

Oracle provides us with the powerful feature “database links”. Database links allow for mostly seamless querying of remote databases. These distributed query cause unique performance and tuning issues.

The DRIVING_SITE hint forces query execution at a user selected site rather than at a site selected by the database.

SELECT *
FROM emp e, dept@dev d
WHERE e.deptno = d.deptno;

With Hint

SELECT  /*+ DRIVING_SITE(d) */ *
FROM emp e, dept@dev d
WHERE e.deptno = d.deptno;

With Hint

While this hint has minor performance impact on small remote tables, it can have enormous impact on medium to large tables.

101 queries. 0.680 seconds.
Powered by Wordpress
theme by cmoanz