ORAganism

How to Create a Database Link in Another User’s Schema

Posted in Oracle by Neil Johnson on December 2, 2009

I saw a post recently describing how to create a database link in another user’s schema. I wanted to post an alternative method so created this post in order to refer to in a comment on the original post. The original post demonstrates how to hijack a user account’s password temporarily, which is a useful piece of information to have, and can be viewed here:

Marko Sutic’s Oracle Blog – Create a Database Link in another user’s schema

The method below shows how to achieve the same without hijacking the password and hence – reduced risk to user upset. However this method does involve rather powerful privileges and the creation of some objects.

Create a test user who is not allowed to create database links.

CREATE USER otheruser IDENTIFIED BY unknown_pwd;
GRANT create session TO otheruser;

Now create our power user with CREATE & DELETE ANY PROCEDURE and CREATE DATABASE LINK WITH ADMIN privileges

CREATE USER poweruser IDENTIFIED BY known_pwd;
GRANT create session TO poweruser;
GRANT create database link TO poweruser WITH ADMIN OPTION;
GRANT create any procedure TO poweruser;
GRANT drop any procedure TO poweruser;
GRANT execute any procedure TO poweruser;

Now we can use this powerful account to create a procedure in the unprivileged user’s schema which then gives us the ability to run DDL as that user (thanks to definer rights).

CONN poweruser/known_pwd

CREATE PROCEDURE otheruser.cre_db_lnk AS
BEGIN
	EXECUTE IMMEDIATE 'CREATE DATABASE LINK newlink '
			||'CONNECT TO remoteuser IDENTIFIED BY pw '
			||'USING ''remotetns''';
END cre_db_lnk;
/

Procedure created.

We can then briefly grant CREATE DATABASE LINK privileges, run the procedure and revoke the privileges again.

GRANT create database link TO otheruser;

exec otheruser.cre_db_lnk

REVOKE create database link FROM otheruser;

DROP PROCEDURE otheruser.cre_db_lnk;

And hey presto, we’ve created a database link in the other schema.

SELECT owner,db_link FROM dba_db_links;

OWNER           DB_LINK
--------------- ---------------
OTHERUSER       NEWLINK

This example is a bit scruffy but you could wrap the power user’s tasks and privileges up in another procedure and lock the account. I’ve used a method similar to this in the past as part of a scheduled refresh of test databases. After the clone a standard set of procedures was called to recreate database links in various schemas.

Using an Index to Shortcut a MIN/MAX Aggregate – Part 2

Posted in Oracle by Neil Johnson on November 27, 2009

I’ve previously commented about the Index Scan MIN/MAX optimisation. My description of this operation from that post is below:

the optimiser can take a shortcut in satisfying a MIN or MAX aggregate by plucking the first or last value from the index, thus avoiding scanning the whole segment

I’ve been meaning to revisit and define this a bit more clearly. The target is not necessarily the first or last value in the index, the optimiser can use a similar operation to pluck a value from the middle of a composite index by using an “INDEX RANGE SCAN (MIN/MAX)” rather than “INDEX (FULL SCAN (MIN/MAX))”. An example is below:

Create a test table.

CREATE TABLE tab1 AS
SELECT MOD(ROWNUM,5) group_id
, ROWNUM id
FROM dual
CONNECT BY LEVEL <= 20000;

SELECT MIN(group_id), MAX(group_id) FROM tab1;

MIN(GROUP_ID) MAX(GROUP_ID)
------------- -------------
            0             4

CREATE INDEX tab1_i ON tab1 (group_id,id);

The index is on GROUP and ID so the query below using GROUP_ID “0″ will be doing as I previously described – plucking the first value from the index (note: I executed the queries below previously to first charge the shared pool).

set autotrace on
SELECT MIN(id) FROM tab1 WHERE group_id = 0;

---------------------------------------------------------------------------------------
| Id  | Operation                    | Name   | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |        |     1 |    26 |     2   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE              |        |     1 |    26 |            |          |
|   2 |   FIRST ROW                  |        |  4000 |   101K|     2   (0)| 00:00:01 |
|*  3 |    INDEX RANGE SCAN (MIN/MAX)| TAB1_I |  4000 |   101K|     2   (0)| 00:00:01 |
---------------------------------------------------------------------------------------

Statistics
----------------------------------------------------------
          2  consistent gets

