Storing a variable in Oracle PL SQL - sql

I hope you can help - I'm strying to assign a date to a variable, and then call that variable in my select query. The code I'm posting is only part of what I'm strying to do, I will be calling that variable more than once.
I've tried to google for help, but I'm stuck on using the Select Into statement, as I've got so many selects already.
DECLARE
CurrMonth DATE := '27 may 2012'; -- Enter 27th of current month
BEGIN
SELECT
a.policynumber
,a.cifnumber
,a.phid
,a.policystartdate
,b.sistartdate
,c.dateofbirth
,'28/02/2013' AS TaxYearEnd
--Complete tax year end in the SELECT statement (once for tax year end and once for the age at tax year end)
,round ((months_between('28 feb 2013',c.dateofbirth)/12),8) AS AgeAtTaxYearEnd
,b.sifrequency AS CurrSIFrequ
,b.sivalue AS CurrentSIValue
,b.simode AS CurrentSIMode
,d.anniversarydate AS CurrentAnnDate
,d.anniversaryvalue AS CurrentAnnValue
,b.ruleeffectivedate
,b.sistatus AS CurrentSIStatus
,b.paymentbranchcode AS CurrSIBranchCode
,b.transferaccounttype AS CurrSIAccountType
,b.transferaccountnumber AS CurrSIAccountNo
,SUM(k.unitbalance) AS unitbalance
,a.latestrule
FROM fcislob.policytbl a
,fcislob.policysitbl b
,fcislob.unitholderdetailtbl c
,fcislob.policyanniversaryvaluetbl d
,fcislob.unitholderfundtbl k
WHERE a.policynumber = b.policynumber
AND a.policynumber = d.policynumber
AND b.policynumber = d.policynumber
AND a.phid = c.unitholderid
AND a.phid = k.unitholderid
AND c.unitholderid = k.unitholderid
AND a.ruleeffectivedate = b.ruleeffectivedate
AND a.ruleeffectivedate = d.ruleeffectivedate
AND b.ruleeffectivedate = d.ruleeffectivedate
AND a.latestrule <> 0
AND c.authrejectstatus = 'A'
AND a.phid LIKE 'AGLA%'
AND b.sistatus <> 'C'
AND k.unitbalance >0
AND b.transactiontype = '64'
AND b.sistartdate <= CurrMonth
AND b.sifrequency = 'M'
GROUP BY a.policynumber, a.cifnumber, a.phid, a.policystartdate, b.sistartdate , c.dateofbirth,b.sifrequency, b.sivalue, b.simode, d.anniversarydate, d.anniversaryvalue, b.ruleeffectivedate,
b.sistatus, b.paymentbranchcode, b.transferaccounttype, b.transferaccountnumber, b.policynumber, a.latestrule;
END;

You have a group by clause so you need to group by all collumns which aren't aggregated.
Are you sure you have only one record in the result ?
As #TonyAndrews said, you need the into clause. You need to declare a variable for every collumn and insert into it,
i.e.:
DECLARE
v_policynumber fcislob.policytbl.policynumber%TYPE;
v_cifnumber fcislob.policytbl.cifnumber%TYPE;
v_phid fcislob.policytbl.phid%TYPE;
-- and so on ...
v_sum number;
BEGIN
SELECT SUM(k.unitbalance), a.policynumber, a.cifnumber, a.phid -- and so on ...
INTO v_sum, v_policynumber, v_cifnumber, v_phid -- and so on ...
FROM fcislob.policytbl a -- and so on ...
GROUP BY a.policynumber, a.cifnumber, a.phid -- and so on ...
END;
The way you deal with dates is not "healthy", IMO it's better to use to_date and not realy on NLS parameters

