Showing posts with label pl/sql. Show all posts
Showing posts with label pl/sql. Show all posts

Friday, November 26, 2021

PL/SQL Blocks, Roles and Definer's and Invoker's Rights

As per Oracle security doc "definer's rights and invoker's rights are used to control access to the privileges necessary during the execution of a user-created procedure, or program unit.
In a definer's rights procedure, the procedure executes with the privileges of the owner. The privileges are bound to the schema in which they were created. An invoker's rights procedure executes with the privileges of the current user, that is, the user who invokes the procedure.
"
However, due to how roles are treated in pl/sql blocks an odd behaviour could be observed as below. The test case involves creating a user with a custom role that has few privileges.
SQL> create user testuser identified  by test321 default tablespace users quota unlimited on users;

User created.

SQL> create role testrole;

Role created.

SQL> grant create session, create table, create procedure to testrole;

Grant succeeded.

SQL> grant testrole to testuser;

Grant succeeded.
Connect as the testuser and verify user can execute actions using each of the privileges.
sqlplus testuser/test321@devpdb

SQL*Plus: Release 19.0.0.0.0 - Production on Thu Sep 23 20:49:03 2021
Version 19.12.0.0.0

Copyright (c) 1982, 2021, Oracle.  All rights reserved.


Connected to:
Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production
Version 19.12.0.0.0

SQL> create table x  (a number);

Table created.

SQL> create or replace function t return number as
  2  begin
  3  return 1;
  4  end;
  5  /

Function created.
So user can connect, create a table and a function.

Next create a function that dynamically creates a table.
SQL> create or replace function testfunc return number as
begin
    execute immediate 'create table  testtable as select * from x';
    return 1;

end;
/
Function created.
This function executes a creat table as statement. However, calling the function result in an error.
SQL> declare
  2  i number;
  3  begin
  4  i := testfunc;
  5  end;
  6  /
declare
*
ERROR at line 1:
ORA-01031: insufficient privileges
ORA-06512: at "TESTUSER.TESTFUNC", line 3
ORA-06512: at line 4
Intresting that it says insufficient privileges. If the create table as statement is run from a sql prompt it runs fine.
SQL> create table  testtable as select * from x;

Table created.



The issue here is that "all roles are disabled in any named PL/SQL block that executes with definer's rights". By default, each procedure is a definer's rights unit.
In this case both the executor of the procedure (invoker) and owner (definer) of the objects referenced by it are the same. Nontheless, because roles are disabled in the named PL/SQL block the function fails as it can't see the create table privilege needed.
This seems strange error to have since failure only happens when invovked through the function. Each statement inside the function would execute without any error when run outside the function on its own.

There are two ways to fix this.
One is to grant the create table privilege directly to the user rather than to the role.
grant create table to testuser;

SQL> declare
    i number;
begin
   i := testfunc;
end;
/

PL/SQL procedure successfully completed.
Second method is to run the function with invoker's right (even though in this case both definer and invoker are the same). Named PL/SQL blocks that execute with invoker's rights and anonymous PL/SQL blocks are executed based on privileges granted through enabled roles (Remember to revoke the create table from user if it was granted in previous method).
create or replace function testfunc return number authid current_user as
begin
    execute immediate 'create table  testtable as select * from x';
    return 1;

end;
/

SQL> declare
    i number;
begin
   i := testfunc;
end;
/

PL/SQL procedure successfully completed.
Another odd behaviour is when PL/SQL object references other objects. Using the same example as above grant execute on dbms_lob to role.
SQL> grant execute on dbms_lob to testrole;

Grant succeeded.
Assuming there's a table with a blob column, the dbms_lob package could be used to get the length of the data in lob column.
SQL> select DBMS_LOB.GETLENGTH(b) from blob_table;

DBMS_LOB.GETLENGTH(B)
---------------------
                    2
But if the call to dbms_lob is included in a function it will fail to compile. Complain is it cannot identify the dbms_lob package.
SQL> CREATE OR REPLACE FUNCTION blobsize RETURN NUMBER AS
i number;
BEGIN
    select DBMS_LOB.GETLENGTH(b) into i from blob_table;
    RETURN i;