And now we query using GROUP_ID “3″. This is going to find its target value somewhere near the middle of the index.

set autotrace on
SELECT MIN(id) FROM tab1 WHERE group_id = 3;

---------------------------------------------------------------------------------------
| Id  | Operation                    | Name   | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |        |     1 |    26 |     2   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE              |        |     1 |    26 |            |          |
|   2 |   FIRST ROW                  |        |  4000 |   101K|     2   (0)| 00:00:01 |
|*  3 |    INDEX RANGE SCAN (MIN/MAX)| TAB1_I |  4000 |   101K|     2   (0)| 00:00:01 |
---------------------------------------------------------------------------------------

Statistics
----------------------------------------------------------
          2  consistent gets

2 consistent gets again. The operation causes us to navigate the index tree to the start of GROUP_ID 3 and at that point we can stop as we have the minimum value of ID right there.

Tagged with: , ,

Converting Control Files to be Oracle Managed Files

Posted in Oracle by Neil Johnson on November 5, 2009

No bombshells in this post – just a little task I’d not come across before and a complaint at the end.

On a test 10.2.0.4 database at home I have 2 control files, one in the DB_CREATE_FILE_DEST and one in the DB_RECOVERY_FILE_DEST as can be seen below:

NAME
--------------------------------------------------------------------
/u01/oradata/ORCL1/controlfile/o1_mf_5h1bxxg0_.ctl
/u01/app/oracle/flash_recovery_area/ORCL1/controlfile/o1_mf_5h1bxy2m_.ctl

During a bit of testing too mundane to go into I ended up with non-Oracle managed control files:

NAME
--------------------------------------------------------------------
/u01/oradata/orcl1/ORCL1_1.ctl
/u01/oradata/orcl1/ORCL1_2.ctl

After my unproductive testing I then wanted to put the control files back as they were. This was not something I’d considered before and it wasn’t immediately obvious how to go about it. The steps are below for my future reference (and perhaps yours).

Blank out the CONTROL_FILES parameter in the SPFILE and then restart the instance.

SYS @ orcl1 >alter system reset control_files scope=spfile sid='*';

System altered.

SYS @ orcl1 >startup force nomount
ORACLE instance started.

The CONTROL_FILES parameter appears to be set to a location in the Oracle home. This confused me at first but just turn a blind eye to it for now.

SYS @ orcl1 >show parameter control_files

NAME                                 TYPE        VALUE
------------------------------------ ----------- ------------------------------
control_files                        string      /u01/app/oracle/product/10.2.0
                                                 /db_1/dbs/cntrlorcl1.dbf

If you restore the control files from an RMAN session the resulting files find their way back to being managed by Oracle.

RMAN> RESTORE CONTROLFILE FROM '/u01/oradata/orcl1/ORCL1_2.ctl';

Starting restore at 05-NOV-09
using target database control file instead of recovery catalog
allocated channel: ORA_DISK_1
channel ORA_DISK_1: sid=156 devtype=DISK

channel ORA_DISK_1: copied control file copy
output filename=/u01/oradata/ORCL1/controlfile/o1_mf_5h6ml6to_.ctl
output filename=/u01/app/oracle/flash_recovery_area/ORCL1/controlfile/o1_mf_5h6ml74f_.ctl
Finished restore at 05-NOV-09

Nice.

Complaint time:

DB_RECOVERY_FILE_DEST. A little thing I like is the naming of this parameter. Perfect – it accurately describes the sort of files you’ll find in there. A little thing that I don’t like is that it’s Sunday best name is “Flash Recovery Area”. In which meeting did that seem like a good idea! Oh – that will be the marketing one.

When to Gather Fixed Object Optimiser Statistics

Posted in Oracle by Neil Johnson on July 21, 2009

Lets start with some advice from Metalink note 272479.1: Gathering Statistics For All fixed Objects In The Data Dictionary.

With Oracle Database 10G it is now recommended that you analyze the data dictionary.
The data dictionary has many fixed tables, such as X$ tables and collecting statistics
for these objects is suggested by Oracle. The GATHER_FIXED_OBJECTS_STATS
procedure gathers statistics for all fixed objects (dynamic performance tables) in the
data dictionary.

So we know we should do it – but when? Advice from the Oracle documentation this time.

