I have a query like this
SELECT ID,REF_ID,BATCHNO FROM reporttbl
where POSTING_DT >= '06/01/2020' and POSTING_DT <= '06/30/2020'
and I need it every month, so I would like to put it in a view, but as the date changes every month, it would be great to have a date parameter that I can pass to the view when calling it. Is there a way on how can i achieved this?
I'm new to oracle, appreciate every help. Thank youu.
From 19.6 you can create parameterized views using SQL macros.
create or replace function get_month (
tab dbms_tf.table_t, start_date date, end_date date
) return varchar2 sql_macro as
retval int;
begin
return 'select * from tab
where dt >= start_date and dt < end_date + 1';
end get_month;
/
create table t (
c1 int, dt date
);
insert into t
with rws as (
select level c1, add_months ( date'2019-12-25', level ) dt
from dual
connect by level <= 10
)
select * from rws;
select * from get_month (
t, date'2020-06-01', date'2020-07-01'
);
C1 DT
6 25-JUN-2020 00:00:00
select * from get_month (
t, date'2020-08-01', date'2020-09-01'
);
C1 DT
8 25-AUG-2020 00:00:00
This query return the data for the previous month, i.e. the month befort the current month at the time of the query (= sysdate).
You use the trunc with 'MM' to get the months first and the arithmetic with add_months
SELECT ID,REF_ID,BATCHNO FROM reporttbl
where POSTING_DT >= add_months(trunc(sysdate,'MM'),-1) and POSTING_DT < trunc(sysdate,'MM')
Another way to do this is using a function that retrieves the parameters from a table, thereby you don't need to manipulate any DDL. The idea here is
Using a table to store the parameters, basically you need parameter value and a parameter description.
Using a function to retrieve the value of that parameter when the input is the parameter name
Using the function call inside the view.
You can then manipulate the view automatically by modifying the values of the parameter table.
Table
create table my_param_table
( param_description varchar2(100) ,
param_value varchar2(100),
enabled varchar2(1)
) ;
Function
create or replace function f_retr_param ( p_value in varchar2 )
return varchar2
is
declare
v_value my_param_table.value_param%type;
begin
select value into v_value from my_table_of_parameters
where upper(value_param) = upper(p_value) ;
return v_value;
exception when others then raise;
end;
/
View
create or replace force view my_view as
SELECT ID,REF_ID,BATCHNO FROM reporttbl
where POSTING_DT >= f_retr_param ( p_value => 'p_start_date' );
and POSTING_DT <= f_retr_param ( p_value => 'p_end_date' );
There are ways to "parameterize" a view e.g. using Oracle contexts, but they aren't often useful and certainly not for your case.
If your query really just selects from one table with just the dates as predicates then a view doesn't add much value either. You could create a SQL script (in a file e.g. myquery.sql) using bind variables:
SELECT ID,REF_ID,BATCHNO FROM reporttbl
where POSTING_DT >= to_date(:from_date) and POSTING_DT <= to_date(:to_date);
Then every month you can just open the file and run it, and it will prompt you for the 2 dates. Or you can run as a script like this and it will also prompt you:
#myquery.sql
Or if you use substitution strings '&1.' and '&2.' instead:
SELECT ID,REF_ID,BATCHNO FROM reporttbl
where POSTING_DT >= to_date('&1.') and POSTING_DT <= to_date('&2.');
Then you can pass the dates in on the command line like this:
#myquery '06/01/2020' '06/30/2020'
(Because &1. means first parameter on command line, etc.)
Related
I need to write a function in oracle plsql that with take a date as an input and return records from a table for that particular day. If no date is given then fetch the records for current day.
Note that the column (purchase_date) is a timestamp(6) type not null column and has an index on it so I would not like to use trunc() function on the column.
Example value present in purchase_date column is --> 01-DEC-21 06.14.06.388855001 AM
create or replace FUNCTION getRecordsForDate(
input_date DATE DEFAULT SYSDATE
) RETURN sys_refcursor IS
data_out SYS_REFCURSOR;
BEGIN
OPEN data_out FOR
SELECT
p.product_name,
p.product_type,
p.purchased_by
FROM
product_details p
WHERE
AND p.purchase_date BETWEEN TO_DATE(input_date, 'DD-MON-YY')
-- AND TO_DATE('03-MAR-22 23:59:59', 'DD-MON-YY HH24:MI:SS'); --harcoded value works but I need to use input_date
AND 'TO_DATE' ||'(''' || input_date || ' 23:59:59''' ||',' || '''YYYY-MM-DD HH24:MI:SS''' ||')';
return data_out;
END getRecordsForDate;
My concatenation is not working in the last line. It gives me ORA-01858: a non-numeric character was found where a numeric was expected. Not sure what's wrong here. Would someone be able to help.
Do not use TO_DATE on a DATE.
The last line of the cursor will not work as it is a (concatenated) string literal that cannot be converted to a TIMESTAMP or a DATE.
Even if it did work (which it will not), your purchase_date is a TIMESTAMP(6) data type so you are going to exclude all value from the time 23:59:59.0000001 until 23:59:59.999999.
You want to use:
create or replace FUNCTION getRecordsForDate(
input_date DATE DEFAULT SYSDATE
) RETURN sys_refcursor
IS
data_out SYS_REFCURSOR;
BEGIN
OPEN data_out FOR
SELECT product_name,
product_type,
purchased_by
FROM product_details
WHERE purchase_date >= TRUNC(input_date)
AND purchase_date < TRUNC(input_date) + INTERVAL '1' DAY;
return data_out;
END getRecordsForDate;
/
What is the difference between creating a 'normal' UDF in Oracle and one that is a macro? For example, for the macro, they give an example of:
CREATE FUNCTION date_string(dat DATE)
RETURN VARCHAR2 SQL_MACRO(SCALAR) IS
BEGIN
RETURN q'{
TO_CHAR(dat, 'YYYY-MM-DD')
}';
END;
/
And for a function.
What would be a use-case where a macro would be more useful than a normal function? And when a function would be better used that a macro? Does one ever have either performance benefits or limitations over the other?
The SQL & PL/SQL languages have separate runtime engines. This means every time a SQL statement calls a PL/SQL UDF, there's a context switch (and vice-versa).
While each switch is fast, calling a PL/SQL function thousands or millions of times in a SQL statement can make it significantly slower.
SQL macros work differently. At parse time the database resolves the expression to become part of the statement. It searches for the parameter names in the return string. Then effectively does a find/replace of these with the text of whatever you've passed for these parameters.
For example, if you run:
select date_string ( date_col )
from some_table;
The final SQL statement is effectively:
select to_char ( date_col, 'yyyy-mm-dd' )
from some_table;
This means there's no runtime context switch. This can lead to good performance gains, for example:
create function date_string_macro ( dat date )
return varchar2 sql_macro(scalar) is
begin
return q'{ to_char(dat, 'yyyy-mm-dd') }';
end;
/
create function date_string_plsql ( dat date )
return varchar2 is
begin
return to_char(dat, 'yyyy-mm-dd' );
end;
/
declare
start_time pls_integer;
begin
start_time := dbms_utility.get_time ();
for rws in (
select *
from dual
where date_string_plsql ( sysdate + level ) > '2021'
connect by level <= 1000000
) loop
null;
end loop;
dbms_output.put_line (
'PL/SQL runtime = ' || ( dbms_utility.get_time () - start_time )
);
start_time := dbms_utility.get_time ();
for rws in (
select *
from dual
where date_string_plsql ( sysdate + level ) > '2021'
connect by level <= 100000
) loop
null;
end loop;
dbms_output.put_line (
'Macro runtime = ' || ( dbms_utility.get_time () - start_time )
);
end;
/
PL/SQL runtime = 570
Macro runtime = 54
Around 10x faster in this case!
Because the expression becomes part of the SQL statement, as a side benefit the optimizer has full visibility of the underlying expression. This may lead to better execution plans.
This enables you to get the code-reuse benefits of PL/SQL functions (e.g. common formulas, string formatting, etc.) with the performance of pure SQL.
So why not make all existing PL/SQL function macros?
There are a couple of other important differences between them.
First up, order of argument evaluation. PL/SQL uses application-order, macros use normal-order. This can lead to behaviour differences in some cases:
create or replace function first_not_null (
v1 int, v2 int
)
return int as
begin
return coalesce ( v1, v2 );
end first_not_null;
/
select first_not_null ( 1, 1/0 ) from dual;
ORA-01476: divisor is equal to zero
create or replace function first_not_null (
v1 int, v2 int
)
return varchar2 sql_macro ( scalar ) as
begin
return ' coalesce ( v1, v2 ) ';
end first_not_null;
/
select first_not_null ( 1, 1/0 ) from dual;
FIRST_NOT_NULL(1,1/0)
1
Secondly - and more importantly - resolving the expression only happens in SQL. If you call a SQL macro in PL/SQL, it returns the string as-is:
exec dbms_output.put_line ( first_not_null ( 1, 0 ) );
coalesce ( v1, v2 )
Finally SQL macros also have a table variant. This allows you to create template queries you can pass tables and columns to.
I've discussed table macros in several blog posts, showing how you can use table macros to write generic top-N per group functions and reusable CSV-to-rows functions
I am working on a stored procedure which has the following date based select query inside cursor like below
CURSOR inv_cur ( v_hotel_id IN NUMBER, v_stay_date IN DATE)
IS
SELECT id,
start_date ,
room_count ,
booked_room_count
FROM inventory
WHERE hotel_id = v_hotel_id
AND TRUNC( start_date ) = TRUNC(v_stay_date);
For some reason it does not work and I am not getting records. But when i execute without the date comparison or execute the query in the stand alone manner like below, it works
SELECT id,
start_date ,
room_count ,
booked_room_count
FROM inventory
WHERE hotel_id = 1509622
AND TRUNC( start_date ) = TO_DATE('08-03-18', 'dd-mm-yy');
This is how I am invoking the cursor call
PROCEDURE (.....)
IS
v_date_temp DATE;
BEGIN
v_date_temp := p_check_in_date;
FOR inventory_rec IN inv_cur
(
v_hotel_id, v_room_type_id, v_date_temp
)
LOOP
v_room_count := inventory_rec.room_count;
v_book_room_count := NVL ( inventory_rec.booked_room_count,0 );
v_inventory_id := inventory_rec.id;
dbms_output.put_line(' Inv id' || v_inventory_id);
END LOOP;
Since there are no records returned, it is not going inside loop.
I think there is something wrong with the date comparison. Can someone help me figure out this puzzling issue with my date comparison ?
I have a big query with nesting and left join and Ineed to create a view out of it so as not to run it from the application. The issue is I need the date range and some other fields as input parameters since it will vary from the front end for each request.
I just looked up and saw some posts referring to using SYS_CONTEXT for parameterized views and need to know exactly how do I create the view for example with 2 parameters - fromdate, todate and how I invoke the view from the application.
Just for info I am using grails/groovy for developing the application.
and here is the query I want to create view out of..
select
d.dateInRange as dateval,
eventdesc,
nvl(td.dist_ucnt, 0) as dist_ucnt
from (
select
to_date(fromdate,'dd-mon-yyyy') + rownum - 1 as dateInRange
from all_objects
where rownum <= to_date(fromdate,'dd-mon-yyyy') - to_date(todate,'dd-mon-yyyy') + 1
) d
left join (
select
to_char(user_transaction.transdate,'dd-mon-yyyy') as currentdate,
count(distinct(grauser_id)) as dist_ucnt,
eventdesc
from
gratransaction, user_transaction
where gratransaction.id = user_transaction.trans_id and
user_transaction.transdate between to_date(fromdate,'dd-mon-yyyy') and to_date(todate,'dd-mon-yyyy')
group by to_char(user_transaction.transdate, 'dd-mon-yyyy'), eventdesc
) td on td.currentdate = d.dateInRange order by d.dateInRange asc
The context method is described here: http://docs.oracle.com/cd/B28359_01/network.111/b28531/app_context.htm
e.g. (example adapted from the above link)
CREATE CONTEXT dates_ctx USING set_dates_ctx_pkg;
CREATE OR REPLACE PACKAGE set_dates_ctx_pkg IS
PROCEDURE set(d1 in date, d2 in date);
END;
/
CREATE OR REPLACE PACKAGE BODY set_dates_ctx_pkg IS
PROCEDURE set(d1 in date, d2 in date) IS
BEGIN
DBMS_SESSION.SET_CONTEXT('dates_ctx', 'd1', TO_CHAR(d1,'DD-MON-YYYY'));
DBMS_SESSION.SET_CONTEXT('dates_ctx', 'd2', TO_CHAR(d2,'DD-MON-YYYY'));
END;
END;
/
Then, set the dates in your application with:
BEGIN set_dates_ctx_pkg.set(mydate1, mydate2); END;
/
Then, query the parameters with:
SELECT bla FROM mytable
WHERE mydate
BETWEEN TO_DATE(
SYS_CONTEXT('dates_ctx', 'd1')
,'DD-MON-YYYY')
AND TO_DATE(
SYS_CONTEXT('dates_ctx', 'd2')
,'DD-MON-YYYY');
The advantage of this approach is that it is very query-friendly; it involves no DDL or DML at runtime, and therefore there are no transactions to worry about; and it is very fast because it involves no SQL - PL/SQL context switch.
Alternatively:
If the context method and John's package variables method are not possible for you, another one is to insert the parameters into a table (e.g. a global temporary table, if you're running the query in the same session), then join to that table from the view. The downside is that you now have to make sure you run some DML to insert the parameters whenever you want to run the query.
I have just made a workaround for this annoying Oracle disadvantage. Like this
create or replace package pkg_utl
as
type test_record is record (field1 number, field2 number, ret_prm_date date);
type test_table is table of test_record;
function get_test_table(prm_date date) return test_table pipelined;
end;
/
create or replace package body pkg_utl
as
function get_test_table(prm_date date) return test_table pipelined
is
begin
for item in (
select 1, 2, prm_date
from dual
) loop
pipe row (item);
end loop;
return;
end get_test_table;
end;
/
it still requires a package, but at least i can use it in more convinient way:
select *
from table(pkg_utl.get_test_table(sysdate))
i am not sure about performance...
To use parameters in a view one way is to create a package which will set the values of your parameters and have functions that can be called to get those values. For example:
create or replace package MYVIEW_PKG as
procedure SET_VALUES(FROMDATE date, TODATE date);
function GET_FROMDATE
return date;
function GET_TODATE
return date;
end MYVIEW_PKG;
create or replace package body MYVIEW_PKG as
G_FROM_DATE date;
G_TO_DATE date;
procedure SET_VALUES(P_FROMDATE date, P_TODATE date) as
begin
G_FROM_DATE := P_FROMDATE;
G_TO_DATE := P_TODATE;
end;
function GET_FROMDATE
return date is
begin
return G_FROM_DATE;
end;
function GET_TODATE
return date is
begin
return G_TO_DATE;
end;
end MYVIEW_PKG;
Then your view can be created thus:
create or replace view myview as
select
d.dateInRange as dateval,
eventdesc,
nvl(td.dist_ucnt, 0) as dist_ucnt
from (
select
MYVIEW_PKG.GET_FROMDATE + rownum - 1 as dateInRange
from all_objects
where rownum <= MYVIEW_PKG.GET_FROMDATE - MYVIEW_PKG.GET_TODATE + 1
) d
left join (
select
to_char(user_transaction.transdate,'dd-mon-yyyy') as currentdate,
count(distinct(grauser_id)) as dist_ucnt,
eventdesc
from
gratransaction, user_transaction
where gratransaction.id = user_transaction.trans_id and
user_transaction.transdate between MYVIEW_PKG.GET_FROMDATE and MYVIEW_PKG.GET_TODATE
group by to_char(user_transaction.transdate, 'dd-mon-yyyy'), eventdesc
) td on td.currentdate = d.dateInRange order by d.dateInRange asc;
And to run it you must set the values first:
exec MYVIEW_PKG.SET_VALUES(trunc(sysdate)-1,trunc(sysdate));
And then calls to it will use these values:
select * from myview;
I am setting a bind variable in a PL/SQL block, and I'm trying to use it in another query's IN expression. Something like this:
variable x varchar2(255)
declare
x varchar2(100);
begin
for r in (select id from other_table where abc in ('&val1','&val2','&val3') ) loop
x := x||''''||r.id||''',';
end loop;
--get rid of the trailing ','
x:= substr(x,1,length(x)-1);
select x into :bind_var from dual;
end;
/
print :bind_var;
select *
from some_table
where id in (:bind_var);
And I get an error (ORA-01722: Invalid number) on the query that tries to use the bind variable in the "IN" list.
The print statement yiels '123','345' which is what I expect.
Is it possible to use the bind variable like this or should I try a different approach?
(using Oracle 10g)
Clarification:
This is for a reconcilliation sort of thing. I want to run
select *
from some_table
where id in (select id from other_table where abc in ('&val1','&val2','&val3'))
before the main part of the script (not pictured here) deletes a whole bunch of records. I want to run it again afterwards to verify that records in some_table have NOT been deleted. However, the data in other_table DOES get deleted by this process so I can't just refer to the data in other_table because there's nothing there. I need a way to preserve the other_table.id values so that I can verify the parent records afterwards.
I would store the other_table.id's in a PL/SQL table and reference that table in the query afterwards:
type t_id_table is table OF other_table.id%type index by binary_integer;
v_table t_id_table;
-- fill the table
select id
bulk collect into v_table
from other_table
where abc in ('&val1','&val2','&val3');
-- then at a later stage...
select *
from some_table st
, table(cast(v_table AS t_id_table)) idt
where st.id = idt.id;
You can't use comma-separated values in one bind variable.
You could say:
select * from some_table where id in (:bind_var1, :bind_var2)
though
You're better off using something like:
select * from some_table where id in ("select blah blah blah...");
I would use a global temporary table for this purpose
create global temporary table gtt_ids( id number ) ;
then
...
for r in (select id from other_table where ... ) loop
insert into gtt_ids(id) values (r.id) ;
end loop;
...
and at the end
select *
from some_table
where id in (select id from gtt_ids);
changed the loop to use listagg (sadly this will only work in 11gr2).
but for the variable in list, I used a regular expression to accomplish the goal (but pre 10g you can use substr to do the same) this is lifted from the asktom question linked.
variable bind_var varchar2(255)
variable dataSeperationChar varchar2(255)
declare
x varchar2(100);
begin
select listagg(id,',') within group(order by id) idList
into x
from(select level id
from dual
connect by level < 100 )
where id in (&val1,&val2,&val3) ;
select x into :bind_var from dual;
:dataSeperationChar := ',';
end;
/
print :bind_var;
/
select *
from (
select level id2
from dual
connect by level < 100
)
where id2 in(
select -- transform the comma seperated string into a result set
regexp_substr(:dataSeperationChar||:bind_var||','
, '[^'||:dataSeperationChar||']+'
,1
,level) as parsed_value
from dual
connect by level <= length(regexp_replace(:bind_var, '([^'||:dataSeperationChar||'])', '')) + 1
)
;
/*
values of 1,5, and 25
BIND_VAR
------
1,5,25
ID2
----------------------
1
5
25
*/
EDIT
Oops just noticed that you did mark 10g, the only thing to do is NOT to use the listagg that I did at the start
Ok, I have a kind of ugly solution that also uses substitution variables...
col idList NEW_VALUE v_id_list /* This is NEW! */
variable x varchar2(255)
declare
x varchar2(100);
begin
for r in (select id from other_table where abc in ('&val1','&val2','&val3') ) loop
x := x||''''||r.id||''',';
end loop;
--get rid of the trailing ','
x:= substr(x,1,length(x)-1);
select x into :bind_var from dual;
end;
/
print :bind_var;
select :x idList from dual; /* This is NEW! */
select *
from some_table
where id in (&idList); /* This is CHANGED! */
It works, but I'll accept an answer from someone else if it's more elegant.