END;
/  
Warning: Function created with compilation errors.

SQL> show errors;
Errors for FUNCTION BLOBSIZE:

LINE/COL ERROR
-------- -----------------------------------------------------------------
4/5      PL/SQL: SQL Statement ignored
4/12     PL/SQL: ORA-00904: "DBMS_LOB"."GETLENGTH": invalid identifier
This situation is different to earlier one. Oracle development guide states that "if the subprogram or package that you create references schema objects, then you must have the necessary object privileges for those objects. These privileges must be granted to you explicitly, not through roles".
Even though it may seems odd, but the behaviour is by design. To fix this grant exeucte on dbms_lob direct to user.
SQL>  grant execute on dbms_lob to testuser;

Grant succeeded.
Afterwards package compiles without error.
SQL> alter function blobsize compile;

Function altered.

Tuesday, August 30, 2011

Scalar subqueries to reduce PL/SQL to SQL switching

Tom Kyte has written an excellent article on the current issue (September/October) of Oracle Magazine which shows how to reduce switching between pl/sql and sql using scalar subquery.

Tom describes a scalar subquery as "What exactly is a scalar subquery? It is a subquery in a SQL statement that returns exactly one column and zero rows or one row. That single column can be a complex object type, so it can consist of many attributes, but the subquery returns a single scalar value (or NULL if the subquery returns zero records). A scalar subquery can be used anywhere a literal could have been used."

On the article there are many comparisons of using scalar subquery vs deterministic pl/sql, result cache and etc.
Below is the simplest case of performance comparison with and without scalar subquery.

1.Create a big table to query
SQL> create table bigtable as select * from all_objects nologging;
2. Create the function that will be used in this test case
create or replace function mytest(p_owner varchar2) return number is
obj_count number :=0;
begin 
 select count(*) into obj_count from bigtable bt where bt.owner = p_owner; 
 return obj_count; 
end; 
/
3. Run the first test case without scalar subquery
set timing on
variable cpu number;
begin
:cpu := dbms_utility.get_cpu_time;
end;
/

SQL> select owner,mytest(owner) from bigtable;
....
SYS                                    31328
SYS                                    31328

72232 rows selected. -- this is all the rows in the table
Elapsed: 00:05:11.01

SQL> select dbms_utility.get_cpu_time-:cpu cpu_hsecs from dual;

CPU_HSECS
---------
30931
4. Second test case with scalar subquery caching
set timing on
variable cpu number;
begin
:cpu := dbms_utility.get_cpu_time;
end;
/

SQL> select owner,(select mytest(owner) from dual) mo from bigtable;

SYS                                                    31328
SYS                                                    31328
SYS                                                    31328

72232 rows selected.  -- all the rows in the table

Elapsed: 00:00:17.05

SQL> select dbms_utility.get_cpu_time-:cpu cpu_hsecs from dual;
CPU_HSECS
---------
56
As seen from the outputs both cpu usage and elapsed time has reduced. How this happens has been explained in the above mentioned article.

Thursday, August 19, 2010

Result Cache in PL/SQL Packages

To use result cache with any function or procedure in a PL/SQL package result_cache must be defined on the specification.
Create Or Replace Package Result_Cache_Test As
Function Get_Country_City (In_City In Varchar2, In_Country In Varchar2)
Return Varchar2
result_cache;
end;
/
On the package body list the set of tables underlying sql relies on
Create Or Replace Package Body Result_Cache_Test As
Function Get_Country_City(In_City In Varchar2, In_Country In Varchar2)
Return Varchar2
Result_Cache
Relies_On (city,country)
Is
Ret_City Varchar2(200);
ret_country varchar2(200);
Begin
Select Ci.Name,Co.Name Into Ret_City,Ret_Country
From City Ci, Country Co
Where Ci.Country = Co.Code
And Ci.Code = In_City
And Co.Code = In_Country;

