Saturday, April 21, 2007

ODP.NET Tip: Anonymous PL/SQL and CommandType

When using an Anonymous PL/SQL block in your ODP.NET application code, one "gotcha" that may arise is setting the CommandType property of an OracleCommand object to an incorrect value. You may be tempted to set the value to CommandType.StoredProcedure. After all, we are talking about PL/SQL, and PL/SQL code means a stored procedure, function, package, etc. of some kind, right? Well, not exactly...

For details on PL/SQL and Anonymous Blocks, I suggest reviewing Chapter 1: Overview of PL/SQL in the Oracle® Database PL/SQL User's Guide and Reference 10g Release 2 (10.2) available for free on the Oracle Technology Network website. In a nutshell, an Anonymous PL/SQL block is a block of PL/SQL code that is not stored in the database, rather, it is created (or generated) in a client application and submitted to the database-side PL/SQL engine. As such, it is a block of text and not stored code. What is the appropriate CommandType to use when submitting a block of text to the database? CommandType.Text of course!

What happens when you accidentally do set CommandType to CommandType.StoredProcedure? You will see an ugly and somewhat confusing error message similar to:

ORA-06550: line 1, column 246:
PLS-00103: Encountered the symbol "(" when expecting one of the following:

begin case declare end exception exit for goto if loop mod
null pragma raise return select update while with
<an identifier> <a double-quoted delimited-identifier>
<a bind variable> << close current delete fetch lock insert
open rollback savepoint set sql execute commit forall merge
pipe
The symbol "case" was substituted for "(" to continue.
ORA-06550: line 1, column 261:
PLS-00103: Encountered the symbol ";" when expecting one of the following:

* & = - + < / > at in is mod remainder not rem when
<an exponent (**)> <> or != or ~= >= <= <> and or like LIKE2_
LIKE4_ LIKEC_ between overlaps mult


If you wish to see this in action, simply uncomment the following line of code in the sample application code below:

// cmd.CommandType = CommandType.StoredProcedure;

The sample application code uses an Anonymous PL/SQL block to perform a "select into" operation. This code does not use typical objects such as a data reader, etc. Instead, it uses parameter objects in the ODP.NET application code that correspond to the bind variables used in the Anonymous PL/SQL block.


The Sample C# Code:


using System;

using System.Data;

using Oracle.DataAccess.Client;

using Oracle.DataAccess.Types;

 

namespace SelectIntoTest

{

  class Program

  {

    // connection string using the "hr" sample user/schema

    // be sure to modify as necessary for your environment

    const string constr = "User Id=hr;" +

                          "Password=hr;" +

                          "Data Source=otndemo;" +

                          "Enlist=false;" +

                          "Pooling=false";

 

    // anonymous pl/sql to perform a select into

    // bind variables are used for the "output values"

    // as well as in the "country_id" in the "where clause"

    const string anonymous_block = "begin " +

                                  "  select  a.country_name, " +

                                  "          b.region_name " +

                                  "  into    :p_country_name, " +

                                  "          :p_region_name " +

                                  "  from    countries a, " +

                                  "          regions b " +

                                  "  where    a.country_id = :p_country_id " +

                                  "  and      a.region_id = b.region_id; " +

                                  "end;";

 

    static void Main(string[] args)

    {

      // create and open the connection

      OracleConnection con = new OracleConnection(constr);

      con.Open();

 

      // create parameter objects for the bind variables in the anonymous pl/sql block

      OracleParameter p_country_name = new OracleParameter("p_country_name",

                                                          OracleDbType.Varchar2,

                                                          40,

                                                          "",

                                                          ParameterDirection.Output);

 

      OracleParameter p_region_name = new OracleParameter("p_region_name",

                                                          OracleDbType.Varchar2,

                                                          25,

                                                          "",

                                                          ParameterDirection.Output);

 

      OracleParameter p_country_id = new OracleParameter("p_country_id",

                                                        OracleDbType.Varchar2,

                                                        "UK",

                                                        ParameterDirection.Input);

 

      // create command object and set CommandText

      // CommandType defaults to Text which is correct

      // for an anonymous pl/sql block

      OracleCommand cmd = con.CreateCommand();

      cmd.CommandText = anonymous_block;

 

      // it is incorrect to set CommandType = CommandType.StoredProcedure

      // cmd.CommandType = CommandType.StoredProcedure;

 

      // add the parameters to the collection for the command object

      cmd.Parameters.Add(p_country_name);

      cmd.Parameters.Add(p_region_name);

      cmd.Parameters.Add(p_country_id);

 

      // execute the anonymous pl/sql block

      cmd.ExecuteNonQuery();

 

      // write the values to the console window

      Console.WriteLine("Country Id '{0}' is country '{1}' located in region '{2}'\n",

                        p_country_id.Value.ToString(),

                        p_country_name.Value.ToString(),

                        p_region_name.Value.ToString());

 

      // keep the console window from automatically closing

      // such as when running from Visual Studio environment

      Console.WriteLine("ENTER to continue...");

      Console.ReadLine();

 

      // clean up

      p_country_id.Dispose();

      p_region_name.Dispose();

      p_country_name.Dispose();

      cmd.Dispose();

      con.Dispose();

    }

  }

}