Statistics on fixed objects, such as the dynamic performance tables, need to be
manually collected using GATHER_FIXED_OBJECTS_STATS procedure. Fixed objects record
current database activity; statistics gathering should be done when database has
representative activity.

I have an example below of how important the timing of this “representative activity” can be. Here is a simple query – the likes of which are often run by monitoring tools such as Oracle Grid Control – executed on a database that has had statistics for all fixed objects gathered previously.

select count(*) from v$rman_backup_job_details;

  COUNT(*)
----------
        23

Elapsed: 00:01:43.39

Nearly 2 minutes for a 23 row result set. The execution plan is a bit long to post here but below is a snippet that set some gentle alarm bells ringing.

------------------------------------------------------------------------------
| Id  | Operation                           | Name                   | Rows  |
------------------------------------------------------------------------------
... snip ...
|* 11 |            HASH JOIN OUTER          |                        |     1 |
|  12 |             MERGE JOIN CARTESIAN    |                        |     1 |
|  13 |              MERGE JOIN CARTESIAN   |                        |     1 |
|  14 |               FIXED TABLE FULL      | X$KCCRSR               |     1 |
... snip ...
|* 36 |           HASH JOIN OUTER           |                        |     1 |
|  37 |            MERGE JOIN CARTESIAN     |                        |     1 |
|  38 |             MERGE JOIN CARTESIAN    |                        |     1 |
|  39 |              FIXED TABLE FULL       | X$KCCRSR               |     1 |
... snip ...
------------------------------------------------------------------------------

A quick glance at the optimiser statistics for the X$KCCRSR table tells us some interesting information.

select rowcnt, blkcnt, analyzetime, samplesize
from
 tab_stats$ where obj#=(select OBJECT_ID from V$FIXED_TABLE where name = 'X$KCCRSR');

    ROWCNT     BLKCNT ANALYZETI SAMPLESIZE
---------- ---------- --------- ----------
         0          0 26-APR-09          0

Zero rows. And after gathering fixed object statistics…

exec dbms_stats.gather_fixed_objects_stats;

    ROWCNT     BLKCNT ANALYZETI SAMPLESIZE
---------- ---------- --------- ----------
       225          0 21-JUL-09        225

select count(*) from v$rman_backup_job_details;

  COUNT(*)
----------
        23

Elapsed: 00:00:00.19

The optimiser now knows there are 225 rows in X$KCCRSR and returns it’s result set in under a second. Now I’m not suggesting anyone regularly schedule a call to GATHER_FIXED_OBJECTS_STATS just consider the timing of this call a bit. Perhaps after your first couple of RMAN backups may do the trick :)

As an interesting aside I stumbled across the following method to view when statistics have been gathered.

select operation,start_time from DBA_OPTSTAT_OPERATIONS order by start_time;

OPERATION                             START_TIME
------------------------------------- -------------------------------------
gather_database_stats(auto)           17-JUL-09 10.00.01.560328 PM -03:00
gather_database_stats(auto)           18-JUL-09 06.00.03.189531 AM -03:00
gather_database_stats(auto)           20-JUL-09 10.00.01.363156 PM -03:00
gather_fixed_objects_stats            21-JUL-09 09.52.59.646815 AM -03:00

I’ve just noticed there are no system statistics in there – that’s an investigation for another day.

Bind variable peeking and partitioned tables

Posted in Oracle by Neil Johnson on May 23, 2009

We all know about the pitfalls of bind variables combined with histograms on skewed data. The example below shows how table partitions can act just like a histogram and how bind variale peeking can provide the same headaches.

CREATE TABLE ptab
(	partid	number
,	id1	number
,	id2	number
,	desc1	varchar2 (30)
,	desc2	varchar2 (30))
PARTITION BY LIST (partid)
(	partition p1 values (1)
,	partition p2 values (2)
,	partition p3 values (3)
,	partition p4 values (4)
,	partition p5 values (5)
,	partition p6 values (6));

INSERT /*+ APPEND */ INTO ptab (partid,id1,id2,desc1,desc2)
SELECT	MOD(ROWNUM,5)+1
,	MOD(ROWNUM,2000)+1
,	ROW_NUMBER() OVER (PARTITION BY MOD(ROWNUM,5)+1 ORDER BY ROWNUM)
,	RPAD('a',30,'a')
,	RPAD('b',30,'b')
FROM	dual
CONNECT BY ROWNUM true);
COMMIT;

