Executing function embedded SELECT sql query to increase performance - oracle

I have the following SQL query that will return the name of a column in a specific table. Let's say it return 'USER_PK' as column name when it runs.
the query:
SELECT max(COLUMN_NAME)
FROM ALL_TAB_COLUMNS
WHERE OWNER= 'DW_01'
AND table_name='D_O_USERS'
AND COLUMN_NAME<>'USER_PK';
Now I would like to run the above query as part of a function but instead of running it and storing the value it returns in a variable (using INTO or attribution like initial_sql: = '...', followed by exec ) I would need to have it run inside one line of code as below (see part in bold)... So far I have been unsuccessful as it is interpreted as a string when using quotes ...
CREATE OR REPLACE function DW_01.EXECUTE_AUTO (db_schema IN VARCHAR2, db_table IN VARCHAR2, pk_name IN VARCHAR2, id_pk IN INTEGER) RETURN VARCHAR2
IS
result VARCHAR2(4000);
begin
EXECUTE IMMEDIATE 'select STANDARD_HASH( '|| **SELECT max( COLUMN_NAME) FROM ALL_TAB_COLUMNS WHERE OWNER='' || db_schema || '' AND table_name=''||db_table ||'' AND COLUMN_NAME<>'' ||pk_name ||'** ,''SHA512'' ) from '||db_table||' where '|| pk_name ||'='||id_pk into RESULT ;
return result;
end;
Many thanks in advance for your thoughts!

You need to amend you r code like below -
CREATE OR REPLACE function DW_01.EXECUTE_AUTO (db_schema IN VARCHAR2,
db_table IN VARCHAR2,
pk_name IN VARCHAR2,
id_pk IN INTEGER) RETURN VARCHAR2
IS
result VARCHAR2(4000);
begin
EXECUTE IMMEDIATE 'select STANDARD_HASH( ' || pk_name || ',256 )
from '||db_table||' where '|| pk_name ||'='||id_pk into RESULT;
return result;
end;
/

There are only a few ways to run dynamic SQL in SQL, and they're neither pretty nor fast. The function below uses DBMS_XMLGEN.GETXML to dynamically run a SQL statement.
create or replace function execute_auto(db_schema in varchar2, db_table in varchar2, pk_name in varchar2, id_pk in integer) return varchar2
is
v_column_name varchar2(128);
v_result varchar2(4000);
begin
select standard_hash(to_number(extractValue(xml_results, '/ROWSET/ROW/' || max_column)), 'SHA512') hash_value
into v_result
from
(
--Create a single XML file with the ROWIDs that match the condition.
select max(column_name) max_column, xmltype(dbms_xmlgen.getxml
('
select '||max(column_name)||'
from '||db_schema||'.'||db_table||'
where id = '||id_pk
)) xml_results
from all_tab_columns
where owner = db_schema
and table_name = db_table
and column_name <> pk_name
);
return v_result;
end;
/
For example, let's create this sample table with 100,000 rows:
--drop table test1;
create table test1(id number, a number, b number, constraint pk_test1 primary key(id));
insert into test1
select level, level, level from dual connect by level <= 100000;
commit;
This shows how to use the function;
select execute_auto(user, 'TEST1', 'ID', id) hash
from test1 where id = 1;
HASH
----
A36753F534728ED84A463ECB13750B8E920A7E4D90244258DE77D9800A0F3DAF8CBAD49602E960A2355933C689A23C30377CE10FC4B8E1F197739FF86C791022
In addition to problems with type conversion and SQL injection, the performance is terrible. Selecting all 100,000 rows this way takes 200 seconds on my machine.
select sum(length(execute_auto(user, 'TEST1', 'ID', id))) from test1;
Normally, running everything in a single SELECT statement is a good way to improve performance. But this extreme type of dynamic SQL will never run fast. You probably want to rethink your approach. Instead of trying to optimize the SQL inside a function that is run one-row-at-a-time, try to change the process to process once-per-table

Related

How can I make table name from two string column?

I need to make a PL/SQL script.
The inputs are a schema name and a table name. How can I make it to a table name?
So e.g. I'd like to do this:
create or replace procedure proc(schema in varchar2, table in varchar2) is
begin
select * from 'schema.table';
end;
begin
proc('db', 'items');
end;
So I'd like to get everything from db.items.
I've tried concat, ( 'schema' || '.' || 'table'), put it in a variable, but non of these has worked.
What you need is dynamic sql. Example that will return and print the count of rows (you can change it accordingly to your needs):
SQL> set serveroutput on -- to be able to see the printed results.
SQL> create or replace procedure proc(p_schema in varchar2, p_table in varchar2) is
v_sql varchar2(100);
v_result number;
begin
v_sql := 'select count(*) from :1' || '.' || ':2';
EXECUTE IMMEDIATE v_sql into v_result USING p_schema, p_table;
DBMS_OUTPUT.PUT_LINE ('Total rows in table: '|| v_result );
end;

