Oracle procedure to display detail about an item - oracle

I'm trying to create a procedure that takes a parameter (ID number) and output some information about that particular item. Assume that I have two tables:
product( id, name, desc, price)
line_item (prod_id, total, quantity, cust_id)
This is what I have so far:
CREATE OR REPLACE PROCEDURE product_query (p_id IN Number)
RETURN NUMBER
IS
v_product_id NUMBER;
v_description NUMBER;
v_sub NUMBER;
v_total_quantity NUMBER;
v_order_count NUMBER;
BEGIN
SELECT p.product_id, sum(l.subtotal), sum(l.quantity), count(*)
INTO v_product_id, v_sub, v_total_quantity, v_order_count
FROM line_item l, product p
WHERE p.product_id = p_id
AND
l.product_id = l.product_id
group by p.product_id;
DBMS_OUTPUT.PUT_LINE('ID: ' || p_product_id);
DBMS_OUTPUT.PUT_LINE('Subtotal: ' || v_sub);
DBMS_OUTPUT.PUT_LINE('Total Qt: ' || v_total_quantity);
DBMS_OUTPUT.PUT_LINE('Total Order: ' || v_order_count);
END product_query;
But it doesn't display any output. I don't see any issue with the code. Should I change the way I output the information? Is there a better way than DBMS_OUTPUT.PUT_LINE?
Thanks,

I would assume that you're getting a compilation error when you try to create the procedure-- procedures do not return anything so RETURN NUMBER isn't valid, that would only be valid in a function. If you're getting a compilation error, though, posting the error would be helpful.
Once the procedure compiles, I'd expect you'd get a runtime error (indicating that your query returned too many rows) when you called the procedure because the join is wrong-- you're joining the line_item table to itself rather than to the product table.
AND l.product_id = l.product_id
should, presumably, be
AND l.product_id = p.product_id
This is one of the reasons that I prefer the SQL 99 join syntax over the old syntax you're using here-- it makes it much easier to differentiate join conditions from filter predicates which makes it easier to notice this sort of error.
Once your procedure compiles and can be called successfully, you'd need to enable output. That's going to be done differently in different client tools but here's a question that walks through enabling dbms_output in SQL*Plus and SQL Developer.
Of course, in real code, you almost certainly wouldn't define a procedure whose only purpose was to write to dbms_output because you generally wouldn't assume that anyone would ever see that data. dbms_output is really only used for quick-and-dirty debugging. I assume, though, that you are a student and that this is part of a homework assignment.

Related

Bulk collect sql

i made a data_object by myself :
CREATE OR REPLACE TYPE my_object AS OBJECT(
number_type NUMBER,
varchar_type VARCHAR2(20)
)
and then i create type
CREATE OR REPLACE TYPE my_nt IS TABLE OF my_object;
And I want with nested table and this object make a procedure, that will be return number of employyes of some departments. I ve got Two tables: employees and department a this is my code:
DECLARE
enum_dname my_nt := my_nt();
PROCEDURE print_l IS
BEGIN
DBMS_OUTPUT.put_line('---------------------------------------------------------');
FOR i IN 1..enum_dname.COUNT
LOOP
DBMS_OUTPUT.PUT_LINE(enum_dname(i));
END LOOP;
END;
BEGIN
SELECT COUNT(emp_id) as number_of, department_name
BULK COLLECT INTO enum_dname
FROM employees e, department d
WHERE e.department_id = d.department_id
GROUP BY department_name;
print_l;
END;
And it show me errors : PLS - 00306: Wrong numbers of argument in call type: PUT_LINE
and PL\SQL : ORA - 00947:not enough values
THANK YOU!
You have two errors. As #SudiptaMondal pointed out (and as here) you can't pass an object to put_line(), you have to pass a single string value - or something which evaluates to a string, which can be concatenated or implicit converted or whatever. So here you might do:
DBMS_OUTPUT.PUT_LINE(enum_dname(i).varchar_type || ': ' || enum_dname(i).number_type);
or however you want to format that output. Using dbms_output for anything except debugging generally not a good idea as you have no control over whether someone using your code has output enabled. But this may be enough for this exercise.
The second problem, causing the ORA-00947, is because your query is trying to bulk collect two scalar variables into a collection of objects. You need to include the object constructor:
SELECT my_object(COUNT(emp_id), department_name)
BULK COLLECT INTO enum_dname
...

Using array of Records in 'IN' operator in Oracle