SELECT partition_name, num_rows
FROM user_tab_partitions
WHERE table_name = 'PTAB'
ORDER BY 1;

PARTITION_NAME                   NUM_ROWS
------------------------------ ----------
P1                                 600751
P2                                 601714
P3                                 598125
P4                                 602704
P5                                 602127
P6                                      0

Note above that we have 5 evenly populated partitions and one empty one. The optimiser is aware of these figures as you can see in the queries/execution plans below.

set autotrace traceonly explain
SELECT desc1, desc2 FROM ptab WHERE partid = 1 AND id1 = 1;

---------------------------------------------------------------------------------------------------------------
| Id  | Operation                          | Name     | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
---------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                   |          |  1502 |   101K|  1484   (1)| 00:00:18 |       |       |
|   1 |  TABLE ACCESS BY GLOBAL INDEX ROWID| PTAB     |  1502 |   101K|  1484   (1)| 00:00:18 |     1 |     1 |
|*  2 |   INDEX RANGE SCAN                 | PTAB_IDX |  1474 |       |     7   (0)| 00:00:01 |       |       |
---------------------------------------------------------------------------------------------------------------

set autotrace traceonly explain
SELECT desc1, desc2 FROM ptab WHERE partid = 6 AND id1 = 1;

----------------------------------------------------------------------------------------------
| Id  | Operation             | Name | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |      |     1 |    60 |     2   (0)| 00:00:01 |       |       |
|   1 |  PARTITION LIST SINGLE|      |     1 |    60 |     2   (0)| 00:00:01 |     6 |     6 |
|*  2 |   TABLE ACCESS FULL   | PTAB |     1 |    60 |     2   (0)| 00:00:01 |     6 |     6 |
----------------------------------------------------------------------------------------------

The optimiser is aware that an index is the best option for a populated partition but a full partition scan (note the “Pstart” and “Pstop” columns) is acceptable for the empty partition. Now we introduce bind variables into the equation.

set autotrace traceonly explain
variable v_partid number
exec :v_partid := 6
SELECT desc1, desc2 FROM ptab WHERE partid = :v_partid AND id1 = 1;

----------------------------------------------------------------------------------------------
| Id  | Operation             | Name | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |      |   299 | 20631 |  1259   (3)| 00:00:16 |       |       |
|   1 |  PARTITION LIST SINGLE|      |   299 | 20631 |  1259   (3)| 00:00:16 |   KEY |   KEY |
|*  2 |   TABLE ACCESS FULL   | PTAB |   299 | 20631 |  1259   (3)| 00:00:16 |   KEY |   KEY |
----------------------------------------------------------------------------------------------

exec :v_partid := 1
SELECT desc1, desc2 FROM ptab WHERE partid = :v_partid AND id1 = 1;

----------------------------------------------------------------------------------------------
| Id  | Operation             | Name | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |      |   299 | 20631 |  1259   (3)| 00:00:16 |       |       |
|   1 |  PARTITION LIST SINGLE|      |   299 | 20631 |  1259   (3)| 00:00:16 |   KEY |   KEY |
|*  2 |   TABLE ACCESS FULL   | PTAB |   299 | 20631 |  1259   (3)| 00:00:16 |   KEY |   KEY |
----------------------------------------------------------------------------------------------

Yikes! Now we’re full scanning our 600k row partition and performance has taken a nose dive. This can be particularly problematic where partitions are added for specific time periods (e.g. each month) and then populated over that time period. Just after the addition you have an empty partition which could cause queries on other partitions to be affected.

The solution? Accurate statistics and literal values for partition keys.

Listing Top Sessions by CPU in SQL

Posted in Oracle by Neil Johnson on May 3, 2009

In the past I’ve considered writing a SQL version of the Unix TOP utility to view busy database sessions. I never got around to it but a recent post by Christian Antognini gave me the inspiration to finally write my script and a few ideas on how best to go about it. Please note my script is purely reporting sessions as a percentage of an instance’s CPU workload – not of a host server’s potential CPU.

The script is below for anyone who may want to give it a try. I’ve included a user to own the code in order to isolate the privileges and make tidying up easy. There are also 2 types to support the PL/SQL code (idea “borrowed” from the post by C.Antognini referenced previously).