PLS-00306: wrong number or types of arguments in call to 'ADD_MONTHS'

I have created the below procedure to only retain the last two months data only and delete the rest one against a table in oracle, below is the procedure but i am getting exception, please advise how to overcome from this
create or replace package TEST_TABLE AS
PROCEDURE TEST_TABLE;
END TEST_TABLE;
create or replace PACKAGE BODY TEST_TABLE AS
PROCEDURE TEST_TABLE IS
BEGIN
FOR cc IN
(
SELECT partition_name, high_value
FROM user_tab_partitions
WHERE table_name = 'TEST_TABLE'
)
LOOP
BEGIN
IF sysdate >= ADD_MONTHS(cc.high_value,2) THEN
EXECUTE IMMEDIATE
'ALTER TABLE TEST_TABLE DROP PARTITION ' || cc.partition_name;
Dbms_Output.Put_Line('Dropping partition is completed.');
END IF;
END;
END LOOP;
EXCEPTION WHEN Others THEN Dbms_Output.Put_Line( SQLERRM );
END TEST_TABLE;
END TEST_TABLE;
The error that I am getting is:
Error(12,6): PL/SQL: Statement ignored
Error(12,20): PLS-00306: wrong number or types of arguments in call to 'ADD_MONTHS'
Firstly, It's insane to call table name, package name and procedure name all by TEST_TABLE as being done by you, as if there's no other name available. I've named them appropriately.
HIGH_VALUE cannot be directly used in DATE related functions as it's of LONG TYPE. There's a simple method to convert it to date using dynamic SQL(EXECUTE IMMEDIATE)
CREATE OR replace PACKAGE BODY PKG_test_table AS
PROCEDURE pr_test_table
IS
v_high_value DATE;
BEGIN
FOR cc IN (
SELECT partition_name,
high_value
FROM user_tab_partitions
WHERE table_name = 'TEST_TABLE'
) LOOP
BEGIN
EXECUTE IMMEDIATE 'BEGIN :v_high_val := '|| cc.high_value || '; END;'
USING OUT v_high_value;
IF
SYSDATE >= add_months(v_high_value,2)
THEN
EXECUTE IMMEDIATE 'ALTER TABLE TEST_TABLE DROP PARTITION '
|| cc.partition_name;
dbms_output.put_line('Dropping partition is completed.');
END IF;
END;
END LOOP;
EXCEPTION
WHEN OTHERS THEN
dbms_output.put_line(sqlerrm);
END pr_TEST_TABLE;
END PKG_test_table;
/
Calling the procedure
BEGIN
PKG_test_table.pr_test_table;
END;
/
Your procedure does not accept any parameter. You can't pass any arguments to it.
The HIGH_VALUE column from USER_TAB_PARTITIONS is a long data type, I'm not going copy code from another web site, but if you google "oracle convert high value to date" you should get some ideas on how to create a function that you can use to convert the 'long' to a date.
My reputation is too low to post this as a comment, so I added it as an answer, it should help though it is not a good answer :(
As the error says it all ADD_MONTHS takes a DATE and you are passing in as LONG.
Try something like this and it should be ok.
Example:
DECLARE
DT LONG(1000) := 'TO_DATE('||''''||'2018-08-01 00:00:00'||''''||',' ||''''|| 'SYYYY-MM-DD HH24:MI:SS'||''''||','||''''||'NLS_CALENDAR=GREGORIAN'||''''||')';
BEGIN
DBMS_OUTPUT.PUT_LINE(DT);
EXECUTE IMMEDIATE
'BEGIN
DBMS_OUTPUT.PUT_LINE(TO_CHAR(ADD_MONTHS('||DT||',2),'||''''||'YYYY-MM-DD HH24:MI:SS'||''''||
')); END;';
END;
Output:
TO_DATE('2018-08-01 00:00:00','SYYYY-MM-DD HH24:MI:SS','NLS_CALENDAR=GREGORIAN')
2018-10-01 00:00:00
Oracle does not allow functions over long such as cast, substr, add_months over long type however … read below.
Long type
describe user_tab_partitions;
...
SUBPARTITION_COUNT NUMBER
HIGH_VALUE LONG
HIGH_VALUE_LENGTH NUMBER
...
Function to convert long to varchar2
FUNCTION long_to_varchar2 ( p_table_owner IN VARCHAR2,p_table_name IN VARCHAR2, p_partition_name IN VARCHAR2) RETURN VARCHAR2
is
l_tmp long;
BEGIN
select high_value
into l_tmp
from all_tab_partitions
where table_owner = p_table_owner
and table_name = p_table_name
and partition_name = p_partition_name ;
RETURN l_tmp;
END long_to_varchar2;
3.Use your new function
select tpar."OWNER",tpar."TABLE_NAME",tpar."PART_MIN",tpar."PART_MIN_HV",tpar."PART_MAX",tpar."PART_MAX_HV",tpar."NR_PART"
,pkey.column_name as partitioned_by
,ptab.partitioning_type as partition_type
,ptab.status
from
(select p1.table_owner as owner
,p1.table_name
,pmin.partition_name as part_min
,to_date(substr(long_to_varchar2(p1.table_owner,p1.table_name,pmin.partition_name),11,10),'yyyy-mm-dd') as part_min_hv
,pmax.partition_name as part_max
,to_date(substr(long_to_varchar2(p1.table_owner,p1.table_name,pmax.partition_name),11,10),'yyyy-mm-dd') as part_max_hv
,p1.nr_part+1 as nr_part
from (select min(part.partition_position) as minp
,max(part.partition_position) as maxp
,count(*) as nr_part
,part.table_name
,part.table_owner
from all_tab_partitions part,
dba_tables tbls
where part.table_name=tbls.table_name
and part.table_owner=tbls.owner
and part.PARTITION_NAME <> 'P_CURRENT'
group by part.table_name, part.table_owner) p1
,all_tab_partitions pmin
,all_tab_partitions pmax
where p1.table_name = pmin.table_name
and p1.table_owner = pmin.table_owner
and p1.minp=pmin.partition_position
and p1.table_name = pmax.table_name
and p1.table_owner = pmax.table_owner
and p1.maxp = pmax.partition_position) tpar
,ALL_PART_KEY_COLUMNS pkey
,ALL_PART_TABLES ptab
where tpar.owner=pkey.owner
and tpar.table_name=pkey.name
and tpar.owner=ptab.owner
and tpar.table_name=ptab.table_name
and pkey.object_type='TABLE';
The only issue is that you will be doing an implicit varchar2 to date conversion and I see no way of doing it otherwise.

how to store and call a sql query from a table column?

Insert statement below holds details of another Table that has 3 columns (id,ins_dt,text_stuff)
INSERT INTO swpurge_config
(schema
,table_name
,table_alias
,driving_column
,deletion_predicate
,retention_period_type
,retention_period_value)
VALUES
('CUSTOMERS_OWNER'
,'LAST_NAMES'
,'LN'
,'ins_dt'
,'WHERE ln.ins_dt < SYSDATE - p_retention_period_value
AND ora_hash(ln.rowid, 8) = 1'
,'month'
,'6');
Aim:
I am essentially trying to add a delete predicate in a varchar2 column. The idea is to call this column in a Procedure which will delete records up to 1000 rows:
PROCEDURE delete_rows
(
p_schema IN VARCHAR2
,p_table_name IN VARCHAR2
,p_table_alias in varchar2
,p_retention_period_value IN VARCHAR2
,p_delete_predicate IN VARCHAR2
) IS
v_sql varchar2 (32000);
v_row_limit pls_integer (1000);
BEGIN
v_sql := 'delete from ' || p_schema ||'.'|| table_name ||p_table_alias
'older than '|| p_retention_period_value || p_delete_predicate;
dbms_output.put_line(v_sql);
END delete_rows;
Not sure about 2 things:
1. How to store the sql where clause in a table column?
2. How to execute the where clause as a statement in the procedure?
Thanks
you are talking about Dynamic SQL.
You can just store it inside VARCHAR2 in string format. then retrieve it
select deletion_predicate into v_sql from swpurge_config where ...
then
v_sql := "SELECT ...... FRom .... " || v_sql;
finally execute it
EXECUTE IMMEDIATE v_sql;
if your sql statement also includes parameters then
EXECUTE IMMEDIATE query_str
using param1;

passing in table name as plsql parameter

I want to write a function to return the row count of a table whose name is passed in as a variable. Here's my code:
create or replace function get_table_count (table_name IN varchar2)
return number
is
tbl_nm varchar(100) := table_name;
table_count number;
begin
select count(*)
into table_count
from tbl_nm;
dbms_output.put_line(table_count);
return table_count;
end;
I get this error:
FUNCTION GET_TABLE_COUNT compiled
Errors: check compiler log
Error(7,5): PL/SQL: SQL Statement ignored
Error(9,8): PL/SQL: ORA-00942: table or view does not exist
I understand that tbl_nm is being interpreted as a value and not a reference and I'm not sure how to escape that.
You can use dynamic SQL:
create or replace function get_table_count (table_name IN varchar2)
return number
is
table_count number;
begin
execute immediate 'select count(*) from ' || table_name into table_count;
dbms_output.put_line(table_count);
return table_count;
end;
There is also an indirect way to get number of rows (using system views):
create or replace function get_table_count (table_name IN varchar2)
return number
is
table_count number;
begin
select num_rows
into table_count
from user_tables
where table_name = table_name;
return table_count;
end;
The second way works only if you had gathered statistics on table before invoking this function.

Returning dataset with PL/SQL and a variable table name

I'm trying to write a PL/SQL function to store a select statement with a variable table name (a bit weird i know but it is actually a good design decision). The following code does not work...but I'm not sure how to both take a variable table name (building the query) and return a dataset. Anyone have any experience in this? TIA.
CREATE OR REPLACE FUNCTION fn_netstat_all (casename in varchar2)
RETURN resultset_subtype
IS
dataset resultset_subtype;
v_sql varchar2(25000);
v_tablename varchar2(50);
begin
v_sql := 'SELECT * FROM ' || casename || '_netstat;';
OPEN dataset FOR
execute immediate v_sql;
return dataset;
end;
If your resultset_subtype is a ref_cursor (or just replace resultset_subtype with a ref_cursor) you could:
CREATE OR REPLACE
FUNCTION fn_netstat_all (
casename IN VARCHAR2
)
RETURN resultset_subtype
IS
dataset resultset_subtype;
BEGIN
OPEN dataset
FOR 'SELECT * FROM ' || casename || '_netstat';
RETURN dataset;
END fn_netstat_all;
FWIW, you might want to look into the DBMS_ASSERT package to wrap the casename variable to help protect against SQL Injection attacks in your dynamic SQL.
Hope it helps...
Below it is assumed all tables are similar. It's also possible to select a subset of colums that are similar in every table without using DBMS_SQL. I have also paid some attention to SQL injection mentioned by Ollie.
create table so9at (
id number(1),
data varchar2(5)
);
insert into so9at values (1, 'A-AAA');
insert into so9at values (2, 'A-BBB');
insert into so9at values (3, 'A-CCC');
create table so9bt (
id number(1),
data varchar2(5)
);
insert into so9bt values (5, 'B-AAA');
insert into so9bt values (6, 'B-BBB');
insert into so9bt values (7, 'B-CCC');
create table secret_identities (
cover_name varchar2(20),
real_name varchar2(20)
);
insert into secret_identities values ('Batman', 'Bruce Wayne');
insert into secret_identities values ('Superman', 'Clark Kent');
/* This is a semi-secure version immune to certain kind of SQL injections. Note
that it can be still used to find information about any table that ends with
't'. */
create or replace function cursor_of (p_table_id in varchar2)
return sys_refcursor as
v_cur sys_refcursor;
v_stmt constant varchar2(32767) := 'select * from ' || dbms_assert.qualified_sql_name(p_table_id || 't');
begin
open v_cur for v_stmt;
return v_cur;
end;
/
show errors
/* This is an unsecure version vulnerable to SQL injection. */
create or replace function vulnerable_cursor_of (p_table_id in varchar2)
return sys_refcursor as
v_cur sys_refcursor;
v_stmt constant varchar2(32767) := 'select * from ' || p_table_id || 't';
begin
open v_cur for v_stmt;
return v_cur;
end;
/
show errors
create or replace procedure print_values_of (p_cur in sys_refcursor) as
type rec_t is record (
id number,
data varchar2(32767)
);
v_rec rec_t;
begin
fetch p_cur into v_rec;
while p_cur%found loop
dbms_output.put_line('id = ' || v_rec.id || ' data = ' || v_rec.data);
fetch p_cur into v_rec;
end loop;
end;
/
show errors
declare
v_cur sys_refcursor;
begin
v_cur := cursor_of('so9a');
print_values_of(v_cur);
close v_cur;
v_cur := cursor_of('so9b');
print_values_of(v_cur);
close v_cur;
/* SQL injection vulnerability */
v_cur := vulnerable_cursor_of('secret_identities --');
dbms_output.put_line('Now we have a cursor that reveals all secret identities. Just see DBMS_SQL.DESCRIBE_COLUMNS ...');
close v_cur;
/* SQL injection made (mostly) harmless - will throw ORA-44004: invalid qualified SQL name */
v_cur := cursor_of('secret_identities --');
close v_cur;
end;
/

Resources