January/February 2017
PL/Scope is a powerful code analysis tool for PL/SQL program units, first introduced in Oracle Database 11g. Now with Oracle Database 12c Release 2 (Oracle Database 12.2), that analysis includes the SQL statements inside your PL/SQL code base. This significant enhancement enables developers to perform comprehensive impact analysis of changes to tables, such as changing a column’s name.
Turning on PL/ScopePrior to Oracle Database 12.2, you would start PL/Scope by executing this statement in your session:
ALTER SESSION SET plscope_settings='identifiers:all' /
After that, whenever you compiled a PL/SQL program unit, the compile would gather information about all PL/SQL identifier usages and make that data available via the ALL_IDENTIFIERS view.
In Oracle Database 12c Release 2, to start PL/Scope and gather information about SQL statements, you execute this statement:
ALTER SESSION SET plscope_settings='identifiers:all, statements:all' /
Then, after compilation of a program unit, you will find information about SQL statements in the ALL_STATEMENTS view, and the ALL_IDENTIFIERS view contains information about SQL identifiers, such as table and column names.
When you compile a program unit with PL/Scope enabled for statement gathering, the PL/SQL compiler places a row in ALL_STATEMENTS for each SQL statement in the program unit. The following information is recorded for those statements:
Before I go further into my exploration of PL/Scope, I need to create some database objects. For this article, I am going to keep track of endangered species, their location, and where research is done on those species.
So I create three tables, as shown in Listing 1.
Code Listing 1: Creating three tables
CREATE TABLE species_locations ( id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, location VARCHAR2 (100) UNIQUE ) / CREATE TABLE endangered_species ( id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name VARCHAR2 (100) UNIQUE, location_id NUMBER REFERENCES species_locations (id) ) / CREATE TABLE research_locations ( id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name VARCHAR2 (100) UNIQUE, location_id NUMBER REFERENCES species_locations (id) ) /
In the table creation, note that GENERATED AS IDENTITY is an Oracle Database 12c Release 1 feature.
After enabling PL/Scope, I then create a package, species_mgr, to help manage data in these tables, as shown in Listing 2.
Code Listing 2: Creating the species_mgr package
CREATE OR REPLACE PACKAGE species_mgr
AUTHID DEFINER
IS
FUNCTION location_id_from_name (
location_in IN species_locations.location%TYPE)
RETURN species_locations.id%TYPE;
END;
/
CREATE OR REPLACE PACKAGE BODY species_mgr
IS
FUNCTION location_id_from_name (
location_in IN species_locations.location%TYPE)
RETURN species_locations.id%TYPE
IS
l_return species_locations.id%TYPE;
BEGIN
SELECT sl.id
INTO l_return
FROM species_locations sl
WHERE sl.location = location_id_from_name.location_in;
RETURN l_return;
END;
END;
/
Next, I populate my tables, as shown in Listing 3.
Code Listing 3: Populating the three tables
BEGIN
/* Amazon Data */
INSERT INTO species_locations (location)
VALUES ('Amazon');
INSERT INTO endangered_species (name, location_id)
VALUES (
'Black Spider Monkey',
species_mgr.location_id_from_name ('Amazon'));
INSERT INTO endangered_species (name, location_id)
VALUES ('Sloth', species_mgr.location_id_from_name ('Amazon'));
INSERT INTO endangered_species (name, location_id)
VALUES (
'Amazon River Dolphin',
species_mgr.location_id_from_name ('Amazon'));
INSERT INTO endangered_species (name, location_id)
VALUES (
'Poison Dart Frog',
species_mgr.location_id_from_name ('Amazon'));
INSERT INTO endangered_species (name, location_id)
VALUES ('Macaw', species_mgr.location_id_from_name ('Amazon'));
INSERT INTO endangered_species (name, location_id)
VALUES ('Jaguar', species_mgr.location_id_from_name ('Amazon'));
INSERT INTO research_locations (name, location_id)
VALUES (
'Smithsonian Tropical Research Institute',
species_mgr.location_id_from_name ('Amazon'));
INSERT INTO research_locations (name, location_id)
VALUES ('IBAMA', species_mgr.location_id_from_name ('Amazon'));
/* Galapagos Data */
INSERT INTO species_locations (location)
VALUES ('Galapagos');
INSERT INTO endangered_species (name, location_id)
VALUES (
'Sea Turtle',
species_mgr.location_id_from_name ('Galapagos'));
INSERT INTO endangered_species (name, location_id)
VALUES (
'Leatherback Turtle',
species_mgr.location_id_from_name ('Galapagos'));
INSERT INTO endangered_species (name, location_id)
VALUES ('SEI Whale', species_mgr.location_id_from_name ('Galapagos'));
INSERT INTO endangered_species (name, location_id)
VALUES (
'Green Turtle',
species_mgr.location_id_from_name ('Galapagos'));
INSERT INTO endangered_species (name, location_id)
VALUES (
'Giant Tortoise',
species_mgr.location_id_from_name ('Galapagos'));
INSERT INTO endangered_species (name, location_id)
VALUES (
'Galapagos Penguin',
species_mgr.location_id_from_name ('Galapagos'));
INSERT INTO research_locations (name, location_id)
VALUES (
'Charles Darwin Foundation',
species_mgr.location_id_from_name ('Galapagos'));
INSERT INTO research_locations (name, location_id)
VALUES (
'Galapagos Science Center',
species_mgr.location_id_from_name ('Galapagos'));
COMMIT;
END;
/
Finally, I create two additional PL/SQL procedures—add_species and show_location_info—that work with these three tables, as shown in Listing 4.
Code Listing 4: Creating the add_species and show_location_info procedures
CREATE OR REPLACE PROCEDURE add_species (NAME_IN IN VARCHAR2,
location_in IN VARCHAR2)
AUTHID DEFINER
IS
l_info VARCHAR2 (32767);
BEGIN
INSERT INTO endangered_species (name, location_id)
VALUES (NAME_IN, species_mgr.location_id_from_name (location_in));
SELECT s.id || '-' || s.name || '-' || l.location
INTO l_info
FROM endangered_species s, species_locations l
WHERE s.location_id = l.id;
DBMS_OUTPUT.put_line ('Inserted ' || l_info);
END;
/
CREATE OR REPLACE PROCEDURE show_location_info (
location_id_in IN species_locations.id%TYPE)
AUTHID DEFINER
IS
BEGIN
FOR rec IN ( SELECT r.location_id, r.name
FROM research_locations r
WHERE r.location_id = show_location_info.location_id_in
ORDER BY r.name)
LOOP
DBMS_OUTPUT.put_line (rec.location_id || '-' || rec.name);
END LOOP;
END;
/
I’ve got my code base. Now I’d like to do some analysis of it.
Program Unit “Dump” of Identifiers and StatementsLet’s take a look at what PL/Scope can tell us about the add_species procedure. And let’s start with the views that will tell us what’s going on.
The two views with PL/Scope-generated information, ALL_IDENTIFIERS and ALL_STATEMENTS, share many columns, such as USAGE_ID and SIGNATURE. And there are columns unique to each, such as STATEMENT_TYPE in ALL_STATEMENTS.
The USAGE_ID value is unique across the ALL_IDENTIFIERS and ALL_STATEMENTS views for a specific program unit. That makes it easier to understand how statements and identifiers relate to each other.
The query in Listing 5 executes a union of these two views for all rows related to add_species. It uses the CONNECT BY syntax to display identifiers and statements intuitively.
Code Listing 5: Looking at ALL_IDENTIFIERS and ALL_STATEMENTS for references to add_species
WITH one_obj_name AS (SELECT 'ADD_SPECIES' object_name FROM DUAL)
SELECT plscope_type,
usage_id,
usage_context_id,
LPAD (' ', 2 * (LEVEL - 1)) || usage || ' ' || name usages,
line,
col
FROM (SELECT 'ID' plscope_type,
ai.object_name,
ai.usage usage,
ai.usage_id,
ai.usage_context_id,
ai.TYPE || ' ' || ai.name name,
ai.line,
ai.col
FROM all_identifiers ai, one_obj_name
WHERE ai.object_name = one_obj_name.object_name
UNION ALL
SELECT 'ST',
st.object_name,
st.TYPE,
st.usage_id,
st.usage_context_id,
'STATEMENT',
st.line,
st.col
FROM all_statements st, one_obj_name
WHERE st.object_name = one_obj_name.object_name)
START WITH usage_context_id = 0
CONNECT BY PRIOR usage_id = usage_context_id
Figure 1 displays the output of the Listing 5 query from Oracle SQL Developer.