If you are only using PL/SQL to be able persist the date value between several plain select statements, then it will complicate things a lot - switching to select into isn't straightforward if you just want to display the results of the query, particularly if there are multiple rows.
Since you mention you have many selects, I'm guessing you have them in a script file (example.sql) and are running them through SQL*Plus, like sqlplus user/password #example. If so you can keep your plain SQL statements and use positional parameters, substitution variables, or bind variables to track the date.
First option is if you want to pass the date on the command line, like sqlplus user/password #example 27-May-2012:
set verify off
select 'Supplied date is ' || to_date('&1', 'DD-Mon-RRRR') from dual;
This uses the first positional parameter, which is referenced as &1, and converts it to a date as needed in the query. The passed date has to be in the format expected by the to_date function, which in this case I've made DD-Mon-RRRR. Note that you have to enclose the variable in single quotes, otherwise (unless it's a number) Oracle will try to interpret it as a column name rather than a value. (The set verify off suppresses messages SQL*Plus shows by default whenever a substitution variable is used).
You can reference &1 as many times as you want in your script, but you may find it easer to redefine it with a meaningful name - particularly useful when you have multiple positional parameters - and then use that name in your queries.
define supplied_date = &1
select 'Supplied date is ' || to_date('&supplied_date', 'DD-Mon-RRRR') from dual;
If you don't want to pass the date from the command line, you can use a fixed value instead. I'm using a different default date format here, which allows me to use the date literal syntax or the to_date function.
define curr_date = '2012-05-31';
select 'Today is ' || date '&curr_date' from dual;
select 'Today is ' || to_date('&curr_date', 'YYYY-MM-DD') from dual;
You may want to derive the date value, using the result of one query in a later one. You can use the column ... new_value SQL*Plus command to do that; this defines a substitution variable curr_date with the string value from the today column (alias) from any future query, and you can then use it in the same way:
column today new_value curr_date
select to_char(sysdate, 'DD-Mon-YYYY') as today from dual;
select 'Today is ' || to_date('&curr_date', 'DD-Mon-YYYY') from dual;
You can also use bind variables, which you define with the var[iable] command, and set with exec:
var curr_date varchar2(10);
exec :curr_date := '2012-05-31';
select 'Today is ' || to_date(:curr_date, 'YYYY-MM-DD') from dual;
(exec is actually a wrapper around an anonymous PL/SQL block, so it means begin :curr_date := '2012-05-31'; end;, but you only really see that if there's an error). Note that it knows the bind variable is a string, so you don't enclose this in single quotes.
You can mix and match, so if you passed a date on the command line you could assign it to a bind variable with exec :supplied_date := '&1'; or to use current date exec :curr_date := to_char(sysdate, 'YYYY-MM-DD').
Many combinations possible, so you'll need to pick what suits what you're trying to do, and which you find simplest. Most, if not all, of these should work in SQL Developer too, but not sure about other clients.

Related

Using between operator for string which stores numbers

I have a column in which numbers are stored as string because of the nature of the column where any kind of data type is expected like date, numbers, alpha numeric,
etc.
Now i need to check if the values in that column is in defined range or not here is sample data for testing
create table test (val varchar2(10));
insert into test values ('0');
insert into test values ('67');
insert into test values ('129');
insert into test values ('200');
insert into test values ('1');
Here expected range in which value should be is 0-128 if values are not in range then i need to filter them out for further processing.
For this i have written some queries but none of then is giving requires output.
select *
from test
where val not between '0' and '128';
select *
from test
to_number(val, '9') not between to_number('0', '9') and to_number('128', '9999');
select * from test where
to_number(val, '9') < TO_NUMBER('0', '9')
or
to_number(val, '999') > TO_NUMBER('128', '999')
;
These above queries are producing desired output !! :(
I ma using DB version --
Oracle Database 12c Enterprise Edition Release 12.1.0.2.0 - 64bit Production
Just leave the format out of to_number():
select *
from test
where to_number(val) not between to_number('0') and to_number('128');
The numeric format is needed for conversion to a character. If you pass it in to to_number(), then it expects a number of that format -- and you might get the number of digits wrong.
Or, better yet:
select *
from test
where to_number(val) not between 0 and 128;
Or, even better yet, change the column to contain a number rather than a string.
EDIT:
If the problem is that your value is not a number (which is quite different from your original question), then test for that. This is one situation where case is appropriate in the where clause (because case guarantees the order of evaluation of its arguments:
where (case when regexp_like(val, '[^-0-9]') then 'bad'
when cast(val as number) < 0 then 'bad'
when cast(val as number) > 128 then 'bad'
else 'good'
end) = 'bad'
#GordonLinoff's answer works with the sample data you've shown, but it will error with ORA-01722 "invalid number" if you have any values which do no represent numbers. Your sample data only has good values, but you said that for your real field "any kind of data type is expected like date, numbers, alpha numeric, etc."
You can get around that with a function that attempts to convert the stored string value to a number, and returns null if it gets that exception. A simple example:
create function safe_to_number (p_str varchar2) return number is
begin
return to_number(p_str);
exception
when value_error then
return null;
end;
/
You can then do
select *
from test
where safe_to_number(val) not between 0 and 128;
VAL
----------
129
200
Anything that can't be converted and causes an ORA-06502 value-error exception will be seen as null, which is neither between nor not between any values you supply.
If you need to check date ranges you can do something similar, but there are more errors possible, and you may have dates in multiple formats; you would need to declare exceptions and initialise them to known error numbersto catch the ones you expect to see. This isn't complete, but you could start with something like:
create function safe_to_date (p_str varchar2) return date is
l_formats sys.odcivarchar2list;
format_ex_1 exception;
format_ex_2 exception;
format_ex_3 exception;
format_ex_4 exception;
format_ex_5 exception;
pragma exception_init(format_ex_1, -1840);
pragma exception_init(format_ex_2, -1841);
pragma exception_init(format_ex_3, -1847);
pragma exception_init(format_ex_4, -1858);
pragma exception_init(format_ex_5, -1861);
-- add any others you might get
begin
-- define all expected formats
l_formats := sys.odcivarchar2list('YYYY-MM-DD', 'DD/MM/YYYY', 'DD-MON-RRRR'); -- add others
for i in 1..l_formats.count loop
begin
return to_date(p_str, l_formats(i));
exception
when format_ex_1 or format_ex_2 or format_ex_3 or format_ex_4 or format_ex_5 then
-- ignore the exception; carry on and try the next format
null;
end;
end loop;
-- did not match any expected formats
return null;
end;
/
select *
from test
where safe_to_date(val) not between date '2016-02-01' and date '2016-02-29';
Although I wouldn't normally use between for dates; if you don't have any with times specified then you'd get away with it here.
You could use when others to catch any exception without having to declare them all, but even for this that's potentially dangerous - if something is breaking in a way you don't expect you want to know about it, not hide it.
Of course, this is an object lesson in why you should store numeric data in NUMBER columns and dates in DATE or TIMESTAMP fields - trying to extract useful information when everything is stored as strings is messy, painful and inefficient.
I think the best approach you can try in this condition is use
TRANSLATE function to eliminate the alphanumeric characters. Once its
done all now is OLD school technique to check the data by using NOT
BETWEEN function Hope this helps.
SELECT B.NM
FROM
(SELECT a.nm
FROM
(SELECT '0' AS nm FROM dual
UNION
SELECT '1' AS nm FROM dual
UNION
SELECT '68' AS nm FROM dual
UNION
SELECT '129' AS nm FROM dual
UNION
SELECT '200' AS nm FROM dual
UNION
SELECT '125a' AS nm FROM dual
)a
WHERE TRANSLATE(a.nm, ' +-.0123456789', ' ') IS NULL
)b
WHERE b.nm NOT BETWEEN 1 AND 128;

COPY csv using custom filename path

I'm getting some issues while trying to export a query to CSV using the COPY function.
The COPY runs ok and exports the query successfully if not using custom filenames on the TO.
The issue is related to add a "datestamp" (kinda) to the filename created.
declare var1 varchar(25);
DECLARE STATEMENT TEXT;
select into var1 current_date -1;
STATEMENT := 'COPY (SELECT * from myTable) To ''E'C:\\Exports\\export_'||var1||'.csv' ''With CSV';
EXECUTE STATEMENT;
In this case, var1 gets a value like 2013-12-16 and I need to add that to the filename to obtain export_2012-12-16.csv
I'm assuming that the ' are misplaced. I've tried several combinations without success and off course the error is ERROR: syntax error at or near "C".
plpgsql code could work like this:
...
DECLARE
var1 text;
BEGIN
var1 := to_char(current_date - 1, 'YYYY-MM-DD');
EXECUTE $$COPY (SELECT * from myTable)
TO E'C:\\Exports\\export_$$ || var1 || $$.csv' WITH CSV$$;
...
Your quotes got tangled. Using dollar-quoting to simplify. Note that syntax highlighting here on SO is misleading because it does not understand dollar-quoting.
DECLARE is only needed once (not an error though). Plus, BEGIN was missing.
And to_char() makes the text representation of a date independent of the locale.

Procedure need for SQL Query (Oracle)

I am a bit stuck on this to be honest, and I cant find a nice solution. No solution at all.
I have this table actor(id,Actor_Name) where the name is formatted as Actor-01 .. etc:
id Actor_Name
--------------------
01 Actor-01
02 Actor-02
03 Actor-03
What I need is, to increase the name of every one of these actors by one, using a procedure . Like this:
id Actor_Name
--------------------
01 Actor-02
02 Actor-02
03 Actor-04
Probably I need some kind of iteration , and I have tried a bit with cursors, but its mostly a disaster. If Anyone could provide some neat solution , or something I would be really happy!
I don't think you need a cursor. I'm not 100% sure about desired format, but something like
select CAST(REGEXP_SUBSTR('Actor-01','[[:digit:]]+')as int) from dual; returns 1 that can be incremented and stored (for instance UPDATE table1 set col1 = 'Actor-0' || CAST(REGEXP_SUBSTR(col1,'[[:digit:]]+')as int)+1 WHERE ....
Here are some concepts that may point toward a workable solution.
You can use INSTR to find the position of the minus sign.
SELECT INSTR('ACTOR-74','-') FROM DUAL
You can use SUBSTR to pull off the DIGITS
SELECT SUBSTR('ACTOR-74', 6) FROM DUAL
You can use TO_NUMBER to turn the digits into a number
SELECT TO_NUMBER('1234') FROM DUAL
You can use concatenation to build a replacement value
SELECT SUBSTR('ACTOR-74',1,6) ||
TO_CHAR(74 + 1, '09') FROM DUAL
Which yields a value that can be used in the update.
UPDATE TABLE
SET ACTOR_NAME = the-replacement-value
Try something like (not tested)
update actor set actor_name = 'Actor-' || lpad( to_char( id + 1), 2, '0' );
commit;
No need for a procedure.
merge into actor
using (
select id,
substr(actor_name, 1, instr(actor_name, '-')) as name,
to_number(substr(actor_name, instr(actor_name, '-') + 1)) as nr
from actor
) t on (t.id = actor.id)
when matched then update
set actor_name = t.name || to_char(nr + 1, 'FM09');
SQLFiddle example: http://sqlfiddle.com/#!4/93c02/1
Another approach:
update (select id
, replace( Actor_name
, substr(actor_name, -2)
, To_char(to_number(substr(actor_name, -2)) + 1, 'fm00')
) as New_Actor_name
, Actor_name
from actor
) a
set a.actor_name = a.new_actor_name;
SQLFiddle Demo
Using a stored procedure for such a simple task seems like overkill.
update ACTOR
set substr(ACTOR_NAME,1,length(ACTOR_NAME)-2)||to_number(substr(ACTOR_NAME,-2,2))+1
where substr(ACTOR_NAME,-2,2) between '00' and '99'
You may have been close using a cursor, but probably didn't know that cursors are READ-ONLY by default. To toggle this behavior, use the for update option in your cursor defintion:
http://www.techonthenet.com/oracle/cursors/for_update.php
(an additional explanation of the for update clause)
Then when looping through the cursor, you need to also use a clause which allows you to update the record last fetched by the cursor: where current of
http://www.techonthenet.com/oracle/cursors/current_of.php
("where current of" examples)
This should work with both anonymous pl/sql blocks or by a stored procedure definition.

selecting the name of the column as values from different column

I have a table that has following columns
End_date.
count_all_transactions.
count_timeouts.
request_key.
Now I need to calculate percentagetimeout for two different dates and display them simultaneously. I have built my query.
My query is as follows:
Select PercentageTimeout ,
PerTimeout,
Transaction
From
(
select
T.COUNT_ALL_TRANSACTIONS as Total,
T.END_DATE as DT1,
T.COUNT_TIMEOUTS as Timeouts,
round (((T.COUNT_TIMEOUTS / T.COUNT_ALL_TRANSACTIONS) * 100)
,2) as PercentageTimeout,
R.INTERFACE_NAME as Transaction
from rpt_timeout T, dim_request R
where
T.REQUEST_KEY = R.REQUEST_KEY
AND
T.END_DATE='27 AUG 2012'
)
left outer join
(
select T1.COUNT_ALL_TRANSACTIONS as Total1,
T1.END_DATE as DT2,
T1.COUNT_TIMEOUTS as Timeouts1,
round (((T1.COUNT_TIMEOUTS / T1.COUNT_ALL_TRANSACTIONS) * 100)
,2) as PerTimeout,
R1.INTERFACE_NAME as Transaction1
from rpt_timeout T1, dim_request R1
where T1.REQUEST_KEY = R1.REQUEST_KEY
AND T1.END_DATE='20 AUG 2012'
) on Transaction = Transaction1;
This query is giving me correct result but what I am struggling with is that I need the name of the column as the corresponding END_DATE in that table i.e:
my output should be like
27-AUG-2012 20-AUG-2012 Transaction
0.28 0.1 Qpay
0.09 0.06 Payment
You need to use dynamic SQL for this. Since this is specific to Oracle, that means using PL/SQL to construct a query, and then execute it with the EXECUTE IMMEDIATE statement.
See the link below...
http://docs.oracle.com/cd/B10500_01/appdev.920/a96590/adg09dyn.htm
Here's some example code; it's not complete and may even be syntactically incorrect (I don't have an Oracle server handy, and haven't worked with Oracle in a little while), just a fragment to give you an idea of what I'm talking about.
DECLARE
query VARCHAR2(4000);
date1 VARCHAR2(25);
date2 VARCHAR2(25);
BEGIN
date1 := '27-AUG-2012';
date2 := '20-AUG-2012';
query := 'Select PercentageTimeout as ' || date1 ||
', PerTimeout as ' || date2 ||
', Transaction From <the rest of your query goes here>';
EXECUTE IMMEDIATE query;
END;
Now you might want to create a cursor instead out of your dynamically constructed query string and iterate through the records; whatever you need to do. The main point is, if you want to dynamically specify column aliases in a query, you need to dynamically construct that query.

Inserting a resultset into a Table in Oracle

Heyho,
I've gotta write a Procedure which Inserts a resultset from a select-statement into a table.
A co-worker of mine did something similar before to copy values from one table to another. His statement looks like this:
CREATE OR REPLACE PROCEDURE Co-Worker(
pId IN INT
)
AS
BEGIN
INSERT INTO Table1_PROCESSED
SELECT * FROM Table1
WHERE ID = pId;
DELETE FROM Table1
WHERE ID = pId;
END Co-Worker;
/
The two tables mentioned here got the same structure (in fact table1_processed is just a copy of table 1).
So I thought like "Hey! I get a resultset from my select-satement too! So why I just don't adjust it a bit do to the same!"
So I created my Table like this:
MyTable:
TIMEID (number) | NAME (varchar2 - 128)
-----------------------------------
VALUE | VALUE
VALUE | VALUE
VALUE | VALUE
and my Procedure like this:
CREATE OR REPLACE procedure MyProcedure(
pdate in date,
pJobtype in number default 3,
pTasktype in number default 4,
pJobstatus in number default 1,
pTaskstatus in number default 4
)
AS
pformateddate date;
BEGIN
Select to_date(to_char(to_date(pdate, 'DD.MM.YYYY HH24:MI:SS'), 'DD.MM.YYYY'), 'DD.MM.YYYY')
into pformateddate
from dual;
Insert into MyTable (TIMEID, NAME)
Select Function_GETTIMEID(to_date(st, 'DD.MM.YYYY HH24')) TIMEID
,to_char(ext) NAME
from(
Select to_char(arch_job.exec_start, 'DD.MM.YYYY HH24') st
,file.name ext
, count(file.id) cnt
from
arch_task_file
left join file on arch_task_file.File_ID = file.ID
left join arch_task on arch_task_file.Task_ID = arch_task.ID
left join arch_job on arch_task.Job_ID = arch_job.ID
where
arch_job.exec_start > pformateddate
and arch_job.exec_end <pformateddate + 1
and arch_job.jobtype_id = pJobtype
and arch_job.jobstatus_id = pJobstatus
and arch_task.Tasktype_ID = pTasktype
and arch_task.Taskstatus_ID = pTaskstatus
group by
file.name,
to_char(arch_job.exec_start, 'DD.MM.YYYY HH24'
)
);
End MyProcedure;
/
the Result for the large Select-Statement ALONE looks like this:
TIMEID | NAME
-----------------------------------
VALUE | VALUE
VALUE | VALUE
VALUE | VALUE
But If I execute this procedure and give it a dummydate (sysdate - 12 or a date like '16.07.2010 10:32:50') my Toad-gives my a message "Procedure completed" my table stays empty...!
But as said before the large Select-Statement gives results so there shouldn't be a try to insert an empty resultset...! Can anyone tell me why my procedure ain't work?
Thx for every useful answer. =)
Greetz!
P.S.:
The
Select to_date(to_char(to_date(pdate, 'DD.MM.YYYY HH24:MI:SS'), 'DD.MM.YYYY'), 'DD.MM.YYYY')
into pformateddate
from dual;
is required to shorten the pDate-value! i tested it, so that works too and you can ignore it in the whole logic. It's just here to give you a complete picture of the situation!
This is a very common pattern in SQL forums. The pattern is the OP says
"I run this SQL in my TOAD worksheet
(or whatever) and it works. But when
I include it in a different context -
such as a stored procedure - it
doesn't work. What gives?"
What gives is that the two statements are not the same. Somewhere there is a mis-transcription. Perhaps a join has been omitted or an extra one added. The most likely source of errors is the replacement of literals in the worksheet with parameters in the stored procedure.
Obviously I cannot tell you where the difference lies. All I can do is urge you to closely inspect the two SQL statements and figure out the discrepancy.
If you really cannot find any difference then you will need to debug your code. The quickest way to start is with the Devil's Debugger. After the insert statement add this line:
dbms_output.put_line('Rows inserted = '||to_char(sql%rowcount));
You'll need to enable DBMS_OUTPUT in TOAD; there's a tab for it somewhere. This will at least tell you whether the query really is returning zero rows or your procedure is inserting rows and you're not seeing them for some reason. Those are two different problems.
You would need to COMMIT in the Toad session where you ran this procedure before you could see that data in the table in any other session, such as the table browser. Did you remember to do that?
Not really relevant to your problem but this
Select to_date(to_char(to_date(pdate, 'DD.MM.YYYY HH24:MI:SS'), 'DD.MM.YYYY'), 'DD.MM.YYYY')
into pformateddate
from dual;
is just a long winded way of removing the time element from the passed parameter. This does the same thing:
Select trunc(pdate)
into pformateddate
from dual;
Or indeed as Tony points out, a straightforward assignment:
pformateddate := trunc(pdate);