Sei sulla pagina 1di 113

Course Introduction

What is PL/SQL?
Hi. Welcome to Pluralsight. My name is Pankaj Jain, and
welcome to this course, Oracle PL/SQL Fundamentals - Part 1. So
what is PL/SQL? Oracle PL/SQL is Oracle's procedural extension
to SQL. Oracle database allows us to create tables, objects,
execute all the familiar SQL statements, like INSERT, UPDATE,
and DELETE. In addition, it also offers PL/SQL a very powerful,
and flexible procedural language, which gives us the ability to tie
these SQL statements together. It gives us the ability to write
procedural logic, to create loops, perform conditional executions,
and provides us with a whole lot of other powerful constructs,
which gives us a lot of fine grain control in executing these
statements. It helps us define the business logic more effectively,
and allows us to create reusable units of work, which I think is
one of the greatest benefits of using it.
Why learn PL/SQL?
So why should we learn PL/SQL? Here are a few reasons. Oracle
database is one of Word's largest relational databases used by
thousands of companies across the globe. PL/SQL is Oracles
procedural language used extensively in database applications,
and other Oracle tools, like Oracle Forms, Oracle Reports, Oracle
Application Express, just to name a few. Having knowledge about
Oracle PL/SQL will enable you to work on these applications and
tools, and will open up a lot of opportunities for you. Instead of a
client making multiple calls to the database in a sequence, Oracle
PL/SQL engine can execute them together in a block, thus
reducing network traffic, and roundtrip calls to the server. So say
you have an e-commerce website, which places orders by first
checking the items table to check for available inventory. Then it
does a SELECT against the customer's table to ensure enough
account balance, and then it inserts it according to the order of its
table. Instead of making these multiple calls, if you can clump
them together in a named PL/SQL unit inside the database, then
they're all executed as one call to the named unit, thus reducing
network traffic and latency, and increasing response time. It helps
create blocks of code, or units of work, which are useable. So it
can help your application become more modular. It is a feature
rich language with procedural capabilities, and error handling
mechanisms, giving you a fine grain control of your business
logic execution. It also supports object oriented programming.
Audience
So who can benefit from this course? This course is for
programmers new to Oracle database programming who want to
learn Oracle PL/SQL in order to support existing applications, or
to create new applications within the database. It can be useful to
even experienced programmers who have worked in Oracle
before. By revisiting the basics, they may get some takeaways
from this course. This course is for beginner programmers. I will
be talking about some of the basic programming concepts, and
constructs, and if you are new to programming, it will help you
build a solid base. This course would also be useful for web
developers working in say, Java, C#, etc., who would like to learn
how to define business logic in the database tier versus a web tier,
and possibly gain some performance optimization in the process.
This course assumes that you have some familiarity with basic
SQL commands, like the INSERT, UPDATE, and DELETE
statements, and understanding of some basic SQL datatypes.
How is PL/SQL Processed?
Let us take a look at how PL/SQL blocks and units are processed.
So the PL/SQL block is sent to the PL/SQL engine. The PL/SQL
engine can reside inside the database server, or inside a client tool,
like Oracle Forms. The PL/SQL engine processes the procedural
code, and sends off any SQL statements for processing to the SQL
engine, which resides inside the database server. So the pieces of
code doing calculations, or conditional checks using SQL, which
can be done using PL/SQL instead, can be processed totally by
the PL/SQL engine, and thus avoiding the trip to the SQL engine,
saving time and database resources.
Tools
Let us talk about the tools. For the demos, I will be using Oracle
11G Express Edition, which you can download from Oracle's
website. It is an entry-level, small footprint database, free to
develop, deploy, and distribute. Most of the features we'll talk
about should work with older versions of the database also. I will
be using Oracle SQL Developer, a free IDE, or development
environment, which, again, you can download from Oracle's
website. SQLPLUS is a command line utility which you can
install on the client machine, and use it to talk to the database. You
can use it for executing script against the database. You can use
SQLPLUS for running most of the examples we will talk in this
course. However, if you have access to, and are more familiar to
other IDEs, like Toad, and SQL Navigator, just to name a few,
feel free to use them to try out the exercises on your own. I will,
however, use SQL Developer exclusively for running the demos
in this course. So let's get started.
Anonymous Blocks
Overview
Hi. Welcome to Pluralsight. My name is Pankaj Jain, and
welcome to this module on Anonymous Blocks. Anonymous
block is the most fundamental unit in PL/SQL, and having a solid
understanding of the anonymous block will give you a good
foundation for building your PL/SQL programming knowledge.
In this module, we will talk about what anonymous blocks are,
and where you can use them. How anonymous blocks are
structured, and how you can put comments in them. We will see
how and where you can declare variables in anonymous blocks.
Scope and visibility are variables when we have nested blocks,
and how to label blocks to resolve some of the visibility issues.
All these are basic, and important concepts, and having a good
handle of them will go a long way in your understanding, and
usage of the PL/SQL programming language.
What is an Anonymous Block?
So, what is an anonymous block? An anonymous block, as the
name suggests, is a block without name. It is the most basic and
fundamental unit in PL/SQL programming. It allows you to
combine together a bunch of SQL statements, and run them as a
single block of code. Let me try and explain it to you with an
example. So, I enjoy doing photography in my free time, and my
Cannon DSLR has some prebuilt modes, like the scenic mode, or
the portrait mode. But my camera also allows me to put my
custom settings in the manual mode, which I like to use in certain
situations. Oftentimes I go out with my photography buddies to
shoot photos, and sometimes I pass them my custom settings for
them to try. But I find that telling them all these settings one by
one is tiring, and then I may forget to specify some of my custom
settings. So I write them on a piece of paper, which I pass on to
them. Think of the anonymous block like the piece of paper with
custom instructions. Unlike the prebuilt modes, which we can call
by name, and anyone can understand, and get to them, these are
known only to me, or to whomsoever I pass on to. The place
where you will use anonymous block is, in most situations, where
you want to have a one off execution, so you're running a one off
script to fix something in production, or something I need to run
just a few times for my testing. Having a good understanding of
anonymous block will provide us a good foundation to understand
the other program units, like the procedures and functions.
Structure of Anonymous Blocks
Let us now take a look at the structure of an anonymous block.
The anonymous block consists of three sections; the declaration
section, that starts with the DECLARE keyword, and ends at the
BEGIN keyword. In this section you declare all the variables to
be used within your block. You can declare variables, which can
be based on simple datatypes, variables based on advanced data
types, cursors, etc. We will talk about datatypes, cursors, in detail
later in the course, but for now, for illustration purposes, I have
declared a number variable called l_counter. Some of you coming
from Java, or C# background, are used to putting the datatype first,
followed by the name of the variable. But in PL/SQL, the order is
reversed; something to take note of. Another thing I wanted to
point out was in PL/SQL all lines have to end with a semicolon
character. This is with the exception of the DECLARE, BEGIN,
and EXCEPTION keywords, for the exception handler lines, like
when, others, then, or here, or for beginning constructs of
conditional statements, and loops, which I will point to you as we
talk about those. You still need to put a semicolon after the END
keyword to mark the end of the anonymous block. Next we have
the execution section which starts with the BEGIN keyword. This
is a main body of the anonymous block, and consists of
executable lines of code like SQL statements, numerical
calculations, etc. Here I have assigned a variable we declared
earlier, a value of 1. Notice that the assignment operator is a colon
equal to sign, which is kind of unusual. In Java or C#, it is the
equals operator. In PL/SQL, the equals operator is a comparison
operator. Most of the other operators, like greater than, less than,
etc., are the same with other languages, but I will pointing you the
other differences as you go along. The last section is the exception,
or error handling section. Here we can handle different kinds of
errors. We will talk about exception handling in detail later, but
for now, to understand this code, WHEN OTHERS is a catchall
exception handler, which traps any exception if there is no
specific exception handler defined for that error in the exception
block. The anonymous block ends with the END keyword. Of
these sections, only the execution section is required. The
declaration and exception sections are optional. So we can just
have a block which starts with a BEGIN, and ends with an END
keyword, but I do not see much use of a block with no declared
variables to work with.
Comments in Anonymous Blocks
Good documentation in the code is always very helpful for the
maintenance of the code, and comments help with that. There are
two kinds of comments you can put in PL/SQL. First is a single
line comment. These can go on the line by their own, or can also
be put in line with code towards the end that start with two dashes.
The other type of comments you can put are multi-line comments
which start with a /*, and end with a */. Talking about best
practices, along with comments, I think it is also very helpful to
have a proper indentation of your code to improve readability. I
generally like to have two spaces for indentation from the
beginning of the main, or parent section, but it is up to your
preferences. Also, proper naming convention goes a long way in
easier understanding, and maintenance of the code. I like to have
my local variables start with an l prefix, my cursors with a cur
prefix, exceptions with an e prefix, and so on. Also, let me
mention here that PL/SQL is not case sensitive except for the
character literals, which we will talk about later. So you can have,
say, the BEGIN keyword in upper, lower, or mixed case, but I like
to always have DECLARE, BEGIN, EXCEPTION, and END
keywords in uppercase, variable declarations and executable
statements in lowercase. It is up to you, and your team, to decide
on the naming conventions, and cases, and spacing your code. But
once you decide on the standard, you should follow it consistently
throughout your code.
Demo of Anonymous Block Structure and Comment Styles
Let us now do a demonstration to understand the structure and
comments in anonymous block. For the demos, I will be using
Oracle SQL Developer; however, if you have access to, and are
more familiar with other IDEs, like SQL Navigator, Toad, etc.,
feel free to use them to try out the exercises on your own. Here I
am connected to Oracle database Express Edition version 11G.
We start the anonymous block with a DECLARE keyword. Then
we write a single comment that starts with two dashes. We then
declare a variable, l_counter of type NUMBER. The BEGIN
keyword begins the execution section. We assign a value of 1 to
l_counter. Notice the colon equal to sign, which is how you do
assignments in PL/SQL, unlike other languages, like Java or C#,
where you use the equal sign. Also notice, my use of single line
comments that start with two dashes, in this case, at the end of the
line. Dbms_output is a package in Oracle database, and put_line
is a procedure. For now, think of it as a utility provided by Oracle
to send messages to screen, or the console. And notice the two
pipe signs. That is the concatenation character in PL/SQL for
joining two strings together. And as you might have guessed it,
l_counter is implicitly converted to character, and then
concatenated. In order to see messages using
dbms_output.put_line, you have to enable your session to send
print messages. In SQL Developer, you have to go to View,
choose Dbms Output. That opens up this window at the bottom.
Click on the plus sign, and then choose the Connection for which
you want the output to be enabled. Now, our session is enabled
for Dbms Output. Notice the Buffer Size here. This is the buffer
size used by dbms_output to output the character data. For most
purposes, this should be sufficient, but if you need to increase it,
this is where you would come to increase it. Next, I show you how
to write multi-line comments, starting with /*, and ending with */.
The EXCEPTION keyword starts the exception block. We have
a catchall exception handler. We are not doing much inside it
though. We will talk more about exception handling in a later
module. Null is a special keyword in PL/SQL, which stands for,
well, null. We end the block with the END keyword. Let us run
this block using the Run Script button here to the top. As you can
see, the anonymous block completed successfully. We see the
output message in the Output window, which is l_counter in the
inner block is 1, which is the value of l_counter we assigned over
here.
Nesting of Anonymous Blocks
We can nest one anonymous block into another. You can have
multiple number of nestings in your code, but typically you need
not go more than one nesting deep for most cases. Here, we have
an outer block, and an inner block. Each block can have its own
declaration, execution, and exception sections. One reason you
might want to do it is, when some statements in your block may
possibly raise an exception, but you would like the execution of
the rest of the statements to still continue. You can then wrap
those SQL statements inside an inner block. Have an exception
handler section, which handles the exception, and that way, after
the inner block terminates, execution resumes for the remaining
statements in your outer block. We will take a look at an example
later when we talk about exceptions.
Scope and Visibility of Variables in Nested Blocks
Let us now talk about the scope of variables in the context of
nested blocks. The variables defined in the outer block have a
scope in the outer block, as well as all the inner blocks. So l_outer
is in scope, and can be accessed by the outer, as well as the inner
block. However, variables declared in the inner block have a
scope limited only to the inner block, and its sub-blocks if any,
but not to the outer block. So, l_inner is in scope only within the
inner block, and the outer block does not know about it, and
cannot access it. What happens if we define the variable with the
same name in both the outer, and the inner block? The inner block
variable will overwrite the visibility of the variable of the same
name in the outer block. In this case, l_var is defined both in the
outer and the inner block. L_var defined in the outer block is
visible in the outer block, but its visibility is overwritten by the
variable of the same name in the inner block. So if I do a
dbms_output.put_line for l_var in the inner block, it'll come up
with a value of 2 in the inner block. In the outer block, the l_var
defined in the inner block is not in scope, and hence not visible,
so it'll print a value of 1.
Labels
What if I want to access the l_var defined in the outer block, even
though it's visibility is hidden by the declaration of a variable with
the same name in the inner block? In that case, I can label my
outer block, and use that label as a qualifier to access the l_var in
the outer block. So let us label the outer block as parent. Now, I
can access both the l_vars, the one defined in the outer block as
parent.l_var, and the one in the inner block without using any
qualifier, as it has the default visibility in the inner block. I can
label the inner block 2, and access the l_var, say, by saying
inner.l_var, but that it is not needed.
Demo: Scope and Visibility of Variables and Block Labels
Let us now do a demo to demonstrate the scope and visibility of
variables in nested blocks. In this code snippet, I have got two
blocks, one nested within the other. The outer block starts with the
DECLARE keyword, after which I have declared two variable,
l_var, and l_outer, both of type NUMBER. The BEGIN keyword
starts the execution section for the outer block. I have assigned a
value of 1 to l_outer, as well as l_var. Next, I have the DECLARE
keyword, which starts the inner block. Here again, I have declared
two variables, l_var, and l_inner of type NUMBER. Since l_var
is also declared in the outer block, at this point of time the
declaration of l_var in the inner block has overwritten the
visibility of l_var in the outer block. The BEGIN keyword starts
the execution section of the inner block. I assign a value of 2 to
l_inner and l_var. I have got then three dbms_output.put_line
statements printing the values of l_var, l_inner, and l_outer in the
inner block. We should expect that the value of l_var printed over
here would be the value which we have assigned in the inner block,
which is 2. We end the inner block with the END keyword. We
then print the value of l_var in the outer block. The value of l_var
in the outer block should be the value of 1, which we have
assigned over here. Let us run this block. As you were expecting,
the value of l_var and l_inner in the inner block is 2, which is
what we had assigned to those variables in the inner block. We
can still access l_outer in the inner block, and print its value of 1.
L_var in the outer block will print a value of 1, which we have
assigned over here. So this demonstrates that when we have a
variable of the same name defined in the outer block and the inner
block, the variable in the inner block overwrites the visibility of
the variable in the outer block. Also, a variable declared in the
outer block has a scope within the outer block, as well as the inner
blocks. However, a variable declared in the inner block doesn't
have a scope in the outer block. Let us look at the same example
again; however, in this case I have labeled the outer block as
parent. Now, inside the inner block I can access both the l_vars,
the 1 declared in the outer block, as well as a 1 declared in the
inner block. In the inner block, I can access the l_var declared in
the inner block without using any qualifier as it has the default
visibility. However, now I can also access the l_var declared in
the outer block using parent.l_var. Let us run this block. As you
can see, the block completed successfully. L_var in the inner
block prints a value of 2, which we have assigned over here. L_var
from the outer block using the label parent.l_var prints a value of
1, which is the value we assigned to l_var in the outer block. L_var
in the outer block prints a value of 1, because that's the value in
the outer block it can see for l_var, as it cannot see the value of
l_var declared in the inner block. So this goes to demonstrate that
when we have the variable of the same name declared in both the
outer block, and the inner block, then we can access the variable
declared in the outer block inside the inner block using a qualifier,
which is the label for the outer block.
Summary
Oracle supports anonymous blocks in PL/SQL programming
language, which are useful for say, running one off scripts for
production fixes, or testing. It consists of declaration, execution,
and exception sections. Specifying comments in the code is a
great way of documenting what we are doing, and greatly
simplifies maintenance. And so is following standard naming
conventions, spacing, and cases in our code. Oracle provides the
ability to provide both single line, and multi-line comments.
Anonymous blocks can be nested within each other, which is
useful in situations where we want to catch exceptions, and
resume execution. Variables in the outer block have scope both
within the inner and outer block, unlike the variables in the inner
block, which have scope only within the inner block. However, it
is important to remember that variable of the same name in the
inner block overwrites the variable of the same name in the outer
block, something which block labels can help us resolve. Finally,
we saw a few demos to see all those concepts in action.
PL/SQL Commonly Used Datatypes
Introduction
Hi. Welcome to Pluralsight. My name is Pankaj Jain, and
welcome to this module on PL/SQL Commonly Used Datatypes.
Having a good understanding of some of the most commonly used
PL/SQL datatypes will help you write PL/SQL code more easily,
and efficiently. Oracle PL/SQL supports all the basic SQL types,
as well as has a few specialized types of its own. It supports the
scalar datatypes, which do not have internal components. We will
be talking about datatypes, like numbers, characters, dates, etc.
over here. Composite datatypes have internal components, which
can be accessed using the dot notation. These include records and
collections. We will talk about records in this module, and
collections will be covered in a later course. Oracle also supports
reference datatypes, like ref cursors, which we will talk about
when we take a look at cursors later in this course. Oracle also
supports a lot of other datatypes; for instance datatypes like
CLOBs, BLOBs, BFILEs, which help store large objects like
images, files, etc. We will not be covering those in this course.
Oracle PL/SQL supports a lot of datatypes, and it is not possible
to cover all of them in a single module. My aim is to introduce
you to some of the commonly used datatypes, which I think you
will be using most of the time.
Number: Fixed and Floating Type
Scalar datatypes are the basic datatypes which have no internal
components. These can be divided into numeric, character,
datetime and interval, and Boolean datatypes. The numeric
datatypes, again, can be divided into the NUMBER type,
PLS_INTEGER, or BINARY_INTEGER type, and
BINARY_FLOAT, or BINARY_DOUBLE datatype. Let us take
a look at the NUMBER datatype first. The NUMBER datatype is
the most common numeric datatype, which will serve your
purpose for most applications. The advantage of the NUMBER
datatype is that it is portable across different Oracle database
platforms. They can store a vast range of values, ranging from 1
exponential -130, to 1 exponential 126, including the number 0.
This should satisfy the needs of most of the applications; however,
you should always evaluate your application to make sure the
numerical computations will fall within the range. A value which
is lower than its range will get drawn into 0. Higher literal values
will cause compilation errors, and higher values, as a result of
computations, can cause undefined results. Something to be aware
of. The NUMBER datatype can be defined both as a fixed point
number, as well as a floating point number. Let us see how it can
be defined as a fixed point number first. It is called fixed point as
you are fixing the scale, or number of decimal digits while
declaring their number variable. Precision refers to total number
of digits in the number. It has a max value of 38. Scale refers to
where the rounding occurs, and has a min value of -84, and a max
value of 127. Let us understand it more with a few examples. Let
us define a number with a precision of 4, and a scale of 2. This
means that the max value of digits in the NUMBER can be 4, and
since scale is a positive value of 2, it will allow two numbers to
the right of the decimal point, and for bigger decimals, it will
round them to 2. So, a variable of type NUMBER(4,2), can accept
25.21 perfectly; 25.157 will satisfy a position of 4, and a scale of
2, will get rounded to 25.16, to the right of the decimal point;
125.15 will, however, not fit in, as the number to the left of
decimal points cannot be rounded, and we need a scale of 2. So,
we need a precision of 5 for this number. Thus, it will result in an
ERROR as it exceeds the precision of 4. Let us see what happens
when we have a negative scale. A negative scale causes rounding
to the left of the decimal point, as opposed to rounding to the right
of the decimal point for a positive scale. For example, say we have
a NUMBER variable defined with a precision of 5, and a scale of
-2. If we assign a literal value of 12345.678 to it, it will have a
precision, or significant digits to be 5. Since the scale is -2, it will
be rounded two places to the left of the decimal point, making it
12300. Similarly, 156.456 will be rounded to two places to the left
of decimal point, making it 00200, or 200. Numbers can also be
used to represent floating point numbers. That is, where we do not
explicitly specify the precision and the scale, and the decimal
point can float to represent the number assigned. So, we can
assign a variable of type NUMBER, and that one variable can
hold all these different numbers, even though they have varying
number of floater digits, and varying number of digits after the
decimal point.
Assignments, Defaults and Constants
Let us now take a look at how to assign values to variables. This
is common to all datatypes; however, since we have just talked
about numbers, so we will illustrate it with the NUMBER type
over here. The scalar variables, like numbers, characters, dates,
and Boolean, if unassigned have a null value. So in this example,
l_first has a null value in the declaration section. The values can
be assigned right at the time of declaration, as we did for l_second,
with the := operator. This is different from languages like C# and
Java, where an equal operator is an assignment operator. The
value can be assigned in the execution section, as we did for l_first
over here. The value assigned earlier can be overwritten, as we
did for l_second. The values of l_first and l_second would be
initialized in the declaration section every time the block is called.
We can assign DEFAULT values to the variables in the
declaration section if you know that it is a fairly typical value the
variable will have. We can, of course, overwrite it later in the
execution section. So if we have assigned a default value of 2.21
to l_second, which we overwrite later in the code. Constants are
the equivalent of declaring variables with the final keyword in
Java, where once defined, the value cannot be changed. It is
assigned in the declaration section, as we have done for l_second
in this code example. If you try and change it later, you will get
an error.
Demo: Numbers
In this demo, we will look at how to declare variables, and assign
values to them. We start off by declaring a variable l_num of type
NUMBER. Since we have not assigned it a value, it is null at this
point of time. If we define l_num CONSTANT, which is a
constant, and as constants, they have to be assigned value, and
declaration, which cannot be changed later on. We assign it a
value of 5. L_num_default is defined as a fixed point number with
a precision of 5, and a scale of 2, and assigned a DEFAULT value
of 5.2. L_num_float is defined as a floating point number with an
initial value of null. Later, in the execution section, we assign it a
value of 3.245. Then, we have a few
DBMS_OUTPUT.PUT_LINE statements printing the values of
these variables. Let us run this block. As expected, l_number is
null; l_num_constant has a value of 5; l_num_default has a value
of 5.2; and l_num_float, which is a floating point number, retains
the decimal digits in 3.245. Let us take a look at how scale comes
into play when we define the number as a fixed point number.
Here, we have defined l_num as a fixed point number with a
precision of 5, and a scale of 2. So the total number of digits
cannot exceed 5, with a maximum of 2 numbers after the decimal
point. It is assigned an initial value of 123.45, and later the value
is overwritten to be 123.789. We have a couple of
DBMS_OUTPUT.PUT_LINE statements to print the value
before and after. Let us run this block. (Typing) As expected, from
the DBMS_OUTPUT we observe that l_num retains its initially
assigned value of 123.45 as it satisfies the precision and scale
requirements of 5 and 2. In the second case, 123.789 is rounded
to 123.79, as only 2 digits after the decimal point are allowed due
to a scale of 2. What happens if we assign l_num a value of
1234.56? This would raise an error as .56 would be retained to
satisfy a scale of 2, and 1234, before the decimal, would make the
total digits to be 6, and we are allowing a precision of 5. Let us
run this block to confirm this. (Typing) As expected, we see that
there is an error which has been raised for a number precision
being too large. What if we define the scale as a negative number?
In that case, all it will round is the actual value of the specified
number of places to the left of the decimal point. Here, we define
l_num as a number with a precision of 5, and a scale of -2, and
assign it an initial value of 12345.678. Later, we overwrite the
value to be 156.456. Let us run this block. As from the
DBMS_OUTPUT.PUT_LINEs in the Console, we notice that
12345.678 gets rounded to 12300. So, rounded to two places to
the left of decimal point. Similarly, 156.456 gets rounded to 200.
Subtypes
Subtypes are subsets of the base types, like number, character, or
datetypes. They support the same operations as the base type.
They can be constrained. For instance, INTEGER, which is a
subtype of NUMBER, is a number with the precision of 38, and
a scale of 0, and so it doesn't allow any decimals. They can be
unconstrained. For example, subtype NUMERIC precision, scale
is the same as NUMBER precision, scale. The reason to create
subtypes, which are unconstrained, is to provide ANSI ISO
compatibility, or sometimes to improve the readability of the code.
They're also useful for constrained enforcement, so if, say, an
application needs a constraint, the number cannot have decimal
points, it can use the INTEGER subtype, which will enforce that
at the lowest level regardless of which client code is calling it. Let
us quickly look at the NUMBER subtypes. First, we have the
DEC, DECIMAL, or NUMERIC type, which are fixed point
numbers with a max precision of 38 digits. Then we have
DOUBLE PRECISION, or FLOAT type, which are floating point
numbers with a maximum precision of 38 digits. REAL is also a
floating point number with a maximum precision of 38 digits. Just
that, it is internally stored with 4 bytes compared to DOUBLE
PRECISION and FLOAT, which take 8 bytes. INT, INTEGER,
and SMALLINT all are integer types, with a max precision of 38
digits. All of these are under the covers NUMBER datatype with
different constraints on them. You might recognize some type
names from other languages. As I had mentioned earlier, one of
the reasons of creating subtypes, is to give them compatibility
with ISO and C types. You can use these in your code as they
provide easy readability, or if you need certain constraints on your
variables, which these subtypes provide; for instance, non-
decimal value with integer. Oracle provides us with a lot of
subtypes, like the INTEGER, NUMERIC, etc., as we saw in the
previous slide. It also gives us the ability to define our own
subtypes. The syntax for defining subtypes is SUBTYPE name of
the subtype IS <base_type> with optional constraint, and an
optional NOT NULL clause. For example, we can define a
SUBTYPE myinteger, which is a base-type NUMBER with a
constraint of precision 38, and a scale of 0, and is additionally
NOT NULL. Let us understand subtypes with this demo. Here, I
have defined l_int as type INTEGER, which is a predefined
subtype from Oracle, which does not allow decimal points. Then
we define SUBTYPE myinteger as NUMBER(38,0). So our
SUBTYPE also does not allow decimal points due to a scale of 0,
just like the INTEGER subtype. We assign the same value of 1.8
to both. Let us run this block. From the Console Output, we see
that both l_int, and l_myinteger are rounded off with the nearest
integer value, which is 2.
%TYPE
%TYPE is an anchored type. So what that means is that a variable
declared with %TYPE takes the datatype of its anchor. It can be
anchored to a previously declared variable. For instance, if you
look at this code snippet, we have declared l_num, which is a
fixed point number, with a precision of 5, and a scale of 2. Further,
it is defined as a NOT NULL variable, and assigned a DEFAULT
value of 2.21. L_num_vartype is anchored to l_num with
the %TYPE attribute, and so it inherits the datatype and
constraints like NOT NULL, etc., from l_num. So, it also
becomes a fixed point NOT NULL NUMBER with a precision of
5, and a scale of 2. A variable can also be anchored to a database
table column. For example, here, we have a table, departments,
with a number column, department ID. We can DECLARE
l_num_coltype variable, which is anchored to this column, with
the %TYPE anchor. So it will also get the number datatype. Note,
however, when we anchor to the database table column, the
variable only gets the datatype, and not the constraint you find on
that column. So l_num_coltype will not get the NOT NULL
constraint you find on the department ID column, and is
NULLABLE. If your PL/SQL variable is linked to a database
table column, then it dynamically adapts to any changes in the
base table type. For instance, if you increase the length of the
database table column, the PL/SQL variable also automatically
gets the change without you having to make any code changes.
That is one of the main advantages of defining a variable
with %TYPE. And I prefer to use it when a PL/SQL variable is
fetching, or inserting to a database column, and is closely linked
to it. For this demo, let us create a table departments that's two
columns, dept_id, which is a NUMBER NOT NULL, and
declared as the PRIMARY KEY. And dept_name which is of type
VARCHAR2(60). Let's create this table first. (Typing) And on the
block, we DECLARE l_num as a NUMBER with a precision of
5, and a scale of 2, apply NOT NULL constraint to it, and give it
a DEFAULT value of 2.21. We DECLARE l_num_vartype,
which is of type l_num. So this variable would inherit that NOT
NULL constraint, and the datatype of NUMBER from l_num.
Then we define l_num_coltype, which is of
departments.dept_id%TYPE. This will acquire only the datatype
from dept_id column of type NUMBER, but not the NOT NULL
constraint, as %TYPE with table columns do not inherit the
constraints. Let us run this block. (Typing) L_num-vartype was
assigned a value of 1.123, which it rounds to 1.12, since it inherits
a scale of 2 from l_num. L_num_coltype was not assigned any
value, and is NULL, and demonstrates that it just got the datatype,
and not the constraint from the table column with the %TYPE.
PLS_INTEGER or BINARY_INTEGER
PLS_INTEGER, which is the same as BINARY_INTEGER,
allows us to store signed integers. They require less storage, and
are faster as they use hardware arithmetic instead of software
arithmetic like numbers do. They support a good range of values,
so you should use them for all loop counters, and for integer
calculations which fall within its range. One thing to notice that
if integer value goes above, or below the range, they will give an
overflow exception; something to be aware of. These are the
subtypes for PLS_INTEGER, or BINARY_INTEGER.
NATURAL subtype includes positive PLS_INTEGER values
including 0's, and NATURALN adds a Not Null constraint to it.
POSITIVE subtype includes positive PLS_INTEGER values, not
including 0's, and POSITIVEN adds a Not Null constraint to the
POSITIVE subtype. SIGNTYPE has three values, -1, 0, and 1,
and is useful for performing logic for greater than, equal to, or less
than values. SIMPLE_INTEGER, which was introduced in
Oracle version 11G, has the same range as the PLS_INTEGER,
but has a Not Null constraint, and allows overflows. So when the
value reaches say the max range, adding 1 will not cause numeric
overflow exception, but would rather swing the other end of the
range. So in situations where you need not worry about overflow,
these can perform faster because of less checking overhead.
Again, these subtypes are useful to enforce constraints on the
variables according to application needs, and are cleaner, and
easier to read.
BINARY_DOUBLE and BINARY_FLOAT
Introduced in Oracle version 10G, BINARY_DOUBLE and
BINARY_FLOAT are IEEE 754 format floating point numbers.
BINARY_DOUBLE has a 64 bit precision, and
BINARY_FLOAT has a 32 bit precision. They have binary, or
base2 precision, unlike NUMBER, which has a decimal, or
base10 precision. Since arithmetic for these numbers is performed
in machine hardware, they offer superior performance.
BINARY_FLOAT and BINARY_DOUBLE take 5 and 9 bytes
of storage respectively, and hence are very efficient in the storage.
However, you have to be careful with the BINARY_DOUBLE,
and BINARY_FLOAT types, because if you go beyond, or
exceed the range, overflow and underflow does not raise any
exceptions. There are some predefined constants against which
you should check the results of your numerical computations
involving these datatypes, to check for error conditions. For
example, the BINARY_DOUBLE_NAN, and the
BINARY_FLOAT_NAN constant checks if the
BINARY_DOUBLE, or BINARY_FLOAT is not a number. The
BINARY_DOUBLE_INFINITY and
BINARY_FLOAT_INFINITY checks for conditions like
division by 0. Max and min constants define the absolute
maximum and minimum numbers you can represent using these
datatypes. Also, a word of caution is, that computations involving
these numbers can cause rounding errors, and for those monetary
applications involving dollars and cents, which require high
accuracy, they are probably not suitable. But they can be suitable
for a number of printing significant applications, or applications
where you are doing estimations of predictions, and where
decimal accuracy is not that crucial. So, for example, if you have
two BINARY_DOUBLE variables having values 2 and 1.8, their
multiplication will result in 3.599, and not 3.6. Another thing to
take care is the numeric precedence. So if an expression involves,
say, a NUMBER, and a BINARY_FLOAT, they will be
converted to BINARY_FLOAT. And if one of the variables in the
expression is BINARY_DOUBLE, the result is of type
BINARY_DOUBLE, which has the highest precedence.
SIMPLE_FLOAT and SIMPLE_DOUBLE are Not Null subtypes
of BINARY_FLOAT, and BINARY_DOUBLE respectively. In
this demo, I want to demonstrate to you the Type Promotion when
we have an expression involving BINARY_FLOAT and a
NUMBER, as well as to demonstrate the rounding errors which
occur using the BINARY_FLOAT datatype. Here, we have
defined three numbers, l_num1, l_num2, and l_num3, with
l_num1 having assigned a value of 0.51. We also defined a
BINARY_FLOAT variable l_bin_float, which is assigned a value
of 2f. Note that putting f is optional. Let me remove it. We are
first multiplying the NUMBER and BINARY_FLOAT variables,
so 0.51 with 2. You would have normally expected the result to
be 1.02, but since one of the variable involved was a
BINARY_FLOAT, then that expression will get promoted to
BINARY_FLOAT, resulting in a value of 1.01999. Next, we
multiply the same two variables, but restrict the conversion to
BINARY_FLOAT by casting the BINARY_FLOAT to a
NUMBER using the TO_NUMBER built-in function. So this time
the result should be 1.02. Finally, we are dividing the
BINARY_FLOAT variable with 0, and if it was a NUMBER here,
and not a BINARY_FLOAT variable, it would have raised a
divide by 0 error. Anything divided by 0 is infinity. In this case,
we can compare the built-in constant
BINARY_FLOAT_INFINITY with a result to confirm that, if
there is a conditional construct to compare two values. We are
going to cover it in more details later. I just wanted to demonstrate
why it is important to consider exceptional situations and
expressions involving BINARY_FLOAT and
BINARY_DOUBLE, and how to find them using predefined
constants. Let us run this block. So as we had expected, in the first
case the expression involving BINARY_FLOAT got promoted to
BINARY_FLOAT printing a value of 1.01999. In the second case,
preventing the conversion, and keeping the expression as a
NUMBER type, results in a value of 1.02. Finally, the one where
0 did not raise any error with BINARY_FLOAT, and using the
comparison operator, we were able to find the situation. So we
should be cognizant of the type PROMOTION, which can happen
with expressions involving BINARY_FLOAT and
BINARY_DOUBLE variables. And know that there can be
rounding errors too. Also, we demonstrated the use of predefined
constants to compare the computation results to discover
erroneous situations.
Character Datatypes
Character datatypes are used to store alphanumeric data, like
words and text in the table columns. In languages, like Java, or
C#, they are called strings. They help us store information, like
names, addresses, etc., in the database, as well as manipulate
character data. They're also used by the database to store table
names, and column names internally in the dictionary tables, as
well as to store the SQL and PL/SQL code in the database. Every
database is installed with a database character set to support the
languages which might be used against the database application,
say, English, or French, or Chinese. These are of type CHAR,
VARCHAR2, and CLOB. The UNICODE character set is a
universally encoded character set, which allows us to store
different language characters in the database using a single
character set, as each language character is given a unique
numeric code by the system. So if your application is a global
application, it makes sense to make a UNICODE character set as
the database character set to support globalization. Especially, if
you're writing a new system, it makes sense to use a UNICODE
character set as the database character set, even if your application
is not global today, but things can change tomorrow, and having
the ability to store characters from multiple languages will give
you a competitive advantage in the long run. There is also
something called a National character set, which allows you to
store in an alternate character set in the database in the UNICODE
format. The National character set are of type NCHAR,
NVARCHAR2, and NCLOB. They are most useful in situations
where the database supports, say, English character set, but you
want to store French characters in some columns. So you can
define these columns as types NCHAR, or NVARCHAR2, and
store French characters over there, while the rest of the columns
are of type CHAR, or VARCHAR2, and store the default database
character set of English. We will talk about the CHAR, and
VARCHAR2 datatypes, which are most commonly used. Let us
talk about the CHAR and NCHAR datatypes. CHAR is defined
as the keyword CHAR, followed by an optional size in either
characters or bytes. In the case of NCHAR, we can only specify
the size and characters. If you do not specify char or bytes, the
database interprets it as the setting of
NLS_LENGTH_SEMANTICS, in the nls_instance_parameters
table. So here, in this code snippet, SELECT * FROM
nls_instance_parameters for NLS_LENGTH_SEMANTICS
gives us a value of BYTES. The maximum size of CHAR, or
NCHAR datatype, is 32767 bytes. It is better to define the length
in terms of characters. That way, you tell the database explicitly
the number of characters you expect to store in the datatype. If
you specify it in bytes, you will have to be mindful of the multi
byte character sets where one character may be represented
internally, say, by 4 bytes. So the maximum number of characters
you will be able to store will be the FLOOR value of 32767
divided by 4, or 8191 characters. If you do not specify the size, it
defaults to 1. CHAR and NCHAR are fixed width character types.
So if, say you define a CHAR type with a length of 4, even though
the actual value assigned to it may be ab, or 2 characters, the
database will append 2 trailing spaces to store it as ab space space
with a length of 4. In Oracle, the character literals are specified in
single codes as opposed to double codes in languages like Java
and C#. The character type which you would be using most of the
time in your applications would be VARCHAR2, which again can
be specified using bytes, or characters. But unlike CHAR, the size
is not optional, and you'll need to specify it. NVARCHAR2 like
NCHAR can only accept the size and characters. These types
again have a maximum size of 32767 bytes. However, one thing
to be mindful is that for these types, the database table columns
can go to a maximum length of 4000 bytes, and so if your PL/SQL
code is interacting with the table, and storing and retrieving values
from it, then you should not exceed PL/SQL variable length to be
more than 4000 bytes. It is different from CHAR in that it has a
variable width, and it does not blank-pad the data to match the
length defined for the variable. So ab will be stored as ab with a
length of 2. That's just more efficient in terms of storage. Let us
talk about the comparison semantics involving character data. If
you compare a CHAR with a CHAR, it blank pads them before
comparison. So a CHAR with space variable assigned a value of
ab space space, would be equal to a CHAR available without
space, assigned a value of ab without space, when both have a
size of 4. However, when you compare a CHAR with
VARCHAR2, no blank padding happens for VARCHAR2, and
so VARCHAR2 ab is not the same as CHAR ab space space. Note
that equal to and not equal to are comparison operators in PL/SQL.
Equal to is not an assignment operator, unlike some other
languages, like Java and C#. So some general guidelines for using
these character types. Use CHAR only when you require ANSI
compatibility for comparison, and then trailing blanks don't
matter. VARCHAR2 is efficient in storage space. I find myself
using VARCHAR2 almost always in my applications.
Demo: Character
In this demo, we will see how to define a CHAR and a
VARCHAR2 variable, and explore the comparison semantics.
Here I have defined two CHAR variables of length 4,
l_char_with_space, and l_char_without_space.
L_char_with_space is assigned a value of ab space space.
L_char_without_space is assigned a value ab with no spaces. I
have defined a VARCHAR2 variable, l_varchar2_without_space
of length 4, and assigned it a value of ab with no spaces. IF is a
conditional construct in PL/SQL to compare two values, and you
use the equal operator to compare. Don't worry too much about it,
as we will cover it later in the course. In the first comparison
involving l_char_with_space, and l_char_without_space, since
Oracle blank pads CHAR variable to expand its width to the
declared length, l_char_with_space would compare equal to
l_char_without_space. In the second comparison of
l_char_with_space with l_varchar2_without space, since
VARCHAR2 is not blank-padded, they will not compare equal.
Notice the not equal operator for the comparison. Let us run the
block to confirm. As expected, l_char_with_space is blank-
padded, and is equal to l_char_without_space, and
l_varchar2_without_space is not blank-padded, and so is not
equal to l_char_with_space.
DateTime Datatypes
Let us now talk about the DateTime datatypes in Oracle. The first
one is the DATE datatype. The DATE type has several uses. It
allows you to keep track of the insert times of the database records,
for schedules, even dates, and a whole lot of other places where
we use dates in our applications. The DATE datatype has been
around a long time, and you will see it in a lot of existing
applications. It stores a day, month, year part of the date, as well
as the hour, minute, and seconds. And in this date format is the
default date format, which you can find for the instance from the
table nls_instance_parameters. We can DECLARE a DATE type
and assign it a character literal value, as we see in the code snippet
here, where we assign the character literal value of 10-nov-13 to
l_date. However, I like to use the TO_DATE built-in function in
the Oracle database to specify exactly what the day, month, year,
hour, minutes, and seconds are in my character literal. And so it
will always evaluate to a date as I want it, and will not error out
if the NLS_DATE_FORMAT for the instance changes. So in the
second line we say that convert the character literal of 10-NOV-
2013 15:25:34 where 10 is DD, NOV is MON, 2013 is RRRR, 15
is hour in the 24-hour format, HH24, 25 is minute, or MI, and 34
is seconds, or SS. This gives me the flexibility to specify it in the
way my DATE literal is, like in the second example, it is in the
MM/DD/RRRR format. Note that in these examples, NOV, or
DD, MON, RRRR, etc., are not case-sensitive, and can be
specified in upper, lower, or mixed cases. I also wanted to point
you to two built-in functions often used in PL/SQL programs to
find the current date and time. CURRENT_DATE returns the
session date, and SYSDATE returns the date as on the database
server, which might be different from the date and time for the
session. Now let us take a look at the TIMESTAMP datatype,
which was introduced in Oracle version 9.9. Along with storing
date, month, year, hour, minutes, and seconds, it also stores
fractional seconds up to a billionth range. Oftentimes when you
have to find a precise difference between two DateTimes, a DATE
datatype is not sufficient, as it cannot provide the granularity of
fractional seconds. TIMESTAMP datatype provides that. It is
specified as TIMESTAMP with an optional
fractional_seconds_precision, which can range from 0 to 9, and
defaults to 6 if not specified. So, for example, in the following
code snippet, we have defined l_tstmp variable as TIMESTAMP
with a precision of 3 fractional seconds. Just like for DATE,
Oracle provides a TO_TIMESTAMP built-in function as it allows
me to specify exactly what I am representing in my character
string, and is not affected by the NLS TIMESTAMP format
setting. Notice here, the additional FF, highlighted in red, which
indicates the fractional seconds. TIMESTAMP WITH
TIMEZONE contains the day, month, year, hour, minutes,
seconds, fractional seconds as the TIMESTAMP type, and then it
adds the TIMEZONE to the TIMESTAMP. It can be specified by
either the TIMEZONE offset, which is the difference in hours
between the local time, and UDC, or Coordinated Universal Time,
like -8 for the Pacific Standard Time, TZH being the abbreviation
for the hour offset, and TZM the abbreviation for the minute offset.
It can also be specified by the TIMEZONE region name. For
instance, America/Los_Angeles, which is the same as
TIMEZONE offset of -8. Another way of specifying it is with the
TIMEZONE abbreviation of PST that stands for Pacific Standard
Time. In the last example, the TIMEZONE reason of
America/Los_Angeles is abbreviated by the format element TZR.
This datatype also allows us to store the Daylight Savings Time.
For example, the Pacific Daylight Savings Time, or PDT, which
is abbreviated by the TZD format element. This helps prevent
ambiguity in time during the hours when the switch happens.
TIMESTAMP WITH TIMEZONE is defined as TIMESTAMP
fractional_seconds_precision WITH TIME ZONE. The precision
of fractional seconds is from 0 to 9, with a default value of 6. In
this code snippet, we are defining a TIMESTAMP datatype with
3 fractional_seconds_precision. We are again using the built-in
Oracle function TO_TIMESTAMP_TZ to convert the string
literal to TIMESTAMP WITH TIME ZONE datatype. Here, we
are defining 2nd November 2013 10 a.m. Pacific Standard Time,
Pacific DateTime, denoted by the format elements TZR and TZD.
I would also like to mention two built-in functions provided by
Oracle to get the TIMESTAMPs. CURRENT_TIMESTAMP
gives the session timestamp with time zone, and
SYSTIMESTAMP gives the database timestamp with time zone,
and you might find these handy when doing PL/SQL
programming.
Demo: DateTime Datatypes
Let me first show you how to find the session timestamp, and
database timestamp using the built-in functions, and how to alter
the timestamp of precision for the testing purposes. So let me
show the select, which uses the built-in function,
current_timestamp to get the session timestamp, and
systimestamp to get the database timestamp. As you can see, in
my case, they are both in the Pacific Time Zone, one output using
the time zone of PSD, and the other one using the offset of -8,
both of which are equivalent. Now I can issue the alter session
time zone to EST, to change my time zone to Eastern Standard
Time. (Typing) Now it's showing the same query that show that
my session time zone is now in the Eastern Time. So from the
Output, we can see that my session timestamp has changed to
Eastern Time Zone; however, the SYSTIMESTAMP, which is a
TIMESTAMP on the database server, is still in the Pacific Time
Zone. So let us see how we can declare the datetime variables. I
have declared l_date as a DATE datatype, and assigned it the
session date using the built-in function current_date. Next, I have
defined the l_systimestamp, which is of type timestamp with time
zone, and assigned it the database timestamp using the built-in
function, systimestamp. Next, I have defined
l_current_timestamp again as timestamp with time zone, and
assigned the session timestamp using current_timestamp function.
Lastly, I have defined a timestamp variable called l_timestamp,
and assigned it the session timestamp with the current_timestamp
function. Then, I have dbms_output.put_line statements in the
executable section of the block to display their values. Let us run
this block. (Typing) So l_date is displayed in DD-MON-RRRR
format. L_systimestamp shows the database timestamp. Notice it
is in the Pacific Zone as indicated by the offset of -8.
L_current_timestamp shows the session timestamp in Eastern
Zone. Finally, the l_timestamp variable shows the same session
timestamp, but it does not have the time zone information, as it
was just defined as a TIMESTAMP datatype.
Interval Datatype
Let us now talk about the interval type. As the name suggests,
they are used to express time intervals. The INTERVAL YEAR
TO MONTH indicates a time interval in years and months. It is
specified as the keyword INTERVAL YEAR year_precision TO
MONTH where year_precision can be from 0 to 9, with a default
value of 2. For example, in the code snippet, we have defined a
variable l_int of type INTERVAL YEAR TO MONTH, which
can take up to 3 years precision. I'm illustrating the various ways
of assigning literal values to it, so I can express it with
INTERVAL 123-2 YEAR TO MONTH format, where 123 stands
for years and 2 for months. I can just use the literal 123-2 to assign
a value to it. I can assign value just for the year as an INTERVAL
123 YEAR, and just for the month as an INTERVAL 12 MONTH.
INTERVAL DAY TO SECOND is used to express interval of
time in days and seconds. It is of use when we want to get time
differences between two timestamps, down to the fractional
seconds. The syntax for declaring a variable of this type is
INTERVAL DAY day_precision TO SECOND with
fractional_second_precision. The day precision ranges from 0 to
9, with a default of 2, and fractional second range is from 0 to 9
with a default of 6. Here is a code snippet where we have declared
a variable l_int, which is of type INTERVAL DAY TO SECOND
with a day precision of 2, and a second precision of 2. We can
assign value to it in the form of a literal, such as 10 day, 1 hour,
10 minutes, 11 seconds, and 12 milliseconds. We can also express
the same by assigning INTERVAL 10 day, 1 hour, 10 minutes,
11.12 DAY TO SECOND literal to it. However, note that in
PL/SQL you cannot specify DAY or SECOND precision in a
DATE lateral, when assigning it to a DAY TO SECOND variable.
You can assign just an INTERVAL of 10 DAYs, or 12 SECONDs
to the variable. INTERVAL DAY TO SECOND is also extremely
useful in finding time differences between timestamps down to
fractional seconds, which might be required for some applications.
Here, we have defined two timestamps WITH TIME ZONE
datatype, l_tsmp_begin, which is assigned a value of 2nd
November 2013 10 a.m. Pacific Standard Time with Daylight
Savings. L-tsmp_end is assigned a value of 3rd November 2013
10 a.m. Pacific Standard Time. Daylight Savings Time ended on
November 3rd. Then we define a variable, l_int of type
INTERVAL DAY TO SECOND to hold the difference of the two
TIMESTAMPs. You will notice that the interval reported would
be 2 days and 1 hour, and not just 2 days, as it is also accounting
for the Daylight Savings Time. I took these days in this example
just to make the point that how TIMESTAMP WITH TIME
ZONE datatypes, and INTERVAL datatypes take into account the
Daylight Savings Time, and are very accurate for applications
dealing with schedules, time differences, etc. In this example, I
want to illustrate to you how the TIMESTAMP WITH TIME
ZONE datatype can be useful in calendar, or scheduling
applications, and can help avoid the ambiguities with the time
changes for the Daylight Savings. I define a TIMESTAMP
variable l_tsmp, and the TIMESTAMP WITH TIME ZONE
variable l_tsmp_tz. I also define l_tsmp_new of type
TIMESTAMP, and l_tsmp_tz_new of type TIMESTAMP WITH
TIME ZONE to hold the new values. I have defined an
INTERVAL DAY TO SECOND variable with the precision of 2
for day and second, and assigned it a value of 7 days. I could have
alternately redone it simply by using INTERVAL 7 DAY. I have
purposely chosen the date as 2nd November 2013 as the Daylight
Savings Time ended in the USA on November 3rd. I have
assigned a starting date of 2nd November 2013 10 a.m. Pacific
Time to both the variables, and then I add 7 days interval to both
the times. After that, I'm printing the new values onto the screen.
Let me run this block. (Typing) Notice, the new time with the
TIMESTAMP datatype is 9th November 2013 10 a.m.; however,
the TIMESTAMP WITH TIME ZONE datatype variables value
is 9th November 2013 9 a.m., accounting for the adjustment for
the end of Daylight Savings. To demonstrate how INTERVAL
type accounts for time changes, let me define TO-TIMESTAMP
with TIMESTAMP variables, l_tsmp_begin, and l_tsmp_end.
L_tsmp_begin has been assigned a value of 2nd November 2013
10 a.m. Pacific Time, and l_tsmp_end has been assigned a value
of 4th November 2013 10 a.m. Pacific. I have defined l_int_ds as
DAY TO SECOND INTERVAL datatype with a precision of 2
for days and fractional seconds. Just to show you how to define
INTERVAL YEAR TO MONTH variable, I have defined
l_int_ym as INTERVAL YEAR TO MONTH type, with a 3 year
precision. I can take the difference of the TO_TIME variables,
and assign it to INTERVAL DAY TO SECOND variable as I
have done in the execution section, assigning l_int_ds, the
difference of the TO_TIME variables. Let us run this block.
(Typing) Notice from the Console Output that the difference in
the time between the two variables is not just 2 days, but 2 days
and 1 hour to account for the Daylight Saving changes. Here, we
saw how to use INTERVAL datatype for getting fractional second
time differences, and also illustrated that the interval datatype
accounts for the Daylight Saving changes.
Time Datatype Usage Guidelines
Some general guidance on choosing the DateTime datatypes. Use
timestamp as it offers you more precision in fractional seconds,
and especially if your application requires such time precision. If
not, date should be fine. Timestamp with time zone is particularly
suitable for global applications, or applications dealing with
schedulers, calendars, etc., as it also retains the time zone
information. Another thing to keep in mind is, generally a
PL/SQL code will be fetching and inserting data into tables, and
it's generally a good idea to match the column DateTime datatype
with a PL/SQL variable type to avoid unnecessary conversions.
Boolean Datatype
BOOLEAN datatype is a PL/SQL only datatype. And it supports
the values TRUE, FALSE, and NULL. PL/SQL does not allow
you to use 1 and 0 for TRUE and FALSE, which some of the
languages allow. Note, TRUE, FALSE, and NULL are not case
sensitive. Here is a code snippet declaring a BOOLEAN variable,
l_boolean, and assigning it a value of TRUE. This datatype is
often useful in implementing conditional logic in PL/SQL.
Composite Datatype: Records
Oracle offers more advanced datatypes, like the composite
datatypes, which help you group related variables together, or
form a collection, or array. It offers records, which allow
grouping variables together. It also allows collection types, which
are of type nested tables, varrays, and associative arrays. In this
course, we are going to cover only records. The collection
datatypes will be covered in a later course. A record is a composite
datatype which allows us to group related variables together. It is
conceptually like a row in a table with various fields, which can
be accessed by the dot notation. It is useful in situations where
you want to combine a few variables which go logically together,
like say, a customer record, where you have customer name,
customer address, customer phone, and other customer related
information logically grouped together. The syntax for defining a
record is the keyword TYPE, followed by the record name,
followed by one or more field declarations. Each field declaration
is like how we define variables with a Field_Identifier, followed
by a datatype, and optional attribute, like NOT NULL, and
assignment of values using the := to operator, or the DEFAULT
keyword. Let us take a look at a code snippet to understand
records better. Here is our familiar table departments containing
dept_id, and dept_name. In the second code snippet, we are
defining an employee record as type emp_rec, which contains
three fields, employee name of type VARCHAR2(60), dept_id,
which is a number. It is deriving its datatype using the %TYPE
syntax from the dept_id column of the departments table. It
contains the location column of type VARCHAR2 with a
DEFAULT value of CA, or California. So it is useful in grouping
together related variables, and accessing them as one. From this
point on, it is like another datatype, which, however, can be used
only within the block defining it. Note, you cannot define records
at the database level, but only in PL/SQL. Next, we define the
l_emprec of type emp_rec. Inside the execution section, I set the
values to the inner fields, like the emp_name, and dept_id, using
the dot notation. And I can get to those values again using the dot
notation as in the dbms_output.put_line statements. Note of the
_____ and the RECORD is not a sign of value. It follows the same
rule as its base type for initialization, and is generally NULL. But
the l_emprec itself is not null, and accessing it where inner fields
are not assigned values, will not raise any errors. For this demo,
we will be using the departments table, departments having two
columns, dept_id and dept_name. Let's first create this table.
(Typing) Now let us create a record, emp record, using type
emp_rec IS RECORD syntax, and define three fields, emp_name,
a VARCHAR2 field, dept_id, a field based on
departments, .dept_id%TYPE, and loc field, which is a
VARCHAR2 field with a DEFAULT location of CA, or
California. From this point on, the anonymous block can use this
as a valid type for variable declaration. So now we define
l_emprec of type emp_rec. We can set values to the fields using
the dot notation, l_emprec.emp_name. It's assigned a value of
John, and l_emprec.dept_id is assigned a value of 10. We can get
the values again using the dot notation, as shown in the
DBMS_OUTPUT.PUT_LINE calls. Let us run this. (Typing) So
the employee name and location values could be obtained using
the dot notation.
Composite Datatype: %ROWTYPE
%ROWTYPE allows us to define a record based on the database
table, or a view, or even a cursor, which we will see later in this
course. So, in this code snippet, I have defined l_dept_rec as
departments%ROWTYPE using the same departments table we
saw earlier. This will implicitly define the record with the fields
as table column names with their respective datatypes. So this
record will have two fields, dept_id, and dept_name. Again, you
can assign values using the dot notation as we have done in
l_dept_rec.dept_id, and assigning it a value of 10. And you can
get to the values using the dot notation as we have done in the
DBMS_OUTPUT.PUT_LINE statement. Here, I want to show
you the usage of %ROWTYPE to define the record based on an
_____ table. In this case, I define the record l_dept_rec based on
the departments table using the departments%ROWTYPE. I set
the dept_id using the dot notation on the l_dept_rec as
l_dept_rec.dept_id, and set it to a value of 10. I similarly access it
in the DBMS_OUTPUT.PUT_LINE statement. Let us run this
block. So we did successfully set and get the value of 10 for the
department ID. Finally, I wanted to show you that it is possible to
nest a record within another record. Here, I have nested a record,
deptrec, based on departments%ROWTYPE inside the employee
record, employee_rec. Then I define the local variable,
l_employee_rec of type employee_rec. Notice, I can access the
department record by first using l_employee_rec.deptrec notation,
and then I can further access dept_id using another dot, and
dept_id. I can similarly get its value and show it in the
DBMS_OUTPUT. Let us run this block to verify. So I can set and
get the employee department.
Other Datatypes
Oracle database offers a rich set of datatypes, and it is not possible
to cover all of them in this course. I just, however, wanted to
briefly mention some of the other datatypes. Often we have data
in the form of images, files, etc., and Oracle large object datatypes
provide support for them. CLOB, or Character Based LOBs are
used for text files. Binary LOBs, or BLOBs are used for binary
files and images, audio, video, etc. BFILE also stores binary files,
but outside the database, and it is a link, or locator to those files.
Then there are collection types, like VARRAYS, where we define
the length of the collection during its declaration. NESTED
TABLES allows us to extend the length of the collection
programmatically. ASSOCIATIVE ARRAYS is another form of
PL/SQL collection, which is indexed. Then, there are datatypes
like ROWID, UROWID. ROWIDs are addresses of rows in the
database. It can be a physical ROWID, which pertains to a
physical ROW in an ordinary table. Or, logical ROWID, which
pertains to rows in index organized tables. A variable of type
ROWID can only store physical ROWIDs, while a variable of
type UROWID can hold both physical, and logical ROWIDs.
Summary
Oracle database offers a rich set of datatypes in PL/SQL to take
care of varying needs of applications built against it. The scalar
datatypes, like number, character, and datetime datatypes are
most commonly used in applications. The Boolean datatype is
useful for conditional evaluations. Records are composite
datatypes, which should be used for logically grouping related
information. Oracle also makes it easy to create records based on
an underlying table, view, or a cursor, using the %ROWTYPE,
which makes it easy to work against them. Oracle offers a rich
variety of datatypes; however, we must choose the right type for
the application in question in order to achieve maximum
efficiency.
Loops
Overview
Hi, and welcome to Pluralsight. My name is Pankaj Jain, and in
this module we will talk about Loops. The ability to loop through,
or to drape through procedural logic, or a body of code, is very
important in any programming language. It is useful for a variety
of use cases, say, performing numerical computations in a loop,
executing procedural logic involving SQL statements, using loop
counters, or it's reading over data fetched from database tables.
Now let's take a look at this very important programming
construct. In PL/SQL you can define loops in various ways. It can
be a simple loop, which begins with a loop statement, and ends
with an end loop statement with a certain exit condition. You can
define a FOR loop, which iterates for a defined number of loop
counter variable, and exits automatically when the counter has
reached when the counter has reached its last value. You can also
define a cursor for loop, which iterates over rows fetched from a
cursor. We will talk about the cursor for loops later when we talk
about cursors. Oracle gives the ability to define a WHILE loop,
which keeps looping while a certain condition is TRUE, and ends
when the condition evaluates to FALSE. The WHILE loops are
useful in situations when the exit condition is not based on a
simple counter, but is based on the evaluation of a condition.
Oracle also has bulk collection and update features that loops
when you are fetching data from the database tables, or inserting,
updating, or deleting data in them. These bulk capabilities
increase the performance of the code. We will, however, first
build our understanding of the basic looping mechanisms before
we can go to these more advanced topics in a later course. So, as
you can see, there are multiple ways to loop through, and while it
might be possible to use more than one type of loop for a given
situation, oftentimes there is a certain type of loop which is more
appropriate for what you are trying to do. We will talk about the
general usage guidelines for each loop as we go along. While
knowing how to get the loop started is important, it is equally
important to know how to get out of it. If not, it is going to run as
an infinite loop, possibly crashing the system. Loops like the FOR
loop have a built-in, or implicit exit condition, which is, when we
reach the end of the loop counter. A FOR loop defined for loop
counter values from 1 to 4, will automatically end after it reaches
the value of 4. So a FOR loop has the advantage of having an
identified number of iterations with automatic termination, and so
with it, the risk of running in an infinite loop situation is less. A
WHILE loop will exit when the WHILE condition evaluates to
FALSE. We, however, need to define the WHILE condition
properly. Simple loops do not have any such implicit exit
conditions defined for them, and we have to explicitly exit them.
We can do that using the EXIT keyword, or defining an EXIT
WHEN condition, or using a RETURN keyword, or using a
GOTO statement. A GOTO keyword transfers control to the label
named, and can be used to transfer control outside the loop, thus
terminating the loop. Generally GOTO should be avoided in
coding as it leads to spaghetti code, which is difficult to
understand and maintain. We can also use these explicit ways to
terminate loops for the FOR and WHILE loops also if we choose
to, but generally the FOR loop counter for the FOR loops, and
WHILE condition for WHILE loops are used to terminate these
kind of loops. Another way a loop can end is if an exception
occurs while processing a statement inside a loop. The exception
will move the execution flow to the exception block, which, if
placed outside the loop, will terminate the loop. This is in most
cases not a planned strategy to end the loop, but just happens in
case of an erroneous situation, and you should account for it while
writing your code. So, there are different ways a loop can
terminate, and we'll talk more about them, and see them in demos.
Simple Loops
A simple loop has a simple syntax that starts with the LOOP
keyword, and ends with END LOOP keyword, and a semi-colon.
Inside these two keywords you put your statements, which need
to be executed for each iteration. As I had mentioned earlier, you
have to explicitly write an exit condition for these kind of loops.
So in this code snippet, we see how to define a simple loop. We
have declared two variables, l_counter, which will serve as a loop
counter, and is initialized to a 0. L_sum to hold the running total
of the counter value also initialized to 0. The loop begins with a
LOOP keyword. Inside the LOOP, we sum up l_sum and
l_counter, and assign the result to l_sum to hold a new value. We
then increment the loop counter. We explicitly exit the loop, when
the value of the loop counter is greater than 3. So we are using the
EXIT WHEN clause to exit the loop here. The previous step of
increasing the loop counter value is important. Otherwise,
l_counter will never be greater than 3, and the loop will run
infinitely. So the loop starts iterating, making the counter value 1
at the end of iteration 1, 2 at the end of iteration 2, 3 at the end of
iteration 3. And then in the 4th iteration it becomes 4, the EXIT
WHEN condition evaluates to TRUE, and the loop terminates. Let
us take a look at the same example to understand the different
ways we could have explicitly ended the loop. We saw the EXIT
WHEN condition in the previous example to end the simple loop.
We could have also ended the loop by just using the EXIT
keyword. But this would not make sense as it will exit the loop in
the very first iteration, as soon as it hits the EXIT keyword.
Typically, you are surrounded with a conditional clause, like IF.
Again, we will look at conditional constructs later, but for now, it
says that if the l_counter becomes more than 3, THEN do an EXIT.
Kind of like saying, EXIT when l_counter is greater than 3, as we
saw in the last slide. RETURN works the same way as EXIT. So
we could have written the block in the previous example using the
RETURN keyword instead of EXIT. And again, typically we will
surround it with an IF clause to terminate the loop based on a
certain condition. GOTO can be used to transfer control
unconditionally outside a loop to a label, thus terminating the loop.
A label can be used to label a block, or label a loop, or be placed
in the code, such that there is at least one line of executable code
after it. There can even be the null keyword. As in this example,
we have the label out_of_loop placed before the null keyword. If
the null keyword was not there, or, another executable line of code
was not there, it would have given an error. So here again, GOTO,
like the RETURN and EXIT keywords, would have transferred
control in the first iteration with a label out_of_loop ending the
loop. Typically, we will surround it with an IF clause, as shown
in the example here. When the counter value becomes greater than
3, the GOTO statement transfers control to the out_of_loop label
terminating the loop. While we are talking about GOTO, let us see
how it can be used. With a GOTO statement, you can branch to a
section of code below, or above the GOTO statement. Or, you can
jump to an outer block. However, some general restrictions in the
usage of GOTO statements are that it cannot be used to branch to
a conditional clause, like the IF and the ELSE clause, or go from
an IF section to an ELSE section in the conditional clause. It
cannot transfer control to a nested block. It cannot branch into a
loop. It cannot branch from exception handler section to the
current block. But it can go to the outer block. The GOTO
keyword sometimes can simplify the logic, but in general, should
be sparingly used as it leads to unstructured, and complex code,
or spaghetti code affecting its readability and maintainability.
PL/SQL provides other mechanisms, which should suffice your
needs without necessitating you to use GOTO statement.
Demo: Simple Loops
In this demo, we will see how to execute a simple loop, and how
to terminate it. Here, we have defined two variables, l_counter as
a loop counter, initialize it to 0, and l_sum to keep a running total
of the loop counter. We begin the loop with the LOOP keyword.
Then we increment the l_sum, adding l_counter to it. Then we
increment l_counter by 1. Then we check if the value of l_sum is
greater than 2. If it is, then we use the GOTO statement to EXIT
the loop, and GOTO the out_of_loop label outside of the loop.
Notice a null statement outside the label. This is to satisfy the
condition that there has to be at least one executable statement
after the label. The loop ends with the END LOOP statement. So
in this loop, GOTO is used as a termination mechanism. Let us
run this. (Typing) From the Console Output, we notice that the
running total l_sum prints a value of 0 in the first iteration when
the l_counter is 0. In the next iteration, l_counter becomes 1.
Adding it to l_sum makes l_sum 1. In the third iteration, l_counter
becomes 2. Adding it to l_sum, which has an existing value of 1,
makes l_sum's value of 3. The IF condition of l_sum is greater
than 2 becomes TRUE, terminating the loop. Instead of the GOTO
label, we could have used the exit statement to terminate the loop.
(Typing) Let us run it again. We see the same results. We can also
use the return statement to terminate the loop. (Typing) Let us run
it again. And the same results again.
FOR Loops
The FOR loop is a very commonly used looping mechanism. Its
syntax says an optional label name for the loop, followed by the
FOR keyword, followed by loop_counter IN. REVERSE is an
optional keyword, which you can use if you want the loop to go
in reverse loop counter values. Then you specify the lower_bound,
and the upper_bound for the loop_counter, separated by two dots.
Then you have the LOOP keyword to start the loop. Then you
write the group_of_statements, which have to be looped through.
You end the loop with the END LOOP keyword. You can
optionally put the label name at the end. You define the beginning
value, and ending value for the loop_counter, which go in steps
of 1. The loop_counter is declared implicitly as PLS_INTEGER
by Oracle, and you need not declare it. Oracle also automatically
increments, or decrements it after every iteration. Also, after the
last value is reached, Oracle will automatically exit the loop so
you do not have to do it. So it is a much simpler, and cleaner
looping mechanism, and I prefer to use it if I know the number of
iterations I have to make. Since Oracle takes care of incrementing
the counter, and exiting the loop, it makes it so much more error
free. Here in the code snippet, I have declared a local variable,
l_sum, to keep a running total. Then in the execution section, I
have declared a FOR loop with a loop_counter, l_counter, which
has a lower amount of 1, and an upper amount of 3. Inside, I
maintain a running total of loop counter, and then the loop is
ended with the END LOOP keyword. So this loop will iterate
three times. After iteration 1, loop counter will have a value of 1,
after iteration 2, a value of 2, and after iteration 3, a value of 3,
and then Oracle will automatically terminate the loop. We can
also have a reverse FOR loop by using the REVERSE keyword.
So in this same example which we saw in the earlier slide, if we
use the REVERSE keyword, then after interation 1, the loop
counter will have a value of 3, after interation 2 it will be 2, and
after interation 3 it will be 1.
Demo: FOR Loop
Let us see how to execute a simple FOR loop. Here, we have a
FOR loop that starts with the FOR keyword. L_counter is declared
implicitly as PLS_INTEGER and assigned values 1, 2, and 3
respectively after each iteration. We will print these values inside
the loop. The END LOOP statement ends the loop. Let's run this.
From the Output Console, we notice the values are 1, 2, and 3 for
the loop counter, after which Oracle automatically terminates the
loop. Let us use the REVERSE keyword to run this loop in reverse.
(Typing) Let's run this. From the Output Console, we notice now
the loop is running in reverse, printing the values of 3, 2, and 1.
FOR Loop Counter
The FOR loop counter is implicitly declared as PLS_INTEGER
by Oracle, and so you should not assign values to it which will go
beyond the PLS_INTEGER range. Otherwise, you will get a
numeric Oracle exception. You cannot use the counter variable as
an assignment target. If you do so, you will get an error. The loop
counter variable has a scope only within the loop, and referencing
it outside will give you an error. In this example, if we reference
l_counter outside of the loop, we will get an error. In the block, if
there is a variable with the same name as the loop counter inside
the loop, the loop counter will overwrite the visibility of that
variable. However, if we need to access the block variable inside
the loop, we can do so using labels. So you will label the block,
say outer, and then inside the loop, you can access it as
outer.l_counter. What if the upper and lower bound values of the
loop counter are the same? In that case, the loop will just iterate
once for that value. And what if the upper bound is lower than the
lower bound? In that case, the loop will not iterate at all. The loop
counter bounds can be number literals as we have been seeing in
examples before, and as we see in this code snippet. And the lower
bound need not start with 1. In this code snippet, we started with
2. The loop counter bounds can also be NUMBER variables. For
example, here we have declared two variables, l_lower, and
l_upper, which we use as bounds for the loop. The loop counter
can also be the result of a number expression, as in this code
snippet. The upper bound is l_upper divided by l_lower, which is
5 divided by 2, which is rounded off to a value 3. We have seen
how to use number literals as loop counter bounds. We can also
use local variables to define the lower and upper bound. In this
example, we have declared l_lower of type NUMBER, and
assigned it a value of 2. We have declared l_upper and assigned it
a value of 5. In the FOR loop, we used l_lower variable as the
lower bound, and the upper bound is evaluated from the
expression l_upper divided by l_lower, which is 2.5, and rounded
to 3 as l_counter is defined as PLS_INTEGER, which does not
accept decimals. Let's run this. (Typing) From the Output Console,
we see values of 2 and 3. So along with number literals,
NUMBER variables, as well as expressions can also be used to
define lower and upper bounds for the FOR loops. As I had
mentioned, the loop counter is incremented, or decremented in
steps of 1, unlike some other languages where you can move in
steps other than 1, say 2, or 3. If you have to achieve steps in
PL/SQL loops, you can simply do this by multiplying the counter
with the required step. So in this example, we have declared
l_step_counter, which is l_counter multiplied by 2. So it will have
a value of 2, 4, and 6 after the _____ 3 iterations. Let us see how
we can simulate steps with the FOR loop. Here, we have defined
l_step_counter of type NUMBER. Then we have a FOR loop, and
a loop counter, l_counter, iterating from 1 to 3. Within the loop
we achieved the steps by multiplying l_counter with 2, and
assigning it to l_step_counter giving it values of 2, 4, and 6. We
then print l_step_counter. Let's run this block. As expected, we
see values of 2, 4, and 6 from the Console Output. The loop
counter bound can also be dynamically defined. Here is our
familiar departments table. Inside the block we are fetching the
count of rows for the departments table in the l_dept_count
variable. We will talk about fetching data from the tables later in
the course when we talk about cursors, so don't worry too much
about the syntax for now. In the loop, I'm using the l_dept_count
as the upper bound. Loop bounds can also be dynamically defined,
say based on the table record count. We define the table
departments with two columns, dept_id, and dept_name. Then we
insert a couple of rows. Let's run this. (Typing) Next, we have an
anonymous block where we have declared a variable
l_dept_count of that NUMBER. In the execution section, we fetch
the count from the departments table into this variable. Now, our
FOR loop counter uses the l_dept_count as its upper bound, and
prints its value. Let's run this example. (Typing) From the Output
Console, we notice that l_counter gets a value of 1, and 2, and
stops there, as that is the count of departments in the departments
table.
Continue Statement
The CONTINUE statement dominates the current iteration, and
transfers control to the next iteration. So, in this example, the
CONTINUE statement will terminate the iteration at the point it
is called, and go to the next iteration of the loop. You would most
probably like to surround it with a conditional IF statement. So
here, for the third iteration, when the l_counter is 3, the iteration
is terminated, and the DBMS_OUTPUT.PUT_LINE is not called
for the l_counter value of 3. So we will get 1, 2, 4, and 5 in the
DBMS_OUTPUT. In this demo, we have a FOR loop with a lower
amount of 1, and an upper amount of 4 inside the loop, and the
loop counter becomes 3. We issue the CONTINUE statement,
which will terminate iteration 3 right there without printing the
l_counter value and go on to the 4th iteration. Let's run this.
(Typing) As expected, we see that only 1, 2, and 4, are printed for
l_counter, and the value of 3 is not printed because of the
CONTINUE statement.
Nesting of Loops
You can nest a loop within another loop. I'm not aware of any
limit on nesting, but typically you do not want to go beyond one
or two levels deep to prevent the code from becoming unduly
complex, and unreadable. Nesting is useful in situations where,
say, you have an outer loop which fetches records from the
customer's table, and then you want to have an inner loop, which
takes each customer record and does something with it. Here in
the code example, we have an outer loop, which runs from 1 to 2,
and for each iteration, the inner loop runs from 3 to 4. Thus we
have the output as shown below. For outer loop counter value of
1, the inner loop counter runs giving values 3 and 4, and similarly,
for outer loop counter value of 2, the inner loop iterates with
values 3 and 4. We can label the loops. Labeling helps us identify
the loop, and it is especially useful when we have nested loops.
We specify a loop label name by enclosing it within double angle
brackets at the beginning of the loop. As in this example, we have
label level loop as myloop. Optionally, we can put the label name
at the end of the loop, without the angle brackets, as we have done
here. Having it at the end is optional, but is very useful with nested
loops to identify which loop has ended. So in this example, we
have labeled our outer loop as outer, and the inner loop as inner.
Notice how easy it becomes to identify the beginning and ending
of the loop with the labels.
Exit When & Continue When
In the case of the nested loops, the labels can also help us name
the loop we want to exit. For example, in the code snippet we have
an outer loop, and an inner loop. The outer loop counter is from 1
to 3, and the inner one is from 1 to 5. The outer loop runs with the
l_outer_counter value of 1. We'll print the value using
DBMS_OUTPUT. The inner loop then iterates running for values
1, and then 2. When it reaches the value of 3, the statement EXIT
outer WHEN l_inner_counter equal to 3, evaluates to TRUE, and
then it exits the inner loop, as well as the outer loop. So the EXIT
statement with the loop label exits after the loop named in the
label, exiting all the inner loops along with it. In this example, we
have two loops, one nested within the other. We have the outer
loop, which we have labeled outer, and we have an inner loop,
which we have labeled as inner. The outer loop iterates from 1 to
3, and then it prints the value of the outer counter. Then the inner
loop starts, and it iterates from 1 to 5. Inside the inner loop, when
l_inner_counter becomes 3, we are exiting the outer loop. This
would in effect also terminate the inner loop at this point. Let us
run this. (Typing) The outer loop runs with the l_outer_counter
value of 1, and printing it with DBMS_OUTPUT.PUT_LINE
statement in the Console. The inner loop then iterates, running for
values 1, and then 2. When it reaches the value of 3, the statement
EXIT outer WHEN l_inner_counter equal to 3 evaluates to TRUE,
and exits the inner loop, as well as the outer loop. So the EXIT
statement with the loop label ends the loop named in the label,
exiting all the inner loops also along with it. CONTINUE WHEN
statement when used in nested loops with labels will terminate the
current iteration of the label loop and go to the next iteration
terminating all the inner loops up to the label loop. So in this code
snippet, we have an outer loop which iterates from 1 to 3. We
printed the l_outer_counter in the outer loop. Then we have an
inner loop labeled inner, which again iterates from 1 to 3. In that
loop, we are printing the inner counter, but CONTINUE to outer
WHEN the inner counter value is 2. So, as with the output shown
in the lower window, the l_outer_counter starts iteration with a
value of 1, and prints that value. Then the inner loop starts
printing the value of 1 for l_inner_counter. In the second iteration,
when l_inner_counter becomes 2, the CONTINUE outer WHEN
l_inner_counter equal to 2 evaluates to TRUE, and the inner loop
is terminated at that point, the current iteration of outer loop is
also terminated at that point, and the outer loop starts at the next
value of 2, and this process continues. As a result of CONTINUE
WHEN statement, the last DBMS_OUTPUT.PUT_LINE
statement is never reached. In this example, we have two loops,
one nested within the other. We have the outer loop, which we
have labeled outer, and we have an inner loop, which we have
labeled as inner. The outer loop iterates from 1 to 3, and the inner
loop also from 1 to 3. Inside the inner loop, when l_inner_counter
becomes 2, we continue the outer loop. This would in effect also
terminate the inner loop at this point, terminate the current
iteration of the outer loop, and it will go to the next iteration of
the outer loop. Let us run this. (Typing) So, as the Output shows,
the l_outer_counter starts iteration with a value of 1, and prints
the value. Then the inner loop starts printing the value of 1 for
l_inner_counter. In the second iteration, when l_inner_counter
becomes 2, the CONTINUE outer WHEN l_inner_counter equal
to 2 evaluates for TRUE, and the inner loop is terminated at this
point, the current iteration of the outer loop is also terminated at
this point, and the outer loop starts at the next value of 2, and this
process continues. As a result of CONTINUE, the last
DBMS_OUTPUT.PUT_LINE statement is never reached.
While Loop
The WHILE loop is useful in situations where you're not exactly
sure of the number of iterations, and the number of iterations is
dependent on the evaluation of a condition. It starts with the
WHILE keyword, followed by the condition to be evaluated. If
the condition is TRUE, then the loop starts with a LOOP keyword.
Inside are the statements to be executed in the loop. The loop ends
with the END LOOP statement. After the statement and the loop
are over, the condition is again evaluated to see if the loop should
continue. If it evaluates to FALSE, the loop terminates. Typically,
inside the loop, you will set a variable, which is also a part of the
evaluation condition. It may so happen that the very first
evaluation of the WHILE condition is FALSE, resulting in the
loop not being evaluated at all. So, in this code snippet, we have
declared an INTEGER variable l_check, and given it an initial
value of 1. Now the WHILE loop starts with the evaluation of
condition l_check less than 5, which is TRUE, since we have set
it to 1. Inside, we reassign l_check with a random integer between
1 and 10. DBMS_RANDOM.VALUE is a built-in procedure
provided by Oracle for generating random values. Here, we are
passing it two arguments, who tell it the range within which it
should generate random values. After that, we print the l_check
variable, reach the end of the loop, again go back to the start to
see if l_check assigned is still less than 5. If it is, the loop
continues, otherwise, we exit the loop. In this demo we have
declared an INTEGER variable, l_check, and given it an initial
value of 1. Now the WHILE loop starts with the evaluation of the
condition l_check less than 5, which is TRUE, since we have set
it to 1. Inside, we reassign l_check with a random integer between
1 and 10 using DBMS_RANDOM.VALUE procedure provided
by Oracle for generating random values. After that, we print the
l_check variable, reach the end of the loop, again go back to the
start to see if l_check value assigned is still less than 5. If it is, the
loop continues. Otherwise, we exit the loop. Let us run this. As
we can see that this loop runs until the value of l_check remains
less than 5.
Summary
Having the ability to loop through lines of code is very important
in any programming language. It has several use cases, like
numerical computations, or performing SQL operation for every
record in an integrated fashion, etc. Oracle provides a robust set
of looping constructs, like the simple loop, FOR loop, and
WHILE loop. Simple loops are useful in situations where you
want to have a fine control of when to exit. FOR loops are useful
in situations where we know the number of iterations. FOR loops
provide a lot of convenience in terms of implicitly declaring the
loop counter, incrementing it, and exiting the loop, thus saving
you work, as well as making the code less prone to errors. Also,
if there are no DML statements inside the FOR loop, Oracle can
optimize the FOR loop for you. WHILE loops should be used
when you are not sure of the number of iterations, and you want
to exit based on the evaluation of a condition.
Conditional Execution
Overview
Hi, and welcome to Pluralsight. My name is Pankaj Jain. In this
module, we will talk about Conditional Execution. I played a lot
of chess growing up as a kid. For those of you who have played
chess, you would agree that it is a fun game, but, at the same time,
not the easiest as you have to think hard, strategize, evaluate
several options before you make your next move. Evaluating
options is not just in the game of chess. We do it in our day to day
lives while making personal and business decisions. Oracle
PL/SQL provides a very robust support for conditional evaluation
in PL/SQL, and makes it simple for us by easy to understand
constructs. Let us take a closer look. There are two kinds of
conditional statements in Oracle. The IF statement. We have seen
it a few times in the previous modules, though we did not talk
much in detail then. The other type is the CASE statement and
expressions, which can sometimes make the code more compact,
and readable.
If Elseif Else Statements & Nesting
The IF statement starts with the IF keyword, followed by a
condition, or a group of conditions. If the conditions evaluate to
TRUE, then the group of statements inside the IF clause are
executed. The IF statement ends with the END IF statement,
followed by a semi-colon. So here, in the code snippet, we have
declared two variables, l_sales_amt which stores the yearly sales
made by a sales rep. We have assigned it a value of 40,000.
L_commission is the commission to be given to the sales rep,
which is initialized to a value of 0. Here is an example of a simple
IF statement, where we are evaluating a condition that if the sales
amount for the year is greater than 50,000, then give the sales rep
a commission of 10%. Since the sales amount is less than 50,000,
the statement inside the IF clause will not get executed, and the
commission will not get assigned a 10% value. In some languages,
like Java and C#, you have to surround the condition within
brackets, but that is optional in PL/SQL. The IF statement
condition can be more complex than the one we saw earlier. Here,
we should use brackets to logically group our set of conditions,
which also helps with the readability of the code. So here, in the
code snippet, we have declared three variables, l_sales_amt which
stores the yearly sales made by a sales rep. As before, we have
assigned it a value of 40,000. L_no_of_orders stores the number
of orders the sales rep brought in. We have assigned it a value of
120. Again, l_commission is the commission to be given to the
sales rep. Here we have two logical groups in our condition, which
we have surrounded by brackets. The first logical group is
l_sales_amt greater than 50,000 AND number of orders greater
than 50. The second logical group of condition is sales amount is
less than 50,000, and the number of orders greater than 100. These
two are separated by an OR. So, if either of these is TRUE, the IF
statement evaluates to be TRUE. Since our order amount is
40,000, the first condition fails. The second set of conditions
passes, as our sales amount is less than 50,000, and the order
amount of 120 is greater than 100. Thus, the IF statement
evaluates to TRUE, granting the control for the statement inside
the IF statement where we assign a 10% commission. The second
form of the IF statement involves the ELSE clause, followed by a
group of statements. Note that there is no THEN after the ELSE
keyword. If the condition in the IF statement evaluates to FALSE,
the control passes to the ELSE part. So in this code snippet, we
again have the same sales commission example with the three
variables, sales amount, number of orders, and the commission
given. Here we are seeing that if the sales amount is less than
50,000, then assign l_commission a value of 5%. For all other
values, which would be sales amount greater than or equal to
50,000, go to the ELSE clause where we assign a commission of
10%. Now the important thing to keep in mind here is that IF
l_sales_amt was NULL, the IF condition would have evaluated to
FALSE, making the control go to the ELSE clause, assigning a
higher commission value of 10%, something we did not intend.
So make sure that you take care of NULL evaluations in your IF
clause. Here, you can have an OR clause saying, IF sales amount
is less than 50,000 OR l_sales_amt IS NULL. IS NULL is a way
of checking NULL values for variables. Since two conditions are
single conditions each, I haven't surrounded them by brackets. If
there were a set of conditions in each group, separated by the OR
clause, I would have used brackets then. Another way of
preventing NULL evaluation is using the built-in NVL function.
NVL evaluates the first parameter passed in, and if it is null, it
returns the second parameter. So here, IF the l_sales_amt is null,
it will return a value of 0 for it. The third form of the IF statement
is the IF ELSIF ELSE statement. ELSE clause, however, is
optional when using ELSIF. ELSIF clause is also followed by a
condition, just like the IF clause. We can have any number of
ELSIF statements, and Oracle will keep evaluating the IF and
ELSIF from top to bottom until it finds the first one which
evaluates to TRUE. If none of the conditions are true, and if you
have an ELSE clause too, it'll go there. If you do not have an ELSE
clause, then it comes out doing nothing. Always try and keep the
most probable evaluations on top to save the database the extra
work of evaluating more conditions, costing more database
resources. Looking at the same example of the sales commission
system we looked earlier, we have assigned a sales amount of
60,000. Here we have a condition that if the sales amount is
greater than 50,000, then give a 10% commission. If that is not
TRUE, it'll go to the ELSIF clause to check if the sales amount is
greater than 40,000, and then it will give a commission of 5%. For
all other cases, it goes to the ELSE clause, giving a 2%
commission. The ELSE clause is optional. Notice here, the order
in which we put conditions is important. The sales amount of
60,000 is greater than both 50,000 and 40,000, and if we had put
the 40,000 condition earlier, the commission would have been
incorrectly assigned a 5% value. Here is the IF ELSIF without the
ELSE clause. Here, we are trying to define the logic for a ticket
management system for customer support. So we have declared
l_ticket_priority, a VARCHAR2 variable, which holds the
priority of the incoming ticket. We have assigned it a value of
MEDIUM for our example. Then we have the l_support_tier
variable, which is the support tier group to which the ticket is
assigned. So we say that IF the ticket priority is HIGH,
l_support_tier is 1. ELSIF if the ticket priority is MEDIUM,
l_support_tier is 2. ELSIF if the priority is LOW, l_support_tier
is 3. Note, there is no ELSE clause here. So no other value of the
ticket priority other than HIGH, MEDIUM, and LOW would be
accepted. We can nest an IF statement within another IF statement.
The nesting can happen anywhere within the IF, ELSIF, or ELSE
clauses. To prevent the code from becoming too complex, I do
not like to go beyond one or two levels of nesting. I always try to
evaluate other ways of writing the logic to keep the nesting to a
minimum. For example, here is the same logic which we looked
at earlier. Here, I have an outer IF clause evaluating for HIGH
priority tickets. Then, there is an ELSE clause for all other values.
Within the ELSE clause, I'm having a nested IF clause with
checks for MEDIUM and LOW priority. If I am nesting IFs,
proper indentation, or spacing is important, indenting each IF
from its parent to keep the code readable. So here, we could have
avoided the nesting if we would have written the code using the
ELSIF clauses. This is the code snippet we saw in the previous
slide where we have defined the same logic using ELSIF clauses.
Notice that this is much easier to read, and cleaner than the
previous code snippet.
Demo: If Elsif Else Statements & Nesting
Let us take a look at a demo to demonstrate the IF statement. We
have declared two variables, l_sales_amt, which is the sales
generated by the salesman. It is of type NUMBER, and assigned
a value of 10,000. L_commission is the commission earned by the
salesman, and it is initialized to a value of 0. In the execution
section, we have an IF statement that says that if a sales amount
is less than 25,000, then l_commission should be 2%. If it is less
than 35,000, then it should be 5%, and in all other cases, it should
be 10%. So our sales amount of 10,000 will satisfy the first
condition of sales amount less than 25,000, and stop right there,
and will do no further evaluations for the ELSIF and ELSE
clauses. L_commission will be assigned a value of 2, which we
are printing at the end. Our sales amount of 10,000 would have
also satisfied the ELSIF condition of sales amount less than
35,000, but since the first IF condition was met, it did not have to
go further down. So that highlights the importance of ordering
your conditions properly to get the right results, and avoid
unnecessary work to be done by the system. Let's run this. From
the Output, we see that l_commission was assigned a value of 2.
What if we do not assign a value to the l_sales_amt, and make it
NULL? (Typing) Note, IF and ELSIF clauses will evaluate to
FALSE for a NULL condition, and the execution will fall to the
ELSE clause erroneously, assigning a higher commission of 10%.
Let us run this block to see the situation. So from the Output, we
see that a value of 10 was assigned when we should have assigned
a value of 2. (Typing) To fix this, we put in the condition
l_sales_amt is less than 25,000 OR l_sales_amt IS NULL. Let us
run this again. Now, we see the correct result of 2. So it is
important to make sure you take care of the NULL situation.
Finally, we could have rewritten the same code as a nested IF
block, written the first IF with an ELSE, and inside having another
IF clause for less than 35,000, and an ELSE for the rest of the
values. This would have given us the same results. Let's run this.
We get the same value of 2. I just wanted to show you how you
can nest IF statements, but our aim should be to avoid nesting if
possible.
Case Statement
CASE statement is an alternate to the IF statement, and it can
sometimes make the code more compact, and readable. We can
express the CASE as a statement where we execute conditional
logic based on value comparisons, or conditions. It can also be
written as an expression where we directly assign the value
returned by it to a variable. This is useful in situations where all
we are doing in our conditional code is to assign a value to the
same variable. Let us look in more details at this important
construct. In the simple case statement, we compare a variable
value to different alternates, and return the very first match. It
starts with a CASE keyword, followed by a variable, or an
expression, whose results are evaluated to the values in the
WHEN clauses from top to bottom. The very first match stops
further evaluation of remaining WHEN clauses under it, and
executes the statements related to that WHEN clause. If none of
the WHEN clauses match, it goes to the ELSE clause, and
executes the statements there. Note, however, that the ELSE
clause is optional. If the ELSE clause is not defined, and there is
no match, the CASE statement will raise a CASE_NOT_FOUND
Exception. The END CASE ends the CASE statement. Let us look
at this code example to understand the simple case statement
better. Here is the same logic we talked about earlier for our ticket
management system for customer support. We have declared
l_ticket_priority, which is the incoming ticket priority. We have
assigned it a value of MEDIUM. L_support_tier is a support tier
assigned based on the ticket priority. So the CASE statement
compares the variable l_ticket_priority to the WHEN clause
values, since it is assigned a value of MEDIUM, it'll match the
CASE WHEN l_ticket_priority is MEDIUM, and will assign a
support tier of 2. If we are assigned a value other than LOW,
MEDIUM, and HIGH for the l_ticket_priority, it will have
assigned a value of 0 to l_support_tier. If the ELSE clause would
not have been there, it would have raised a CASE_NOT_FOUND
exception. A searched case statement is an alternate to the IF,
ELSIF clauses. The syntax is CASE, followed by a bunch of
WHEN clauses, each one of which evaluates a condition. The
condition in the WHEN clauses are evaluated from top to bottom,
and when a condition is matched, it stops evaluating any other
condition under it, and executes a statement in that WHEN clause.
Again, we can define an optional ELSE clause if there is no match.
If there is no ELSE clause it raises a CASE_NOT_FOUND
exception just like the simple case statement. Let us rewrite the
example we looked at with a simple case statement now as a
searched case statement. Notice, here there is no comparison
value after the CASE statement, and each WHEN clause is
evaluating a condition comparing l_ticket_priority to HIGH,
MEDIUM, and LOW, and there is an ELSE clause for the rest.
The conditions can be more complex, and you can do much more
in the statements, but I just wanted to illustrate to you the concept
and usage in this example.
Case Expressions
The simple case expression is similar to the simple case statement,
in that it compares a value, or expression to the values in the
WHEN clauses, and returns the value from the first match to
WHEN clause. However, the difference is that here it assigns the
written value of the case expression directly to a variable in the
PL/SQL block. The return value should match the datatype of the
variable it is assigned to, like number, date, character, etc. So for
the simple case statements that all we are doing is assigning value
to a single variable inside the WHEN clauses, it can greatly
simplify the code, and make it more compact, and readable.
However, the only thing we can do here is return a value, and can
have no other statements in the WHEN clauses. Notice, that here
there is no END CASE clause, but just the END clause to end the
case statement. ELSE clause here is also optional, like the case
statement, but if you omit the ELSE clause, and no match is found
with any of the WHEN clauses, it does not return a
CASE_NOT_FOUND exception, but just returns NULL. The
maximum number of arguments in a case expression is 255, but
if you're hitting that limit, then you need to reevaluate your logic,
and find a better way of doing it. Let us take a look at how we can
write a simple case expression. Here is the code snippet we saw
earlier for a simple case statement, where based on the incoming
ticket priority of HIGH, MEDIUM, or LOW, we assign a value of
1, 2, or 3 to the support tier. For all other cases, we assign a value
of 0. Let us see how we can rewrite the same example using the
simple case expression. Since all we are doing here is assigning a
value to l_support_tier in the WHEN clauses, we can directly
assign the value to l_support_tier using the simple case
expression. We again evaluate the l_ticket_priority, and for a
value of HIGH return 1, MEDIUM return 2, LOW return 3, and 0
otherwise. Notice how simple and compact our code becomes
making it more readable, and easier to maintain. Searched case
expression is similar to the searched case statement, just that the
return value is directly assigned to a variable. Again, the return
value has to be of the same datatype as the variable it is assigned
to. Just like the simple case expression, it helps in making our
code compact for situations where the only thing we are doing
inside the WHEN clauses is assigning value to the same variable,
and executing no other statement. It can serve as a great
replacement for the Oracle built-in decode function, but we can
do is look at a variable value, and return different values based on
that. Searched case expressions allows us to evaluate more
complex conditions to return values, and hence is more powerful.
Again, it ends with just an END statement. And if there is no
ELSE clause, and none of the WHEN clauses match, it returns a
NULL instead of the CASE_NOT_FOUND exception. Again, to
compare and contrast here with the searched case statement, let
us take a look at the example for the searched case statement again,
where we are evaluating the incoming ticket, and assigning the
support tier based on the incoming priority of HIGH, MEDIUM,
and LOW. In case of no match, we assign a value of 0. Let us
rewrite this using the searched case expression. Here, we assign
the value that it will do l_support_tier based on the evaluation of
l_ticket_priority in the searched case expression. When ticket
priority is HIGH, we return 1, for a MEDIUM 2, LOW 3, and
ELSE we return a value of 0.
Demo: Case Statement & Expressions
In this demo, we will see how to use a simple case statement. We
have declared l_ticket_priority with a value of MEDIUM.
L_support_tier is a support tier to be assigned based on the ticket
priority. So the CASE statement compares a variable
l_ticket_priority to the WHEN clause values. Since it is assigned
a value of MEDIUM, it will match the case when l_ticket_priority
is MEDIUM, and will return a support tier of 2. Let us run this to
confirm. As we see from the Output, l_ticket_priority was
correctly assigned a support tier of 2. Since all we were doing in
our example was assigning value to the l_support_tier in the
WHEN clauses, let us rewrite the same example as a simple case
expression. Notice that we directly assign to l_support_tier the
value returned from the CASE expression. Here again, for
MEDIUM, we get a match, and a value of 2 is assigned to
l_support_tier. Let us run this. As expected, the support tier is
assigned a value of 2. Here again is the same example of the
customer support system now written using searched case
statement. Now, instead of having l_ticket_priority with the
CASE, we are comparing it in each WHEN clause. Again, we
have assigned our priority as MEDIUM. So the code starts
evaluating for the first WHEN clause for l_ticket_priority of
HIGH. That is not a match. Then it goes to the second WHEN
clause and compares for the l_ticket_priority to have a value of
MEDIUM. That is a match. The evaluation of conditions stops
right there, and it assigns a support tier of 2 in the statement under
the WHEN clause. Let us run this. (Typing) As we had expected,
a value of 2 is shown in the Console Output. Let us rewrite the
same example now as a searched case expression. So here, we
directly assign l_support_tier the result of the evaluation of the
CASE now written as a searched case expression. Notice the
comparison happening in each WHEN clause as the CASE for the
searched case syntax. The results are returned from the WHEN
clauses, and directly assigned to l_support_tier. Here again,
MEDIUM will match, returning a value of 2. Let us run this. As
expected, we see a value of 2 in the Console Output.
Summary
Evaluating options, and choosing appropriate actions is a
fundamental aspect of any business logic. And PL/SQL provides
several constructs to support that. The IF, ELSIF, and optional
ELSE statement provides a way to evaluate conditions. The very
first condition which evaluates to TRUE from top, stops further
evaluation down, and so we should try and put the most probable
conditions first to avoid unnecessary evaluations. Further, we
must be careful of the NULL variables, as they will make a
condition FALSE, and may lead to incorrect results. CASE is an
alternate to IF statement, and can be expressed as a statement, as
well as an expression. I find myself using CASE more often than
IF statement, especially since it can make the code more compact
and readable, for instance, when used as expressions to assigned
variable values.
Cursors
Introduction
Hi. Welcome to Pluralsight. My name is Pankaj Jain, and
welcome to this module on Cursors. A cursor is a handle, or a
name to an area in memory used by Oracle for processing of SQL
statements. For example, to fetch a bunch of rows from the orders
table, or updating the year and revenue, and expense information
in the DWS accounting tables. It is a very fundamental and
important construct in PL/SQL. Understanding how to define and
use cursors is of upmost importance if you want to do SQL
processing, and interact with the database tables, and views in
your PL/SQL code. Whenever you issue a SELECT, INSERT,
UPDATE, or a DELETE in your SQL code directly, Oracle
creates an implicit cursor, or an area in memory to parse and
execute the query and hold its results. The implicit cursors are
handled by Oracle, but they have certain limitations, which we
will talk about later. Oracle also gives you the ability to define
cursors explicitly based on a query. Oracle assigns a name area in
memory, parses and executes a query for you, but gives you
control to open, fetch, and close the cursor. This is the mode you
will be using cursors most of the times. Let us look at the lifecycle
of a cursor when you use it in the code. The first stage is the Open
phase of the cursor. In this stage, area in memory is assigned, the
SQL statement is parsed, and variables, if any, are binded. The
statement is executed. And for a SELECT statement, the results
are obtained, and the pointer moved to the first row, if results are
found. Next is the Fetch state. If the SQL statement returns rows,
especially in case of queries, this step retrieves the row. The row
where the pointer is returned, and the pointer is moved to the next
row. In the case of an explicit cursor, if the query returns more
than one row, an error is raised. In the case of an explicit cursor,
we keep issuing the Fetch until the last row is retrieved, and there
are no more rows to return. Finally, is the Close phase, which
releases the memory and related resources, and closes the cursor.
All these phases are managed for you automatically by Oracle for
implicit cursors. For explicit cursors, you are responsible for
calling the different phases, but you also get more control.
Implicit Cursors
Before we go forward, let me introduce you to the tables we will
be using in this module; the departments table, having two
columns, dept_id, and dept_name, and the employee table, having
emp_id, emp_name, emp_dept_id, emp_location, and emp_sal
column, the emp_dept_id being a foreign key to the dept_id
column of the departments table. I like to use prefixes for each of
my table columns, like emp for employee, and dept for
department columns. This helps with the readability in the queries.
Also, I like to use the source table_foreign table_fk format for
naming my foreign key constraints. For example, emp_dept_fk
which tells me right away the relationship. Again, the standard
naming conventions help readability, and maintainability of your
code, and is a good practice to follow. Implicit cursors are open
for you when you issue a SELECT, INSERT, UPDATE, or a
DELETE statement in PL/SQL. Oracle opens these cursors for
you, fetches the row, and closes it for you automatically. In this
code snippet, we issue a SELECT to get the dept_id, and
dept_name from the departments table. And fetch INTO l_dept_id,
and l_dept_name. Note the SELECT, INTO syntax in PL/SQL,
which is different from SQL statements. Here, you need to have
the same number and type of variables declared to fetch the
SELECT columns in the SQL query. For example, here we have
l_dept_id to fetch the dept_id, and l_dept_name to fetch the
dept_name from the SELECT statement. If your number and type
of variables differ from the SELECT columns, Oracle will give
you an error for too many values if we have more variables than
the SELECT columns, and not enough values, if we have more
columns selected than the variables to hold them. SQL%FOUND
is a built-in implicit cursor attribute, which returns TRUE if the
SELECT statement fetched a row, or if an UPDATE statement
updated any rows, or if a DELETE statement deleted any rows. It
returns FALSE otherwise. SQL%NOTFOUND is just the
opposite of SQL%FOUND. SQL%ROWCOUNT gives the
number of rows fetched, or updated, or deleted. For a SELECT
query if you're sure that's going to fetch a row, and only one row,
then using an implicit SELECT cursor is the most efficient way to
go. However, if there are more than one rows fetched, or no rows
fetched at all, then Oracle raises errors. Sometimes you might
want to use them when you actually want an error to be raised for
these conditions. It might also help someone reading your code to
understand that this SELECT statement is supposed to fetch one
row only, and it is an erroneous condition if that is not the case. I
wanted to show you an example of how to issue a DELETE
statement in PL/SQL as in this code snippet below, where we
delete from the departments table. In the next code snippet we
show how to use an UPDATE statement in PL/SQL to update
dept_name to Marketing for dept_id too. Note that the DELETE
and UPDATE statements follow the same syntax we use as when
normally writing SQL statements. Implicit cursors are okay to use
for UPDATEs, and DELETEs in PL/SQL, and for SELECTs
where you're sure that only one row is going to be fetched. What
happens if there is more than row fetched, as in this code snippet,
where we have removed the WHERE condition, and now, this
query will fetch dept_id and dept_name for all the departments?
In this case, Oracle will raise an error, EXACT FETCH
RETURNS MORE THAN REQUESTED NUMBER OF ROWS,
as it can only use a single variable to hold a collection of values.
And if your fetch returns no row, as in this example, we are trying
to fetch information for dept_id 10, which does not exist. In this
case, Oracle will raise NO DATA FOUND EXCEPTION. If you
are expecting more than one row, explicit cursors would be the
way to go.
Demo: Implicit Cursors
Let us first create the two tables we will be using in our examples.
First is the departments table with two columns, dept_id, and
dept_name. Then we're inserting a couple of rows in the table, and
committing them. Next is the employee table with columns,
emp_id, emp_name, emp_dept_id, emp_location, and emp_sal,
with emp_dept_fk creating a foreign key to the departments table.
Then, we insert some records in the table. Let's run this. (Typing)
So everything got created successfully. Let us see an example of
implicit cursors. Here, we have two variables, l_dept_id, and
l_dept_name to hold the dept_id and dept_name from the
departments table. Then we issue a SELECT to fetch the dept_id,
and dept_name into the two variables we declared earlier FROM
the departments table, WHERE dept_id is 1. If the fetch was
successful, SQL%FOUND would be TRUE, and inside you print
SQL%ROWCOUNT, which should have a value of 1. Let's run
this. (Typing) As expected, SQL%FOUND is TRUE, resulting in
a Console Output of 1 for SQL%ROWCOUNT. What if the
dept_id does not exist? Let's make the dept_id to a nonexistent
dept_id of 10. Let's make this change, and run this block again.
(Typing) We get no data found exception. Now let's fetch more
than one row by removing the WHERE condition, which will
fetch all the rows in the department table. Let's make this change
and run this block again. (Typing) As expected, Oracle raises the
error exact fetch returns more than requested number of rows. In
this demo, I wanted to show you the syntax of implicit cursor
fetch with a SELECT statement, and highlight the conditions
which can raise errors, and the corresponding error descriptions.
This way, you are aware of them, and can code appropriately to
avoid them.
Explicit Cursors
Before we talk about explicit cursors, I just wanted to briefly
touch upon COMMIT and ROLLBACK, which are used in
transaction management in Oracle. We will talk about transaction
management in detail in a later course, but I think knowing about
COMMIT and ROLLBACK is useful when you are trying to
INSERT, UPDATE, or DELETE data in the database tables.
COMMIT saves the data to the database, and makes the changes
permanent. ROLLBACK, on the other hand reverts back the
changes and brings the data to the state before the change. For
example, in this code snippet, I'm updating the departments table,
and setting the department name to Marketing for department ID
2. After the update goes through, until I issue the COMMIT, no
other session will be able to see the changes I have made. If I want
to make the changes permanent, I can issue a COMMIT in my
session. If it is the first time issuing COMMIT in my session, then
all DML changes I have made up to that point will be made
permanent. If I had issued a COMMIT, or ROLLBACK earlier in
my session, then all changes from that point on will be committed.
In case there is some problem with the update, and an exception
is raised, I would like to ROLLBACK the changes, or undo the
changes. Again, if it is the first time I am issuing a ROLLBACK
in my session, it will undo all the DML changes made up to that
point. Otherwise, it will roll back the changes made since the last
COMMIT or ROLLBACK. We define an explicit cursor in the
declaration section. The most basic syntax is the keyword
CURSOR, followed by the cursor_name IS and then the
select_statement. So in this code snippet, we start off by declaring
two variables, l_dept_id, and l_dept_name to hold the dept_id,
and dept_name. And then we DECLARE a CURSOR
cur_get_departments as CURSOR cur_get_departments IS
SELECT dept_id dept_name FROM departments WHERE
dept_id is equal to 1. In the execution section, we OPEN the
cursor using its name. This will assign an area in memory for the
cursor, parse, bind, and execute the query, and get the results. We
then issue the FETCH to get the first row from the result set. Note
here, the number, and type of variables should match the SELECT
columns. So l_dept_id should match the type of dept_id, and
l_dept_name should match the type of dept_name. And since
there are two columns in the SELECT, there should be only two
variables used in the FETCH; otherwise, Oracle will give us errors.
We print the name of the department, and then we CLOSE the
cursor releasing the area in memory allocated for it. One
advantage of using the cursor is that even if there is no data found,
there will not be an error raised, and Oracle gives you cursor
attributes to check that situation in order to handle it, which I think
is a more elegant way of handling it. We will take a look at cursor
attributes shortly. In the previous example, we were fetching just
a single row from the departments table. What if you want to fetch
more than row? Let us take a look at this example where we are
fetching the emp_id, and emp_sal, manipulated by 0.10, which we
have given an alias of bonus for all the employee records. At this
point, I would like to introduce you to the cursor attribute,
<cursor>%ROWTYPE. It declares a record based on the
CURSOR columns, which can be accessed using the dot notation.
So here, it is cur_get_employees%ROWTYPE. If you have an
expression as a part of your SELECT, like our salary multiplied
by 0.10 expression, you have to use an alias in order to be able to
access it within the record. You can declare individual variables
to hold the SELECT columns, but I prefer to use it this way, as it
requires less coding, and also avoids the accidental errors due to
wrong number of values in the INTO list of the FETCH statement.
Again, you OPEN the cursor. Now we use a simple LOOP to go
through the rows returned by the cursor. So we put the FETCH
inside the LOOP, and FETCH INTO the cur_get_employees_var.
We then define the EXIT condition for our simple LOOP, which
is EXIT WHEN cur_get_employees%NOTFOUND. Time for me
to introduce you to the other cursor attribute,
<cursor>%NOTFOUND. After the cursor is fetched, this attribute
will have a Boolean value of FALSE if there was a row obtained
with the FETCH, and TRUE if there were no further results. So
this can be used as a handle to exit the loop, as we have done in
our code snippet saying EXIT WHEN
cur_get_employees%NOTFOUND. If you try to access it before
the cursor is open, it is going to give you an error. And trying to
access it after the cursor is closed will give you similar results.
Before the FETCH, it will be NULL; however, the way we have
used it in our code is the most common way for it to be used for
exiting the CURSOR LOOP. <cursor>%FOUND is the reverse of
<cursor>%NOTFOUND, and so is TRUE when the FETCH
returns a row, and FALSE when it does not. We can use a dot
notation to access the fields inside, as I have shown for the
employee ID. The name will match the column name. If you have
used an alias for the column SELECT, then use the alias name
after the dot, like bonus in our example. So each FETCH will
bring the next row, until all the rows are exhausted, and the LOOP
will then CLOSE with the END LOOP statement. We CLOSE the
cursor outside the LOOP. It is important to remember not to
CLOSE it inside the LOOP, as otherwise the next FETCH will
raise an error, reporting cursor not found. What if we had not
fetched the rows in the loop, but had done just a single fetch, and
closed the cursor? In that case, even though the cursor had
multiple rows in the result set, we just fetch the first one. Let us
look at another cursor attribute, which is
<cursor>%ROWCOUNT. It is the count of cumulative fetches in
the cursor loop. After the cursor is OPEN, it is set to 0. The first
FETCH from the cursor increments it to 1. Next FETCH will
make it 2, and so on. Trying to access it before the cursor is open
will raise an exception, or after the cursor is closed will also raise
an exception. <cursor>%ISOPEN is a cursor attribute used to
check if the cursor is still open. It will be TRUE if it is, and
FALSE otherwise. Notice, if you try and open a cursor which is
already open, it will give you an error. And if you try and close a
cursor which was closed previously, Oracle will give you an error
for invalid cursor. You can pass parameters to cursors to
customize its behavior. They can accept multiple parameters,
which are in the default N mode. The other modes that can pass
parameters are the Out mode, where the value is returned back,
and the In/Out mode, where input values are taken, and then also
returned, are not valid for cursor parameters. The parameter
follows the format parameter name followed by its datatype, and
an optional DEFAULT keyword. The DEFAULT keyword
provides the default value if not passed in. They can always be
overwritten later on. In this code snippet, we DECLARE a
CURSOR cur_get_departments, which takes in an input
parameter of p_rows of type NUMBER with a DEFAULT value
of 5. This limits the number of rows returned by the SELECT
statement by using it in the WHERE clause of ROWNUM less
than or equal to p_rows. ROWNUM is a built-in from Oracle,
which provides the number of rows returned from the SELECT.
So the first row will have a ROWNUM of 1, second row a
ROWNUM of 2, and so on. We then declare
cur_get_departments_var of type cur_get_departments. Now
notice the way we opened the cursor. We are passing in a value
of 2 for p_rows, and that will limit the number of rows returned
from the SELECT to 2. Let us look at the same example in action
to understand explicit cursors and cursor attributes. So here, we
have a cursor, cur_get_departments, which takes in a parameter
p_rows of type NUMBER, with a DEFAULT value of 5. In the
CURSOR, we FETCH dept_id, and dept_name FROM
departments, and limit the number of rows by the rows parameter
passed in. We DECLARE cur_get_departments_var of type
cur_get_departments%ROWTYPE. In the execution block we
open the cursor passing in a value of 2 for p_rows, so that only 2
rows will be fetched. Now in the LOOP, we FETCH the dept_id
and dept_name in cur_get_departments_var. We EXIT WHEN
cur_get_departments%NOTFOUND is TRUE. We then print the
Dept ID using the dot notation on the cur_get_departments_var
record. You print the current count of rows fetched using the
cursor %ROWCOUNT attribute. We then use the attribute,
cursor %ISOPEN to see if the cursor is still open, and if TRUE,
CLOSE it. Let's run this. (Typing) As expected, we see the Dept
ID followed by the RowCount printed in the Console Output.
Cursor FOR Loop
The cursor FOR loop is a very efficient way of fetching data from
cursors. The syntax is the keyword FOR cur_rec, which is the
cursor record, IN cursor_name or sql_query followed by the
LOOP keyword. Then you have the statements to be executed in
the loop. The END LOOP keyword ends the loop. The cursor
record is implicitly declared as a record by Oracle of type
Cursor%ROWTYPE. It is in scope only within the cursor. This
kind of loop can make the code very compact as a lot of operations
are handled by Oracle. Oracle opens the cursor, and fetches the
rows into implicitly declared cursor record, and when there is
nothing more to fetch from the cursor, the cursor is closed
automatically. So since you do not have to worry about opening,
fetching, and exiting the loop, it makes the code so much more
error free. And starting Oracle 10G, Oracle can optimize these
kind of loops. This kind of cursor is definitely cleaner, and
compact, and is good when you have to fetch, say, rows in the
hundreds. But if you are dealing with a large amount of data, and
really need good performance, you should use the bulk collect
feature of the FOR loop. Bulk collect is a little more complex and
advanced topic, and we will cover it in a later course. We can have
a cursor FOR loop with implicit cursor, if we have
reported a SQL statement directly in the loop, or explicit
cursors where we use a cursor. Let's take a look at these. In the
explicit cursor FOR loop, we use a name cursor we have declared
earlier in the declaration section, and put it in the IN section of the
cursor FOR loop. So if you need to reuse a cursor at several places
in the code, declaring a name cursor, and using it in the loop
makes more sense. Here in the code snippet, we have a cursor,
cur_get_employees, which gets the employee ID, and salary
multiplied by 0.10, which is given an alias of bonus FROM the
employees table. And then we start the cursor for loop as FOR
cur_get_employees_var IN cur_get_employees, where
cur_get_employees_var is implicitly declared as
cursor%ROWTYPE, and cur_get_employees is the cursor name.
Then the LOOP starts, and inside the LOOP we access the emp_id
as cur_get_employees_var.emp_id. If we have an expression in
the SELECT, which we need to access in the LOOP, we need to
have an alias for it, like the bonus field, which we are accessing
next. The END LOOP closes the LOOP. The implicit cursor FOR
loop directly uses a query in the loop. So if you have a query,
which is not to be used anywhere else, then this type of cursor
might be appropriate to use. Here is a code snippet of a cursor
FOR loop with an implicit cursor. Here we again have the
implicitly declared cursor record cur_get_departments_var, but
now instead of a name cursor, we have a query right inside the
loop within the brackets. Notice the SQL query uses the variable
l_num when it builds a query, and executes it. However, a cursor
is a snapshot of data at a point, and even if we change l_num later,
it would not automatically refresh the result set unless we open
the cursor again. Oracle allows us to nest the cursors. This is
useful where the row returned by the first cursor is used by the
second cursor to fetch data. For example, here we have two
cursors. The first one is cur_get_dept_info. It takes p_rownum
input parameter to limit the number of department rows returned.
The second cursor, cur_get_emp_info, which takes p_dept_id
parameter to SELECT the emp_name from the employees table
for the past dept_id. We have an outer cursor FOR loop, which
we have labeled as dept_loop, in which we fetch into the
cur_get_dept_info_var cursor record, the values from the cursor
cur_get_dept_info, passing a parameter of 2. Next we have an
inner LOOP, which we have labeled as emp_loop, which fetches
into the cur_get_emp_info_var cursor record for the cursor
cur_get_emp_info, which takes the dept_id fetched from the outer
cursor, cur_get_dept_info_var.dept_id as the input parameter.
Inside, it displays the employee name. The inner cursor then
closes, and we have the ending loop label, emp_loop. Then the
outer loop closes with the END LOOP, followed by the optional
label dept_loop. Let us take a look at how we define an implicit,
and explicit cursor loop, as well as how to nest them. In this
example, I have the cur_emp_info cursor, which takes a dept_id
parameter, and returns the emp_name from the employee table for
the department. Inside the execution section, we start the outer
loop with the label dept_loop. Here we define an implicit cursor
FOR loop with the line FOR cur_dept_info_var IN, and the
implicit cursor query, SELECT dept_id from departments.
Cur_dept_info_var is an implicitly declared record consisting of
columns returning from the query. We start a simple LOOP, print
the dept_id, then we start an inner LOOP labeled emp_loop,
where we have an explicit cursor FOR loop based on the
cur_emp_info cursor declared earlier. It takes the dept_id fetched
from the outer loop, cur_dept_info_var.dept_id, as an input
parameter. Inside, we start a simple LOOP, and print the
employee name fetched. Let us run this. (Typing) As expected,
we see the Department ID from the outer loop, and the
corresponding Employee Names from the inner loop printed in the
Console.
Cursor FOR Update
When you open a cursor, and fetch a set of rows based on a query,
Oracle does not log them right away. It only logs them once you
have changed them. This helps reducing the locking time to a
minimum, and increases general level ability. But then, there is a
chance that some other session can obtain the same row, and
change the data and commit it before your session gets a chance
to issue an update and log the row. The cursor will have a snapshot
of the data from the point it was open, and it will not be able to
see the latest changes to the data that was committed outside of
the session after the cursor has started. And if you are basing your
change on a certain column value, which was changed by another
session, and since your cursor still sees the old value, it will do
the update based on that, which might be incorrect. It will make
the changes without knowing how the data last looked.
Sometimes, you might want exclusive lock on rows right from the
time you select them in your cursor to ensure that no one else can
change it outside your session. These records will be available to
others only after a session issues a COMMIT or a ROLLBACK.
This ensures that as you tred over the rows in the result set, you
can be certain that these are the most current values. Cursors FOR
UPDATE can help with these kind of situations. This kind of
locking may not be required most of the times. Remember, any
extra lock you place on your rows, makes others wait for them,
and slows everyone down. So use these kind of cursors with
discretion. The syntax for declaring these kind of cursors is
putting the FOR UPDATE clause at the end, followed by an
optional column list. There are two uses of naming the columns.
One, it clarifies your intention in the code about the columns you
want to work with. However, this still does not stop you from
updating other columns if you want to. Secondly, in a multi-table
query, Oracle will lock only the rows of the tables whose columns
appear in this list, and so is helpful in reducing extra locks. If you
do not mention the columns, Oracle will obtain row locks on all
the tables involved in the query. NOWAIT keyword is again
optional, and that tells Oracle to come back immediately if the
table is locked by some other session, and not to wait for that
session to release the lock. The other part of the FOR UPDATE
cursor is the CURRENT OF clause, which you use while issuing
the update, or delete inside of the cursor. It identifies the current
row being worked on from the cursor. You do not have to specify
the WHERE clause to identify the row to update or delete, and I
think it is a good convenience feature. Another thing is that even
though you have opened the cursor FOR UPDATE, it does not
require you to necessarily do an update, or a delete inside the
cursor. However, I do not see much use for using a cursor FOR
UPDATE in that situation. Here is an example of using cursor
FOR UPDATE statement. We DECLARE a CURSOR,
cur_move_emp, which takes in the location parameter p_emp_loc
of type employee.emp_loc%TYPE. It is always a good practice to
use %TYPE to define variables and parameters based against
table columns. That way, if there are changes to the table column,
the change is applied dynamically to these without any code
changes. In the CURSOR, I am selecting emp_id and dept_name
from the departments and employee table, making a join on
emp_dept_id with dept_id and location equal to the one specified
in the parameter. The FOR UPDATE keyword at the end tells
Oracle to lock the rows fetched by this cursor. Since we are
specifying column emp_loc in for the FOR UPDATE clause, it
will only lock the rows of the employee table, and not the
department table, and so helps in avoiding unnecessary locking.
NOWAIT tells Oracle to come back immediately with a
notification if the table is already locked by another session. Then
we open the cursor passing in the location of California. We set
the location of those employees to Washington with an UPDATE
statement using the CURRENT OF cur_move_emp clause. We do
not have to repeat the WHERE clause off of our cursor query here,
and the CURRENT OF clause makes is simpler to identify the
row uniquely. Since our selection queried area for these records is
employees with the location in California, selecting them for
update ensures that no one else will have a chance to change them
before we make our changes. We finally END the LOOP, which
also closes the cursor automatically. Then we issue a COMMIT
to make the changes permanent, and release the locks on the rows.
Notice, it is important to issue COMMIT outside the loop. If you
issue a COMMIT inside the loop, Oracle will release the lock on
the rows immediately, and the next cursor fetch will error out with
out of sequence error, as it needs a lock to operate. In this demo,
we will see how to declare and use a FOR UPDATE cursor. In the
declaration section, we define the CURSOR, cur_move_emp,
which takes in the location parameter, p_emp_loc. The query
fetches emp_id and dept_name, joining the departments and
employee tables on the dept_id column, where the employee
location is the passed parameter of p_emp_loc. It then further
qualifies this as a FOR UPDATE cursor with the clause FOR
UPDATE OF emp_loc NOWAIT. So since emp_loc is mentioned
in the FOR UPDATE clause, only the employee table will be
locked, and not the departments table. In the execution section,
we open the cursor with a parameter of CA for California, and
UPDATE the employees with a location of Washington using
WHERE CURRENT OF cur_move_emp clause. Note we do not
have to repeat the entire WHERE condition to uniquely identify
the row with the WHERE CURRENT OF clause. We END the
LOOP, and then COMMIT. It is important to keep the COMMIT
outside of the LOOP to prevent the out of sequence error. Let's
run this. The block completed successfully updating the location
of the records.
Ref Cursors or Cursor Variables
A ref cursor, or cursor variable, is a reference, or a pointer to a
cursor. It is explicitly named, and can be used for multiple queries,
so you can open it for one query, and then, later open it for another
query if you need to. Before you point it to the next query, you
need not close it. It will just lose reference to the old query. After
we are done using it, you need to close it to release the memory.
It can be a parameter to a program unit, like a procedure, or a
function. We will talk about procedures and functions in a later
course, but ref cursors used as parameters can help us query
results from one program to another. They can be assigned values,
and used as expressions. We will take a look at some examples
later. They cannot, however, accept parameters, and cannot be
used in cursor FOR loops. They can be strongly typed when you
specify the return type, or weakly typed when the return type is
not specified. So the ref cursors offer a lot of flexibility in terms
of reusing them for several queries, and returning results of
different types. They also allow passing of query results from one
subprogram to another, and hence can help with centralized data
retrieval. They can be used to pass result sets within the client
applications, like Pro-C Oracle Forms, and the database server. It
can help reduce network traffic as the host, or planned application
can combine all the open FOR cursor statements, and send it to
the server in one go, where they are open, preventing multiple
round trips. Since a cursor variable is a pointer to a work area, the
client applications thereafter can continue to work with it like a
regular cursor. Ref cursors, or cursor variables can be declared as
TYPE, followed by the ref_cur_name IS REF CURSOR with an
optional RETURN clause. You can use all cursor attributes, like
<cursor>%Found, <cursor>%NOTFOUND, <cursor>%ISOPEN,
and <cursor>%ROWCOUNT with ref cursors. You cannot use
ref cursors in cursor FOR loops, but only with the explicit OPEN,
FETCH, CLOSE syntax. With the strongly typed cursor variables,
you declare the return type, and so there will be a compile time
error if you fetch into a different datatype. The compile time
validation is one of the benefits of using strongly typed cursors.
So in this code snippet we DECLARE a REF CURSOR, rc_dept.
We say, TYPE rc_dept IS REF CURSOR which RETURNS
departments%ROWTYPE making it strongly typed. Again, I like
to use rc prefix to distinguish ref cursors in my code as a standard
naming practice. I highly encourage you to also standardize
naming convention in your code for better maintainability. Then
I declare a variable rc_dept_cur of type rc_dept. You cannot use
the rc_dept cursor directly, but just like datatypes, you have to
declare a variable of its type, which you can use in your code later
on. Next, I define l_dept_rowtype, which is of
departments%ROWTYPE, to hold the return record from the
cursor. L_id is a variable of TYPE number with a value of 1.
L_dept_id and l_dept_name would be used to hold dept_id, and
dept_name respectively. In the execution section, I OPEN
rc_dept_cur FOR my query, SELECT *FROM departments,
where dept_id is equal to l_id. Here I want you to note two things.
One, the OPEN rc_dept_cur statement makes the cursor point to
this query, and its work area in memory. Secondly, I wanted to
demonstrate that the cursor can use any variable defined earlier at
the time it is building and executing the query, as it does with l_id.
However, when we change l_id later, that will not cause the query
to be reevaluated. Only if this cursor is reopened, then it will
evaluate the value of l_id at that time, and take its current value
when building the query. Next, we start the LOOP and FETCH
rc_dept_cur INTO l_dept_rowtype. We follow the simple LOOP
syntax and EXIT WHEN rc_dept_cur is not found, print the value
of dept_id, and then end the loop. Now I am reusing the same
rc_dept_cur to open a totally different query from the first one,
SELECT *FROM departments WHERE dept_name is
Accounting. I do not have to close the cursor. Opening it again
made it point to the new query, and the previous query is lost.
Next in the LOOP, we are fetching the dept_id, and dept_name in
two separate variables. Even though the cursor in its declaration
is returning departments%ROWTYPE, but since these are the
only two columns in that table, Oracle allows you to fetch them
this way, into individual columns also. Finally, after the second
LOOP, we CLOSE the rc_dept_cur to reclaim memory. Ref
cursors can also return user defined record types. We have
declared a record TYPE, dept_rec, with two columns, dept_id and
dept_name. This time, we DECLARE the cursor as TYPE rc_dept
IS REF CURSOR RETURN dept_rec. We DECLARE a variable
rc_dept_cur of the REF CURSOR type, and l_dept_rec of the
dept_rec type. We have declared a NUMBER variable, l_choice,
and given it a value of 1. Now based on the value of l_choice, we
open the same cursor variable for either one of the two queries,
one with dept_id equal to 1, and the other WHERE dept_name is
Accounting. Again, in the LOOP, you FETCH into the dept_rec,
and display its value. So ref cursors can fetch into a record type,
or row type, or individual variables, as we saw in the last example.
You can assign value to a strongly typed cursor variable. It can be
another strongly typed cursor variable based on the same type, and
returning the same datatype. So here, we have declared a REF
CURSOR rc_dept_row, which returns departments%ROWTYPE,
and declared two variables using its TYPE. Obviously, these
variables cannot be assigned to a cursor variable, which returns
say, employee%ROWTYPE; however, they can be assigned to
each other. Let us declare a record dept_rec with two columns of
the department table, dept_id, and dept_name. We declare ref
cursor rc_dept_rec based on dept_rec. Again, we have declared
two variables of type rc_dept_rec. These two variables can also
be assigned to each other. However, the variables of the two
cursor types, even though they essentially return the same
columns, one in the form of a row type, and the other in the form
of a record, they cannot be assigned to each other. Oracle is
flexible in fetching fetches of a cursor variable returning row type,
can go to a record variable as we saw earlier, but it cannot assign
the cursor variable of the two types to each other. What if we
change the cursor on the right to also return
departments%ROWTYPE? Even then, the variables of the
cursors on the right and the left cannot be assigned to each other,
as the base type of one is rc_dept_row, and for the other is
rc_dept_rec. For them to be assigned to each other, both the base
type, and the return datatype should match exactly. So here is an
example of assignment to strongly typed cursor variables. I
DECLARE a REF CURSOR rc_dept returning
departments%ROWTYPE. DECLARE two variables,
rc_dept_cur1, and rc_dept_cur2, both of type rc_dept.
L_dept_rowtype is declared to hold the returned data from the
cursor. We OPEN rc_dept_cur1 FOR the query, then we assign it
to rc_dept_cur2. Then inside the LOOP, we work with
rc_dept_cur2, which now points to the work area of the original
query, fetching, and exiting it, and later closing it. So this is how
variables can be assigned to each other and used. In this demo, we
take a look at how to define strongly typed cursor variables, and
how to assign them values. We start off by declaring a strong ref
cursor rc_dept, which returns departments%ROWTYPE. Then
we DECLARE two variables, rc_cur_initial, and rc_cur_final,
based on it. We DECLARE l_dept_rowtype to hold the results of
the FETCH. L_choice is a number with a value of 1. L_lower is
assigned a value of 1, and l_upper is assigned a value of 2. Now
in the execution block, based on the value of l_choice, we open
either query 1, or query 2, both of which are very different for the
same cursor variable. Since l_choice is 1, we open rc_cur_initial
for selecting from departments table where dept_id is between
l_lower and l_upper, or 1 and 10. Later, we assign rc_cur_initial
to rc_cur_final, and from this point on, it points to the query work
area, and is used. We FETCH an l_dept_rowtype, display the
results, and finally CLOSE it. Let's run this. (Typing) As expected,
we get a result of 1, and 2 in the Console Output.
Weak Ref Cursors
For weakly typed cursor variables, we do not specify the return
type. So they are very flexible as the same cursor variable can be
used, say for a query against employee table, as well as a query
against the department table. But the tradeoff is that there is no
compile time check, and there is a possibility of runtime error if
the return value from the cursor is not fetched in appropriate
variables. SYS_REFCURSOR is a predefined weak ref cursor
from Oracle. We will show its use soon. The weaker ref cursor
can be assigned to a weak, or a strong ref cursor. In this example,
we DECLARE a weak cursor as TYPE rc_weak IS REF
CURSOR. Notice there is no return type. Next we DECLARE a
variable, rc_weak_cur of type rc_weak. We then define
l_dept_rowtype of departments%ROWTYPE, and
l_emp_rowtype of employee%ROWTYPE. We OPEN the
rc_weak_cur first for a query against the departments table. In the
LOOP, we FETCH its result in l_dept_rowtype record, and
display the dept_name. We then use the same rc_weak_cur and
OPEN it for a query against the employee table. Now in the LOOP,
we FETCH its result in the l_emp_rowtype, and display the
emp_id. Finally, we CLOSE it. So this demonstrates the
versatility and flexibility of weakly typed ref cursors. Here I want
to show you how weakly typed cursor variables can be assigned
and interchanged, as well as an alternate syntax, which can be
used with weakly typed cursor variables. Here, I have declared
rc_weak as a weak ref cursor, and a variable rc_weak_cur based
on its type. Next, I have defined rc_sys_cur of type
SYS_REFCURSOR, a weak cursor variable provided by Oracle.
I declare l_dept_rowtype to hold the returned data from the cursor.
L_lower is a NUMBER with a value of 1, and l_upper is a
NUMBER with a value of 10. Notice the alternate way of
specifying the query for the weak cursor, r_sys_cur as a single
coded string. SELECT *FROM departments WHERE dept_id
BETWEEN :1 AND :2; :1 and :2 are placeholders the values of
which are provided with a using clause USING l_lower and
l_upper. So you specify the bind placeholder values using the
comma separated list in the USING clause. This syntax might
seem more familiar to someone coming from, say, the Java World
where you provide bind variables to prepared statements. Next,
I'm assigning rc_sys_cur to rc_weak_cur, which is then used from
this point on, for fetching inside the LOOP, and displaying the
dept_name. We finally CLOSE the rc_weak_cur. Let us see a
demo as to how to use the weak ref cursors. Here, we have defined
a weakly typed ref cursor, rc_weak, and declared rc_weak_cur of
its type. L_dept_rowtype is a record of type department, and
l_emp_rowtype is a record of type employee. We OPEN the
rc_weak_cur first, FOR the query, selecting from the departments
table, fetching its result, and displaying the dept_name. Later, we
use the same cursor for a query against the employee table, fetch
its result, and display the emp_id. Finally, we CLOSE it. Let's run
it. (Typing) As expected, we see the dept_name, and emp_ids.
Both queries run with the same ref cursor, thus showing its
flexibility.
Cursor Expressions
Cursor expressions return nested cursors. The syntax is CURSOR,
followed by the Query in brackets. They are useful when we want
to bring over some related dataset, along with each record of the
result set of the main cursor. They cannot be used with implicit
cursors, but with explicit cursors. They cannot also be used in
views. They are used as an outer SELECT column of the main
cursor. So when the main cursor is open and fetched, with each
row it returns a column, which is another cursor. You then fetch
from this cursor using the simple loop. The nested cursor is closed
automatically when the main cursor is closed, or re-executed, or
some error in the fetch of the main cursor can also close it. It can
also be closed explicitly. So in this code snippet, we have declared
a main CURSOR, cur_dept_info, which is selecting dept_id from
departments. Along with it, there is also a nested cursor set
defined by the cursor expression, starting with the CURSOR
keyword, and with a Query inside. This CURSOR is selecting the
emp_ids of the department row fetched, by joining emp_dept_id
with the dept_id of the main cursor. We have aliased this nested
cursor as emp_info. Next, we define l_dept_id to hold the value
of dept_id, rc_emp_info as a SYS_REFCURSOR, which will
hold the nested cursor, emp_info. L_emp_id is declared to hold
the emp_id selected by the nested cursor. We OPEN the main
cursor, cur_dept_info. Then in a simple LOOP, we FETCH rows
from it, holding dept_id and l_dept_id, and emp_info in the ref
cursor, rc_emp_info. We put the EXIT condition of the LOOP
WHEN cur_dept_info is not found. Now we start a nested LOOP
to FETCH the employee rows from the nested rc_emp_info cursor
to get the employee records for the dept_id fetched. Again, we
EXIT this LOOP when rc_emp_info is not found. We print the
emp_id, and then CLOSE both the LOOPs. So this is how we can
obtain related dataset for each row of the main cursor using cursor
expressions. Let us see the same example in action. We have our
CURSOR, cur_dept_info, which gets the dept_id, and with a
CURSOR expression, the related employee IDs for the
department record from the employees table. The passed
parameter, p_num, is used to limit the rows returned for both the
cursors. Thus, parameters in the outer CURSOR, are also
accessible to cursor expressions. We DECLARE l_dept_id to hold
the dept_id, rc_emp_info of type SYS_REFCURSOR to hold the
nested cursor, and l_emp_id to hold the employee ID fetched from
it. In the execution section, we OPEN cur_dept_info with an input
parameter of 2. In the LOOP, we FETCH the result in l_dept_id,
and rc_emp_info. We display the dept_id fetched. Then, in the
nested LOOP, we FETCH results from rc_emp_info and
l_emp_id, and display them. We have the EXIT criteria of
rc_emp_info NOTFOUND to EXIT the LOOP. Finally, we
CLOSE the cur_dept_info cursor, which will also CLOSE the
rc_emp_info cursor. Let's run this. (Typing) So we see the dept_id
and the corresponding emp_ids fetched for it in the Console
Output.
Summary
In this module, we looked at cursors, a very fundamental
programming construct in Oracle for working with SQL
statements. We looked at implicit cursors, which are created when
you issue a SELECT, INSERT, UPDATE, or DELETE directly
in the code. The SELECT implicit cursor is the most efficient way
for single row retches. However, for multiple row fetches, we
looked at explicit cursors. We get more control opening, fetching,
and closing them, and the cursor attribute allows us to work with
them more effectively. This is the kind of mechanism I would use
for multi-row selects when I want more control, and I want to
EXIT conditionally. Also, OPEN, FETCH, CLOSE is the only
mechanism you can use when working with ref cursors. We
looked at the cursor FOR loops, which are very efficient,
relatively error- free, and compact, because Oracle takes care of
implicitly declaring the cursor record, opening, and closing the
cursor for you. These are suitable for fetching small to medium
datasets in your application. Starting Oracle 10G, Oracle can also
optimize them for you automatically. When I'm dealing with large
data, the bulk collect feature of the cursor loop is the best
performing, and we will take a look at it in a later course. We
looked at cursor FOR UPDATE clause, if you need to be
absolutely sure no one can modify our results while we are
working with it. Finally, we looked at the cursor variables, which
are particularly useful if we want to pass query result sets between
subprograms, and between client applications, and database
server. They provide us with a lot of flexibility.
Exceptions
Introduction
Hi. Welcome to Pluralsight. My name is Pankaj Jain, and
welcome to this module on Exceptions. However much we do not
want them to happen, exceptions do occur. I was driving my
friend to the airport the other day. The normal commute time is
around 15 minutes, but I took an extra 15 minutes in case anything
unexpected happened, and sure enough, there was horrible traffic
on the way because of an accident, and we barely made it to
airport in time. This is an example of how exceptional situations
can happen when you do not normally expect them, and how you
can prepare for them. Exceptions happen in PL/SQL processing
also. It can be a design error, where we forget to put a unique
constraint on a column, leading to multiple rows being fetched in
a SELECT statement, causing TOO_MANY_ROWS error. Or, it
can be an application error where we are trying to close an already
closed cursor, raising invalid cursor exception. It can be a system
error, say, running out of memory, or storage space. So errors can
be raised for many situations. Oracle provides us with a powerful,
and elegant error handling framework in PL/SQL to handle these
errors. It provides us with many predefined exceptions to cover a
lot of these situations. It also gives us the ability to define our own
exceptions to take care of business logic validations. In this
module, we will look at these in detail, and understand the
exception propagation rules. Oracle does compile time checks to
check for syntax errors, and can also give warnings. However, for
runtime exceptions, there are three groups of exceptions.
Internally defined. These are the errors which are raised by Oracle
implicitly when the error condition happens. For example, when
you're trying to assign a two character literal to a character
variable, which can hold only one character, Oracle raises ORA-
6502, or numerical value error. Predefined exceptions are
internally defined exceptions; however, they have been given a
name by Oracle, for example, 0 divide error, or invalid cursor
error. Finally, Oracle allows you to declare exceptions of your
own for your business logic validations, as well as name those
internal exceptions which do not have predefined names. Let us
see each one of these.
Oracle Internal Exceptions
The internally defined exceptions start with the ORA keyword,
followed by the negative numeric error code. These kind of errors
are raised automatically by Oracle when the error condition
happens. So in this code snippet, we define a number variable of
type PLS_INTEGER. Later, we try and assign it a value outside
its range. This will cause Oracle to raise a runtime exception with
error code ORA-01426 for numeric overflow. Note, it reports an
error stack. The first error code, ORA-6512, is a common error
code, and tells the line number where the error occurred. In our
case, at line 4. At the top of the stack is the actual error code of
ORA-01426. In the bottom section, Oracle lists the error code, and
tries to tell you the possible causes and actions to fix them. I have
noticed over the years, Oracle has gotten better with their
description of errors, and documenting causes and actions
wherever applicable to help the defects. The way the code is
written now, it will throw errors to the host environment, as there
is nothing to handle it. We do not want our clients, and client
applications to see this kind of error stack as, first, it might not
make much sense to them, and secondly, it gives a poor
impression about our application coding practices. Our code
should handle errors as far as possible, and give meaningful
messages to them. Let us take a look at the exception handling
section before we go to the predefined exceptions. We took a look
at the structure of the block consisting of the declaration, and
execution section earlier. There is also an optional exception
handler section at the end. This is where we put exception
handlers. We took a look at the WHEN OTHERS exception
handler earlier too. It is a catchall exception handler. You can
have multiple exception handlers in the block, and we will talk
about these in just a minute. But if there is an error raised, and
there is no specific handler for it, it falls in the bucket of WHEN
OTHERS exception handler. WHEN OTHERS is an option to
declare. You should try and define specific exception handlers to
handle as many situations you can think of, but I think it is always
a good idea to have a WHEN OTHERS exception at the least.
That way, you can at least log the error, and give a more useful
message to the client, like contact customer support, instead of
throwing out the stack to them. If you have many exception
handlers, then WHEN OTHERS should be placed at the end, so
that the specific exceptions get evaluated against the error first
before falling in the catchall bucket of WHEN OTHERS.
SQLCODE and SQLERRM are functions provided by Oracle to
get more information about the errors. They cannot be called
directly in SQL statements, but have to be assigned to variables
first. SQLERRM has a length of 512 bytes. Here is the last code
snippet we saw where the errors were thrown to the client terminal.
Here SQLCODE is a number -01426, and SQL error message is
the complete string, ORA-01426 numeric overflow. How do we
obtain them in the code? So, here is the same code, but along with
l_num, we have declared two other variables, l_sqlcode of type
NUMBER to hold the SQLCODE, and l_sqleerm of type
VARCHAR2(512) to hold the SQL error message. We again
cause the numeric overflow error by assigning a number beyond
PLS_INTEGER range. But now we have an exception section
with WHEN OTHERS exception handler. So the EXCEPTION
will be called by it, and inside we capture the latest error code
information in l_sqlcode by calling the SQLCODE function. The
associated error message is caught in the l_sqlerrm variable by
using the SQLERRM function. So running the block, and
displaying these variables will give us the values shown below,
with SQLCODE being -1426, and SQLERRM being the entire
string ORA-01426 numeric overflow. These functions are useful
in WHEN OTHERS block to find which error occurred, and can
also help us create specific handlers for these, as we will see later.
The SQLCODE function when used in the exception handler will
give the last error code which occurred. It is a negative number
for internally defined errors, as we saw -1426 for numeric
overflow. The only exception is the NO_DATA_FOUND internal
exception, which has a SQLCODE of +100. When it moves
outside the exception section, this function will return a value of
0. And for user defined exceptions, it will return the value of
positive 1, or the actual error code, if we have associated the user
exception with an internal Oracle error code. We will talk about
user defined exception shortly. The SQLERRM function can be
used with, or without arguments. Used without arguments inside
the exception section for internally defined exceptions that are
done by the last error message. For example, we saw the function
return ORA-01426 numeric overflow error message in the last
example. For user defined exceptions, they return back the
standard string, user defined exception, or if you have associated
the user defined exception with an internal Oracle error code, then
they will return the actual error message. If called outside the
exception section, they always return normal successful
completion. The SQLERRM function can also take arguments. In
that format, when you pass it the negative internal code, it returns
the actual error message, for instance, passing -01426 to the
SQLERRM function will return ORA-01426 numeric overflow.
The only exception is passing a value of +100 returns ORA-01403,
or no data found exception. If you pass the error code -01403 to
the function, then also we get the same message. When passed a
value of 0, it gives a message, normal successful completion,
which is again what we get when we call it outside the exception
section. In this demo, we will see how the internal errors are raised
by Oracle. So here we are issuing a SELECT from the
departments table for dept_id 10, which does not exist. Let us see
what happens when we run this. As we see from the Output
Console, Oracle raises an error code, ORA-01403 for no data
found, and also tells us in the stack that the error occurs at line 4.
Instead of showing this ugly stack to the end user, I would
recommend always using a WHEN OTHERS exception handler,
where you can log errors, rollback any DML, and let us throw
something more user friendly to the client's screen. Let us put a
WHEN OTHERS exception handler over here. So I'm displaying
the SQLCODE, and SQLERRM over here to show you what
these functions return. Let us run this. (Typing) So we see the
SQLCODE of 100, which also, along with -1403, stands for no
data found, which is the SQL error message we see in the Output
Console.
Predefined Exceptions
Predefined exceptions are internally defined exceptions, but with
a name. Oracle has declared them in a package, DBMS standard,
which defines the PL/SQL environment, and so they are available
globally to PL/SQL code. These are some of the most commonly
occurring errors which have been given a name by Oracle. One of
them is the NO_DATA_FOUND exception. So, as you can see in
this code snippet, we are trying to fetch from the departments
table for dept_id 10, which if nonexistent will raise the predefined
NO_DATA_FOUND exception, which you can catch by having
a specific exception handler. So here in the exception block, we
are catching that EXCEPTION with a specific
NO_DATA_FOUND exception handler. It is an internally
defined error, as we can see from its SQLCODE and SQLERRM.
So you can have specific exception handlers for the predefined
exceptions, as they have a name. This is useful as you may want
to execute specific actions for specific exceptions. Here is a
sample of some of the predefined exceptions. We have talked
about some of these in our earlier modules, like the
CASE_NOT_FOUND, TOO_MANY_ROWS,
INVLID_CURSOR, CURSOR_ALREADY_OPEN, etc.
Duplicate value on index exception is raised when you try and
store duplicate values on the database column with a unique index.
NOT_LOGGED_ON, as is evident from the name, is when you
try and issue a SQL statement without being connected to the
database. TOO_MANY_ROWS is a predefined exception. So
here in this example, we are selecting from the departments table,
but this select is going to fetch more than one row throwing this
exception. We have a specific named exception handler, WHEN
TOO_MANY_ROWS, to handle this exception. Let's run this. As
expected, when TOO_MANY_ROWS handler got the exception,
and will display the SQLCODE of -1422, and the SQL error
message of ORA-01422 exact fetch returns more than requested
number of rows.
User Defined Exceptions
Predefined exceptions allow us to name exceptions, which allows
us to handle them specifically. But what if you want to raise errors
when validation fails for business logic, or what if you want to
name Oracle internally defined exceptions which do not have any
predefined name, so that you can handle them specifically? These
are the situations in which user defined exceptions can be useful.
Let us see how we can define our own exceptions, and use them
for business logic validations. User defined exceptions are
declared simply by giving a name to the exception, followed by
the EXCEPTION keyword. So they are declared like a variable in
the declaration section, but unlike normal variables, you cannot
assign values to them. Further, unlike internally defined
exceptions, which are raised automatically by Oracle, you have to
raise them explicitly, unless they are mapped to Oracle internal
errors, which we'll talk about shortly. So, in this code snippet, we
have declared an EXCEPTION, invalid_quantity, using the
EXCEPTION keyword. We declare a NUMBER variable,
l_order_qty, and give it a value of -2. In the execution section, we
check to see if the order quantity is less than 0, and if so, RAISE
invalid_quantity exception. Notice the RAISE keyword, followed
by the exception name. This will cause the execution to jump from
here, straight to the exception handler section, to the
invalid_quantity handler, where we log it, and do whatever else is
necessary. I wanted to show you the SQLCODE, and SQL error
message values for user defined exceptions, which is 1 for
SQLCODE, and a string, User-DefinedException, for a
SQLERRM. Now let us talk about naming Oracle internally
defined exceptions. That will help us handle them specifically in
the exception section. The way you do it is by using the PRAGMA
EXCEPTION_INIT directive. A pragma is a compiler directive
that is processed at compile time, and not at runtime. PRAGMA
EXCEPTION_INIT tells the compiler to associate an
exception_name with an Oracle error number. That helps you
name an Oracle error, which allows you to write a specific handler
for it in the exception section. You put PRAGMA
EXCEPTION_INIT in the declaration section. The first
parameter it takes is an already defined user exception_name. The
second parameter is the -Oracle error you want to associate with
it. In the declaration section, it has to come after you have declared
the user defined exception, so that it can refer to it. When you see
an error stack, or a sequence of error messages, the one on the top
is the one that you can trap and handle. So here in this code snippet
we saw earlier, we have a PLS_INTEGER variable, and we were
trying to assign it a value outside its range causing unhandled
numeric flow exception. Let us see how we can write an exception
handler for this internal error. So from the error stack, take the
error code on top, which is -01426. Now let us create a user
defined exception to handle it using a pragma. So in this other
code snippet, we have the same l_num variable of type
PLS_INTEGER. We defined too_big as an EXCEPTION. Now
through PRAGMA EXCEPTION_INIT directive, we associate to
our previously declared exception too_big, the error code -1426.
Now when the error is raised in the execution block at the time of
assignment, it goes to the exception section, to the WHEN too_big
exception, and gets handled over there. If we try and print the
SQLCODE and SQLERRM, we get the correct error code of -
1426, and the SQLERRM we expected for numeric overflow. So
the error gets raised automatically. You can always raise a
predefined exception, or a pragma associated exception by name
if you wish to. We can raise, for instance, too_big ourselves, but
most situations you will let Oracle raise them when the error
occurs. Let us see how we can define user defined exceptions to
map Oracle internal errors. Here in this code snippet, we have
declared l_num as PLS_INTEGER. In the execution section, we
assign it a value outside its range. Let's run this. We get the
internal error code, ORA-01426 for numeric overflow. Let us see
how we can create a user defined exception to handle this. We
define a user defined exception too_big. Then the PRAGMA
EXCEPTION in the directive, we tell Oracle to associate too_big
with error code -1426. Then in the exception handler section, we
define an exception handler for WHEN too_big, where we display
the SQLCODE and SQL error message. Let's run this. (Typing)
As expected, the WHEN too_big exception handler called the
error, and the SQLCODE of -1426, and the error message of
numeric overflow confirms this.
Exception Scope & Propagation
Let us talk about the scope of exceptions, and how that affects the
exception propagation. The predefined exceptions from Oracle,
like a NO_DATA_FOUND, TOO_MANY_ROWS, are a part of
DBMS standard package, which makes them available globally to
PL/SQL environment. User-defined exceptions used with, or
without pragma directive follow the same rules of scope as
variables. So, in the exception in the outer block is in scope for
that block, and all the inner blocks. But the exception in the inner
block is not visible to the outer block. Just like variables, if you
declare an exception with the same name in the inner block, it
overwrites the visibility of the exception in the outer block. So
here in this first code snippet, we have an outer block where we
have declared an exception invalid_quantity. Then there is an
inner block where we can refer to it, and raise it, since it can see
the exception declared in the outer block. What if we declare an
exception with the same name in the inner block? In this case, it
will overwrite the visibility of the exception of the outer block. If
we want to refer to the exception in the outer block, inside the
inner block we can label the outer block, say, outer, and then refer
to the same named exception in the inner block using the label
like outer.invalid_quantity. Having an understanding of the scope,
now let us take a look at some examples to understand exception
propagation. In the first code snippet, we have a user defined
exception, invalid_quantity declared in the outer block, and
l_order_qty which is given a value of -2. In the inner block we
check IF l_order_qty is less than 0, and if so, RAISE the exception
invalid_quantity. Since invalid_quantity is defined in the outer
block, the inner block can see it, and can raise that exception. So
the execution goes from the RAISE statement to the
invalid_quantity exception in the outer block, where it is handled.
Note, there is also the catchall WHEN OTHERS in the outer
block. But since it is kept lower than the invalid_quantity
exception handler, invalid_quantity gets evaluated first, and is
used to handle the error. If WHEN OTHERS was kept above
invalid_quantity, the exception would have been caught there,
without getting a chance to reach invalid_quantity. This shows the
importance of having WHEN OTHERS as the last exception
handler. In the next code snippet, we have declared the
invalid_quantity EXCEPTION in the inner block also. Now, this
will overwrite the visibility of invalid_quantity of the outer block.
Even though they have the same name, they are different
exceptions for Oracle. So since there is no handler for
invalid_quantity in the inner block, the exception is thrown to the
outer block from the point it is raised, and it goes to WHEN
OTHERS in the outer block. It does not go to invalid_quantity, as
even though it has the same name as the one in the inner block,
they are two totally different exceptions for Oracle. Continuing
the same example of having invalid_quantity EXCEPTION in the
inner and outer block, we have in addition this time declared a
handler for it in the inner block. So when the exception is raised
inside the inner block, it is handled by the handler in the inner
block. The inner block END statement is reached, and then
execution resumes normally in the outer block. What happens if
there is an exception in the declaration section? It is raised to the
outer block if one exists, or it goes to the client application. For
instance, in the inner block, we have declared a NUMBER
variable, l_single, with a precision of 1, but are assigning it a two
digit number, 10. This would cause a numeric error -6502 to be
raised. The execution immediately jumps to the exception handler
section of the outer block, and if there is no outer block, it gets
thrown to the client. We may have WHEN OTHERS in the inner
block, but it does not matter. In the outer block, if there is a
specific exception handler for the error raised, as in our case, we
have defined an exception numeric error in the outer block, and
associated it with error code -6502. So the numeric error
exception handler would handle the error in the outer block. If
there was no specific exception handler, it would have gone to the
WHEN OTHERS handler in the outer block. And what about the
case if you have an exception in the exception section itself? Here
again, it is raised to the outer block if one exists, or goes to the
client application. Here in the inner block, we are raising the
invalid_quantity EXCEPTION, which makes it go to the specific
handler in the inner block. Here we assign a two-digit number to
the l_single variable, which can hold just one digit. That would
raise the numeric and value error with error code -6502. Even
though there is a WHEN OTHERS just below, still the execution
immediately goes to the exception handler in the outer block.
Numeric_error is defined in the outer block, and mapped to error
code -6502, with PRAGMA EXCEPTION_INIT. So the
exception is handled by the numeric_error handler. If it was not
defined, it would have gone to the WHEN OTHERS handler in
the outer block. Let us look at the RAISE statement, which can be
used only within an exception handler. This statement
immediately throws the exception of the same type to the outer
block if one exists; otherwise, to the client application. So in this
example, we have the RAISE invalid_quantity EXCEPTION
raised in the inner block, which makes the exception go the
invalid_quantity exception handler in the inner block. Typically,
you might want to roll back the error here, or display messages,
and then, with a RAISE statement, raise the invalid exception to
the outer block, where it is handled by the WHEN OTHERS,
where you might to do some logging, etc. Notice that
invalid_quantity handler is defined in the outer block too, but it is
not the same as invalid_quantity in the inner block, and so Oracle
handles this error with the WHEN OTHERS exception handler in
the outer block. Let us understand the scope of exceptions, and
how they propagate with this demo. We start off by defining a
user defined exception, invalid_quantity, and l_order_qty of type
NUMBER with a value of -2. In the execution section, we start
the inner block. In the execution section of the inner block, we
check if the order quantity is less than 0, and if so, we RAISE the
invalid_quantity exception. Since the exception is declared in the
outer block, it is in scope for the inner block. Since there is no
exception handler in the inner block, the exception is thrown to
the outer block, and handled in the WHEN invalid_quantity
exception handler. Let's run this. (Typing) As expected, it got
caught by the outer block's invalid_quantity exception handler
printing inside outer block invalid_quantity handler. What if we
declared the same exception, invalid_quantity, in the inner block
also? (Typing) Now this will overwrite the visibility of the
invalid_quantity exception handler in the outer block. Let's run
this now. (Typing) Note it got handled by the outer blocks WHEN
OTHERS exception handler, as is evident from the Console
Output, since there is no exception handler for this exception in
the inner block, and the outer block sees it as a totally different
exception. Now let us add the invalid_quantity exception handler
in the inner block also. (Typing) Let us run this again. Now it gets
caught by the inner block's invalid_quantity handler after which
the execution resumes in the outer block, printing the statement,
Resuming Outer Block. Now let us raise the WHEN
invalid_quantity from the inner block's exception handler using
the RAISE keyword. (Typing) Let's run this again. (Typing) From
the Console Output, we see that the inner block's invalid_quantity
exception handler still handles it, but the RAISE statement throws
invalid_quantity exception to the outer block also. Since Oracle
does not see it as the invalid_quantity exception defined in the
outer block, the WHEN OTHERS exception handler catches it as
we see from the Console Output.
Summary
Despite our best efforts, exceptions do occur. Oracle PL/SQL
provides a very flexible, and powerful exception handler
mechanism. It automatically raises internally defined exceptions
when an error occurs. It also names some commonly occurring
internal errors, like TOO_MANY_ROWS, 0 divide, etc. But then
gives us the ability to define our own exceptions, which we can
use for business logic validations, as well as for mapping to other
internal Oracle errors to specifically handle them. Thinking
through and providing specific exception handlers for situations
we can anticipate is a good idea, but we should have a WHEN
OTHERS exception handler in the outer block to handle any
unanticipated errors, and to provide a more user friendly error
message to the client, versus throwing the stack at them.
Debugging
Introduction
Hi. Welcome to Pluralsight. My name is Pankaj Jain, and
welcome to this module on Debugging. Sometimes, when you
find that the business logic in your application is not giving you
the expected results, or that errors are being raised from your
PL/SQL block, you will need to debug in order to find out what's
going wrong. In case of errors, you might want to know, along
with the error message, where exactly in your code the error
occurred, as sometimes the code may span thousands of lines.
Also, you might want to try what if situations to find how the logic
will be processed, say with an alternate value. Oracle provides
you with powerful debugging tools for PL/SQL. Oracle SQL
Developer provides us with robust debugging capabilities with an
interactive debugger. Let us take a look at this very important
topic. Let us talk about some of the debugging options. Oracle
provides us with a package DBMS_OUTPUT, which allows us to
send debug messages from our anonymous blocks, and
subprograms onto the client applications, or the Console. We have
been seeing this package in use throughout our course, but we will
talk more about it here, and point some of the settings and
limitations. DBMS_UTILITY is again a built-in package from
Oracle with several programs to help with debugging. We will
look at the format errors stack function, which formats the error
stack. We will also look at the format error back trace function,
which helps in getting the line number where the error occurred.
Lastly, we will look at SQL Developer Debugger, and see how
we can debug a piece of PL/SQL code with it.
DBMS_OUTPUT
DBMS_OUTPUT is a built-in package from Oracle, which is
very useful for debugging. We have been using the
DBMS_OUTPUT.PUT_LINE procedure extensively in our
course for showing the messages to the Console Output, and to
help us understand what was going on. This is its most common
procedure you would find and use for debugging, and displaying
messages. This procedure takes in a VARCHAR2 input message
parameter. The older version, which takes number, is still
supported for Legacy reasons. You should use the 2CHAR
function to pass in number input to this procedure. It puts an end
of line marker at the end of your input message, and puts it in the
output buffer. There are limits to message length you can pass in.
Starting with version 10.2 and above, you can pass 32767 bytes;
however, you can only pass a message length up to 255 bytes for
older versions of Oracle. So, in this code snippet, we DECLARE
l_num as PLS_INTEGER, and we assign it a value of 1234, and
then, using DBMS_OUTPUT.PUT_LINE, we print its value to
the Console. DBMS_OUTPUT.PUT is similar to PUT_LINE, in
that it also takes in a VARCHAR2 input with the same size limits.
The only difference it has with PUT_LINE is that it does not put
an end of line marker to the input message. So it just puts the
partial line in the buffer. You can call it multiple times to build
your line, as shown in this code snippet to add them to buffer
without any new line character in between them. The
DBMS_OUTPUT.NEW_LINE procedure is called without any
input parameters. After issuing calls to the PUT statements, you
should call the NEW_LINE procedure to put a new line marker
manually at the end. If you add messages to the buffer using
DBMS_OUTPUT.PUT calls, but do not call the NEW_LINE
procedure after that to put a new line marker, calls to get line will
not fetch anything. So in this code snippet, we call the PUT
procedure twice, and later call the NEW_LINE procedure to put
the end of line marker. If this line was to be fetched from the
buffer, and printed, it will look as below with no end of line
marker between the two lines. The DBMS_OUTPUT.GET_LINE
procedure can be called in another subprogram to obtain a single
line of message from the output buffer. Along with getting the line,
which is an OUT parameter, it also has an OUT status parameter,
which is 0 for a successful fetch, and 1 when there is nothing more
to fetch. So it can be called in the loop to get the messages one
line at a time, until the status becomes 1.
DBMS_OUTPUT.GET_LINES gets an area of lines back as a
CHARARR, or as DBMS_OUTPUT.LINESARRAY collection
objects. We will talk about collection objects in a later course.
Numlines parameter specifies the number of lines you want to get.
This prevents multiple roundtrips to the server.
DBMS_OUTPUT.ENABLE enables the calls to the PUT_LINE,
PUT, NEW_LINE, GET, and GET_LINES procedures. If this is
not called, then the calls to these subprograms are ignored, and
the message output is disabled. It takes in a parameter of buffer
size, which has a default value of 20,000 bytes. The minimum
value for this parameter is 2000 bytes, and the maximum is
1,000,000 bytes when specified. Starting with version 10.2, if you
pass NULL to it, or unlimited in SQL Developer, it sets the buffer
size to unlimited. DBMS_OUTPUT.DISABLE is the opposite of
DBMS_OUTPUT.ENABLE. It disables calls to other procedures,
and also disables them as an output. You may possibly come
across two errors when using DBMS_OUTPUT. ORA-20000,
ORU-10027: Buffer overflow, limit of <buffer_limit> bytes. This
error indicates that you have run out of your buffer space. You
can increase it, or, set it to unlimited, as you will see soon. ORA-
20000, ORU-10028: Line length overflow, limit of 32767 bytes.
This is self-explanatory, and you need to reduce the length of a
single line of message you are putting in the
DBMS_OUTPUT.PUT_LINE, or PUT call. You will be using
DBMS_OUTPUT.PUT_LINE in most cases for your unit tests of
PL/SQL code. To debug a piece of code, you will put the
DBMS_OUTPUT.PUT_LINE procedure calls at several places
along the code to observe the values of variables through the
output messages. It requires you to put a lot of messages all
throughout the code for effective debugging, but it's still very
widely used. If you do not enable the DBMS_OUTPUT messages,
then the code will just ignore it. One thing to be aware of is that
the messages are sent to the client only after the block, or program
has completed execution. You cannot see the output while it is
running, and you have to wait for the block to finish. The client
needs to take control of calling the enable, or disable with the
required buffer size, and then call the GET_LINE, or
GET_LINES procedure to see the output buffer. In SQLPLUS,
however, when you issue set serveroutput on, it is equivalent to
automatically calling the enable procedure of the
DBMS_OUTPUT with a default buffer size, and printing the
output by issuing the GET_LINES procedure. If we just use the
DBMS_OUTPUT.ENABLE, then you would have to call the
GET_LINES yourself. Almost always I use a set serveroutput on
in SQLPLUS so that SQLPLUS does the work of dumping the
messages for me. I generally leave the default buffer size, but if I
run into buffer overflow situations, I can set it to unlimited, or a
specific value using the syntax, for example, issuing set
serveroutput on size 200000, or issuing set serveroutput on size
unlimited to set the buffer size to unlimited. Similarly, SQL
Developer also allows us to enable the DBMS_OUTPUT and set
the buffer size as we will see shortly. I almost always use
DBMS_OUTPUT with SQLPLUS, or SQL Developer. These
tools fetch the output for me automatically. So we will only look
at using the DBMS_OUTPUT.PUT_LINE procedure in the code,
and using SQLPLUS, or SQL Developer to get and display the
output for us. This is the way I think you will be using too most
of the times.
DBMS_UTILITY
DBMS_UTILITY is a built-in package from Oracle.
FORMAT_ERROR_STACK is a procedure inside it, which
returns back a formatted error stack. It gives output similar to
SQLERRM. The difference is that it returns up to 2000 bytes of
error stack, versus SQLERRM, which can return only 512 bytes
of it, and truncates the rest. It should be placed in the exception
handlers. You should use this instead of the SQLERRM function.
So, here in the code snippet, when I try and assign a
PLS_INTEGER variable a value outside its range, the numeric
overflow exception is raised. Inside the EXCEPTION block, I use
DBMS_UTILITY.FORMAT_ERROR_STACK to get the error
stack. Note, it is the same message you will get with SQLERRM,
but just that it allows more than 512 bytes of error message if it is
longer. When an exception in, say, WHEN OTHERS, the
FORMAT_ERROR_STACK, or the SQLERRM will only give
the error code, and the message, but will not indicate the line
number in the code where the error occurred.
DBMS_UTILITY.FORMAT_ERROR_BACKTRACE gives us
the line number where the error occurred in the code. So, as we
saw earlier, if we let an error unhandled, then the Output in the
Console shows us the line number where the error happened. But
once we catch it in the WHEN OTHERS exception handler, we
lose that information. Having exception handler is a good practice,
but at SQLCODE, and SQLERRM, we lose the line number
where the error occurred. And sometimes when the code runs in
thousands of lines, it might get very difficult, and time consuming
to debug without the line number information. This is where the
FORMAT_ERROR_BACKTRACE function comes in handy,
which we place in the exception handler section. As shown in the
code snippet, it gives us the line number of the error. This is a
simple piece of code, but you will appreciate its use when our
code calls subprograms, which in turn calls other subprograms,
and the error occurs somewhere inside one of them. It tells you
the precise line number of each subprogram as the error
propagates up. You can hold its results in variables, and log it in
tables, etc. Since it does not give the error message, you would
use it, along with
DBMS_UTILITY.FORMAT_ERROR_STACK to get that piece
of information.
Demo: DBMS_OUTPUT & DBMS_UTILITY
In this demo, we create a table agent_sales to keep track of the
sales made by the agents. It has the agent_id, agent_sales_amt,
and agent_loc columns. Let us create this table and insert some
rows in it. (Typing) In this code snippet, we have declared a
variable, l_location, which is of type agent_sales.agent_loc.
L_total_sales will hold the total sales made.
L_previous_year_comm is storing the previous year's
commission. L_new_commission stores the current year's
commission, which is initialized to a value of 0. L_ratio is to get
the ratio of previous year commission to the new year's
commission. We start by initializing the l_previous_year_comm
to 0. Then we open a cursor FOR loop for selecting from the
agent_sales table where the agent_loc is l_location, which in our
case is set to New York. Inside the LOOP, we calculate the
l_total_sales as the current value of l_total_sales, plus the
agent_sales_amt. Then outside of the LOOP we check if the
l_total_sales is greater than 2000. If so, we give a 10%
commission. If it is less than, or equal to 2000, then we give a 5%
commission. Then we calculate the ratio by dividing the previous
year commission with the new year's commission. Let's run this.
(Typing) So, we notice that an exception was raised, which was
captured by the WHEN OTHERS THEN exception handler.
Inside the exception, using DBMS_OUTPUT.PUT_LINE, we are
printing the SQLERRM, which is ORA-01476, divisor is equal to
0. However, this error message does not tell us the line number.
Let us add the format_error_stack, and the
format_error_backtrace function to get that information using
DBMS_OUTPUT. Let us make that change. (Typing) If you
recall, we had learned earlier to view the Dbms Output Window
if not visible. Let's close this for now. (Typing) By going to the
View menu option, and then selecting the Dbms Output option
from there. Then we clicked on the plus sign to enable it. From
here, we choose our Connection, and press OK. The default buffer
size of 20,000 is selected. We can, however, change it over here
if we want to. Let's make it unlimited. (Typing) Let's run this block
again. (Typing) From the Output Console, we notice that the
format_error_stack function reports the same error message,
which was reported by the SQLERRM function. However, the
difference is that if the message was longer than 512 bytes, it
would have shown it up to 2000 bytes. Format_error_backtrace
gives us the line number, which is 18. At line 18, we notice that
we are calculating the ratio. According to the error message,
l_new_commission is reported to be 0. But we are calculating the
l_new_commission for l_total_sales greater than 2000, as well as
for the case where it is less than, or equal to 2000. So it seems that
we are covering both the situations, and we should not have a 0
value for it. So let us now use some
DBMS_OUTPUT.PUT_LINE statements to debug and find
what's going on. Here is the same piece of code, but now, we will
use DBMS_OUTPUT to debug, and see what's going on. We have
put a DBMS_OUTPUT.PUT_LINE inside the LOOP telling us
the Agent Id, Agent Sales Amt, and l_total_sales as it happens in
the LOOP. We have put another one outside the LOOP telling us
the l_total_sales amount before the IF clause. Then we have put
the OUTPUT statement inside the IF, as well as the ELSIF, telling
us the l_new_commission calculated here. With these OUTPUT
statements, we will try and figure out what's happening. Let's run
it again. (Typing) In the Output Console, we see that it reports
Agent Id 20 for location New York, with a Sales Amt of 1000.
But l_total_sales is still NULL. Oh, okay. So we did not initialize
l_total_sales earlier. If you remember, a NUMBER variable not
initialized has a value of NULL, and when we add a NULL
l_total_sales to the agent_sales_amt in the LOOP, it also results
in a NULL, as anything added to a NULL remains a NULL. So
that is why the DBMS_OUTPUT statement outside the LOOP
still reports l_total_sales before the IF clause as NULL. Now if
you remember, another variable in the IF clause, or the ELSIF
clause makes the condition FALSE. Since there is no ELSE clause,
the execution comes out of the IF clause with l_new_commission
not getting assigned any value, so it retains a value of 0, with
which it was initialized. Since l_new_commission is in the
denominator for the ratio, it causes a 0 divide error as reported by
the format_error_backtrace for line 23. (Typing) So the fix would
be to initialize the l_total_sales to 0 before the LOOP starts, so
that it does not stay NULL. Let's make that change. (Typing) Let's
run it again. Now it calculates an l_total_sales of 1000, and then
based on that, it goes to the ELSIF clause, assigning it a
commission of 5%, making the l_new_commission as 100, as
expected. So this is how using the DBMS_OUTPUT.PUT_LINE
statement you can debug the code by placing these statements at
various places along the code, and observing the values of the
variables in order to see what's going on.
SQLDeveloper Debugger: Privileges & Setup
Let us try and see how we can debug using SQL navigator
debugger. Here is the same piece of code. I have removed all the
DBMS_OUTPUT statements, but if you want, you can leave them
there, and SQL Navigator will display them too. Before we do that,
we have to grant some privileges to our session user in order to
enable it to debug the code. Let's do that first. Our session user
needs debug connect session, and debug any procedure user
privileges to run the debugger. For this, you have to request your
administrator to grant your session user those privileges, or if you
are running it on your local machine, then login as sys, or sys-
DBA, or as a user, which has DBA privileges to grant your user
those privileges. I have another session opened up here where I
am logged in as a DBA user. Here, we are running the commands
GRANT DEBUG CONNECT SESSION TO demo, and GRANT
DEBUG ANY PROCEDURE TO demo. Let's run this. So our
demo user now has the necessary grants. Let's go back to our
original session. (Typing) Now let me show you how to setup
SQL Navigator for debugging anonymous blocks. Let us go to
Tools, then click Preferences. Then click on the Debugger option.
The Show Debug Actions on the Main Debug Menu will show
you the options when you are running the code in the debug mode.
Let us check this option. The other options are to Show Tool Tip
in Code Editor, and to Show Action Buttons in Log Window
While Debugging. These are checked by default, and I will leave
them so. At the bottom is the Start Debugging Option with three
values. Run Until a Breakpoint Occurs is the default option. Then
there is the Step Over and Step Into option. We will explain these
options when we start debugging. For now, let us check to Step
Into for anonymous block. If you are running other program units,
like procedures, or functions, or packages, then you might want
to leave it at the default option of Run Until a Breakpoint Occurs.
With anonymous blocks, we need to use Step Into option, and that
will make it come at the first line of the anonymous block, and
stop there waiting for us to put breakpoints, and give it further
instructions from that point on. Let us say OK, and come out.
Debugging with SQLDeveloper
Now you can right click, or press Ctrl+Shift+F10 to start
debugging. Let's right click. Let's choose a Debug option. Alright,
the debug started. If you notice, it opens up another window, and
names it ANONYMOUS_BLOCK for the debug. We see the red
arrow. This is the current line of execution sitting right at the top
of the block. Let us first set some breakpoints. We have to click
on the site pane in order to set a breakpoint. And you click on the
breakpoint again in order to remove it. Oracle allows us to set
conditional breakpoints, so you can define a condition for a
breakpoint, and if that condition is TRUE, only then the execution
stops at that breakpoint. Let us create a breakpoint, and set a
condition for it. Let's first click and create a breakpoint. Now right
click, and choose Edit Breakpoint. Go to the Conditions tab. The
Condition is what you would put in the WHERE clause without
the WHERE keyword, and using the variables that code can see
at this point. So it can see the l_location variable at this point. Let
us put a Condition l_location equal to WA. (Typing) This will
evaluate to false, as our location is set to New York, and the
execution should not stop here. As a part of the Actions, the
default is to Halt the Execution, but you can Enable, or Disable a
Group of Breakpoints if you want to from the dropdown list. Let
us leave it as is for now. Click OK. Let us put a breakpoint inside
the LOOP. Let's put another breakpoint on the IF, and let's put
another breakpoint on the l_ratio. I want to show you now the
buttons on the top. The very first red square is the button you can
click to Terminate the debug session. The next one shows the
current Execution Point, or the arrow. The one after that is Step
Over option, which can also be invoked using the F8 shortcut.
Pressing F8 will cause it to execute the current statement, and then
go over to the next executable statement. The next one is the Step
Into, or F7 option. This option will also execute the current
statement, but if the current statement is a call to another program,
say another procedure, and if that procedure is compiled in the
debug mode, it'll take you inside the other procedure, and then you
can see what is going on there. Step Over, or F8 will just execute
that procedure, and go to the next line without going inside the
procedure. However, if you have set a breakpoint inside the
procedure, then F8 will go inside the procedure, and stop at that
breakpoint. In our case, both Step Into and Step Over will behave
the same way of executing the current line, and going over to the
next one as there is no other program being called. Step Out will
make you go out of a program to the program on top. So, if you
were inside a procedure, by calling Step Into, it will execute the
remaining portion of the procedure, and take us out of it to our
main code. But if you are not drilled down into another procedure,
then it will just continue the execution to the next breakpoint. Step
to End of Method will go to the end of method, unless there is a
breakpoint, where it will stop. Finally, is the Resume, which will
make the execution continue, and will make it stop at the next
breakpoint. Let's hit the Resume first. It stops at the second
breakpoint, and not the first. Since you have put a condition,
l_location equal to WA, and since the value of the variable is set
to New York, the evaluation fails, and so the execution does not
stop at the breakpoint. Let me show you now the debugging views
available. Let's come down and click on the Breakpoint's tab. Here,
we see all the breakpoints set with line numbers. Had it been a
name program unit, like a procedure, or a function, it would have
told you the procedure name, and the line, but since we are just
running an anonymous block, so to us it just says Oracle Block.
The next tab with a star is the Smart Data. Oracle will try and
figure the data that it thinks is useful at this point, and shows it.
So it shows us the agent_sales_var record with a plus sign. If you
click on the plus sign, you can see the fields inside, the
AGENT_ID 20, the AGENT_LOCATION NY, and the
AGENT_SALES_AMT 1000 at this point of time.
L_TOTAL_SALES has a value of 0, as we had initialized it to a
value of 0 after we debugged it with the DBMS_OUTPUT in the
last demo. The next window with 123 is the Data tab that shows
all the values for all the variables at this point. This will also
include the variables like L_RATIO, which have not been used in
the code so far, and shows its current value of NULL. Lastly is
the tab with the magnifier icon, or the Watches tab. This is where
you set the watch for any variable you are particularly interested
in. For example, let us keep a watch for the l_total_sales. Let's go
to the Data tab, click on L_TOTAL_SALES, right click, and click
on Watch. Now this adds it to the Watch window. Let's go to the
Data tab again. Let's click on the L_NEW_COMMISSION and
right click again. Over here, you can see there is something called
Inspect. Let's click on this. What this does is that it brings up
L_NEW_COMMISSION in a separate window for us to inspect,
and shows its current value. Let's make the size of the window
small. (Typing) And let's keep it here. Now using this window,
we can watch the value of L_NEW_COMMISSION as it changes
along the flow of execution. Let us now click onto Step Into, or
F7. This makes it complete the first iteration, and brings it back
onto the top of the LOOP. Now this time, let's go over and hit Step
Over, or F8. It brings it back to the end of the LOOP, since there
was no other record for location New York. Let's click on Step
Over again. It goes to the next line of code. Let's hit Step Over
again. Now it brings us to the top of the IF statement, and at this
point of time, if you notice, L_TOTAL_SALES has a value of
1000. So, with this value, the IF clause is going to fail, and it's
going to go to the ELSIF clause, where l_total_sales is less than,
or equal to 2000. With that, the l_new_commission will be
assigned a value of 5% of 2000. I want to show you know how
you can change the value of a variable along the flow of execution,
which helps us with the what if analysis. So currently,
l_total_sales has a value of 1000. Let's click it. Let's right click,
and say Modify Value. And instead of 1000, let's make the value
3000, and click OK. Now it should satisfy the IF condition of IF
l_total_sales is greater than 2000, and now the new commission
should get a 10% value of 2000. Let's hit Resume. So now, the
l_new_commission was assigned a 10% value of 2000, or 200. So
this shows how we can change the value of a variable along the
flow of execution in order to see an alternate flow of execution,
which helps you in doing what if analysis. Now let's hit the
Resume button again. This completes our debugger session, and
if you notice, the anonymous block window is still open. We can
just click it, and close it, and we come back to our original window.
So we saw how we can set breakpoints, put conditions for them,
use various options, like Step Into, Step Over, Resume, etc., to
influence the flow of execution. We saw the various data views,
watches, and inspectors Oracle debugger provides us for
watching the values of the variables along the flow of execution,
as well as to change the values, if we so desire, to see alternate
flows, and to do what if analysis.
Debugging an Exception with SQLDeveloper
Let us re-create the 0 divide error by not initializing the
l_total_sales, and see how the debugger can help us catch that.
Let us remove the initialization. (Typing) Now let us start the
debugger session by right clicking and selecting Debug. (Typing)
Notice, it remembers the Inspector we had kept the last time, and
brings it up automatically. Let us put the breakpoints again in the
middle of the LOOP, and the beginning of the IF statement, and
on the l_ratio statement. Let us hit Resume. (Typing) This brings
it to the first breakpoint. And notice, now the
L_NEW_COMMISSION has a value of 0 since it was initialized
in the declaration section. Let us hit Step Into, or F7. (Typing)
This finishes the first iteration, and brings it to the top of the
LOOP for the second iteration. Now, in the Data window, we
notice that L_TOTAL_SALES at this point of time is NULL. This
is despite the LOOP finding employee ID 20, with a sales amount
of 1000. Let us hit Resume again. It brings us to the beginning of
the IF statement. From the Data tab, L_TOTAL_SALES is still
NULL. Let us do a Step Into, or F7. Because of the NULL variable,
the IF condition evaluates to FALSE, and it goes to the ELSIF
condition. Let's hit F7 again. Again, the NULL variable causes the
ELSIF condition to evaluate to FALSE, and brings it to the end of
the IF statement. Let us hit Step Into again. (Typing) Now we are
at the statement where the ratio is to be calculated. At this point,
we notice that the L_NEW_COMMISSION still has an initial
value of 0. We can watch the value of L_NEW_COMMISSION
in not only the Inspector tab, but if we want, we can also look at
the Data tab, or the Smart Data tab, which will tell you the value
of L_NEW_COMMISSION, and other variables.
L_NEW_COMMISSION is 0, as it was not assigned any new
value in the IF, or the ELSIF clauses. Let us hit F7. The 0 value
for L_NEW_COMMISSION causes the 0 divide error to be
raised, and the execution flows to the EXCEPTION section. Let's
hit F7 again. And it goes to WHEN OTHERS, since we do not
have any specific exception for 0 divide. Let us hit Resume now.
(Typing) A 0 divide error is raised, and the debugger exits
removing the debugger views. We could have kept a breakpoint
also in the EXCEPTION section to make the debugger window
stay, to show us the messages. But we can see it again by going to
the View menu, and selecting Log. So here we see that the
debugger exited the session. We see the DBMS_OUTPUT
statements we had put in the EXCEPTION section showing us the
0 divide error. So the debugger can help us set conditional
breakpoints, watch the values along the flow of execution, and
interactively modify them if you want to. This is so much better
than using DBMS_OUTPUT statements, as it does not require me
to modify my code to put the DBMS_OUTPUT statements, I do
not have to wait until the execution completes to see the
DBMS_OUTPUT statements. It allows me to watch the variable
values as they change in the execution, and modify them if
necessary. So the SQL Developer Debugger gives us a lot of
flexibility and control when we have to debug our code.
Summary
Knowing how to debug your code is equally important to
knowing how to write good code when something goes wrong.
Oracle provides us with several easy to use programs and tools to
debug our code effectively. DBMS_OUTPUT.PUT_LINE
procedure is very useful for understanding how your code is
flowing, and debugging, during your unit tests. DBMS_UTILITY
package provides us with the format_error_stack, and
format_error_backtrace, which provides you with the stack of
exceptions as they occur, along with the line numbers where they
occur, and they're very handy to debug large pieces of code. SQL
Developer provides a sophisticated debugger, which allows you
to put breakpoints, and walk through your code interactively,
making it extremely easy to debug. This is my preferred way of
debugging, as it does not require you to inject your code with a lot
of DBMS_OUTPUT statements.

Potrebbero piacerti anche