Background
I use Oracle 12c. My PL/SQL procedure uses a cursor containing a "Recursive With" SQL statement that selects from a materialized view that in turn does a join on a table called client_client_relationship and a view called vw_all_clients.
The purpose of the cursor is to take an ID for a business (business_client_id) and to return the entire ownership hierarchy for that business, e.g., for business with ID 27, show what business owns it, and then what business owns that business, until finally we reach the persons who own the top-level business.
Note that the cursor uses "path" to return the full hierarchy of ownership, e.g., ABC Corp\DEF Corp\GHI Corp\JKL Corp\John Smith
The problem
This procedure works for many input values (business_client_id's), but for one it does not.
When the cursor-for loop that selects from this cursor for a specific business_client_id input fetches the 6th row, I get a "ORA-06502: PL/SQL: numeric or value error" error message.
What I tried:
Reviewed documentation about this recursive functionality.
Searched for cases where this occurred to somebody else and did not find any.
When I removed the "path" from the SQL statement, the error did not occur, which indicates that this problem is probably caused by the value for the "path" getting too long.
Unfortunately, I can not show the results of the procedure because the client names used are confidential. But I can state that the 6th row returned by the cursor had a path that got significantly larger than the path for the first 5 rows (~ 120 characters), which is evidence in favor of my suspicions about the path length being the cause of this problem.
Question
What can I do to resolve this? If my guess is correct, can I change how long the value for "path" can be? The path can get quite long and there's nothing I can do about that fact.
If there's any other information you need, please let me know and I'll be happy to provide it.
Thanks.
Here is the cursor from this procedure:
-- Create a cursor that recursively selects the owners of the passed client_id.
CURSOR c_vp_owners
IS
WITH base_query (client_id2, owner_name, client_id1, business_name, client_type, path, Levl) -- The aliases for the returned columns.
AS
(
-- Select the root row -- the owner of the passed business.
SELECT
client_id2, owner_name,
client_id1, business_name,
client_type, owner_name,
1 Levl -- The recursive level.
FROM client.mv_client_business_owner_tree -- This mv joins ccr and vw_all_clients.
WHERE client_id1 = p_business_client_id -- the client_id of the business -- passed in. ******
UNION ALL
-- ************************************
-- Select the recursive rows -- if the owner returned above is a business, select its owner/s.
SELECT
mv.client_id2, mv.owner_name,
mv.client_id1, mv.business_name,
mv.client_type, bq.path || '/' || mv.owner_name,
Levl + 1
FROM client.mv_client_business_owner_tree mv, base_query bq
WHERE mv.client_id1 = bq.client_id2 -- The recursive clause.
)
SEARCH DEPTH FIRST BY business_name SET row_sequence -- Traverse the tree down the child rows first, before selecting rows that are at the same level.
SELECT client_id2, owner_name, client_id1, business_name, client_type, path, Levl, row_sequence
FROM base_query;
A few explanatory comments about table client_client_relationship
It contains client_id1, client_id2, client_id1_relation, client_id2_relation, etc.
So an example row would be:
client_id1 -- 100
client_id2 -- 200
client_id1_relation -- OWNER
client_id2_relation -- BUSINESS
Note that the OWNER could itself be a business, which would have another row in the table in which it would be the BUSINESS and another client would be the OWNER, i.e., there is a recursive relationship in this table.
The top-level row would have a person's client_id as the owner of a business.
The procedure that this cursor is in has this Declaration section:
-- Declare a variable for exception handling.
v_client_id2 client.client.client_id%TYPE;
And this executable section:
BEGIN
-- Open the c_vp_owners cursor and populate vessel_permit_owner with the owners of the passed vessel permit.
FOR vp_owners_record IN c_vp_owners LOOP
-- Populate a variable for exception handling.
v_client_id2 := vp_owners_record.client_id2;
INSERT INTO scoq_priv_owner
(priv_id, program, business_owner_client_id, person_owner_client_id, person_owner)
VALUES (p_priv_id, p_program, p_business_client_id, vp_owners_record.client_id2, vp_owners_record.path);
END LOOP;
COMMIT;
END pr_load_scoq_priv_owners;
scoq_priv_owner.person_owner is typed as VARCHAR2(2000) -- plenty of room.
Also,
When I run just the recursive query all by itself in a script, I get the full set of 12 rows with the paths -- no errors. The longest path is 202 characters long.
Thanks to everybody who feels like lending a hand with this.
Related
Trying to write a procedure which takes no values in, adds a sale price column to my existing product table, then loops through to calculate a sale price and insert that into the new column.
I haven't been able to get anything to work, I think it's something to do with Oracle not liking ALTER TABLE to be run from inside a procedure, but I don't know, and I don't know enough to direct my attempts anywhere else.
This is my attempt
CREATE or REPLACE PROCEDURE ProductLineSale as
BEGIN
DECLARE
NewSalePrice NUMBER(6,2):=0;
EXECUTE IMMEDIATE 'alter table ' || Product || 'add or replace column' || 'SalePrice NUMBER(6,2);'
FOR p in (SELECT ProductStandardPrice FROM Product
group by ProductStandardPrice)
LOOP
CASE WHEN p.ProductStandardPrice>=400 THEN NewSalePrice:=.9*price
WHEN p.ProductStandardPrice<400 THEN NewSalePrice:=.85*price
INSERT INTO Product(SalePrice)
VALUES(NewSalePrice)
END LOOP;
END ProductLineSale
Product is the literal name of the Product table in my database. SalePrice is what I would like the new column to be named.
SQLDeveloper won't compile the procedure. The error I get is fairly cryptic as well:
Error(2,10): PLS-00103: Encountered the symbol "=" when expecting one of the following: constant exception table long double ref char time timestamp interval date binary national character nchar.
There are a host of errors... The ones that jump out at me on a first pass.
The requirement doesn't make sense. Adding a column in a procedure doesn't make sense. You create a procedure because you want code to be reusable. Adding a column can only be done once, hence it is by definition not reusable.
A procedure has to be compiled before it can be executed. If there is a reference to a column that doesn't exist, the procedure will fail to compile. Thus, if you want to add a column to the table using dynamic SQL, all subsequent references to the column (i.e. your insert statement) would need to use dynamic SQL as well.
Your DDL statement is incorrect. There is no add or replace clause, it's alter table product add SalePrice NUMBER(6,2). Note that when you're building your string, you also have to ensure that there is a space between the clause add and the column name SalesPrice-- one of the two strings you're concatenating together would need that.
It doesn't make sense to have a declare where you do. You can declare variables between the as and the begin one line above. You are allowed to create a nested PL/SQL block there with the declare but then you'd need a matching begin and end that you don't have.
If you're going to use a case statement in PL/SQL, you'd need an end case. You would also need to have a semicolon ; after each expression.
Your insert statement is also missing a semicolon.
Logically, I am hard pressed to imagine that you really want have an insert here. It doesn't make logical sense to create a bunch of new rows in the table when you add a new column. I would assume that you want to update the value of the new column in existing rows. Which, presumably, requires that your cursor selects the primary key column(s) and potentially changes whether and what you're grouping by.
Product and price are being used as local variables in the execute immediate statement and in the case statement but aren't defined. I'm guessing that you just want to hard code the name of the table you're altering and that price is supposed to reference the name of a column in the table that you need to select in your cursor but I'm not sure.
This case statement is syntactically valid (or would be if price resolves to something valid). Many of the other corrections are less obvious because of the reasons I detailed above.
case when p.ProductStandardPrice>=400
then NewSalePrice:=.9*price;
when p.ProductStandardPrice<400
THEN NewSalePrice:=.85*price;
end case;
If I was to speculate at what you actually want (given that this is a homework assignment with requirements that don't actually make sense), I'd guess something like
CREATE or REPLACE PROCEDURE ProductLineSale
as
begin
execute immediate 'alter table Product add SalePrice NUMBER(6,2)';
execute immediate 'update product ' ||
' set SalePrice = (case when ProductStandardPrice >= 400 ' ||
' then 0.9 * Price ' ||
' else 0.85 * Price ' ||
' end) ';
end ProductLineSale;
If you're going to use dynamic SQL, it almost always makes sense to declare a local variable, build the SQL statement in that variable, and then execute it so that you can debug things by printing out the statement you've build to debug it.
My table looks like this:
+-------------------+
|Name |
+-------------------+
|Name1 |
|Name2 |
|Name3 |
|Name4 |
|Name1Jr |
|Name2Jr |
|Name4Jr |
+-------------------+
My multirow block looks like:
What I wanted to know is how can I insert a record that has the same name with Jr after I insert a name. For example, I inserted Name2, it will also insert Name2Jr into the multirow block. Like this:
Note: I need to get the value of the automatically inserted data from database.
I tried in WHEN-NEW-RECORD-INSTANCE trigger(the answer of Sir #Littlefoot on my last question):
if :system.trigger_record = 1 and :test.name is null then
-- do nothing if it is the first record in a form
null;
else
duplicate_record;
if substr(:test.name, -2) = 'Jr' then
-- you've duplicated a record which already has 'Jr' at the end - don't do it
:test.name := null;
else
-- concatenate 'Jr' to the duplicated record
:test.name := :test.name || 'Jr';
end if;
end if;
And now, what I want is to know if there is a way to do it on WHEN-VALIDATE-RECORD trigger. The problem there is that the duplicate_record can't be used in WHEN-VALIDATE-RECORD trigger. How to do it using procedure, function or something? Thanks in advance!
DUPLICATE_RECORD is a restricted procedure and you can't use it in WHEN-VALIDATE-RECORD trigger (or any other of the same kind).
As you have to navigate to the next record (if you want to copy it), even if you put that restricted procedure into another PL/SQL program unit, everything will just propagate and - ultimately - raise the same error. So ... you're out of luck.
Even if you wrote a (stored) procedure which would insert that "Jr" row into the database somewhere behind the scene, you'd have to fetch those values to the screen. As EXECUTE_QUERY is the way to do it, and as it is (yet another) restricted procedure, that won't work either.
If you planned to clear data block and fill it manually (by using a loop), you'd have to navigate to next (and next, and next) record with NEXT_RECORD, and that's again a restricted procedure. Besides, if it was a data block (and yes, it is), you'd actually create duplicates for all records once you'd save changes so - either it would fail with unique constraint violation (which is good), or you'd create duplicates (which is worse).
BTW what's wrong with WHEN-NEW-RECORD-INSTANCE? What problems do you have when using it?
What We need is
a Data Block (BLOCK1) with
Query Data Source Name is mytable,
and Number of Records Displayed set to more than 1(let it be 5 as in your case).
a Text item named as NAME same as the column name of the table
mytable, and Database Item property set to Yes.
a Push Button with Number of Records Displayed set to 1,
Keyboard Navigable and Mouse Navigate properties are set to No
and has the following code (inside WHEN-BUTTON-PRESSED trigger):
commit;
Query_Block1;
Where Query_Block1 is beneath Program Units node with this code :
PROCEDURE Query_Block1 IS
BEGIN
execute_query;
last_record;
down;
END;
A POST-INSERT trigger beneath BLOCK1 node with code :
insert into mytable(name) values(:name||'Jr');
An ON-MESSAGE trigger at the form level with the code (to suppress the messages after commit):
if Message_Code in (40400, 40401) then
null;
end if;
And WHEN-NEW-FORM-INSTANCE trigger with the code :
Query_Block1;
hi i'm newb in pl/sql :), this is for educational pruposes only.
the schama Dispatching including a table named Employes.and PRF schema that include a table named ZONE.
Dispatching : Employes(num_emp number,name nvarchar2,design_unit varchar2,design_zone varchar2)
and PRF: ZONE(num_zone number,design_zone varchar2,number_of_units number).
the problema is writing a pl/sql procedure to populate ZONE table from Employes table. this is my procedure :
create or replace procedure zoneD as
cursor cur is select design_zone,design_unit from dispatching.employes group by design_zone,design_unit;
varzone cur%rowtype;
begin
open cur;
fetch cur into varzone;loop
exit when cur%notfound;
insert into zone(num_zone,design_zone,nbr_of_unit) values (num_zone.nextval,varzone.design_zone,0);
update zone set nbr_of_unit =( select count(design_unit) from dispatching.employes);
end loop;
close cur;
end zoneD;
the unit is a town , each zone contains many units. in a simple way the prob the procedure does not insert the data i dont know if it is the right way to do that. (sorry about my english :)).
It seems that you are connected as PRF and want to fetch values that belong to DISPATCHING user. In order to do that, DISPATCHING has to grant (at least) SELECT on its EMPLOYEES table to PRF:
-- connect as DISPATCHING
grant select on employees to prf;
A procedure (as you're practicing PL/SQL) should utilize a cursor FOR loop as it is much easier to maintain than a loop which uses explicitly declared cursor (as you don't need to declare it as well as variable(s) you need to store its values into), open it, worry when to exit the loop and - finally - close it. Cursor FOR loop does all of that for you (OK, except writing a SELECT statement which is just the same as the one you'd use while declaring an explicit cursor).
-- connect as PRF
create or replace procedure zoned as
begin
-- cursor FOR loop - you can select both DESIGN_ZONE and count number of
-- units so that you wouldn't have to update that value separately
for cur_r in (select e.design_zone, count(*) number_of_units
from dispatching.employees e -- naming the owner which granted SELECT on its table to PRF user
group by e.design_zone
)
loop
insert into zone (num_zone, design_zone, number_of_units)
values (num_zone.nextval, cur_r.design_zone, cur_r.number_of_units);
end loop;
end;
/
That should do it (unless I made a typo).
Finally, a suggestion, if I may: do format your code properly. The one you posted is a mess difficult to read - no indentation, too long lines (break them!), and it contains only several lines. Imagine what happens when you have thousands of lines of code - who do you expect to debug it? Just a month or two after you've done with that code, you'll forget what you did and why (so comment it), and - if it is unformatted - you'll get a headache. Today's GUI tools offer automatic formatting, so - use it. Otherwise, there are free online formatters, such as Instant SQL Formatter.
I've been tasked with improving old PL/SQL and Oracle SQL legacy code. In all there are around 7000 lines of code! One aspect of the existing code that really surprises me is the previous coder needlessly created hundreds of lines of code by not writing any procedures or functions - instead the coder essentially repeats the same code throughout.
For example, in the existing code there are literally 40 or more repetitions of the following SQL:
CREATE TABLE tmp_clients
AS
SELECT * FROM live.clients;
CREATE TABLE tmp_customers
AS
SELECT * FROM live.customers;
CREATE TABLE tmp_suppliers
AS
SELECT * FROM live.suppliers WHERE type_id = 1;
and many, many more.....
I'm very new to writing in PL/SQL, though I have recently purchased the excellent book "Oracle PL/SQL programming" by Steven Feuerstein. However, as far as I can tell, I should be able to write a callable procedure such as:
procedure create_temp_table (new_table_nme in varchar(60)
source_table in varchar(60))
IS
s_query varchar2(100);
BEGIN
s_query := 'CREATE TABLE ' + new_table_nme + 'AS SELECT * FROM ' + source_table;
execute immediate s_query;
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE = -955 THEN
NULL;
ELSE
RAISE;
END IF;
END;
I would then simply call the procedure as follows:
create_temp_table('tmp.clients', 'live.clients');
create_temp_table('tmp.customers', 'live.customers');
Is my proposed approach reasonable given the problem as stated?
Are the datatypes in the procedure call reasonable, ie should varchar2(60) be used, or is it possible to force the 'source_table' parameter to be a table name in the schema? What happens if the table name is more than 60 characters?
I want to be able to pass a third non-required parameter in cases where the data has to be restricted in a trivial way, ie to deal with cases "WHERE type_id = 1". How do I modify the procedure to include a parameter that is only used occasionally and how would I modify the rest of the code. I would probably add some sort of IF/ELSE statement to check whether the third parameter was not NULL and then construct the s_query accordingly.
How would I check that the table has actually been created successfully?
I want to trap for two other exceptions, namely
The new table (eg 'tmp.clients') already exists; and
The source table doesn't exist.
Does the EXCEPTION as written handle these cases?
More generally, from where can I obtain the SQL error codes and their meanings?
Any suggested improvements to the code would be gratefully received.
You could get rid of a lot of code (gradually!) by using GLOBAL temporary tables.
Execute immediate is not a bad practice but if there are other options then they should be used. Global temp tables are common where you want to extract and transform data but once processed you don't need it anymore until the next load. Each user can only see the data they insert and no redo logs are generated. You can index the data for faster querying if required.
Something like this
-- Create table
create global temporary table GT_CLIENTS
(
id NUMBER(10) not null,
Client_id NUMBER(10) not null,
modified_by_id NUMBER(10),
transaction_id NUMBER(10),
local_transaction_id VARCHAR2(30) not null,
last_modified_date_tz TIMESTAMP(6) WITH TIME ZONE not null
)
on commit preserve rows;
I recommend the on commit preserve rows option so that you can debug your procedure and see what went into the table.
Usage would be
INSERT INTO GT_CLIENTS
SELECT * FROM live.clients;
If this is the route you want to take to minimize changes, then the error for source table does not exist is -942 which you will want to stop for rather than continuing as your temp table would not have been created. Similarly, just continuing if you get an object already exists error will be problematic as you will not have reloaded it with the new data - the create failed so the table still has the data from the last run. So I would definitely do some more thinking about your exception handler.
That said, I also concur that this is generally not the best way to do things. Creating and dropping objects in a multi-user environment is a disaster in the making, and seems a silly waste of resources when there are more appropriate options available.
How do I perform a select on a rather simple view in oracle pl/sql using a stored procedure.
Lets say the view looks like this:
FirstName LastName
-------- -------
Bob Jones
James Kay
etc...
To me its should be so simple:
Procedure SuperSimple()
begin
select FirstName, LastName from SuperSimple
end
However I've been told that this will not work.
So I tried to use a PL/SQL cursor. Still scratching my head trying to figure out why I am using cursors. But it appears to be necessary in 11g.
Procedure AlphaPrime(Results OUT Ref CURSOR) IS
begin
OPEN Results for
select FirstName, LastName from SuperSimple;
end;
Now I was hoping this would work but I'm doing something like this with select statements and it appears to be not working.
Do I also need to add a fetch and another open and a close command to make this thing work? What is the idea behind all this? I've noticed that trying to find info on how to add a very simple select statemetn to a procedure appears to be missing from most documentation that I've read. Is there a reason for this like its too simple to add a select statement to a procedure as it would be better to add it to a view. Something along those lines.
The problem I'm having is I want to start out really simple and tac on a little bit more complexity to the sproc over time... where time ~ 1 to 2 hours. Can someone point me to some docs in Oracle PL/SQL that shows how to add a simple table or view. Also If the permissions for a specific view or table is not allowed does it just fail for that user or does it give an empty result set.
It is not clear from your question what are you intending to do with the query result inside your procedure. So here I make some examples with dbms_output which prints to screen out some message and data from your query. Probably you will replace it with your logic.
Let's have some view (actually it doesn't matter here whether you are querying view or table, but I would stick to your question)
create table some_simple_table(firstname varchar2(30), lastname varchar2(30));
/
create or replace view supersimple_view as select firstname, lastname, 'whatever1' whatever from some_simple_table;
/
The following code does select into variable, this will work only if query returns exactly one row.
create or replace procedure supersimple1 is
vfirstname supersimple_view.firstname%type;
vwhatever supersimple_view.whatever%type;
vsupersimple supersimple_view%rowtype;
begin
select firstname, whatever into vfirstname, vwhatever from supersimple_view;
dbms_output.put_line('I''m doing some logic with this'|| vwhatever );
select * into vsupersimple from supersimple_view;
dbms_output.put_line('I''m doing some logic with this'|| vsupersimple.firstname);
end;
/
Perhaps you can implement implicit cursor loop through results and do some logic.
create or replace procedure supersimple2 is
begin
for rec in (select * from supersimple_view)
loop
dbms_output.put_line('I''m doing some logic with this record '|| rec.firstname);
end loop;
end;
/
Another option is cursor (particularly in case when you will reuse the same select) loop through results and do some logic.
create or replace procedure supersimple3 is
cursor cur is (select * from supersimple_view);
vsupersimple cur%rowtype;
begin
open cur ;
loop
FETCH cur INTO vsupersimple;
EXIT WHEN cur%NOTFOUND;
dbms_output.put_line('I''m doing some logic with this record '|| vsupersimple.firstname);
end loop;
close cur;
end;
/
You can fetch result of your query to collection
create or replace procedure supersimple4 is
type supersimple_colt is table of supersimple_view%rowtype index by pls_integer;
vsupersimple_col supersimple_colt;
begin
select * bulk collect into vsupersimple_col from supersimple_view ;
for i in 1 .. vsupersimple_col.count
loop
dbms_output.put_line('I''m doing some logic with this record '|| vsupersimple_col(i).firstname);
end loop;
end;
/
Instead of PL/SQL type declared in supersimple4 you can create standalone database SQL types and used them to fetch results into. This aproach gives you various features like: possibility to query collection in select statement in table like fashion, converting it to xml by xmltype, etc.
I think I found the answer. For each column that is selected on, it needs a view or table column type, which is sort of like the list of parameters used for the final output. That way when you declare on it you can better know what you are getting, which sorta makes sense.
So if you have two tables or views which were used to generate the output columns, you would need both of those tables or views in your descriptive OUT variables to describe better what you are outputting in the final output result.
See this link.
I'm taking an educated guess with this next part as I'm just beginning to understand it:
This query should work. But if its not it may be due to insuffiecient priviledges. Try a table that you know you have access and select it in a procedure in debug mode. Then try a view.
Procedure AlphaPrime(Results OUT Ref CURSOR) IS
begin
OPEN Results for
select FirstName, LastName from SuperSimple;
end;
Also there is a possibility with Debug mode and your assigned user roles that you may have insufficient priviledges to debug all the objects in the view and they may not be accessible. Sometimes you can just hit the "Ignore" button in Toad to skip over debugging inside a stored procedure. Also you may have priveledges to view the results the object just not view its structure which may also give you insufficient priviledges errors. Again just ignore them to skip over those types of issues and see the results while in debug mode. If you don't debug this, then you should not see any errors and just get the results.