CREATE USER dbtop IDENTIFIED BY dbtop ACCOUNT LOCK;
GRANT SELECT ON v_$sesstat TO dbtop;
GRANT SELECT ON v_$sysstat TO dbtop;
GRANT SELECT ON v_$process TO dbtop;
GRANT SELECT ON v_$session TO dbtop;
GRANT EXECUTE ON dbms_lock TO dbtop;

CREATE OR REPLACE TYPE dbtop.top_rec IS OBJECT (
	sid 		NUMBER
,	pid 		VARCHAR2(12)
,	username	VARCHAR2(30)
,	cpu		NUMBER
,	command		VARCHAR2(25)
);
/

CREATE OR REPLACE TYPE dbtop.top_tab AS TABLE OF dbtop.top_rec;
/

CREATE OR REPLACE FUNCTION dbtop.top(	I_loops IN NUMBER DEFAULT 1
				,	I_interval IN NUMBER DEFAULT 5
				,	I_sessions IN NUMBER DEFAULT 10)
	RETURN top_tab PIPELINED
IS
	L_top_tab1	top_tab;
	L_top_tab2	top_tab;

	L_sys_cpu1	NUMBER;
	L_sys_cpu2	NUMBER;
	L_sys_cpu_delta	NUMBER;

	LC_cpu#	CONSTANT NUMBER := 12;

	CURSOR C_top IS
		SELECT 	top_rec(u.sid,p.spid,u.username,s.value
		, 	SUBSTR(u.program,1,25))
		FROM	v$sesstat s
		, 	v$session u
		, 	v$process p
		WHERE	s.statistic# IN (LC_cpu#)
		AND	s.value > 0
		AND	u.sid = s.sid
		AND	p.addr = u.paddr;

	PROCEDURE record_data(O_sys_cpu OUT NUMBER,O_top_tab OUT top_tab)
	IS
	BEGIN
		SELECT	value
		INTO	O_sys_cpu
		FROM	v$sysstat
		WHERE	statistic# = LC_cpu#;

		OPEN C_top;
		FETCH C_top BULK COLLECT INTO O_top_tab;
		CLOSE C_top;
	END record_data;
BEGIN
	-- record first base line of sessions
	record_data(L_sys_cpu1,L_top_tab1);

	FOR L_count IN 1..I_loops LOOP
		dbms_lock.sleep(I_interval);

		record_data(L_sys_cpu2,L_top_tab2);

		L_sys_cpu_delta := L_sys_cpu2 - L_sys_cpu1;

		-- report delta of snapshots. There's a DECODE on sys_cpu
		-- as my test system has a habit of having 0 cpu!
		FOR R_top IN (
			SELECT	sid,pid,username,cpu_perc,command
			FROM	(
				SELECT	t2.sid,t2.pid,t2.username
				,	DECODE(L_sys_cpu_delta,0,0,ROUND(((t2.cpu - t1.cpu)
						/ L_sys_cpu_delta)*100,2)) cpu_perc
				,	t2.command
				FROM	TABLE(L_top_tab1) t1
				,	TABLE(L_top_tab2) t2
				WHERE	t1.sid (+) = t2.sid
				ORDER BY cpu_perc DESC
			)
			WHERE ROWNUM <= I_sessions
		) LOOP
			PIPE ROW(top_rec(R_top.sid
				,	R_top.pid
				,	R_top.username
				,	R_top.cpu_perc
				,	R_top.command));
		END LOOP;

		PIPE ROW(top_rec(NULL,NULL,NULL,NULL,NULL));

		L_top_tab1 := L_top_tab2;
		L_sys_cpu1 := L_sys_cpu2;
	END LOOP;
END top;
/

Below is an example of how to run the code (note I’ve again borrowed from Mr Antognini’s post with my ARRAYSIZE setting).

SET ARRAYSIZE 1
COLUMN username FORMAT a15
SELECT sid, pid, username, cpu, command FROM table(dbtop.top(2));

       SID PID          USERNAME               CPU COMMAND
