Today, I came across a funny piece of code that I think should not compile. It uses an SELECT ... INTO clause within a FOR r IN ... LOOP. Here's a script that compiles on Oracle 11i. The script is a shortened version of actual PL/SQL code compiled in a package, runing in production.
create table tq84_foo (
i number,
t varchar2(10)
);
insert into tq84_foo values (1, 'abc');
insert into tq84_foo values (2, 'def');
declare
rec tq84_foo%rowtype;
begin
for r in (
select i, t
into rec.i, rec.t -- Hmm???
from tq84_foo
)
loop
dbms_output.put_line('rec: i= ' || rec.i || ', t=' || rec.t);
end loop;
end;
/
drop table tq84_foo purge;
The output, when run, is:
rec: i= , t=
rec: i= , t=
I believe 1) I can safely remove the INTO part of the select statement and 2) that this construct should either be invalid or exhibits at least undefined behaviour.
Are my two assumptions right?
Your assumptions are partly right:
1) Yes, you can safely remove the INTO part of the SELECT statement. But you should change the line in the loop to this format:
dbms_output.put_line('rec: i= ' || r.i || ', t=' || r.t);
That way it will get the data out of the r variable
2) The problem with this code is that the syntax of the SELECT ... INTO should fail if the query return more than one row. If it does not fail so it might be a bug and will have unexpected behaviour.
Related
Odd things going on with procedure below.
When I run procedure it insert into table only 125 records. When I insert data without procedure with the same parameters it gives me 15000 records. Do you have any ideas what could it be?
create or replace procedure calc_p(p_startdate number,
p_end_date number,
p_id number) is
begin
dbms_output.put_line('Start: ' || substr(localtimestamp, 1, 17));
delete rep_pd
where b_id = p_id
and rep_date between p_startdate and p_end_date;
-- commit;
insert into rep_pd
with cont
(rep_date, b_id, con_number, okres_zap) as
(select /*+ materialize*/
rep_date,
b_id,
con_number,
nvl(maturity_date - origin_date, 0) as okres_zap
from con#sg_al
where description not in ('OVER', 'Overn')
and b_id = p_id
and rep_date between p_startdate
and p_end_date)
select mr.suffix1,
mr.data_danych,
ma.rep_date,
b_id,
mr.k_symbol,
konto_nazwa,
case
when mr.k_nazwa likr '%EXT%' then
'04'
when mr.k_symbol like '3731%' then
'12'
else
mr.typ_k_symbol
end as cl_t,
ma.okres_zap,
mr.rezydent_symbol,
mr.w_symbol as cu,
mr.sal_ma_pl as outstanding,
get_pd_account(k_symbol) AS depo substr(to_number(to_char(data_danych, 'yyyymmdd')), 7, 2) as dzien
from abc.a_mr mr
left join cont ma
on ma.b_id = p_id
and to_date(ma.rep_date, 'YY/MM/DD') = mr.data_danych
and ma.con_number = mr.suffix1
where sal_ma_pl <> 0
and get_pd_account(k_symbol) is not null
and nvl(typ_k_symbol, '20') not in ('01', '02', '02A')
and mr.data_danych between to_date(p_startdate, 'YY/MM/DD') and
to_date(p_end_date, 'YY/MM/DD');
-- commit;
dbms_output.put_line('Koniec: ' || substr(localtimestamp, 1, 17));
end calc_p;
There are some errors or unadvised uses in your code:
When I copy-paste your code in my editor, I see an error. There is , missing between your depo and dzien columns in select.
Before anything else, double check that you gave us a correct code and that you haven't erased one part of the code. Also, double check that you haven't missed the same part of the code when transferring it in or out of the procedure.
As Littlefoot stated in the comment to your question, there should be no difference between INSERT stetements inside and outside stored procedures, if they are just being encapsulated with BEGIN END. There is one difference however, and that is the way you're defining your input parameters.
I see that you are defining your p_startdate and p_end_date as NUMBER parameters. Possible difference is that you're really expecting dates. The difference between the results of your insert inside and outside the procedure can easily be due to defining your p_startdate and p_end_date parameters as numbers.
If after these corrections you still get a difference in results, please provide us with some sample table data that is getting you that difference.
I want to code a way to check if there's data in a table prior to executing a stored procedure.
I've created some 'stripped down' test code that 'almost' meets the conditions that I seek, I was hoping someone might be able to help me get that to work. If so, then I can substitute the values for my procedure, instead of just the dbms_output and be up and running.
Creates a test table, with no rows.
CREATE TABLE t (c VARCHAR2(20));
Then the way I am trying to do this...
declare
no_such_table exception;
pragma exception_init( no_such_table, -942 );
EXISTS_1 integer;
BEGIN
for tst in (
select count (c) x from t
)
loop
execute immediate' select count (c) Z from ' ||tst.t into EXISTS_1;
if EXISTS_1 <= 0
then
dbms_output.put_line( 'a' );
else dbms_output.put_line( 'b' );
end if;
end loop;
exception
when no_such_table
then
dbms_output.put_line( 'c' );
WHEN NO_DATA_FOUND THEN
dbms_output.put_line( 'd' );
end;
The first part, with the count, is supposed to hold a numeric value to indicate if there are any rows in the table. Then the execute immediate into EXISTS_1 holds the value to decide what output to give.
Firstly, I can't get the execute immediate bit to actually work. But if I could get it working, I want the output to record 'a' if there's no rows in the table. (Actually, I would execute the procedure here) and to record 'b' if there was data in there, which you can insert with:
insert into t (c) values('x');
commit;
The 'c' and 'd' outputs are just attempts to handle other potential issues that may occur.
As things stand, I get an error indicating that component t must be declared. Can you understand what I 'm trying to do, and if so, hopefully suggest a means to achieve my goal please?
your first select in a loop will always return one row, so there now Need to do a loop.
using tst.t in a Loop is not possible.
is that what are you looking for?
declare
x number := 0;
begin
select count(c) into x from t;
if x <= 0
then
dbms_output.put_line( 'a' );
else
dbms_output.put_line( 'b' );
end if;
end;
I have a Oracle related question. I would like to select a random sample out of a view or table in such a way that the SAMPLE clause is parameterized.
Given the following table.
CREATE TABLE FOO AS
(SELECT LEVEL AS ID
FROM DUAL
CONNECT BY LEVEL < 101
);
The following construct works, using a literal parameter in the SAMPLE clause.
SELECT ID FROM FOO SAMPLE (15); -- this will get a 15% sample
However,
DECLARE
N NUMBER := 50;
BEGIN
FOR r IN
( SELECT ID FROM FOO SAMPLE (N) -- <<< this won't work
)
LOOP
DBMS_OUTPUT.PUT_LINE( r.ID );
END LOOP;
END;
This block blows up when we put a parameter in the SAMPLE clause. It compiles and works if we put it a literal.
But if it is a variable, I get the following:
ORA-06550: line 5, column 33:
PL/SQL: ORA-00933: SQL command not properly ended
Any ideas? I'm racking by brains where the syntax gets broken.
The syntax does not allow a variable there.
One workaround would be to construct the SELECT statement dynamically. For example:
declare
l_rc sys_refcursor;
n number := 5;
begin
-- replace "mtl_system_items" with your table...
open l_rc FOR 'select count(*) from mtl_system_items sample (' || n || ')';
-- replace call to RETURN_RESULT with whatever processing you want
DBMS_SQL.RETURN_RESULT(l_rc);
end;
I've got this PL/SQL procedure which runs for about 4-6 minutes:
DECLARE
i NUMBER := 0;
begin
for x in (select anumber
, position
, character
from sdc_positions_cip
where kind = 'Name')
loop
update sdc_compare_person dcip
set dcip.GESNAM_D = substr(dcip.GESNAM_D, 1, x.position - 1) || x.character ||
substr(dcip.GESNAM_D, x.position + 1, length(dcip.GESNAM_D) - x.position)
where dcip.sourcekey = x.anumber;
i := i + 1;
IF i > 100 THEN COMMIT;
i := 0;
END IF;
end loop;
commit;
end;
/
I'v placed an index on dcip.sourcekey and x.anumber.
The tablespace that it's using is 10GB.
Is there a way to make this procedure (much) faster?
Your performance bottleneck is the loop. It forces your code to switch between PLSQL and Oracle SQL for every single UPDATE-Statement.
In order to eliminate these context switches, you could probably use an UPDATE-Statement containing a subselect, but I more like MERGE, for example like in the following way:
merge into sdc_compare_person dcip
using (
select anumber, position, character
from sdc_positions_cip
where kind = 'Name'
) x
on (dcip.sourcekey = x.anumber)
when matched then update set
dcip.GESNAM_D = substr(dcip.GESNAM_D, 1, x.position - 1) ||
x.character ||
substr(dcip.GESNAM_D, x.position + 1, length(dcip.GESNAM_D) - x.position);
Another option would be to use BULK COLLECT INTO and FORALL to perform bulk selects and bulk inserts. Due to the limited complexity of your procedure, I strongly recommend using a single statement like mine.
You can also try this version:
update
(select dcip.GESNAM_D, x.position, x.character, dcip.sourcekey, anumber
from sdc_compare_person dcip
join sdc_positions_cip on dcip.sourcekey = x.anumber)
set GESNAM_D = substr(GESNAM_D, 1, position - 1) || character || substr(GESNAM_D, position + 1, length(GESNAM_D) - position);
I need to update a non-unique field. I have a table tbl:
create table tbl (A number(5));
Values in tbl: 1, 2, 2, 2 .. 2.
I need to replace all 2 with new non-unique values
New values: 1, 100, 101, 102, 103 ..
I wrote:
DECLARE
sql_stmt VARCHAR2(500);
cursor curs is
select A from tbl group by A having count(*)>1;
l_row curs%ROWTYPE;
i number(5);
new_mail VARCHAR2(20);
BEGIN
i:=100;
open curs;
loop
fetch curs into l_row;
exit when curs%notfound;
SQL_STMT := 'update tbl set a='||i||' where a='||l_row.A;
i:=i+1;
EXECUTE IMMEDIATE sql_stmt;
end loop;
close curs;
END;
/
But I got:
A
----------
1
100
...
100
What can be wrong? Why doesn't the loop work?
what about
update tbl
set a = 100 + rownum
where a in (
select a
from tbl
group by a
having count(*) > 1 )
the subquery finds duplicated A fields and the update gives them the unique identifier starting from 100. (you got other problems here like , what if id 100, 101.... already exists ).
first rule of PLSQL says that what ever you can do with SQL always do with SQL. writing straight up for loop cause allot of context switches between the sql and pl/sql engine. even if oracle automatically converts this to a bulk statement (10g<) it will still be faster with pure SQL.
Your cursor gets one row per unique value of A:
select A from tbl group by A having count(*)>1;
You need to get all the distinct rows that match those values. One way is to do this:
select a, r from (
select a, rowid as r, count(*) over (partition by a) as c
from tbl
) where c > 1;
... and then use the rowid values to do the update. I'm not sure why you're using dynamic SQL as it is not at all necessary, and you can simplify (IMO) the loop:
declare
i number(5);
begin
i:=100;
for l_row in (
select a, r from (
select a, rowid as r, count(*) over (partition by a) as c
from tbl
) where c > 1) loop
update tbl set a=i where rowid = l_row.r;
i:=i+1;
end loop;
end;
/
I've kept this as PL/SQL to show what was wrong with what you were attempting, but #haki is quite correct, you should (and can) do this in plain SQL if at all possible. Even if you need it to be PL/SQL because you're doing other work in the loop (as the new_mail field might suggest) then you might be able to still do a single update within the procedure, rather than one update per iteration around the loop.