Finding dead code using PL/Scope

Can we use PL/Scope to find dead and potentially dead PL/SQL code? Yes! And it takes less than a second!

PL/Scope records declarations and usages of procedures and functions, so it should be easy to do a quick comparison of what’s declared and what’s actually used.

Deleting code makes me feel good, because it clears up clutter and usually means I’ve found a better way to perform a task.

So how do I find code that can be deleted? If you have a properly layered hard-shell PL/SQL-application (also often referred to as #SmartDB), PL/Scope will become very useful and making the task fairly easy, provided that you hav a a good structure in your application.

To demonstrate how this can be achieved, I’ve set up a test-case, where I create an object-type, a package, a standalone function and a standalone procedure with a few references between them:

CREATE OR REPLACE TYPE myType AS OBJECT (
   TypeAttribute INTEGER,
   MEMBER PROCEDURE ReferencedTypeProcedure,
   MEMBER FUNCTION UnreferencedTypeFunction RETURN PLS_INTEGER
);
/

CREATE OR REPLACE TYPE BODY myType AS
   MEMBER PROCEDURE ReferencedTypeProcedure IS
   BEGIN
      NULL;
   END;
   
   MEMBER FUNCTION UnreferencedTypeFunction RETURN PLS_INTEGER IS
   BEGIN
     RETURN 42;
   END;
END;
/

CREATE OR REPLACE PACKAGE myPackage AS
   PROCEDURE MyUnReferencedPublicProcedure(pInput PLS_INTEGER);
   PROCEDURE MyReferencedPublicProcedure(pInput PLS_INTEGER);
   FUNCTION  MyUnReferencedPublicFunction(pInput PLS_INTEGER) RETURN PLS_INTEGER;
   FUNCTION  MyReferencedPublicFunction(pInput PLS_INTEGER) RETURN PLS_INTEGER;
   FUNCTION  MyReferencedPublicFunction(pInput VARCHAR2) RETURN VARCHAR2;
END myPackage;
/

CREATE OR REPLACE PACKAGE BODY myPackage AS
   PROCEDURE MyReferencedInternalProcedure(pInput PLS_INTEGER) IS
   BEGIN
     NULL;
   END;

   PROCEDURE MyUnReferencedPublicProcedure(pInput PLS_INTEGER) IS
   BEGIN
     NULL;
   END;

   PROCEDURE MyUnreferencedInternalProcedure(pInput PLS_INTEGER) IS
   BEGIN
     NULL;
   END;

   PROCEDURE MyReferencedPublicProcedure(pInput PLS_INTEGER) IS
      lMyType myType := myType(NULL);
   BEGIN
      lMyType.ReferencedTypeProcedure;
   END;

   FUNCTION MyUnReferencedPublicFunction(pInput PLS_INTEGER) RETURN PLS_INTEGER IS
   BEGIN
     RETURN pInput;
   END;
   
   FUNCTION MyReferencedPublicFunction(pInput PLS_INTEGER) RETURN PLS_INTEGER IS
   BEGIN
      MyReferencedInternalProcedure(pInput);
      RETURN pInput;
   END;

   FUNCTION MyReferencedPublicFunction(pInput VARCHAR2) RETURN VARCHAR2 IS
   BEGIN
      MyReferencedInternalProcedure(pInput);
      RETURN pInput;
   END;
END myPackage;
/

CREATE OR REPLACE FUNCTION MyStandaloneFunction (pInput PLS_INTEGER) RETURN PLS_INTEGER
AS
BEGIN
   RETURN pInput * 2;
END;
/

CREATE OR REPLACE PROCEDURE MyStandaloneProcedure (pInput PLS_INTEGER) AS
BEGIN
   IF MyStandaloneFunction(pinput) > myPackage.MyReferencedPublicFunction(pInput) THEN
      myPackage.MyReferencedPublicProcedure(pInput);
   END IF;
end;
/

First – before you decide to delete code, make sure that your source has been compiled so that PL/Scope-metadata has been collected for all the involved schemas. You want all relevant objects to be compiled with ‘IDENTIFIERS:ALL’ to find both identifiers/references and statements:

SELECT type, plscope_settings, COUNT(*)
FROM ALL_PLSQL_OBJECT_SETTINGS
WHERE OWNER IN ('MySchema1', 'MySchema2')
GROUP BY type, plscope_settings
ORDER BY type, plscope_settings;

If necessary, compile them to collect the PL/Scope metadata:

ALTER ... COMPILE PLSCOPE_SETTINGS='IDENTIFIERS:ALL';

The best is to set these compiler-setting on the database level, if you’re on dev or test. That way, metadata is collected by default.

Now – let’s get to work:

One of several things PL/Scope collects data about is declaration and definition of procedures and functions.

Finding the declaration of all standalone procedures can be done by:

SELECT Object_Type, Object_Name, Type, Name, Signature, Line
FROM ALL_IDENTIFIERS
WHERE Owner IN ('MySchema1', 'MySchema2')
AND Usage IN ('DECLARATION')
AND Type IN ('FUNCTION', 'PROCEDURE')
AND Object_Type IN ('FUNCTION', 'PROCEDURE');

OBJECT_TYPE  OBJECT_NAME           TYPE               NAME                  SIGNATURE                              LINE
------------ --------------------- ------------------ --------------------- -------------------------------- ----------
FUNCTION     MYSTANDALONEFUNCTION  FUNCTION           MYSTANDALONEFUNCTION  61E40C36B13171D5AACDC9FA40852B3A          1
PROCEDURE    MYSTANDALONEPROCEDURE PROCEDURE          MYSTANDALONEPROCEDURE E94699DD7B3C10FBE76DFF9C97ACC05B          1

Listing functions and procedures in packages is also easy:

SELECT Object_Type, Object_Name, Type, Name, Signature, Line
FROM ALL_IDENTIFIERS
WHERE Owner IN ('MySchema1', 'MySchema2')
AND Usage = 'DECLARATION'
AND Type IN ('FUNCTION', 'PROCEDURE')
AND Object_Type IN ('PACKAGE', 'PACKAGE BODY');

OBJECT_TYPE  OBJECT_NAME TYPE               NAME                            SIGNATURE                              LINE
------------ ----------- ------------------ ------------------------------- -------------------------------- ----------
PACKAGE      MYPACKAGE   FUNCTION           MYREFERENCEDPUBLICFUNCTION      E925718DC7E9F225FF6FC7EC14F9BFEF          6
PACKAGE      MYPACKAGE   FUNCTION           MYREFERENCEDPUBLICFUNCTION      C1DC028F981F2833F5A87E294B52A17A          5
PACKAGE      MYPACKAGE   FUNCTION           MYUNREFERENCEDPUBLICFUNCTION    C47A76539917994464008666B6400C9C          4
PACKAGE      MYPACKAGE   PROCEDURE          MYREFERENCEDPUBLICPROCEDURE     10985A9228A3FF6555AF22C8BD5BF125          3
PACKAGE      MYPACKAGE   PROCEDURE          MYUNREFERENCEDPUBLICPROCEDURE   3FF2BC911D1241E280FD46225FBE4BBC          2
PACKAGE BODY MYPACKAGE   PROCEDURE          MYUNREFERENCEDINTERNALPROCEDURE 6138E62F9BFE6468E21AA584F457948D         12
PACKAGE BODY MYPACKAGE   PROCEDURE          MYREFERENCEDINTERNALPROCEDURE   CBAECA439F3390F40E226F60E563CD38          2

A listing of declarations of functions and procedures; standalone, in packages or in types:

SELECT Object_Type, Object_Name, Type, Name, Signature, Line
FROM ALL_IDENTIFIERS
WHERE Owner IN ('MySchema1', 'MySchema2')
AND Usage = 'DECLARATION'
AND Type IN ('FUNCTION', 'PROCEDURE')
ORDER BY Object_Type, Object_Name, Name, Line;

OBJECT_TYPE  OBJECT_NAME           TYPE               NAME                            SIGNATURE                              LINE
------------ --------------------- ------------------ ------------------------------- -------------------------------- ----------
FUNCTION     MYSTANDALONEFUNCTION  FUNCTION           MYSTANDALONEFUNCTION            61E40C36B13171D5AACDC9FA40852B3A          1
PACKAGE      MYPACKAGE             FUNCTION           MYREFERENCEDPUBLICFUNCTION      C1DC028F981F2833F5A87E294B52A17A          5
PACKAGE      MYPACKAGE             FUNCTION           MYREFERENCEDPUBLICFUNCTION      E925718DC7E9F225FF6FC7EC14F9BFEF          6
PACKAGE      MYPACKAGE             PROCEDURE          MYREFERENCEDPUBLICPROCEDURE     10985A9228A3FF6555AF22C8BD5BF125          3
PACKAGE      MYPACKAGE             FUNCTION           MYUNREFERENCEDPUBLICFUNCTION    C47A76539917994464008666B6400C9C          4
PACKAGE      MYPACKAGE             PROCEDURE          MYUNREFERENCEDPUBLICPROCEDURE   3FF2BC911D1241E280FD46225FBE4BBC          2
PACKAGE BODY MYPACKAGE             PROCEDURE          MYREFERENCEDINTERNALPROCEDURE   CBAECA439F3390F40E226F60E563CD38          2
PACKAGE BODY MYPACKAGE             PROCEDURE          MYUNREFERENCEDINTERNALPROCEDURE 6138E62F9BFE6468E21AA584F457948D         12
PROCEDURE    MYSTANDALONEPROCEDURE PROCEDURE          MYSTANDALONEPROCEDURE           E94699DD7B3C10FBE76DFF9C97ACC05B          1
TYPE         MYTYPE                PROCEDURE          REFERENCEDTYPEPROCEDURE         A44E4B1A4C2B9B107A3FAE2D055DE35D          3
TYPE         MYTYPE                FUNCTION           UNREFERENCEDTYPEFUNCTION        F6D636F3E46C2FDDCDA0884325FFEC79          4

PL/Scope also records calls to functions and procedures, which you can list like this:

SELECT Object_Type, Object_Name, Type, Name, Signature, Line
FROM ALL_IDENTIFIERS
WHERE Owner IN ('MySchema1', 'MySchema2')
AND Usage = 'CALL';

OBJECT_TYPE  OBJECT_NAME           TYPE               NAME                            SIGNATURE                              LINE
------------ --------------------- ------------------ ------------------------------- -------------------------------- ----------
PACKAGE BODY MYPACKAGE             PROCEDURE          MYREFERENCEDINTERNALPROCEDURE   CBAECA439F3390F40E226F60E563CD38         36
PACKAGE BODY MYPACKAGE             PROCEDURE          MYREFERENCEDINTERNALPROCEDURE   CBAECA439F3390F40E226F60E563CD38         30
PACKAGE BODY MYPACKAGE             PROCEDURE          REFERENCEDTYPEPROCEDURE         A44E4B1A4C2B9B107A3FAE2D055DE35D         20
PACKAGE BODY MYPACKAGE             OBJECT             MYTYPE                          07655F23A77221F1F71FD30F99E06F4D         18
PROCEDURE    MYSTANDALONEPROCEDURE PROCEDURE          MYREFERENCEDPUBLICPROCEDURE     10985A9228A3FF6555AF22C8BD5BF125          4
PROCEDURE    MYSTANDALONEPROCEDURE FUNCTION           MYREFERENCEDPUBLICFUNCTION      C1DC028F981F2833F5A87E294B52A17A          3
PROCEDURE    MYSTANDALONEPROCEDURE FUNCTION           MYSTANDALONEFUNCTION            61E40C36B13171D5AACDC9FA40852B3A          3

Putting it all together

So – now we should know enought to combine these to find functions and procedures that are not registered as called from any piece of (static) code:

SELECT Object_Type, Object_Name, Type, Name, Signature, Usage, Line
FROM ALL_IDENTIFIERS
WHERE Owner IN ('MySchema1', 'MySchema2')
AND Usage = 'DECLARATION'
AND Type IN ('FUNCTION', 'PROCEDURE')
AND Signature NOT IN (
 SELECT Signature
 FROM ALL_IDENTIFIERS
 --WHERE Owner IN ('MySchema1', 'MySchema2')
 AND Usage = 'CALL'
)
ORDER BY Object_Name, Object_Type, Name;

OBJECT_TYPE  OBJECT_NAME           TYPE               NAME                            SIGNATURE                        USAGE             LINE
------------ --------------------- ------------------ ------------------------------- -------------------------------- ----------- ----------
PACKAGE      MYPACKAGE             FUNCTION           MYREFERENCEDPUBLICFUNCTION      E925718DC7E9F225FF6FC7EC14F9BFEF DECLARATION          6
PACKAGE      MYPACKAGE             FUNCTION           MYUNREFERENCEDPUBLICFUNCTION    C47A76539917994464008666B6400C9C DECLARATION          4
PACKAGE      MYPACKAGE             PROCEDURE          MYUNREFERENCEDPUBLICPROCEDURE   3FF2BC911D1241E280FD46225FBE4BBC DECLARATION          2
PACKAGE BODY MYPACKAGE             PROCEDURE          MYUNREFERENCEDINTERNALPROCEDURE 6138E62F9BFE6468E21AA584F457948D DECLARATION         12
PROCEDURE    MYSTANDALONEPROCEDURE PROCEDURE          MYSTANDALONEPROCEDURE           E94699DD7B3C10FBE76DFF9C97ACC05B DECLARATION          1
TYPE         MYTYPE                FUNCTION           UNREFERENCEDTYPEFUNCTION        F6D636F3E46C2FDDCDA0884325FFEC79 DECLARATION          4

Notice that I match on signature, since “MyReferencedPublicFunction” is overloaded and one of them is referenced, the other isn’t. This shows that a name match is not sufficient, but the signature is unique for every declaration.

The list over is potentially dead code. I say potentially, because you will have to do further investigation to see if it is called from either dynamic (avoid execute immediates and open for <string>) or external code (Java, C#, PHP, …). But you now have a list to work out from, so that you can search through your other code. You might for instance import your other source code into a search index like Solr, Elasticsearch or even Oracle Text to search for matches from this list.

This also shows how important it is to have well defined external APIs (with proper API-packages, schema setup and grants), so that you know whether a package may be in external use or not. This can also be referred as “hard-shell PL/SQL API” and #SmartDB architecture.

The quick win

If you want a quick win, you can safely remove code that is only declared internally in packages and unreferenced – this is for sure dead code:

SELECT Object_Type, Object_Name, Type, Name, Signature, Usage, Line
FROM ALL_IDENTIFIERS
WHERE Owner IN ('MySchema1', 'MySchema2')
AND Usage = 'DECLARATION'
AND TYPE IN ('FUNCTION', 'PROCEDURE')
AND Object_Type LIKE '% BODY'
AND Signature NOT IN (
 SELECT Signature
 FROM ALL_IDENTIFIERS
 --WHERE Owner IN ('MySchema1', 'MySchema2')
 AND Usage = 'CALL'
)
ORDER BY Object_Name, Object_Type, Name;

OBJECT_TYPE  OBJECT_NAME           TYPE               NAME                            SIGNATURE                        USAGE             LINE
------------ --------------------- ------------------ ------------------------------- -------------------------------- ----------- ----------
PACKAGE BODY MYPACKAGE             PROCEDURE          MYUNREFERENCEDINTERNALPROCEDURE 6138E62F9BFE6468E21AA584F457948D DECLARATION         12

Ending notes

I’ve already mentioned that you will have to work through the signatures, not the names in case of overloads.

Also note that if you remove or plan to remove unreferenced code, you can check again to see if there are any functions or procedures that had the removed code as the only reference.

The contents of execute immediate and open … for “string” are not tracked, but you can find the location of these statements in the view ALL_STATEMENTS, so that you can analyze the contens of these. You’ll soon get tired of that and try to avoid dynamic statements in the future.

If you have an architecture where the client (middle-tier or end-tier) has (close to) full access to your schemas, you’re basically out of control. The further you design towards a hard-shell PL/SQL-API-application (#SmartDB), the more control you get.

On my system with close to 1 million lines of code, these queries takes less than a second.

And – don’t forget to clean up after the demo:

DROP PROCEDURE MyStandaloneProcedure;
DROP FUNCTION MyStandaloneFunction;
DROP TYPE myType;
DROP PACKAGE myPackage;

See my earlier blog-post on PL/Scope for references to other PL/Scope resources.

One thought on “Finding dead code using PL/Scope

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s