---------- ------------ --------------- ---------- -------------------------
       159 28484        SYS                  98.36 sqlplus@localhost.localdo
       142 28487        OPS$NEIL              1.64 sqlplus@localhost.localdo
       147 3968                                  0 oracle@localhost.localdom
       161 3937                                  0 oracle@localhost.localdom
       163 3933                                  0 oracle@localhost.localdom
       165 3929                                  0 oracle@localhost.localdom
       164 3931                                  0 oracle@localhost.localdom
       162 3935                                  0 oracle@localhost.localdom
       160 3939                                  0 oracle@localhost.localdom
       153 3953                                  0 oracle@localhost.localdom

       159 28484        SYS                  98.21 sqlplus@localhost.localdo
       142 28487        OPS$NEIL              1.79 sqlplus@localhost.localdo
       147 3968                                  0 oracle@localhost.localdom
       161 3937                                  0 oracle@localhost.localdom
       163 3933                                  0 oracle@localhost.localdom
       165 3929                                  0 oracle@localhost.localdom
       164 3931                                  0 oracle@localhost.localdom
       162 3935                                  0 oracle@localhost.localdom
       160 3939                                  0 oracle@localhost.localdom
       153 3953                                  0 oracle@localhost.localdom

The SYS session above is a count of records in DBA_EXTENTS by the way. Session 149 is TOP itself.

Tagged with: , ,

A Session Busy Doing What? – Work of Course!

Posted in Oracle by Neil Johnson on May 2, 2009

I was recently called out to look at a batch job exceeding it’s usual execution time. Ordinarily a quick look at the wait events would tell me what I needed to know, however due to a poor execution plan (caused by incorrect statistics) the query nicely highlighted how not all work can be classified as a wait. Below is a reproduction of the scenario I’m describing.

Create some test tables.

CREATE TABLE tab1 AS
SELECT ROWNUM rn, 'xxxxx' pad1, 'yyyyy' pad2
  FROM dual CONNECT BY LEVEL <= 25000;

CREATE TABLE tab2 AS
SELECT ROWNUM rn, 'xxxxx' pad1, 'yyyyy' pad2
  FROM dual CONNECT BY LEVEL <= 25000;

exec dbms_stats.gather_table_stats(null,'tab1');
exec dbms_stats.gather_table_stats(null,'tab2');

A test query is below, it has been hinted to ensure it repeatedly scans the same blocks in memory.

SELECT /*+ USE_NL(tab1 tab2) ORDERED */ COUNT(*)
FROM 	tab1
INNER JOIN tab2 ON tab1.rn = tab2.rn;

------------------------------------------------------------------------------------
| Id  | Operation           | Name | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |     1 |    10 |       |   477K  (6)| 01:35:30 |
|   1 |  SORT AGGREGATE     |      |     1 |    10 |       |            |          |
|*  2 |   NESTED LOOPS      |      | 24859 |   242K|   477K|   477K  (6)| 01:35:30 |
|   3 |    TABLE ACCESS FULL| TAB1 | 24918 |   121K|       |    21   (5)| 00:00:01 |
|   4 |    TABLE ACCESS FULL| TAB2 |     1 |     5 |       |    19   (6)| 00:00:01 |
------------------------------------------------------------------------------------

First off run the query to get it parsed and charge the buffer cache and then reset the session and run it again.

-- may want to ctrl^c once this has run for a few seconds
SELECT /*+ USE_NL(tab1 tab2) ORDERED */ COUNT(*)
FROM 	tab1
INNER JOIN tab2 ON tab1.rn = tab2.rn;

-- conn fresh session
conn /

SELECT sid FROM v$mystat WHERE ROWNUM = 1;

SELECT /*+ USE_NL(tab1 tab2) ORDERED */ COUNT(*)
FROM 	tab1
INNER JOIN tab2 ON tab1.rn = tab2.rn;

From another session check what the original session is up to:

SELECT sid, status, event, state
FROM v$session
WHERE sid = &sid_of_other_session;

       SID STATUS   EVENT                               STATE
---------- -------- ----------------------------------- -------------------
       149 ACTIVE   SQL*Net message to client           WAITED SHORT TIME

SELECT event, time_waited
FROM v$session_event
WHERE sid = &sid_of_other_session;

EVENT                               TIME_WAITED
----------------------------------- -----------
SQL*Net message to client                     0
SQL*Net message from client                1111

No wait events are being recorded yet “top” shows the session to be the number 1 session on the box with 78% of CPU resources.

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
28008 oracle    25   0  344m  20m  18m R 78.3  2.7   5:57.28 oracle

The missing link is that an Oracle instance does not count logical IO as a wait – it is work. V$SESSTAT is where we have visibility of this work.

