Line break in dbms_output.put_line - oracle

I run some Oracle procedure with the help of cursor and get output in logfile via dbms_output.put_line.
What I would like to do is break line on server_name, is it possible with dbms_output.put_line?
currently it list everything together which doesn't look tidy.
Also some server id appears as 1234.9 or 1234.88, is it possible to set numformat like 999.99? somehow I can't do col server_id for 999.99 within procedure.
create procedure proc (vServer IN VARCHAR2, vServerID IN NUMBER)
IS
CURSOR curTable
IS
SELECT server_name, server_id
FROM tab1
WHERE server_name = vServer
and server_id = vServerID;
BEGIN
FOR rec1 IN curTable
LOOP
dbms_output.put_line(rec1.server_name || ' '|| rec1.server_id);
END LOOP;
END proc;
Sample required output:
S1 1234
S1 1234
S1 1234
S2 5678
S2 5678

Doing a break on server_id only makes sense if you specify that column as the sort order. Then, you have to code the break logic yourself. Also, you can use TO_CHAR to format the number as you like.
Here is the code that should do what you want:
create procedure proc (vServer IN VARCHAR2, vServerID IN NUMBER)
IS
CURSOR curTable
IS
SELECT server_name, server_id
FROM tab1
WHERE server_name = vServer
and server_id = vServerID
ORDER BY server_id ;
l_last_server_id tab1.server_id%TYPE := 0;
BEGIN
FOR rec1 IN curTable
LOOP
-- Test for break:
IF last_server_id != rec1.server_id THEN
-- Break detected:
dbms_output.put_line('---'); -- DBMS_OUTPUT will not print a blank line.
END IF ;
dbms_output.put_line(rec1.server_name || ' '|| TO_CHAR(rec1.server_id,'999.99');
l_last_server_id := rec1.server_id ;
END LOOP;
END proc;

This seems like something you'd do with plain SQL, but assuming this is a PL/SQL exercise, you can use a variable to track the last value seen and add an extra line if it changes:
...
last_server_name tab1.server_name%type;
BEGIN
FOR rec1 IN curTable
LOOP
if last_server_name is not null
and rec1.server_name != last_server_name then
dbms_output.new_line;
end_if;
dbms_output.put_line(rec1.server_name
|| ' '|| to_char(rec1.server_id, '99990.00'));
last_server_name := rec1.server_name;
END LOOP;
END proc;
You can use to_char() inside the dbms_output call, with whatever format model is suitable - you're doing an implit conversion anyway; or in the cursor if you don't ned the ID as a number within the loop.
Remember that you'll only see the output if your client is set up for it - with set serveroutput on, for example. You can't generally rely on that, so it's not a good idea to use dbms_output in real code for anything except debugging.
And as Scott K. pointed out, you need to order your cursor results for this to work, so add order by server_name to the cursor query. Except, you're only looking for a single server name and ID anyway as you're filtering on vServer and vServerID, so your original query can't produce your original output anyway...

Related

ORA-06511: PL/SQL: cursor already open. I am closing my cursor but no luck

I am getting the ORA-06511: PL/SQL: cursor already open ERROR.
Not sure why I am getting this error since I am closing my cursor.
Please see code below.
BEGIN
OPEN findem; ---OPENING HERE!
FOR crfindem IN findem LOOP
FETCH findem into other1, other2, other3;
l_CollectionOfRows(Counter).tmps_key := other1;
l_CollectionOfRows(Counter).tmps_cfb_rate := other2;
l_CollectionOfRows(Counter).tmps_engagement_pay_rate := other3;
Counter := Counter + 1;
END LOOP;
CLOSE findem;---CLOSING HERE!
FORALL i IN l_CollectionOfRows.FIRST .. l_CollectionOfRows.LAST
UPDATE Base.Table
SET MARGIN = :PAGE56_MARGIN,
PERCENT = :PAGE56_MARGIN + l_CollectionOfRows(i).rate,
PAY_RATE = (l_CollectionOfRows(i).pay_rate * (:PAGE56_MARGIN + l_CollectionOfRows(i).rate)) + l_CollectionOfRows(i).pay_rate
WHERE tmps_key = l_CollectionOfRows(i).tmps_key;
END;
I read from some online threads that for every Insert/Update statement, Oracle will create an implicit cursor. If this is the case how do you treat those implicit cursors that Oracle creates?
You are getting that error because you are opening the same cursor twice: the FOR construct already does all these things for you:
FOR opens the cursor
FOR implicitly declares a record variable (the one named crfindem, in your code) that will receive the values for each row read from the cursor
FOR loops on every row and assigns the values of the current row to the crfindem variable
FOR automatically closes the cursor at the end of the loop
so you don't need any OPEN/CLOSE/FETCH .. INTO commands if you are using a FOR loop:
see this simple example: itjust works.
declare
cursor cur is select * from user_tab_comments;
begin
for c in cur loop
DBMS_OUTPUT.PUT_LINE( c.table_name || ' - ' || c.comments);
end loop;
end;
but if i try to open the cursor before using the for loop, I will get your same error because the cursor is already open and the for construct is trying to open it again:
declare
cursor cur is select * from user_tab_comments;
begin
open cur; -- this is not needed and will cause problems
for c in cur loop --! ERROR: here I am trying to open AGAIN the same cursor
DBMS_OUTPUT.PUT_LINE( c.table_name || ' - ' || c.comments);
end loop;
end;
so, you either must choose if you want to write this code:
declare
cursor cur is select * from user_tab_comments;
begin
for c in cur loop
DBMS_OUTPUT.PUT_LINE( c.table_name || ' - ' || c.comments);
end loop;
end;
or avoid using the FOR construct and do all the open/fetch/close operations by yourself, by writing this:
declare
-- I cant' use "select *" here:
-- if I use "fetch into" to a precise list of variables,
-- I have to extract exactly the fields I want to assign:
cursor cur is
select table_name,comments
from user_tab_comments;
tabname varchar(100);
tabcomment varchar2(4000);
begin
open cur;
loop
fetch cur into tabname,tabcomment;
exit when cur%notfound;
DBMS_OUTPUT.PUT_LINE( tabname || ' - ' || tabcomment);
end loop;
close cur;
end;
Your error is that your code is trying to do both these things at the same time.
you should have written this:
-- OPEN findem; NO NEED TO OPEN THE CURSOR (when using FOR)
FOR crfindem IN findem LOOP
--- FETCH findem into other1, other2, other3; FOR ALREADY DOES THIS: the values are in crfindem
l_CollectionOfRows(Counter).tmps_key := crfindem.name_of_the_first_field;
l_CollectionOfRows(Counter).tmps_cfb_rate := crfindem.name_of_the_second_field;
l_CollectionOfRows(Counter).tmps_engagement_pay_rate := crfindem.name_of_the_third_field;
Counter := Counter + 1;
END LOOP;
--- CLOSE findem; NO NEED TO CLOSE THE CURSOR (when using FOR)
Now let me add some considerations about your code (and about this example):
I don't see where you initialize your Counter variable: you MUST initialize it to 0 before entering the loop, because otherwise its initial value will be NULL and will stay null for the whole operation because (NULL + 1) evaluates again to NULL.
I don't see how your cursor is declared, so I don't know the names of the fields it extracts. in the code above I used the "fake" names name_of_the_first_field, name_of_the_second_field, name_of_the_third_field... but you must use the correct field names returned by your query
if your cursor returns some calculated value (like "select 1+2, sysdate, null from dual") you must assign a name to the calculated column to make it accessible by giving an alias to each calculated column you extract ("select 1+2 AS first_name, sysdate AS second_name, null as third_name from dual")
Edit... another info: you don't really need to declare a variable for each field even when you are explicitly using open/fetch/close: you can declare a RECORD variable (that will contain all column values with the same column names, exactly like the for loop does) by using the %ROWTYPE syntax. my example becomes like this, using %rowtype:
declare
cursor cur is select * from user_tab_comments;
-- here I am declaring a variable named c that is a RECORD variable:
-- it can contain a whole row returned by cursor cur
c cur%rowtype;
begin
open cur;
loop
fetch cur into c;
exit when cur%notfound;
DBMS_OUTPUT.PUT_LINE( c.table_name || ' - ' || c.comments);
end loop;
close cur;
end;

My long time SQL*Plus loop doesn't print DBMS_OUTPUT.PUT_LINE output during execution

I know that in order to print something on sqlplus like below:
begin
dbms_output.put_line('Hello!');
end;
/
I need to call
set serveroutput on;
before that.
I also know that is not needed, but I can also call
DBMS_OUTPUT.enable;
before, just in case. This is working for me.
But what if I want to keep printing the progress of a long loop? It seems impossible to me. I've tried everything to print some progress on the loop below but just doesn't work. Is there some way of doing that? I even tried to spool to a file and didn't work.
Note 1: I can't truncate or partition this table as the DBA doesn't want to help me with that, so I have to use this nasty loop...
Note 2: I've noticed that once the loop is done, the whole output is printed. Looks like oracle is buffering the output and printing everything at the end. I'm not sure how to avoid that and print on every loop iteration.
set serveroutput on;
declare
e number;
i number;
nCount number;
f number;
begin
DBMS_OUTPUT.enable;
dbms_output.put_line('Hello!');
select count(*) into e from my_big_table where upd_dt < to_date(sysdate-64);
f :=trunc(e/10000)+1;
for i in 1..f
loop
delete from my_big_table where upd_dt < to_date(sysdate-64) and rownum<=10000;
commit;
DBMS_OUTPUT.PUT_LINE('Progress: ' || to_char(i) || ' out of ' || to_char(f));
end loop;
end;
Thank you for any answer.
There are 2 standard ways for such things:
set module and action in your session DBMS_APPLICATION_INFO.SET_MODULE:
SQL> exec DBMS_APPLICATION_INFO.SET_MODULE('my_long_process', '1 from 100');
PL/SQL procedure successfully completed.
SQL> select action from v$session where module='my_long_process';
ACTION
----------------------------------------------------------------
1 from 100
set session_longops:
DBMS_APPLICATION_INFO.SET_SESSION_LONGOPS
I'd recommend it in your case since that is exactly designed for long operations.
Example on Oracle-Base.
----
PS: dbms_output,put_line saves all output in a collection (nested table) variable of dbms_output package, so you can't get it from another session and client can't get it during user call (execution). In addition to set serveroutput on you can also get the output using dbms_output.get_lines: http://orasql.org/2017/12/10/sqlplus-tips-8-dbms_output-without-serveroutput-on/
Btw, in case if you need to filter or analyze output from dbms_output, sometimes it's convenient to get output in a query, so you can use filter strings in where clause or aggregate them: https://gist.github.com/xtender/aa12b537d3884f4ba82eb37db1c93c25
DBMS_OUTPUT will only ever be displayed after the PL/SQL code has terminated and control has returned to the calling program.
Output is, as you found, buffered. When your PL/SQL code finishes, then the calling program (e.g. SQL*Plus) can go and fetch that output.
Insert into another table, maybe call it "MYOUTPUT".
Create the table:
create table myoutput (lineno number, outline varchar2(80));
Add this after your delete:
insert into MYOUTPUT values (i,'Progress: ' || to_char(i) || ' out of ' || to_char(f));
Then select from MYOUTPUT periodically to see progress.
select outline from myoutput order by lineno;
Bobby
You can use UTL_FILE to write output to an external file, as in:
DECLARE
fh UTL_FILE.FILE_TYPE;
nRow_count NUMBER := 0;
BEGIN
fh := UTL_FILE.FOPEN('DIRECTORY_NAME', 'some_file.txt', 'w');
FOR aRow IN (SELECT *
FROM SOME_TABLE)
LOOP
nRow_count := nRow_count + 1;
IF nRow_count MOD 1000 = 0 THEN
UTL_FILE.PUT_LINE(fh, 'Processing row ' || nRow_count);
UTL_FILE.FFLUSH(fh);
END IF;
-- Do something useful with the data in aRow
END LOOP; -- aRow
UTL_FILE.FCLOSE_ALL; -- Close all open file handles, including
-- the ones I've forgotten about...
END;

How do I define another temporary variable and fetch into it?

Here is My code;-
CREATE OR REPLACE PROCEDURE GetDeails
(c_name VARCHAR2,
calories NUMBER)
DECLARE
CURSOR cur IS SELECT CATEGORY.Name FROM CATEGORY INNER JOIN FILLING ON CATEGORY.CategoryID = FILLING.CategoryID
WHERE c_name=FillING.Name AND calories=GramCalories;
fil cur%ROWTYPE;
BEGIN
OPEN cur;
LOOP
FETCH cur INTO fil;
EXIT WHEN (cur%NOTFOUND);
IF fil%NOTFOUND THEN
DBMS_OUTPUT.PUTLINE('NotFound');
ELSE
DBMS_OUTPUT.PUTLINE(fil.c_name, fil.calories);
END IF;
END LOOP;
CLOSE cur;
END GetDetails;
/
Basically your PROCEDURE statement is good, but having some little issues, such as :
Convert the name GetDeails to GetDetails in order to have the
matching name with the one given at the end after the last END
keyword. Indeed, using the PROCEDURE's name twice is redundant, so,
not needed.
There should be IS or AS keyword just after IN parameters' list, and the keyword DECLARE should be removed.
DBMS_OUTPUT.PUTLINE should be converted to DBMS_OUTPUT.PUT_LINE,
and two matching columns( Name and GramCalories ) should be provided in the CURSOR's SELECT list.
cursor attribute may not be applied to non-cursor FIL but to CUR
SQL> SET serveroutput ON
SQL> CREATE OR REPLACE PROCEDURE GetDetails( c_name VARCHAR2, calories NUMBER ) IS
CURSOR cur IS
SELECT f.Name, c.GramCalories
FROM CATEGORY c
JOIN FILLING f
ON f.CategoryID = c.CategoryID
WHERE c_name=f.Name
AND calories=GramCalories;
fil cur%ROWTYPE;
BEGIN
OPEN cur;
LOOP
FETCH cur INTO fil;
EXIT WHEN (cur%NOTFOUND);
IF cur%NOTFOUND THEN
DBMS_OUTPUT.PUT_LINE('NotFound');
ELSE
DBMS_OUTPUT.PUT_LINE(fil.name, fil.calories);
END IF;
END LOOP;
CLOSE cur;
END;
/
The answer by #Barbaros solves most of your issue but can be further refined.
The IF statement within the loop is completely unnecessary as it will never return True when executed. If it were true the exit
statement preceding it would have exited the loop; thus no message. This is redundant; making a test where the result is already known. You could reverse the order and put the exit after the IF...END IF. But then the 'not found' message would always be produced. You can use cur%rowcount after the loop to generate the message correctly.
The dbms_output_put_line(fil.name,fil.calories) has 2 errors.
variable fil.calories does not exist. GramCalories was not selected in your original (as pointed out) nor aliased in the revision. So is not part of the cursor and thus not part of the
cursor row type.
It requires a single string parameter, as is there are 2 parameters.
Taking these into account we get:
create or replace procedure getdetails( c_name varchar2, calories number ) is
cursor cur is
select f.name, c.gramcalories
from category c
join filling f
on f.categoryid = c.categoryid
where c_name=f.name
and calories=gramcalories;
fil cur%rowtype;
begin
open cur;
loop
fetch cur into fil;
exit when (cur%notfound);
dbms_output.put_line(fil.name || ' ' || fil..gramcalories);
end loop;
if cur%rowcount = 0 then
dbms_output.put_line('Not Found');
end if;
close cur;
end getdetails;
/
As a matter of style:
Avoid the CamelCase naming convention. Oracle always folds object names to uppercase. Thus it just makes Oracle generated references difficult to read. Instead use words separated by Underscore (_).
Unlike Barbaros I do not consider using the procedure (function,
package, ...) name on the terminating end as redundant but more as a
closure as much as an 'end if'. Yes it is syntactically optional, but
optional is not the same a redundant. I always exercise that option.
So developed your style (subject to institutional/customer mandated
standards), but be consistent with it.

how do I use if condition in cursor because our professor don't allow us use where clause in the select statement

Our question is showing all the countries that have names that are exactly 5 letter long. This is the cursor code and I want add if condition into it.
declare
cursor cursor_full is select * from country_cont;
begin
for counter in cursor_full
loop
dbms_output.put_line(counter.country|| ' ' || counter.continent);
end loop;
end;
However my professor said that you can't using where clause within the select statement and you should display all the countries and continent.
so i tried this code:
declare
country varchar(50);
cursor cursor_full is select * from country_cont;
begin
if length(country)=5 then
for counter in cursor_full
loop
dbms_output.put_line(counter.country|| ' ' || counter.continent);
end loop;
end if;
end;
the script output show that PL/SQL procedure successfully completed but nothing return in DBMS output
Hope someone can help me, I spent whole night to think about it,please!
Variable country doesn't contain any value, it is null so if condition is never true and loop is never executed. Sample data would help; meanwhile, see if this helps.
begin
for cur_r in (select * from country_cont) loop
if length(cur_r.country) > 5 then
dbms_output.put_line(cur_r.country|| ' ' || cur_r.continent);
end loop;
end;
Don't forget to set serveroutput on.

Pass an array to bind variables

I have a table that contains queries, for example:
select text from queries;
TEXT
1 select item from items where item_no between :low_item_no and :high_item_no and description <> :irellevant
The queries already contains the place holders for the bind variables.
The values themselves exists in variables table:
select * from vars;
ID NAME VALUE
1 1 low_item_no 100
2 2 high_item_no 300
3 3 irellevant_desc old
I have a package that takes the query and execute it with
execute immediate statement
but how do I bind those variables?
I don't know how much variables I have in such query, it's not static.
I wish to have a way to do something like that:
Execute immedaite my_query_str using v_array_of_vars;
Until now I don't know of a way to do something like that, only with list of variables for example:
Execute immedaite my_query_str using v_1, v_2, v_3;
Thanks!
I don't think you can do this with execute immediate as too much is unknown at compile time, so you'll have to use the dbms_sql package instead.
Here's a quick demo that gets the query and variables based on a common query ID. This assumes that the values in vars.name actually match the bind variable names in queries.text, and I haven't included any checks or error handling for that or other potential issues, or dealt with multiple select-list items or data types - just the basics:
declare
my_query_str queries.text%type;
my_cursor pls_integer;
my_result pls_integer;
my_col_descs dbms_sql.desc_tab2;
my_num_cols pls_integer;
my_item items.item%type;
begin
select text into my_query_str from queries where query_id = 42;
dbms_output.put_line(my_query_str);
-- open cursor
my_cursor := dbms_sql.open_cursor;
-- parse this query
dbms_sql.parse(my_cursor, my_query_str, dbms_sql.native);
-- bind all variables by name; assumes bind variables match vars.name
for r in (select name, value from vars where query_id = 42) loop
dbms_output.put_line('Binding ' || r.name || ' || with <' || r.value ||'>');
dbms_sql.bind_variable(my_cursor, r.name, r.value);
end loop;
my_result := dbms_sql.execute(my_cursor);
dbms_output.put_line('execute got: ' || my_result);
dbms_sql.describe_columns2(my_cursor, my_num_cols, my_col_descs);
dbms_sql.define_column(my_cursor, 1, my_item, 30); -- whatever size matches 'item'
-- fetch and do something with the results
while true loop
my_result := dbms_sql.fetch_rows(my_cursor);
if my_result <= 0 then
exit;
end if;
dbms_sql.column_value(my_cursor, 1, my_item);
dbms_output.put_line('Got item: ' || my_item);
end loop;
dbms_sql.close_cursor(my_cursor);
end;
/
You don't seem to really need an array; but if you wanted to you could create and populate an associative array as name/value pairs and then use that fir the binds.
This is just a starting point; you may have to deal with an unknown number and/or types of columns being returned, though if that's the case processing them meaningfully will be a challenge. Perhaps you need to return the result of the query as a ref cursor, which is even simpler; demo using the SQL*Plus variable and print commands:
var rc refcursor;
declare
my_query_str queries.text%type;
my_cursor pls_integer;
my_result pls_integer;
begin
select text into my_query_str from queries where query_id = 42;
dbms_output.put_line(my_query_str);
-- open cursor
my_cursor := dbms_sql.open_cursor;
-- parse this query
dbms_sql.parse(my_cursor, my_query_str, dbms_sql.native);
-- bind all variables by name; assumes bind variables match vars.name
for r in (select name, value from vars where query_id = 42) loop
dbms_output.put_line('Binding ' || r.name || ' || with <' || r.value ||'>');
dbms_sql.bind_variable(my_cursor, r.name, r.value);
end loop;
my_result := dbms_sql.execute(my_cursor);
dbms_output.put_line('execute got: ' || my_result);
:rc := dbms_sql.to_refcursor(my_cursor);
end;
/
print rc
Notice you don't close the cursor inside the PL/SQL block in this scenario.
You could also convert to a ref cursor and then fetch from that within your procedure - there's a bulk-collect example in the docs - but again you'd need to know the number and types of the select-list items to do that.

Resources