How to pass arbitrary number of arguments to a PL/SQL procedure?

I was looking for an answer to this question for quite some time and ended up developing my own approach.

As of version 12.1, Oracle does not offer this feature which is widely available in most modern procedural languages. I found only one Oracle feature that somehow resembles the one in question – a built-in default constructor for PL/SQL collection types. We can pass arbitrary number of values into such constructors.

Here is a simple example:

DECLARE
   TYPE vc_table IS TABLE OF VARCHAR2(30);
   v_table vc_table;
BEGIN
   v_table:=vc_table('ABC', 'DEF', 'GHI');
   FOR i IN 1..v_table.COUNT LOOP
       DBMS_OUTPUT.PUT_LINE(v_table(i));
   END LOOP;
END;
/

Result:

ABC
DEF
GHI

The line that initializes v_table variable references a constructor that takes 3 values as arguments. It can take more (or less) values as well.

How could we exploit this feature to accept variable number of arguments in our procedures/functions?

The problem with the above example is that we have to have a collection type before we can use it and this would make our procedure/function dependent on such custom type.

A necessary help comes from Oracle’s built-in collection types:

sys.odcivarchar2list, sys.odcinumberlist, etc.

Let say we need to mimic Oracle’s built-in GREATEST function for a variable number of numeric arguments. Here is how we could use sys.odcinumberlist type:

CREATE OR REPLACE FUNCTION my_greatest(p_list sys.odcinumberlist)
    RETURN NUMBER
AS
    v_result NUMBER;
BEGIN
    SELECT CASE WHEN SUM(NVL2(COLUMN_VALUE,0,1))>0 THEN TO_NUMBER(NULL)
                ELSE MAX(COLUMN_VALUE)
           END
     INTO v_result
    FROM TABLE(p_list);
    RETURN v_result;
END;
/

Remember, the GREATEST function returns NULL if at least one of its arguments is NULL. That’s why we need to check for NULLs in the p_list collection.

Here is how we could test the function:

SELECT my_greatest(sys.odcinumberlist(45,2,46,65,2,1,0)) "greatest",
       my_greatest(sys.odcinumberlist(45,2,NULL,65,2,1)) "null_greatest"
FROM dual

Result:

greatest null_greatest
65

The use of sys.odcinumberlist constructor is not very elegant as the data type name is very long, but it does do the trick. You can pass as many arguments to the constructor as you wish. To make things look a bit prettier, we can create a short synonym:

CREATE OR REPLACE SYNONYM nl FOR sys.odcinumberlist
/

Now, the last (testing) query will transform to the following:

SELECT my_greatest(nl(45,2,46,65,2,1,0)) "greatest",
       my_greatest(nl(45,2,NULL,65,2,1)) "null_greatest"
FROM dual

It still does not look like true “parameter array” with the arbitrary length, but it is very close.

The following is a short list of Oracle’s built-in collection types that you can use for mimicking “arbitrary number of arguments”:

  • sys.odcidatelist
  • sys.odciobjectlist
  • sys.odcirawlist
  • sys.odcinumberlist
  • sys.odcivarchar2list

For anything more complex, you may need to create your own collection type.

If you find this post useful, please press the LIKE button and subscribe.

My Oracle Group on Facebook:

Also, you may want to join my Oracle group on Facebook: https://www.facebook.com/groups/sqlpatterns/

Suggested Reading:

Would you like to read about many more tricks and puzzles? For more clever tricks and cool techniques check my book “Oracle SQL Tricks and Workarounds”.

6 Solutions to Puzzle of the Week #19

Puzzle of the Week #19:

Produce the department salary report (shown below) with the following  assumptions/requirements:

  • Use Single SELECT statement only
  • DECODE and CASE functions are not allowed
  • An employee’s salary is shown in the corresponding department column (10, 20 or 30), all other department columns should contain NULLs.
  • The query should work in Oracle 11g.

Expected Result:

ENAME              10         20         30
---------- ---------- ---------- ----------
SMITH                        800
ALLEN                                  1600
WARD                                   1250
JONES                       2975
MARTIN                                 1250
BLAKE                                  2850
CLARK            2450
SCOTT                       3000
KING             5000
TURNER                                 1500
ADAMS                       1100
JAMES                                   950
FORD                        3000
MILLER           1300

Solutions:

#1: Using NULLIF, ABS, and SIGN functions

SELECT ename, NULLIF(sal * (1-ABS(SIGN(deptno-10))),0) "10",
              NULLIF(sal * (1-ABS(SIGN(deptno-20))),0) "20",
              NULLIF(sal * (1-ABS(SIGN(deptno-30))),0) "30"
FROM emp

#2: Using NULLIF and INSTR functions

SELECT ename, NULLIF(sal * INSTR(deptno, 10), 0) "10",
              NULLIF(sal * INSTR(deptno, 20), 0) "20",
              NULLIF(sal * INSTR(deptno, 30), 0) "30"
FROM emp

#3: Using NVL2 and NULLIF functions

SELECT ename, NVL2(NULLIF(deptno, 10), NULL, 1) * sal "10",
              NVL2(NULLIF(deptno, 20), NULL, 1) * sal "20",
              NVL2(NULLIF(deptno, 30), NULL, 1) * sal "30"
FROM emp

#4: Using PIVOT clause

SELECT *
FROM  (SELECT deptno, ename, sal
       FROM emp)
PIVOT (MAX(sal)
       FOR deptno IN (10, 20, 30)
      );

#5: Using Scalar SELECT statements in SELECT clause

SELECT ename,
       (SELECT sal FROM emp WHERE empno=e.empno AND deptno=10) "10",
       (SELECT sal FROM emp WHERE empno=e.empno AND deptno=20) "20",
       (SELECT sal FROM emp WHERE empno=e.empno AND deptno=30) "30"
FROM emp e;

#6: Using UNION (different sort order)

SELECT ename, sal "10", NULL "20", NULL "30"
FROM emp
WHERE deptno=10
UNION
SELECT ename, NULL, sal, NULL
FROM emp
WHERE deptno=20
UNION
SELECT ename, NULL, NULL, sal
FROM emp
WHERE deptno=30;

ENAME              10         20         30
---------- ---------- ---------- ----------
ADAMS                       1100
ALLEN                                  1600
BLAKE                                  2850
CLARK            2450
FORD                        3000
JAMES                                   950
JONES                       2975
KING             5000
MARTIN                                 1250
MILLER           1300
SCOTT                       3000
SMITH                        800
TURNER                                 1500
WARD                                   125

My Oracle Group on Facebook:

If you like this post, you may want to join my new Oracle group on Facebook: https://www.facebook.com/groups/sqlpatterns/

Would you like to read about many more tricks and puzzles?

For more tricks and cool techniques check my book “Oracle SQL Tricks and Workarounds” for instructions.