I am having a table Course which have column DepId and Course and some other value. I need to search this table for a some set of (DepId, Course). This set will be decided at runtime.
I want to write a single query in a procedure to fetch all the record pertaining to above set.
For example consider the table has data like
DepId Course ...
------------------
1 A
2 B
3 C
4 D
5 E
6 F
Now only I want to search for below records:
DepId Course ...
------------------
1 A
4 D
What would be most efficient way to write above query?
I was thinking of creating an Array of record and passing it in the 'IN' operator. But I was not able to get any example for this. Can someone guide me on this?
Thanks
Leveraging Oracle Collections to Build Array-typed Solutions
The answer to your question is YES, dimensioned variables such as ARRAYS and COLLECTIONS are viable data types in solving problems where there are multiple values in either or both the input and output values.
Additional good news is that the discussion for a simple example (such as the one in the OP) is pretty much the same as for a complex one. Solutions built with arrays are nicely scalable and dynamic if designed with a little advanced planning.
Some Up Front Design Decisions
There are actual collection types called ARRAYS and ASSOCIATIVE ARRAYS. I chose to use NESTED TABLE TYPES because of their accessibility to direct SQL queries. In some ways, they exhibit "array-like" behavior. There are other trade-offs which can be researched through Oracle references.
The query applied to search the COURSE TABLE would apply a JOIN condition instead of an IN-LIST approach.
The use of a STORED PROCEDURE typed object improves database response. Queries within the procedure call can leverage and reuse already compiled code plus their cached execution plans.
Choosing the Right Collection or Array Type
There are a lot of choices of collection types in Oracle for storing variables into memory. Each has an advantage and some sort of limitation. AskTom from Oracle has a good example and break-down of what a developer can expect by choosing one variable collection type over another.
Using NESTED TABLE Types for Managing Multiple Valued Variables
For this solution, I chose to work with NESTED TABLES because of their ability to be accessed directly through SQL commands. After trying several different approaches, I noticed that the plain-SQL accessibility leads to more clarity in the resulting code.
The down-side is that you will notice that there is a little overhead here and there with respect to declaring an instance of a nested table type, initializing each instance, and managing its size with the addition of new values.
In any case, if you anticipate a unknown number of input variables or values (our output), an array-typed data type (collection) of any sort is a more flexible structure for your code. It is likely to require less maintenance in the end.
The Example: A Stored Procedure Search Query
Custom TYPE Definitions
CREATE OR REPLACE TYPE "COURSE_REC_TYPE" IS OBJECT (DEPID NUMBER(10,0), COURSE VARCHAR2(10));
CREATE OR REPLACE TYPE "COURSE_TBL_TYPE" IS TABLE of course_rec_type;
PROCEDURE Source Code
create or replace PROCEDURE ZZ_PROC_COURSE_SEARCH IS
my_input course_tbl_type:= course_tbl_type();
my_output course_tbl_type:= course_tbl_type();
cur_loop_counter pls_integer;
c_output_template constant varchar2(100):=
'DEPID: <<DEPID>>, COURSE: <<COURSE>>';
v_output VARCHAR2(200);
CURSOR find_course_cur IS
SELECT crs.depid, crs.course
FROM zz_course crs,
(SELECT depid, course
FROM TABLE (CAST (my_input AS course_tbl_type))
) search_values
WHERE crs.depid = search_values.depid
AND crs.course = search_values.course;
BEGIN
my_input.extend(2);
my_input(1):= course_rec_type(1, 'A');
my_input(2):= course_rec_type(4, 'D');
cur_loop_counter:= 0;
for i in find_course_cur
loop
cur_loop_counter:= cur_loop_counter + 1;
my_output.extend;
my_output(cur_loop_counter):= course_rec_type(i.depid, i.course);
end loop;
for j in my_output.first .. my_output.last
loop
v_output:= replace(c_output_template, '<<DEPID>>', to_char(my_output(j).depid));
v_output:= replace(v_output, '<<COURSE>>', my_output(j).course);
dbms_output.put_line(v_output);
end loop;
end ZZ_PROC_COURSE_SEARCH;
Procedure OUTPUT:
DEPID: 1, COURSE: A
DEPID: 4, COURSE: D
Statement processed.
0.03 seconds
MY COMMENTS: I wasn't particularly satisfied with the way the input variables were stored. There was a clumsy kind of problem with "loading" values into the nested table structure... If you can consider using a single search key instead of a composite pair (i.e., depid and course), the problem condenses to a simpler form.
Revised Cursor Using a Single Search Value
This is the proposed modification to the table design of the OP. Add a single unique key id column (RecId) to represent each unique combination of DepId and Course.
Note that the RecId column represents a SURROGATE KEY which should have no internal meaning aside from its property as a uniquely assigned value.
Custom TYPE Definitions
CREATE OR REPLACE TYPE "NUM_TBL_TYPE" IS TABLE of INTEGER;
Remove Array Variable
This will be passed directly through an input parameter from the procedure call.
-- REMOVE
my_input course_tbl_type:= course_tbl_type();
Loading and Presenting INPUT Parameter Array (Nested Table)
The following can be removed from the main procedure and presented as part of the call to the procedure.
BEGIN
my_input.extend(2);
my_input(1):= course_rec_type(1, 'A');
my_input(2):= course_rec_type(4, 'D');
Becomes:
create or replace PROCEDURE ZZ_PROC_COURSE_SEARCH (p_search_ids IN num_tbl_type) IS...
and
my_external_input.extend(2);
my_external_input:= num_tbl_type(1, 4);
Changing the Internal Cursor Definition
The cursor looks about the same. You can just as easily use an IN-LIST now that there is only one search parameter.
CURSOR find_course_cur IS
SELECT crs.depid, crs.course
FROM zz_course_new crs,
(SELECT column_value as recid
FROM TABLE (CAST (p_search_ids AS num_tbl_type))
) search_values
WHERE crs.recid = search_values.recid;
The Actual SEARCH Call and Output
The searching portion of this operation is now isolated and dynamic. It does not need to be changed. All the Changes happen in the calling PL/SQL block where the search ID values are a lot easier to read and change.
DECLARE
my_input_external num_tbl_type:= num_tbl_type();
BEGIN
my_input_external.extend(3);
my_input_external:= num_tbl_type(1,3,22);
ZZ_PROC_COURSE_SEARCH (p_search_ids => my_input_external);
END;
-- The OUTPUT (Currently set to DBMS_OUT)
DEPID: 1, COURSE: A
DEPID: 4, COURSE: D
DEPID: 7, COURSE: G
Statement processed.
0.01 seconds
This is something I havee used in the past in a situation similar to yours. Hopefully it helps.
The main benefit of this method would be that if you only passed it a single paramter it would still return all records for that single parameter. This way a single stored procedure with 5 input parameters could be used to search for all combinations of inputs.
Just call the stored procedure passing in the set and should return all values mathcing the criteria
usp_custom_search '1','A'
usp_custom_search '4','D'
usp_custom_search '4',NULL
usp_custom_search NULL,'A'
etc
Stored Procedure:
CREATE OR REPLACE PROCEDURE custom_search (
dep_id IN VARCHAR2,
course_id IN VARCHAR2,
result_set OUT SYS_REFCURSOR)
BEGIN
query_str VARCHAR2(1000);
query_str := 'SELECT';
query_str := query_str || ' DepId, Course';
query_str := query_str || ' FROM Course';
query_str := query_str || ' WHERE 1=1';
IF (dep_id is not null) then query_str := query_str || ' AND DepId = ''' || dep_id || ''''; END IF;
IF (course_id is not null) then query_str := query_str || ' AND Course = ''' || course_id || ''''; END IF;
open result_set for query_str;
END custom_search;
/

PL/SQL - How to use an array in an IN Clause

I'm trying to use an array of input values to my procedure in an IN Clause as part of the where clause of a cursor. I know that this has been asked before, but I haven't seen how to make my syntax compile correctly.
In the package specification, the type is
TYPE t_brth_dt IS TABLE OF sourceTable.stdt_brth_dt%TYPE INDEX BY PLS_INTEGER;
sourceTable.std_brth_dt is a date column in the table.
Simplified version of my cursor is in the package body is -
cursor DataCursor_Sort( p_brth_dt in t_brth_dt) is
SELECT *
FROM sourceTable
WHERE a.brth_dt IN (select column_value
from table(p_brth_dt))
When I try to compile this, I'm getting the following errors.
[1]:(Error): PLS-00382: expression is of wrong type
[2]:(Error): PL/SQL: ORA-22905: cannot access rows from a non-nested table item
I know this looks similar to other questions, but I don't understand what the syntax error is.
In order to use collection defined as a nested table or an associative array in the from clause of a query you either should, as #Alex Poole correctly pointed out, create a schema level (SQL) type or use one, that is available to you trough ODCIConst package - odcidatelist as you intend to use a list of dates. For example, your cursor definition might look like this:
cursor DataCursor_Sort(p_brth_dt in sys.odcidatelist) is
select *
from sourceTable
where a.brth_dt IN (select column_value
from table(p_brth_dt))
OR
cursor DataCursor_Sort(p_brth_dt in sys.odcidatelist) is
select s.*
from sourceTable s
join table(p_brth_dt) t
on (s.brth_dt = t.column_value)
Note: You should take into consideration the time part of a date when performing a date comparison. If you want to compare date part only it probably would be useful to get rid of time part by using trunc() function.
It is possible to use a PL/SQL-defined nested table type (as opposed to a SQL-defined nested table type) indirectly in an IN clause of a SELECT statement in a PL/SQL package. You must use a PIPELINED function as an intermediary. It felt kind of clever to write, but I don't believe in its fundamental usefulness.
CREATE OR REPLACE PACKAGE so18989249 IS
TYPE date_plsql_nested_table_type IS TABLE OF DATE;
dates date_plsql_nested_table_type;
FUNCTION dates_pipelined RETURN date_plsql_nested_table_type PIPELINED;
PROCEDURE use_plsql_nested_table_type;
END so18989249;
/
CREATE OR REPLACE PACKAGE BODY so18989249 IS
FUNCTION dates_pipelined RETURN date_plsql_nested_table_type
PIPELINED IS
BEGIN
IF (dates.count > 0)
THEN
FOR i IN dates.first .. dates.last
LOOP
IF (dates.exists(i))
THEN
PIPE ROW(dates(i));
END IF;
END LOOP;
END IF;
END;
PROCEDURE use_plsql_nested_table_type IS
BEGIN
dates := NEW date_plsql_nested_table_type();
-- tweak these values as you see fit to produce the dbms_output results you want
dates.extend(5);
dates(1) := DATE '2013-12-25';
dates(2) := DATE '2013-01-01';
dates(3) := DATE '2013-07-01';
dates(4) := DATE '2013-09-03';
dates(5) := DATE '2008-11-18';
FOR i IN (SELECT o.owner,
o.object_name,
o.object_type,
to_char(o.last_ddl_time, 'YYYY-MM-DD') AS last_ddl
FROM all_objects o
WHERE trunc(o.last_ddl_time) IN
(SELECT column_value FROM TABLE(dates_pipelined))
--uses pipeline function which uses pl/sql-defined nested table
)
LOOP
dbms_output.put_line('"' || i.owner || '"."' || i.object_name || '" ("' || i.object_type || ') on ' || i.last_ddl);
END LOOP;
END;
END so18989249;
/
begin so18989249.use_plsql_nested_table_type; end;
/
The type has to be created at SQL level, not in a package. An SQL query doesn't know how to use any types defined in PL/SQL. So you'd have to do:
CREATE OR REPLACE TYPE t_brth_dt IS TABLE OF date;
/
... and remove the type from your package specification. (Or give them different names, at least, and they won't be interchangeable in use). Because it's at SQL level, you also can't use sourceTable.stdt_brth_dt%TYPE in the declaration, unfortunately.

PLSQL Procedure get user input at runtime

Im doing a college assignment that requires me to create a PLSQL procedure that where the user can add a new customer order containing a number of items and the quantity for each item. I came up with the following that would ask the user to input a number of items to be added to the order and would then use a loop to ask for specific details such as product no and quantity. Im having problems with the user input at runtime though... When compiling the code it asks for the product code and quantity and wont ask again at runtime instead it saves the values given earlier at compile...
CREATE OR REPLACE
PROCEDURE Add_Order (Item_amount IN NUMBER, CustNo IN NUMBER) AS
ItemNo NUMBER;
var_Quantity NUMBER;
var_PONo NUMBER;
BEGIN
IF Item_amount BETWEEN 2 AND 9 THEN
SELECT seq_PONo.NEXTVAL INTO var_PONo from dual;
INSERT INTO PurchaseOrder_tab
SELECT var_PONo, REF(C),
SYSDATE,
ItemList_tab()
FROM Customer_tab C
WHERE C.CustNo = CustNo;
FOR i IN 1..Item_amount LOOP
DBMS_OUTPUT.PUT_LINE('INSIDE LOOP');
ItemNo := &Enter_ProdCode;
var_Quantity := &Quantity_Amount;
INSERT INTO TABLE (
SELECT P.ItemList
FROM PurchaseOrder_tab P
WHERE P.PONo = var_PONo
)
SELECT seq_ItemNo.nextval, REF(Pr), var_Quantity
FROM Products_tab Pr
WHERE Pr.ProductCode = ItemNo ;
DBMS_OUTPUT.PUT_LINE('Added '||var_Quantity ||' items of '||ItemNo||' to order No: '||var_PONo);
END LOOP;
ELSE
DBMS_OUTPUT.PUT_LINE('Amount of items entered onto an order must be between 2 - 9. Please try again with correct amount.');
END IF;
EXCEPTION
WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('Operation failed '||'SQLCODE: '||SQLCODE);
DBMS_OUTPUT.PUT_LINE('SQL Error Message '||SQLERRM);
ROLLBACK;
END;
/
Short answer: you can't. PL/SQL is executed inside the database engine, and the database engine has no access to the terminal window (or database tool) you are using to start the procedure.
The code in your quesion seems to partially work, because it asks for input once, but what really happens is that: the tool (SQL*Plus, SQL Developer or whatever) parses over the PL/SQL block and sees the &-Signs, so it asks what to replace them with. Once the input is given, the PL/SQL-Block - including the entered values - is given to the database for execution.
Since you can't do that in PL/SQL, better create a front-end program first that collects the values, then sends them to the database.
Try to play around with :
BEGIN
DBMS_OUTPUT.GET_LINE(:buffer, :status);
END;
Instead of using & refferences
To Get data input from the USer,
SET SERVEROUTPUT ON;
ACCEPT Enter_ProdCode VARCHAR2 PROMPT "Please enter Product code : ";
ItemNo := &Enter_ProdCode;

PL/SQL: re-write SELECT statement using IN parameter in stored procedure

Suppose I have an Oracle 11.2 database containing the following table:
TABLE: SURVEY
PARAMETER MALE FEMALE
--------------------------
SAMPLE_SIZE 102 95
AVG_WEIGHT 170 120
AVG_HEIGHT 5.9 5.4
Here's an example minimal PL/SQL stored procedure that selects the average weight of males and places it (e.g. 170) into variable v_stat.
PROCEDURE get_stat (gender IN VARCHAR2)
AS
v_stat number;
BEGIN
SELECT male INTO v_stat FROM survey WHERE parameter = 'avg_weight';
END get_stat;
Notice the IN parameter gender doesn't do anything here. What I want is to pass in variable gender, which may equal either 'male' or 'female', and use gender somehow in the SELECT statement (instead of 'male'). The goal is to pass in gender as a variable that may be used to return, for example, the average weight of, either male or female, as defined by gender.
I know I can probably use an IF/THEN/ELSE statement with two separate SELECT statements, but I wondered if there was an elegant way to use just one SELECT statement by changing 'male' in the above SELECT statement to something else?
Note that this is a re-do of my previous question here
How to programmatically set table name in PL/SQL?
that was (rightly) criticized for not being a realistic question.
You'd need to use the same dynamic SQL approach that was suggested in your other question
PROCEDURE get_stat (gender IN VARCHAR2)
AS
v_sql varchar2(1000);
v_param varchar2(100) := 'AVG_WEIGHT';
v_stat number;
BEGIN
v_sql := 'SELECT ' || gender || ' FROM survey WHERE parameter = :1';
EXECUTE IMMEDIATE v_sql
INTO v_stat
USING v_param;
END get_stat;
But you'd have the same general objections that were raised in your earlier question-- the data model is fundamentally flawed. You would be much better served having a separate row for MALE and FEMALE survey results rather than having separate columns for male and female results.
You'd need to compose the SELECT statement as string and then use EXECUTE IMMEDIATELY on it.
I have to see how to transform your example, but I know that in SQL Server you can perform dynamic SQL. And, it appears that you can do this in Oracle, also.
It would go something like this (I am only going by examples here as Oracle is not my normal langauge)
avg_weight_str := avg_weight;
stmt_str := 'SELECT :gender INTO v_stat FROM survey WHERE parameter = :avg_weight_str';
EXECUTE IMMEDIATE stmt_str;
The problem with dynamic SQL is that it is not as performant. While not elegant, if you only have a two way if, then I would go with the if block so that the code can be more performant.

Resources