SELECT n.name, s.value
FROM v$sesstat s, v$statname n
WHERE s.sid = &sid_of_other_session
AND n.statistic# = s.statistic#
AND n.name = 'consistent gets'
ORDER BY s.value;

NAME                                                                  VALUE
---------------------------------------------------------------- ----------
consistent gets                                                     1975123
Tagged with: , ,

Optimiser Statistics and Lower Case Columns

Posted in Oracle by Neil Johnson on April 9, 2009

I was contacted recently about a performance problem in an application we support. The query summarises the number of issues that have not been closed. A simplified version is shown below:

SELECT  COUNT(*)
FROM    "issues"
WHERE   "state" IN ('open','new');

There are two things of note here. Firstly your eyes do not deceive you – those are lower case table and column names (more on this later). Secondly the “state” column is indexed yet the query is doing a full table scan on the 400,000 row “issues” table.

The reason for the full scan is skewed “state” data. There are a handful of new and open issues but a heck of a lot of closed ones (and no possibility of setting them all to NULL before you ask). So the index looks unattractive to the optimiser:

400,000 rows / 3 distinct values = rubbish index access

The obvious thing to do is put a histogram on the “state” column. Things did not go quite to plan. My demonstration of this is below (on a 9.2.0.8 database – the same as the application in question).

1) Create a test table with a mixture of upper and lower case columns, notice we have to wrap table and column names in double quotes (“”) to avoid a conversion to upper case.

create table "tab1"(col1 number,"col2" number,"col3" number);

begin
	for i in 1..10 loop
		insert into "tab1" values (i,i,i);
	end loop;
	commit;
end;
/

2) DBMS_STATS also performs upper case conversions

exec dbms_stats.gather_table_stats(null,'tab1');

column column_name format a10
select  tc.table_name,tc.column_name, tc.nullable nulls
,       tc.num_distinct, tc.num_buckets buckets
from    user_tab_columns tc
where tc.table_name = 'tab1'
order by 1,2;

TABLE_NAME   COLUMN_NAM N NUM_DISTINCT    BUCKETS
------------ ---------- - ------------ ----------
tab1         COL1       Y
tab1         col2       Y
tab1         col3       Y

And again with double quotes (“”) to prevent case conversion

exec dbms_stats.gather_table_stats(null,'"tab1"');

column column_name format a10
select  tc.table_name,tc.column_name, tc.nullable nulls
,       tc.num_distinct, tc.num_buckets buckets
from    user_tab_columns tc
where tc.table_name = 'tab1'
order by 1,2;

TABLE_NAME   COLUMN_NAM N NUM_DISTINCT    BUCKETS
------------ ---------- - ------------ ----------
tab1         COL1       Y           10          1
tab1         col2       Y           10          1
tab1         col3       Y           10          1

So far everything works as expected. Now for the histogram.

3) Create histograms on lower and upper case columns

exec dbms_stats.delete_table_stats(null,'"tab1"');
exec dbms_stats.gather_table_stats(null,'"tab1"'-
               ,method_opt=>'for columns col1 size 254');
exec dbms_stats.gather_table_stats(null,'"tab1"'-
               ,method_opt=>'for columns "col2" size 254');

column column_name format a10
select  tc.table_name,tc.column_name, tc.nullable nulls
,       tc.num_distinct, tc.num_buckets buckets
from    user_tab_columns tc
where   tc.table_name = 'tab1'
order by 1,2;

TABLE_NAME   COLUMN_NAM N NUM_DISTINCT    BUCKETS
------------ ---------- - ------------ ----------
tab1         COL1       Y           10          9
tab1         col2       Y
tab1         col3       Y

That wasn’t expected, there’s no histogram on “col2″. After trying an assortment of “special” characters and seeing the following message far too frequently…

ERROR at line 1:
ORA-06502: PL/SQL: numeric or value error
ORA-06512: at "SYS.DBMS_STATS", line 10502
ORA-06512: at "SYS.DBMS_STATS", line 10516
ORA-06512: at line 1

…I did what I should have done originally and searched Metalink:

Bug 4892211 – DBMS_STATS does not recognise lower case / special characters in column names

  • This issue is fixed in*
  • 10.2.0.3 (Server Patch Set) <javascript:taghelp(‘FIXED_A203′)>
  • 11.1.0.6 (Base Release) <javascript:taghelp(‘FIXED_B106′)>

