Is there a PL/SQL pragma similar to DETERMINISTIC, but for the scope of one single SQL SELECT? - oracle

In a SQL SELECT statement, I'd like to execute a function that is deterministic for the scope of that SELECT statement (or transaction would be ok, too):
select t.x, t.y, my_function(t.x) from t
Many values of t.x are the same so Oracle could omit calling the same function again and again, to speed things up. But if I label the function as DETERMINISTIC, the results may be cached between several executions of this query. The reason why I can't use DETERMINISTIC is because my_function uses a configuration parameter that is changed from time to time.
Is there any other keyword I could use? Are there any catches that I should be aware of (memory issues, concurrency, etc)? Or maybe any other tricks, such as analytic functions to call the function only once per t.x value (without major performance impact)?

If you do this:
select t.x, t.y, (select my_function(t.x) from dual)
from t
then Oracle can use subquery caching to achieve reduced function calls.

This is not an answer to your question, but can be a solution for you.
This configuration parameter that you've mentioned, can't be added as a parameter to function?
In this case, my_function(t.x, val1) is a different thing vs my_function(t.x, val2).

A possible simplistic workaround would be to create a second, DETERMINISTIC function that calls the first one; but have the second function take an additional, meaningless parameter, for which you provide a different literal value in each query that uses the function.

Another method is to put the function in a package and set the result as a global variable. Then when you call the function check whether the input variables are the same as before and quickly return the global variable if so:
SQL> create or replace package temp is
2
3 function blah ( PIndex integer ) return integer;
4
5 end temp;
6 /
Package created.
SQL>
SQL> create or replace package body temp is
2
3 GResult integer := 0;
4 GIndex integer;
5
6 function blah ( PIndex integer ) return integer is
7
8 begin
9
10 if Gindex = Pindex then
11 return Gresult;
12 else
13 GIndex := Pindex;
14 GResult := Pindex;
15 end if;
16
17 return Gresult;
18
19 end blah;
20
21 end temp;
22 /
Package body created.
SQL>
SQL> select temp.blah(1) from dual;
TEMP.BLAH(1)
------------
1
SQL>
SQL> select temp.blah(1) from dual;
TEMP.BLAH(1)
------------
1
SQL>
SQL> select temp.blah(2) from dual;
TEMP.BLAH(2)
------------
2
SQL>

Related

How to prevent Oracle from calling a function for every attribute of the function result

I've created a package, containing a function that returns an object.
When retrieving the object details through sql, the function is called multiple times - once for every detail retrieved.
I believe it should be possible for it to just be called once instead.
Following is an example that demonstrates the issue:
CREATE OR REPLACE TYPE t_test AS OBJECT (
v1 VARCHAR2(10),
v2 VARCHAR2(10),
v3 VARCHAR2(10),
times_called NUMBER
);
/
CREATE OR REPLACE PACKAGE test_pkg AS
times_called NUMBER :=0;
FUNCTION test(something IN VARCHAR2) RETURN t_test;
PROCEDURE reset;
END test_pkg;
/
CREATE OR REPLACE PACKAGE BODY test_pkg IS
PROCEDURE reset IS
BEGIN
times_called := 0;
END;
FUNCTION test(something IN VARCHAR2) RETURN t_test IS
BEGIN
times_called := times_called + 1;
RETURN t_test('first', 'second', 'third', times_called);
END;
END test_pkg;
/
Here we can see that the function is invoked four times:
SQL> SELECT t.r.v1, t.r.v2, t.r.v3, t.r.times_called FROM (
2 SELECT test_pkg.test('x') r FROM DUAL
3 ) t;
R.V1 R.V2 R.V3 R.TIMES_CALLED
---------- ---------- ---------- --------------
first second third 4
SQL>
If we reset the counter, and only select two attributes, we can see it's called twice:
SQL> exec test_pkg.reset();
PL/SQL procedure successfully completed.
SQL> SELECT t.r.v1, t.r.times_called FROM (
2 SELECT test_pkg.test('x') r FROM DUAL
3 ) t;
R.V1 R.TIMES_CALLED
---------- --------------
first 2
SQL>
The actual stored procedure is more expensive, so I'd like to avoid re-calling it for every attribute listed.
The solution has to work on Oracle 10gr2
Oracle is not materializing the sub-query and is pushing the function calls to the outer query. You need to force the SQL engine to materialize the inner query either by:
Using a seemingly unnecessary ROWNUM > 0 filter:
SELECT t.r.v1, t.r.v2, t.r.v3, t.r.times_called
FROM (
SELECT test_pkg.test('x') r
FROM DUAL
WHERE ROWNUM > 0
) t;
or, you should be able to use the (undocumented) /*+ materialize */ hint but, for an unknown reason, it doesn't seem to want to materialize this particular query (although it does work for similar problems).
You can also (as pointed out in comments by William Robertson) use the /*+ NO_MERGE */ hint which "causes Oracle not to merge mergeable views":
SELECT t.r.v1, t.r.v2, t.r.v3, t.r.times_called
FROM (
SELECT /*+ no_merge */
test_pkg.test('x') r
FROM DUAL
) t;
db<>fiddle here