Figure 1. Result of query for references to add_species in ALL_IDENTIFIERS and ALL_STATEMENTS
Here are some important takeaways from this report:
Here’s a very common developer question: where and how is a table used in my PL/SQL code? With ALL_DEPENDENCIES, I can find out if a table is referenced, but that reference could be from a %ROWTYPE declaration or a SELECT or a DELETE. There’s no way of telling which. And I certainly can’t find out where and how a particular column is being used.
In addition, I could easily enough write a query against ALL_SOURCE to find all the program units that contain the string “LOCATION_ID”:
SELECT name, line, text FROM ALL_SOURCE WHERE upper (text) LIKE '%LOCATION_ID%'
Unfortunately, that query will return all kinds of false positives, including
With PL/Scope in Oracle Database 12.2, you can now definitively locate all references to the LOCATION_ID column of the ENDANGERED_SPECIES table and nothing else.
Of course, it would be nice to not have to write a special-purpose query every time you want to locate references to another column, so I’ve written the procedure in Listing 6 for just that purpose.
Code Listing 6: Package for looking for column references in ALL_IDENTIFIERS and ALL_STATEMENTS
CREATE OR REPLACE PROCEDURE show_column_usages (owner_in IN VARCHAR2,
table_in IN VARCHAR2,
column_in IN VARCHAR2)
AUTHID DEFINER
IS
BEGIN
DBMS_OUTPUT.put_line (
'References to ' || owner_in || '.' || table_in || '.' || column_in);
DBMS_OUTPUT.put_line ('');
FOR rec
IN ( SELECT idt.line,
idt.owner || '.' || idt.object_name code_unit,
RTRIM (src.text, CHR (10)) text
FROM all_identifiers idt, all_source src
WHERE idt.usage = 'REFERENCE'
AND idt.signature =
(SELECT idt_inner.signature
FROM all_identifiers idt_inner
WHERE idt_inner.object_name =
show_column_usages.table_in
AND idt_inner.TYPE = 'COLUMN'
AND idt_inner.name =
show_column_usages.column_in
AND idt_inner.usage = 'DECLARATION'
AND idt_inner.owner = owner_in)
AND idt.line = src.line
AND idt.object_name = src.name
AND idt.owner = src.owner
ORDER BY code_unit, line)
LOOP
DBMS_OUTPUT.put_line (
'In ' || rec.code_unit || ' on ' || rec.line || ': ' || rec.text);
END LOOP;
END;
/
When I execute the show_column_usages procedure as follows:
BEGIN
show_column_usages (owner_in => USER,
table_in => 'ENDANGERED_SPECIES',
column_in => 'LOCATION_ID');
END;
/
I see the following results:
STEVEN.ENDANGERED_SPECIES.LOCATION_ID References
In STEVEN.ADD_SPECIES on 7:
INSERT INTO endangered_species (name, location_id)
In STEVEN.ADD_SPECIES on 13:
WHERE s.location_id = l.id;
In STEVEN.SHOW_SPECIES on 8:
INSERT INTO endangered_species (id, name, location_id)
In STEVEN.SHOW_SPECIES on 14:
WHERE s.location_id = l.id;
Note that there are no references to the RESEARCH_LOCATIONS table. Nice!
Let’s take a look at how the show_column_usages procedure gets its job done.
First, interestingly, note that I am not even querying the new-to-Oracle Database 12.2 ALL_STATEMENTS view. That’s because I am not interested in SQL statements, per se—simply which SQL identifiers are used, and where. So ALL_IDENTIFIERS contains all the information I need.
Next, because I am looking for all occurrences of a specific identifier, I am working with the SIGNATURE column. PL/Scope assigns a unique identifier to each distinct identifier in my code base, and that’s what is stored in the SIGNATURE column.
So the first thing I need to do is find the signature of the LOCATION_ID column. The inner query inside the show_column_usages procedure does this for me:
SELECT idt_inner.signature
FROM all_identifiers idt_inner
WHERE idt_inner.object_name = show_column_usages.table_in
AND idt_inner.owner = owner_in
AND idt_inner.name = show_column_usages.column_in
AND idt_inner.TYPE = 'COLUMN'
AND idt_inner.usage = 'DECLARATION'
This query says: get the signature of the declaration of the column identifier, for the names specified by the procedure parameters. I then use that inner query for an equality match on idt.signature. In the query below, for readability purposes, I use <my_column_signature> in place of the full query. Then the outer query becomes
SELECT idt.line,
idt.owner || '.' || idt.object_name code_unit,
RTRIM (src.text, CHR (10)) text
FROM all_identifiers idt, all_source src
WHERE idt.usage = 'REFERENCE'
AND idt.signature = <my_column_signature>
AND idt.line = src.line
AND idt.object_name = src.name
AND idt.owner = src.owner
This query finds all the references to my_column_signature in ALL_IDENTIFIERS. I then join those identifier rows to the ALL_SOURCE table, so that I can display the line of source code.
Hey, that’s not so hard!
How Are My Tables Used?The primary purpose of PL/SQL is to build APIs around data structures, controlling access to tables and ensuring data integrity and consistency. This means that PL/SQL code is usually full of SQL statements, and that can make it difficult to keep track of which tables are used where and how. With PL/Scope in Oracle Database 12.2, it is now easy to get answers to questions such as “Which program units perform an INSERT into a particular table?” The show_dml_statements_on procedure in Listing 7 provides a way to answer this and other data manipulation language (DML) use questions.
Code Listing 7: The show_dml_statements_on procedure
CREATE OR REPLACE PROCEDURE show_dml_statements_on (
owner_in IN VARCHAR2,
table_in IN VARCHAR2,
statement_type_in IN VARCHAR2 DEFAULT 'ALL')
IS
BEGIN
DBMS_OUTPUT.put_line (
'Locations of '
|| statement_type_in
|| ' DML Statements Against '
|| owner_in
|| '.'
|| table_in);
DBMS_OUTPUT.put_line ('');
FOR rec
IN (SELECT idt.line,
idt.owner || '.' || idt.object_name code_unit,
RTRIM (src.text, CHR (10)) text
FROM all_identifiers idt, all_source src, all_statements st
WHERE idt.usage = 'REFERENCE'
AND idt.TYPE = 'TABLE'
AND idt.name = table_in
AND idt.owner = owner_in
AND idt.line = src.line
AND idt.object_name = src.name
AND idt.owner = src.owner
AND idt.usage_context_id = st.usage_id
AND ( st.TYPE = statement_type_in
OR statement_type_in = 'ALL'))
LOOP
DBMS_OUTPUT.put_line (
'In ' || rec.code_unit || ' on ' || rec.line || ': ' || rec.text);
END LOOP;
END;
/
When I execute the show_dml_statements_on procedure with this block:
BEGIN show_dml_statements_on (USER, 'ENDANGERED_SPECIES', 'INSERT'); END; /
I see the following output:
Locations of INSERT DML Statements Against STEVEN.ENDANGERED_SPECIES
In STEVEN.ADD_SPECIES on 7:
INSERT INTO endangered_species (name, location_id)
In STEVEN.SHOW_SPECIES on 8:
INSERT INTO endangered_species (id, name, location_id)
Identifying SQL Injection Vulnerabilities
SQL injection presents one of the greatest vulnerabilities for database developers, and dynamic SQL is the path followed by those who want to inject malicious code.
So it would be very helpful to be able to easily identify all program units that execute dynamic SQL or dynamic PL/SQL statements.
Prior to Oracle Database 12.2, the best you could do was to write queries against ALL_SOURCE to look for strings such as EXECUTE IMMEDIATE and OPEN <cursor> FOR and DBMS_SQL. It is, however, very difficult to make those queries cover all possible cases without, in effect, parsing PL/SQL code. For example, I could write code like this:
BEGIN EXECUTE /* a comment */ IMMEDIATE ... END;
Now with the enhanced PL/Scope in Oracle Database 12.2, it’s a breeze to write a query to unambiguously locate all native dynamic SQL statements, because two possible values in the TYPE column of ALL_STATEMENTS are EXECUTE IMMEDIATE and OPEN.
In addition, if you are able to convince your DBA to recompile the SYS.DBMS_SQL package with PL/Scope turned on, you can then query the contents of ALL_IDENTIFIERS for program units that reference that built-in package in any way, as shown in Figure 2.