4) At first I thought a workaround would be to add a histogram to all columns on the table and then remove the ones I don’t want. However I’m faced with the same problem – I can’t specify “col3″ to remove that histogram:

exec dbms_stats.gather_table_stats(null,'"tab1"'-
               ,method_opt=>'for all columns size 254');

TABLE_NAME   COLUMN_NAM N NUM_DISTINCT    BUCKETS
------------ ---------- - ------------ ----------
tab1         COL1       Y           10          9
tab1         col2       Y           10          9
tab1         col3       Y           10          9

exec dbms_stats.gather_table_stats(null,'"tab1"'-
               ,method_opt=>'for columns "col3" size 1');

TABLE_NAME   COLUMN_NAM N NUM_DISTINCT    BUCKETS
------------ ---------- - ------------ ----------
tab1         COL1       Y           10          1
tab1         col2       Y           10          9
tab1         col3       Y           10          9

So in my case the best I could do was put the histogram on all indexed columns and use “method_opt=>’for all columns size repeat’”for future optimiser statistic collections to keep the histogram in place.

Using an Index to Shortcut a MIN/MAX Aggregate

Posted in Oracle by Neil Johnson on April 3, 2009

I was recently discussing with a developer whether a “SELECT MIN/MAX” could benefit from an index on the aggregated column. In the case I was asked about it would be of no help, however I went on to explain where it could be useful and thought I’d reproduce it here as my introductory blog post. I expect this has been well documented elsewhere but it’s a good note for me to practice formatting blog posts on.

First off create a table of a reasonable size

create user mintest identified by mintest default tablespace users;
grant connect, resource to mintest;
grant plustrace to mintest;

conn mintest/mintest

create table tab1 as select rownum rno, object_name, object_type from all_objects;
insert into tab1 select rownum+50000,  object_name, object_type from tab1;
commit;
insert into tab1 select rno+100000,  object_name, object_type from tab1;
insert into tab1 select rno+200000,  object_name, object_type from tab1;
insert into tab1 select rno+300000,  object_name, object_type from tab1;
commit;

create index mintesti on tab1(rno);

exec dbms_stats.gather_table_stats(null,'tab1',cascade=>true);

Now let’s see how the optimiser can take a shortcut in satisfying a MIN or MAX aggregate by plucking the first or last value from the index, thus avoiding scanning the whole segment (note the “FULL SCAN (MIN/MAX)”)

set autotrace on
select min(rno) from tab1;

Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=324 Card=1 Bytes=5)
   1    0   SORT (AGGREGATE)
   2    1     INDEX (FULL SCAN (MIN/MAX)) OF 'MINTESTI' (NON-UNIQUE) (Cost=324 Card=342448 Bytes=1712240)

Statistics
----------------------------------------------------------
          3  consistent gets
          0  physical reads

If we select bot a MIN and MAX in the same query however then the optimiser can’t cope and scans the whole table.

set autotrace on
select min(rno), max(rno) from tab1;

Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=324 Card=1 Bytes=5)
   1    0   SORT (AGGREGATE)
   2    1     TABLE ACCESS (FULL) OF 'TAB1' (Cost=324 Card=342432 Bytes=1712160)

Statistics
----------------------------------------------------------
       2128  consistent gets
        129  physical reads

You can still trick it though by querying the table twice.

select min_rno, max_rno
from 	(select min(rno) min_rno from tab1) m1
,	(select max(rno) max_rno from tab1) m2;

Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=648 Card=1 Bytes=26)
   1    0   MERGE JOIN (CARTESIAN) (Cost=648 Card=1 Bytes=26)
   2    1     VIEW (Cost=324 Card=1 Bytes=13)
   3    2       SORT (AGGREGATE)
   4    3         INDEX (FULL SCAN (MIN/MAX)) OF 'MINTESTI' (NON-UNIQUE) (Cost=324 Card=342432 Bytes=1712160)
   5    1     FIRST ROW
   6    5       VIEW (Cost=324 Card=1 Bytes=13)
   7    6         SORT (AGGREGATE)
   8    7           INDEX (FULL SCAN (MIN/MAX)) OF 'MINTESTI' (NON-UNIQUE) (Cost=324 Card=342432 Bytes=1712160)

Statistics
----------------------------------------------------------
          6  consistent gets
          0  physical reads

This was tested at 9i, 10g and 11g.

So that’s my first post done with. I’ll try to come up with something more original next time :)