return ret_city || ':'||ret_country;
end;
end;
/
Test with PL/SQL Code
set serveroutput on
Declare
val varchar2(400);
Begin
Val := Result_Cache_Test.Get_Country_City('SIN','SG');
dbms_output.put_line(val);
end;
/
Or to know the effects of Result Cache Latch on concurrent access simulate with java code. Main class
public class Main {

public static void main(String[] args) {

try {

OracleDataSource dataSource = new OracleDataSource();
dataSource.setURL("jdbc:oracle:thin:@url");
dataSource.setUser("username");
dataSource.setPassword("password");

String[][] cities = {{"CMB","LK"},
{"LAX","US"},
{"SIN","SG"},
{"LON","GB"}
};

int loops = 1000;
WorkerThread[] threads = new WorkerThread[400];
String sql1 = "begin ? := result_cache_test.Get_Country_City(?,?); end;";

for (int i = 0 ; i < threads.length ; i++){

threads[i] = new WorkerThread(dataSource.getConnection(), sql1,cities,loops);

}

for(int i = 0 ; i < threads.length; i++){

threads[i].start();
}

} catch (Exception ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
Worker Class
public class WorkerThread extends Thread {

private Connection con;
String sql;
String[][] data;
int loops;

public WorkerThread(Connection con, String sql, String[][] data, int loops) {

this.con = con;
this.sql = sql;
this.data = data;
this.loops = loops;
}

@Override
public void run() {
try {
CallableStatement clm;

for (int i = 0; i < loops; i++) {

clm = con.prepareCall(sql);
clm.registerOutParameter(1, Types.VARCHAR);
clm.setString(2, data[i%4][0]);
clm.setString(3, data[i%4][1]);
clm.execute();
String out = clm.getString(1);
Logger.getLogger(WorkerThread.class.getName()).log(Level.INFO,getName() + " "+out);
clm.close();
}

con.close();

} catch (SQLException ex) {
Logger.getLogger(WorkerThread.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
View latch misses, sleeps with
select name,gets, misses, sleeps, wait_time 
from V$latch where Name like '%Result%';

NAME GETS MISSES SLEEPS WAIT_TIME
---------------------- ---------- ---------- ---------- ----------
Result Cache: Latch 1576450 13 3 681
Result Cache: SO Latch 2439 5 2 787
This test was done on a 4 cpu server on 11gR1 with PSU 11.1.0.7.4 applied.

As of 11gR2 'relies_on' is deprecated. From PL/SQL Language Ref Guide "Release 11.2, the database detects all data sources that are queried while a result-cached function is running, and relies_on_clause does nothing. This clause is deprecated."

Wednesday, December 2, 2009

Result Cache for Ref Cursor returning PL/SQL codes

According to Oracle documentation if a PL/SQL SP is returning a ref cursor then result cache is not supported. But there's a way to circumvent with somewhat beneficial results.

Below is a package written for the HR sample schema

create or replace package emps_pkg as

type detail is ref cursor;

function getempdetails (options in INTEGER, departname in VARCHAR2, jobtitle in VARCHAR2) return detail;

end;
/


create or replace package body emps_pkg as

function getempdetails(options in INTEGER,departname in VARCHAR2, jobtitle in VARCHAR2) return detail is

employees detail;
main_query VARCHAR2(2000) := 'select /*+ result_cache */ e.* from employees e,departments d,jobs j
where e.department_id = d.department_id and j.job_id = e.job_id ';
begin
if options = 1 then
main_query := main_query ||'and d.department_name=:1';
open employees for main_query using departname;

return employees;

else
main_query := main_query ||'and j.job_title=:1';

open employees for main_query using jobtitle;

return employees;
end IF ;
end;
end;
/


Package body contains some dynamic sql (there's no real logic behind this package only to demonstrate the use of result cache) which is constructed with the result_cache hint.

execute the package code and observe the plan


select emps_pkg.getempdetails(2,'Purchasing','Purchasing Manager') from dual;
select * from table(dbms_xplan.display_cursor(null,null,'ALL'));

Plan hash value: 980169617

------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 4 (100)| |
| 1 | RESULT CACHE | a68hth9dfygb5ddg0hr8hnty9b | | | | |
| 2 | NESTED LOOPS | | | | | |
| 3 | NESTED LOOPS | | 6 | 570 | 4 (0)| 00:00:01 |
|* 4 | TABLE ACCESS FULL | JOBS | 1 | 27 | 3 (0)| 00:00:01 |
|* 5 | INDEX RANGE SCAN | EMP_JOB_IX | 6 | | 0 (0)| |
|* 6 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | 6 | 408 | 1 (0)| 00:00:01 |
------------------------------------------------------------------------------------------------------------


Recompile the package body without the result cache hint and observe the plan and it would be

Plan hash value: 980169617

-------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 4 (100)| |
| 1 | NESTED LOOPS | | | | | |
| 2 | NESTED LOOPS | | 6 | 570 | 4 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL | JOBS | 1 | 27 | 3 (0)| 00:00:01 |
|* 4 | INDEX RANGE SCAN | EMP_JOB_IX | 6 | | 0 (0)| |
|* 5 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | 6 | 408 | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------


The benefit comes from the fact the sql query inside the SP could use the result cache. But the overhead of executing the PL/SQL code is still there which I believe won't be present in "proper" result cached SPs.

Any changes to depending tables will make the cache invalid just like the expected behavior.

Tuesday, August 26, 2008

Bind Variables in PL/SQL

Bind Variables in PL/SQL

variables in PL/SQL are implicitly bind variables.

 create or replace procedure dsal(p_empno in number)
as
begin
update emp set sal=sal*2 where empno = p_empno;
commit;
end;
/


Now you might be thinking that you've got to replace the p_empno with a bind variable. However, the good news is that every reference to a PL/SQL variable is in fact a bind variable.

Dynamic SQL

In fact, the only time you need to consciously decide to use bind variables when working with PL/SQL is when using Dynamic SQL.

Dynamic SQL, allows you to execute a string containing SQL using the EXECUTE IMMEDIATE command. For next example would always require a hard parse when it is submitted:

create or replace procedure dsal(p_empno in number)
as
begin
execute immediate
'update emp set sal = sal*2 where empno = '||p_empno;
commit;
end;
/


The way to use bind variables instead is to change the EXECUTE IMMEDIATE command as follows:

create or replace procedure dsal(p_empno in number)
as
begin
execute immediate
'update emp set
sal = sal*2 where empno = :x' using p_empno;
commit;
end;
/

And that's all there is to it. One thing to bear in mind, though, is that you can't substitute actual object names (tables, views, columns etc) with bind variables - you can only subsitute literals. If the object name is generated at runtime, you'll still need to string concatenate these parts, and the SQL will only match with those already in the shared pool when the same object name comes up. However, whenever you're using dynamic SQL to build up the predicate part of a statement, use bind variables instead and you'll reduce dramatically the amount of latch contention going on.

The Performance Killer

Just to give you a tiny idea of how huge of a difference this can make performance wise, you only need to run a very small test:

Here is the Performance Killer ....

SQL> alter system flush shared_pool;
SQL> set serveroutput on;

declare
type rc is ref cursor;
l_rc rc;
l_dummy all_objects.object_name%type;
l_start number default dbms_utility.get_time;
begin
for i in 1 .. 1000
loop
open l_rc for
'select object_name
from all_objects
where object_id = ' || i;
fetch l_rc into l_dummy;
close l_rc;
-- dbms_output.put_line(l_dummy);
end loop;
dbms_output.put_line
(round((dbms_utility.get_time-l_start)/100, 2) ||
' Seconds...' );
end;
/


Modified code



declare
type rc is ref cursor;
l_rc rc;
l_dummy all_objects.object_name%type;
l_start number default dbms_utility.get_time;
begin
for i in 1 .. 1000
loop
open l_rc for
'select object_name
from all_objects
where object_id = :x'
using i;
fetch l_rc into l_dummy;
close l_rc;
-- dbms_output.put_line(l_dummy);
end loop;
dbms_output.put_line
(round((dbms_utility.get_time-l_start)/100, 2) ||
' Seconds...' );
end;
/


Modified code runs faster than the earlier version. This is because it reduces the number of parses required. This is good for scalability as less cpu and memory is consumed.