Figure 2. A better search for SQL injection risks in ALL_IDENTIFIERS
In Figure 2, lines 1 through 9 show how easy it is to find all statements in your PL/SQL code base that take advantage of native dynamic SQL. Lines 11 through 25 show how to find all references to the DBMS_SQL package. Note in particular the subquery on lines 17 through 22. I get all the signatures of identifiers declared in the DBMS_SQL package specification. I then look, in line 16, for any matches for those signatures across my code base, excluding any code owned by SYS.
Getting a Handle on SQL HintsAs perhaps too many developers know, you can add hints to your SQL statements to direct the optimizer to alter the plan it comes up with for the execution of your statement. Tom Kyte has called hints a “path of last resort,” because (1) there are usually ways to change the SQL statement or update statistics to fix the problem, (2) your hints might block the optimizer from coming up with a better plan, and (3) data changes over time and could cause a hint that works today to cause big problems tomorrow.
Unfortunately, when developers are working hard on deadline, they will take the shortest, quickest path to a solution that works right now. Over time, applications can end up littered with hints that cause problems that are hard to track down.
But tracking down old hints is not so hard in Oracle Database 12.2. First, compile your program units with statement gathering enabled. Then run the following query against ALL_STATEMENTS to find all statements with one or more hints:
SELECT owner,
object_name,
line,
full_text
FROM all_statements
WHERE has_hint = 'YES'
Know Thy Code, Improve Thy Code
Being able to slice and dice code in a variety of ways to analyze the impact of changes or understand more fully how that code is written is critical to your success. Enhancements in PL/Scope in Oracle Database 12c Release 2 give you powerful new tools for achieving a deep and thorough understanding of your code. This improved understanding will result in the development and delivery of higher-quality applications.
Next StepsLEARN more about Oracle Database 12c.
MEET the Oracle Developer Advocates team.
TEST your SQL and PL/SQL knowledge.
READ more about PL/Scope.
WATCH Feuerstein’s Practically Perfect PL/SQL videos.
READ more about binding PL/SQL datatypes
DISCLAIMER: We've captured these popular historical magazine articles for your reference. Links etc contained within these article possibly won't work. These articles are provided as-is with no guarantee that the information within them is the best advice for current versions of the Oracle Database.