Sample Application Output:



Country Id 'UK' is country 'United Kingdom' located in region 'Europe'


ENTER to continue...

Tuesday, March 20, 2007

ODP.NET Tip: Exercise Explicit Transaction Control

The Oracle Data Provider for .NET (ODP.NET), by default, operates in what is known as "auto-commit" mode. What this means is that when a non-query statement such as an insert, an update, or a delete has been processed, a "hidden commit" is automatically and implicitly performed in the database on your behalf. When processing a single row insert statement, for example, this may well be acceptable. However, what if, rather than that single row, you needed to insert several thousand or several hundred thousand (or more!) rows into a table? In pseudo-code, using the default behavior, this would look something like the following:


Begin loop
Insert row
Commit
If more rows continue loop
End loop


The key point in this pseudo-code is that the implicit "commit" is inside the loop and, thus, will be transparently invoked as many times as the loop executes.


In contrast to the "commit each pass through the loop" approach, a guiding practice in regard to Oracle database transactions is to commit when the transaction is complete. In this example, the transaction is not deemed to be complete until all rows have been inserted into the table. In pseudo-code, again, this would resemble:


Begin loop
Insert row
If more rows continue loop
End loop
Commit


Here the "commit" has been moved outside the main processing loop. While the difference may seem subtle and/or trivial, there can be a large performance difference between the two approaches. The TransactionDemo solution illustrates both of these approaches. The demo code contains two "test" methods: one method inserts 5,000 rows into a table ("demo_table") using a commit after each insert (simply employing the default behavior) while the second method explicitly creates a transaction and only commits that transaction once all rows have been inserted into the table.


To explicitly create, and thus begin, a transaction, you use the "BeginTransaction" method exposed by an OracleConnection object (where "con" is an OracleConnection object):


OracleTransaction txn = con.BeginTransaction();


When you are ready to commit the transaction, you invoke the "Commit" method on the transaction object:


txn.Commit();


Note: You may also invoke the "Rollback" method if necessary.

The following table illustrates the results of executing the demo code five times:


Test/Commit Default Explicit
1 11.4688 1.2188
2 11.1406 1.3906
3 11.6406 1.2031
4 11.2500 1.2031
5 12.1094 1.2031
Min 11.1406 1.2031
Avg 11.5219 1.2437
Max 12.1094 1.3906


Table 1: The results, in seconds, of TransactionDemo over 5 executions



As you can see from the results, committing at the end of the transaction, rather than after each row is inserted, allows for code that executes much faster. The average time for the method that commits after each row is inserted is just over 11.5 seconds, while the average time for the method that performs a single commit at the end of the transaction is just over 1.2 seconds - almost a factor of 10 difference!


In order to run the sample code below unchanged, you need to first perform the following tasks:

- Create a "tns alias" called "orademo" in your tnsnames.ora file to connect to the appropriate database in your environment.

- Create a user called "orademo" in the database.

- Grant the "create session" and "create table" privileges to the "orademo" user.

- Create a table called "demo_table" in the "orademo" user's schema.


Create the ORADEMO User


As a DBA user, perform the following tasks in your database to create the user and grant privileges (adjust as needed for your environment):


create user orademo identified by demo
default tablespace users
temporary tablespace temp
quota unlimited on users;


grant create session to orademo;
grant create table to orademo;


Create the Demo Table


Once you have created the "orademo" user, connect to the database as "orademo" and execute the following to create the table:


create table demo_table
(
c1 varchar2(32),
c2 varchar2(32),
c3 varchar2(32)
);



The Sample C# Code:




using System;
using System.Data;
using Oracle.DataAccess.Client;
using Oracle.DataAccess.Types;


namespace TransactionDemo
{
class Program
{
// be sure to adjust the connection string for your environment
private const string constr = "User Id=orademo; Password=demo; Data Source=orademo; Enlist=false; Pooling=false";


static void Main(string[] args)
{
Console.WriteLine("Beginning TransactionDemo...\n");


// create and open connection
OracleConnection con = new OracleConnection(constr);
con.Open();


// start with empty table
delete_table(con);


// perform insert with commit after each row
multi_commit(con);


// empty the table
delete_table(con);


// perform insert with a single commit at end
single_commit(con);


// clean up
con.Dispose();


// prevent command window from closing automatically when running from visual studio
Console.WriteLine();
Console.WriteLine("ENTER to continue...");
Console.ReadLine();
}


static void multi_commit(OracleConnection con)
{
Console.WriteLine("Starting multi-commit test...\n");


// used to track execution duration
DateTime timeStart;
DateTime timeEnd;
double totalSeconds;


// create command to insert rows into demo table
string sql = "insert into demo_table (c1, c2, c3) values ('This is column 1', 'This is column 2', 'This is column 3')";
OracleCommand cmd = con.CreateCommand();
cmd.CommandText = sql;


// capture test start time
timeStart = DateTime.Now;


// an implicit commit will occur for each ExecutionNonQuery call
for (int i = 0; i < 5000; i++)
{
cmd.ExecuteNonQuery();
}


// capture test end time
timeEnd = DateTime.Now;


// calculate total seconds for this test
totalSeconds = timeEnd.Subtract(timeStart).TotalSeconds;


// display time used for test
Console.WriteLine(" Multi commit: {0,7:##.0000} total seconds.\n", totalSeconds);


// clean up
cmd.Dispose();
}


static void single_commit(OracleConnection con)
{
Console.WriteLine("Starting single-commit test...\n");


// used to track execution duration
DateTime timeStart;
DateTime timeEnd;
double totalSeconds;


// create command to insert rows into demo table
string sql = "insert into demo_table (c1, c2, c3) values ('This is column 1', 'This is column 2', 'This is column 3')";
OracleCommand cmd = con.CreateCommand();
cmd.CommandText = sql;


// capture test start time
timeStart = DateTime.Now;


// create an explicit transaction
OracleTransaction txn = con.BeginTransaction();


// explicit transaction started, therefore no implicit commit
// for each ExecuteNonQuery call
for (int i = 0; i < 5000; i++)
{
cmd.ExecuteNonQuery();
}


// commit the transaction
txn.Commit();


// capture test end time
timeEnd = DateTime.Now;


// calculate total seconds for this test
totalSeconds = timeEnd.Subtract(timeStart).TotalSeconds;


// display time used for test
Console.WriteLine("Single commit: {0,7:##.0000} total seconds.\n", totalSeconds);


// clean up
cmd.Dispose();
}


static void delete_table(OracleConnection con)
{
Console.WriteLine("Deleting from demo_table...\n");


// delete all rows from the table
OracleCommand cmd = con.CreateCommand();
cmd.CommandText = "delete from demo_table";
cmd.ExecuteNonQuery();
cmd.Dispose();
}
}
}


Note: This content is based on my HotSos Symposium 2007 paper.

HotSos Symposium 2007 Materials

The paper and all sample code from my HotSos Symposium 2007 presentation entitled "Oracle Data Provider for .NET (ODP.NET) Performance Tips and Techniques" is available as a single .zip file. To download the .zip file, click here.

Thursday, February 15, 2007

Oracle Error ORA-1578: RMAN to the Rescue

If you are familiar with Oracle error messages, you will recognize that the true error message text for the ORA-1578 error is not, in fact, "RMAN to the Rescue". If you look this error up in the Oracle Database Error Messages manual, you'll find the text is:

ORA-01578: ORACLE data block corrupted (file # string, block # string)
Cause: The data block indicated was corrupted, mostly due to software errors.

Action: Try to restore the segment containing the block indicated. This may involve dropping the segment and recreating it. If there is a trace file, report the errors in it to your ORACLE representative.

Uh-oh. This sounds like it could be some bad news indeed. In particular, the statement "This may involve dropping the segment and recreating it." does not have an especially appealing quality to it when the segment in question is, say, a several hundred million row table, just as an example. It is a time like this that RMAN really can come to the rescue.

"But, we don't use RMAN to backup our database. We only perform user managed backups." I can hear you say. Many people are surprised to learn that even if you don't use RMAN to backup your database (even though you really should!) it can still be used to perform what is known as block media recovery. It is just this situation that I will illustrate here - performing block media recovery on a corrupt data block (ORA-1578) using RMAN but without having an RMAN backup.

Before embarking on this adventure, I think it wise to create a new tablespace, and, thus, datafile dedicated to this purpose. I will, naturally, be using SQL*Plus for this activity:


sqlplus "/ as sysdba"

create tablespace test logging datafile 'D:\ORACLE_DB\LT10GR2\TEST01.DBF' size 1M;

create table ctest (c varchar2(1)) tablespace test;

insert into ctest values ('a');

commit;


So, now I have a test tablespace and a table.

Since I am going to show how to perform block media recovery using a non-RMAN backup, I had better backup the data file! In order to do that, I simply shutdown the database:


shutdown immediate;


Then I backup the file in a Command Prompt window by copying it to a directory called "backup":


D:\oracle_db\lt10gr2>copy TEST01.DBF backup\TEST01.DBF
1 file(s) copied.


NOTE: If your database is in ARCHIVELOG mode, you do not need to shut it down before copying the data file; however, remember to issue an “alter tablespace TEST begin backup;” before copying the data file and an “alter tablespace TEST end backup;” after the file has been copied. As this database is a “sandbox” database on my laptop, it runs in NOARCHIVELOG mode most of the time.


At this time some weird magic happened and a block belonging to the table has become corrupt. It's funny how these things can happen.


Time to start the database back up and see if the block is corrupted from Oracle’s perspective.

Back in my SQLPLUS session:


startup open;


The database has successfully started back up. Now I try to perform a select against the table:


select * from ctest;

ERROR at line 1:
ORA-01578: ORACLE data block corrupted (file # 6, block # 12)
ORA-01110: data file 6: 'D:\ORACLE_DB\LT10GR2\TEST01.DBF'


Hey! There’s a familiar looking error message.

OK, so the data block belonging to my CTEST table is definitely corrupt. I’d say it is time for that RMAN magic!

Because I did not use RMAN to perform any backup operations on this database – the “backup” was simply copying a single datafile to another directory – I do not have any RMAN Recovery Catalog. Therefore I specify the “nocatalog” option when I start RMAN:


rman target=/ nocatalog


After connecting to the database with RMAN, I need to tell RMAN about the backup file which is also known as the “datafilecopy” in RMAN terms. I refer to this as “registering” the file with RMAN. Here’s how I do that:


catalog datafilecopy 'D:\ORACLE_DB\LT10GR2\BACKUP\TEST01.DBF';


RMAN responds with something similar to:


cataloged datafile copy
datafile copy filename=D:\ORACLE_DB\LT10GR2\BACKUP\TEST01.DBF recid=2 stamp=614641757


RMAN is now “aware” of this file and can use it to perform the block media recovery. Also note that all of the redo that was generated from the time I copied the file up to the present time is also available on my system. To recover the block simply enter:


blockrecover datafile 6 block 12;


RMAN responds with text similar to the following:


Starting blockrecover at 15-FEB-07
allocated channel: ORA_DISK_1
channel ORA_DISK_1: sid=148 devtype=DISK

channel ORA_DISK_1: restoring block(s) from datafile copy D:\ORACLE_DB\LT10GR2\BACKUP\TEST01.DBF

starting media recovery
media recovery complete, elapsed time: 00:00:03

Finished blockrecover at 15-FEB-07


Now that RMAN has performed the block media recover, I verify if I can access the information in the block that I previously corrupted by issuing the following statement in SQLPLUS:


SQL> select * from ctest;

C
-
a

1 row selected.


How about that? Having never taken an RMAN backup of the database, I was able to use RMAN to completely recover a corrupted block in my table!

Having successfully recovered the data block, I remove the datafilecopy registration in RMAN like so:


change datafilecopy 'D:\ORACLE_DB\LT10GR2\BACKUP\TEST01.DBF' uncatalog;


You may also use the “delete” command if you wish to both remove the registration and delete the physical file in a single command.

One final reminder: the purpose of this was to demonstrate an RMAN capability. If you encounter a corrupt data block in your database and need assistance, please ensure you contact Oracle Support for the proper procedures to recover. Remember this is for demonstration purposes only. Do not perform this in a production server!