PL/SQL IF statement and variable declaration

I am trying to make the code posted below work, but I am receiving an error about the IF statement:
DECLARE
a number;
BEGIN
SELECT Pers_API.Is_Manager_(10018) INTO a from dual;
EXCEPTION
WHEN no_data_found THEN a := 0;
END;
IF (a >0) THEN
SELECT 1 from dual;
ELSE
SELECT 0 from dual;
END IF;
In the first block I am declaring an 'A' variable and setting it to 0 (by the way, the API call result will be 0 or 1, nothing else). The first block works fine or - at least - I do think so. But the IF block is not working and having this error:
PLS-00103:Encountered the symbol "IF"
Any help is appreciated.
Update: I have tried it this way:
DECLARE
a number:=0;
BEGIN
SELECT Pers_API.Is_Manager_(10018) INTO a from dual;
IF (:a >0) THEN
SELECT 1 from dual;
ELSE
SELECT 0 from dual;
END IF;
END;
I received the exception below:
ORA-01008: not all variables bound
Update
In other words, what I am trying to do is:
if Pers_API.Is_Manager_(10018) returns true (as 1), I have to do another select statement
if it's false (as 0), I have to return null.
Any other ideas are appreciated.
A few objections, if I may.
there's no need to select from dual in order to assign a value to a variable, when that value is returned by a function - simply assign it
exception handler seems to be superfluous. You said that the is_manager_ function returns 0 or 1 and nothing else. Therefore, it'll always return something, so - why do you expect that SELECT to return NO_DATA_FOUND? Besides, such a case should be handled by the function itself (i.e. you have to make sure that it returns 0 or 1 and nothing else)
the last paragraph you wrote is somewhat strange. You said that - if the function returns 1, you have to run another SELECT statement. Otherwise, you have to return null (while code you wrote, select 0 ... suggests a zero (0), not null); so, which one is true?
also, saying that you have to return something (null, 0, whatever) suggests that piece of code you wrote should be a function that returns a value. Is that so?
Code that actually compiles might look like in this example.
First, a function (based on Scott's schema) that checks whether an employee is a manager:
SQL> create or replace function is_manager_ (par_empno in number)
2 return number
3 is
4 /* function checks whether PAR_EMPNO belongs to a manager and returns:
5 - 1 - employee is a manager
6 - 0 - employee is NOT a manager
7 */
8 retval number := 0;
9 begin
10 select max(1)
11 into retval
12 from emp e
13 where mgr = par_empno;
14
15 return retval;
16 exception
17 when no_data_found then
18 return retval;
19 end;
20 /
Function created.
SQL>
Code you posted, rewritten so that it does something:
if an employee is a manager, returns his/her salary
otherwise, it does not
SQL> declare
2 a number := is_manager_(&&l_empno);
3 l_sal emp.sal%type;
4 begin
5 if a > 0 then
6 select sal
7 into l_sal
8 from emp
9 where empno = &&l_empno;
10 dbms_output.put_line('Employee is a manager and his/her salary is ' || l_sal);
11 else
12 null;
13 dbms_output.put_line('Employee is not a manager');
14 end if;
15 end;
16 /
Enter value for l_empno: 7698
Employee is a manager and his/her salary is 2850
PL/SQL procedure successfully completed.
SQL> undefine l_empno
SQL> /
Enter value for l_empno: 7369
Employee is not a manager
PL/SQL procedure successfully completed.
SQL> undefine l_empno
SQL> /
Enter value for l_empno: -1
Employee is not a manager
PL/SQL procedure successfully completed.
SQL>
See if you can adjust it so that it fits your needs. If not, do answer to my objections posted at the beginning of this message, and we'll see what to do next.
It appears to me that what you are really trying to achieve here is to run a query which must use the result obtained from the package function as a condition. If so, the PL/SQL block wouldn't be needed and can be written as a basic select operation.
What is still not clear from your explanation is whether your select query
SELECT 1 from dual is the actual query you are about to use in your code.Generally, in the select statement, we use CASE expression to evaluate the conditional logic, which essentially does what IF ELSE condition was supposed to do to return the desired result.The query would have a structure of this form,
SELECT
CASE
WHEN pers_api.is_manager_(10018) = 1 THEN some_expression
ELSE some_other_expression --If you omit the else condition, it defaults to null when all others are false
END
END
as
FROM DUAL;
More specifically, if you want to have the resultant expression come from another database table, the queries can take the form
SELECT
CASE
WHEN pers_api.is_manager_(10018) = 1 THEN some_expression
ELSE some_other_expression --If you omit the else condition, it defaults to null when all others are false
END
as
FROM table_to_run_query
WHERE where_clauses = some_values;
If you say that your resultant expression from the table involves many other constructs, the challenge would be to compose it in a single query cleverly to avoid any PL/SQL at all. That would be achievable, but it will require that you explain us with some sample data and expected result, as to what exactly you want,preferably in a new question.
It would not be complete if I don't mention the REFCURSOR technique to obtain results from a select query in PL/SQL. It is either through DBMS_SQL.RETURN_RESULT(Oracle 12c and above) or using a REFCURSOR bind variable running it in SQL* Plus or as Script in other tools (F5).
VARIABLE x refcursor;
DECLARE BEGIN
IF
pers_api.is_manager_(10018) = 1
THEN
OPEN :x FOR SELECT some_columns FROM some_tables;
ELSE
OPEN :x FOR SELECT some_columns FROM other_tables;
END IF;
--DBMS_SQL.RETURN_RESULT(:x); --12c and above
END;
/
PRINT x -- 11g

Function created with compilation error in PLSQL

When I compile the below code I am getting a error message "Function created with compilation errors"
create or replace function find_port(ip_ID in int) RETURN int
is
t_count number;
count varchar;
begin
select is_rail into count from table where id = ip_ID;
case
when count ='True' then t_count:=1;
when count ='False' then t_count:=0;
end case;
end;
/
i am getting a error message "Function created with compilation errors"
So the question you should be asking is, "how do I get a list of compilation errors for my PL/SQL code?"
Other people have told you how to fix the current errors in your code, but the more important skill is that you find out how to diagnose your code for yourself.
Oracle is a database, and it stores metadata in a set of special views called the data dictionary. These views include views for compilation errors. This query will work in any SQL environments:
select name, type, line, text -- or just *, obvs
from user_errors ue
order by ue.name, ue.type, ue.sequence;
There are also ALL_ERRORS and DBA_ERRORS views. Find out more.
In SQL*Plus you can run sho err (short for show errors). IDEs like PL/SQL Developer or Oracle SQL Developer will show compilation errors automatically.
Once you know how to get the text of the errors you need to know that LINE will tell you the line where the error is raised. Although with certain classes of error (such as missing commas or unmatched brackets) the indicated line may not be the line where the actual error resides. Unfortunately there is still a need for interpretation and understanding, which requires experience.
Actually, COUNT can be used as a PL/SQL variable:
SQL> create or replace function f_test return int is
2 count number;
3 begin
4 select 1 into count from dual;
5 return 2;
6 end;
7 /
Function created.
SQL> select f_test from dual;
F_TEST
----------
2
SQL>
However, you can't return it:
SQL> create or replace function f_test return int is
2 count number;
3 begin
4 select 1 into count from dual;
5 return count;
6 end;
7 /
Warning: Function created with compilation errors.
SQL> show err
Errors for FUNCTION F_TEST:
LINE/COL ERROR
-------- -----------------------------------------------------------------
5/3 PL/SQL: Statement ignored
5/10 PLS-00204: function or pseudo-column 'COUNT' may be used inside a
SQL statement only
SQL>
Here, #priya, you can see how to help yourself - SHOW ERR will tell you what's wrong with your code.
Apart from that, CASE statement you used was invalidly written; should have been similar to this:
SQL> create or replace function f_test return int is
2 l_count number;
3 t_count number;
4 begin
5 select 1 into l_count from dual;
6
7 t_count := case when l_count = 1 then 1
8 when l_count = 2 then 2
9 end;
10
11 return t_count;
12 end;
13 /
Function created.
SQL> select f_test from dual;
F_TEST
----------
1
SQL>
count is a SQL function and thus not a better choice to be used as a PL/SQL variable. The CASE block can be used within the select statement.
Furthermore, your function does not RETURN any value.
create or replace function find_port(ip_ID in int) RETURN int
is
t_count number;
begin
select case
when is_rail = 'True' then 1
when is_rail = 'False' then 0
end into t_count from yourtable where id=ip_ID;
RETURN t_count;
end;

PL/SQL: Selecting from a table into an assoc array

I am trying to select data into a pl/sql associative array in one query. I know I can do this with a hardcoded key, but I wanted to see if there was some way I could reference another column (the key column) instead.
DECLARE
TYPE VarAssoc IS TABLE OF varchar2(2) INDEX BY varchar2(3);
vars VarAssoc;
BEGIN
SELECT foo, bar INTO vars(foo) FROM schema.table;
END;
I get an error saying foo must be declared when I do this. Is there some way to create my associate array in a single query or do I need to fall back on a FOR loop?
Just read your comment on APC's answer, it sounds like you figured this out on your own. But I figured I'd put the answer in anyway for future searchers.
This is simpler code, but does not have the speed advantage of using BULK COLLECT. Just loop through the rows returned by the query and set the elements in the associative array individually.
DECLARE
TYPE VarAssoc IS TABLE OF varchar2(200) INDEX BY varchar2(30);
vars VarAssoc;
BEGIN
FOR r IN (SELECT table_name,tablespace_name FROM user_tables) LOOP
vars(r.table_name) := r.tablespace_name;
END LOOP;
dbms_output.put_line( vars('JAVA$OPTIONS') );
END;
It would be neat if it were possible but that isn't a straightforward way of acheiving this.
What we can do is load the data into a regular PL/SQL collection and then load that into an associative array. Whethter this is faster than just looping round the table is a matter of tatse: it probably doesn't matter unless we're dealing with loads of data.
Given this test data ...
SQL> select * from t23
2 order by c1
3 /
C1 C2
-- ---
AA ABC
BB BED
CC CAR
DD DYE
EE EYE
ZZ ZOO
6 rows selected.
SQL>
...we can populate an associative array in two steps:
SQL> set serveroutput on
SQL>
SQL> declare
2 type varassoc is table of varchar2(3) index by varchar2(2);
3 vars varassoc;
4
5 type nt is table of t23%rowtype;
6 loc_nt nt;
7
8 begin
9 select * bulk collect into loc_nt from t23;
10 dbms_output.put_line('no of recs = '||sql%rowcount);
11
12 for i in loc_nt.first()..loc_nt.last()
13 loop
14 vars(loc_nt(i).c1) := loc_nt(i).c2;
15 end loop;
16
17 dbms_output.put_line('no of vars = '||vars.count());
18
19 dbms_output.put_line('ZZ = '||vars('ZZ'));
20
21 end;
22 /
no of recs = 6
no of vars = 6
ZZ = ZOO
PL/SQL procedure successfully completed.
SQL>
The real question is probably whether populating an associative array performs better than just selecting rows in the table. Certainly if you have 11g Enterprise edition you should consider result set caching instead.
are you absolutely married to associative arrays? And I assume that you are doing this because you want to be able to do a lookup against the array using a character key.
If so, have you considered implementing this as a collection type instead?
e.g.
CREATE OR REPLACE TYPE VAR_ASSOC as OBJECT(
KEYID VARCHAR2(3),
DATAVAL VARCHAR2(2)
)
/
CREATE OR REPLACE TYPE VAR_ASSOC_TBL AS TABLE OF VAR_ASSOC
/
CREATE OR REPLACE PROCEDURE USE_VAR_ASSOC_TBL
AS
vars Var_Assoc_tbl;
-- other variables...
BEGIN
select cast ( multiset (
select foo as keyid,
bar as dataval
from schema.table
) as var_Assoc_tbl
)
into vars
from dual;
-- and later, when you want to do your lookups
select ot.newfoo
,myvars.dataval
,ot.otherval
into ....
from schema.other_Table ot
join table(vars) as myvars
on ot.newfoo = myvars.keyid;
end;
/
This gives you the lookup by character key value and lets you do everything in bulk.

oracle function to return list of dates as object

To whom it may respond to ,
I am trying to return list of dates and weekdays to be used in other functions. Code below is compiled without error. But it should give output of 15 days (via V_MAX_DAYS variable) and number of the day in that week.
I have tried to implement like this, but cannot get output using DBMS_OUTPUT. I want to test it but got ORA-06532 error at when running .
My aim is to return values to asp.net application as we have done using SYS_REFCURSOR.
How can I achieve that?
Thank you for your concern,
The script is as below :
CREATE OR REPLACE TYPE DATE_ROW AS OBJECT
(
WEEKDAY_VALUE DATE,
DATE_IN_LIST VARCHAR2(5)
)
/
CREATE OR REPLACE TYPE DATE_TABLE as table of DATE_ROW
/
CREATE OR REPLACE FUNCTION FN_LISTDATES
RETURN DATE_TABLE
IS
V_DATE_TABLE DATE_TABLE := DATE_TABLE ();
V_MAX_DAYS NUMBER := 15;
V_CALCULATED_DATE DATE;
V_WEEKDAY VARCHAR2 (5);
BEGIN
FOR X IN -2 .. V_MAX_DAYS
LOOP
SELECT TO_DATE (TO_CHAR (SYSDATE + X, 'DD.MM.YYYY'))
INTO V_CALCULATED_DATE
FROM DUAL;
V_DATE_TABLE.EXTEND;
V_DATE_TABLE(X) := DATE_ROW(V_CALCULATED_DATE, 'Test');
END LOOP;
RETURN V_DATE_TABLE;
END;
/
A few points.
If you want a DATE (V_CALCULATED_DATE) that is X days from SYSDATE with the time component set to midnight, which appears to be your intent here, you would want something like v_calculated_date := TRUNC(sysdate) + x;. A TO_DATE without an explicit format mask is going to create issues if a future session's NLS_DATE_FORMAT happens not to be DD.MM.YYYY
If you really want to return a collection like this, your collection indexes would need to start with 1, not -2. You could accomplish that by doing v_date_table(x+3) := DATE_ROW(v_calculated_date, 'Test');.
However, I would tend to suspect that you would be better served here with a pipelined table function.
The pipelined table function would look something like
SQL> ed
Wrote file afiedt.buf
1 CREATE OR REPLACE FUNCTION FN_LISTDATES
2 RETURN DATE_TABLE
3 PIPELINED
4 IS
5 V_MAX_DAYS NUMBER := 15;
6 V_CALCULATED_DATE DATE;
7 V_WEEKDAY VARCHAR2 (5);
8 BEGIN
9 FOR X IN -2 .. V_MAX_DAYS
10 LOOP
11 v_calculated_date := trunc(sysdate) + x;
12 PIPE ROW( DATE_ROW(v_calculated_date,'Test') );
13 END LOOP;
14 RETURN;
15* END;
SQL> /
Function created.
SQL> select * from table( fn_listDates );
WEEKDAY_V DATE_
--------- -----
30-NOV-10 Test
01-DEC-10 Test
02-DEC-10 Test
03-DEC-10 Test
04-DEC-10 Test
05-DEC-10 Test
06-DEC-10 Test
07-DEC-10 Test
08-DEC-10 Test
09-DEC-10 Test
10-DEC-10 Test
WEEKDAY_V DATE_
--------- -----
11-DEC-10 Test
12-DEC-10 Test
13-DEC-10 Test
14-DEC-10 Test
15-DEC-10 Test
16-DEC-10 Test
17-DEC-10 Test
18 rows selected.

Resources