I'm at the beginning of learning PL/SQL. My question is can views be created inside a function?
CREATE OR REPLACE FUNCTION most_sold_item(in_year INT)
RETURN INT
IS
most_shipped_item INT := 0;
BEGIN
CREATE OR REPLACE VIEW Shipped_Items AS
SELECT ITEM_ID,SUM(QUANTITY) AS Total_Quantity
FROM ORDERS, ORDER_ITEMS
WHERE Orders.Order_ID =Order_Items.Order_ID
AND Status=1
AND Order_Year=in_year
GROUP BY Item_ID;
SELECT Item_ID
INTO most_shipped_item
FROM Shipped_Items
WHERE Total_quantity=(
SELECT MAX(Total_Quantity)
FROM Shipped_Items);
return most_shipped_item;
END;
/
Here's my code. There seems to be some kind of error which I just can't find. Can anyone please help me with this?
Thank you.
Here's the snippet of the datatbase that I'm working on
Here's a rough idea of what you might do, if you wish to use a function for this (Added logic to break ties):
-- Estimated structure based on posted image:
CREATE TABLE orders (
order_id int primary key
, client_id int
, order_year int
, status int
);
CREATE TABLE order_items (
order_id int
, item_id int
, quantity int
, ppu int
, primary key (order_id, item_id)
);
-- The adjusted function:
CREATE OR REPLACE FUNCTION most_sold_item(in_year INT)
RETURN INT
IS
most_shipped_item INT := 0;
BEGIN
WITH shipped_items AS (
SELECT ITEM_ID, SUM(QUANTITY) AS Total_Quantity
FROM ORDERS, ORDER_ITEMS
WHERE Orders.Order_ID =Order_Items.Order_ID
AND Status=1
AND Order_Year=in_year
GROUP BY Item_ID
)
SELECT Item_ID
INTO most_shipped_item
FROM Shipped_Items
WHERE Total_quantity=(
SELECT MAX(Total_Quantity)
FROM Shipped_Items
)
ORDER BY item_id
FETCH FIRST 1 ROW ONLY
;
return most_shipped_item;
END;
/
Don't forget to protect for the case where more than one item_id matches the MAX. That's easy to do, but I don't know how you wish to handle that case. I've adjusted the above solution to protect against this issue.
The question edit queue is full. Here's an executable test case (without data) that could be added to the question:
Executable test case, derived from the posted image (without data)
To answer your "original" question:
Can views be created in PL/SQL Functions?
Yes, they can. Should you do it? Definitely not, that's not how Oracle works (in most cases). In Oracle, we create objects at SQL level and use them in SQL or PL/SQL, but we do not create them from PL/SQL.
Anyway, here you are; read comments within code. I used Scott's sample schema as I don't have your tables, but the principle remains the same.
Function:
SQL> create or replace function f_test(par_deptno in number)
2 return number
3 is
4 -- you can't perform DDL in PL/SQL procedures unless they are
5 -- autonomous transactions
6 pragma autonomous_transaction;
7 retval number;
8 begin
9 -- you can't perform DML in a function unless you use dynamic SQL
10 execute immediate
11 'create or replace view v_emp as ' ||
12 ' select ename, sal ' ||
13 ' from emp ' ||
14 ' where deptno = ' || dbms_assert.enquote_literal(par_deptno);
15
16 -- you can't use an ordinary SELECT statement because at time of
17 -- procedure compilation view V_EMP doesn't exist yet, so compile
18 -- would fail
19 execute immediate
20 'select sum(sal) from v_emp ' into retval;
21
22 return retval;
23 end;
24 /
Function created.
Testing:
SQL> select f_test (20) from dual;
F_TEST(20)
----------
10875
What does the view contain?
SQL> select * from v_emp;
ENAME SAL
---------- ----------
SMITH 800
JONES 2975
SCOTT 3000
ADAMS 1100
FORD 3000
SQL>
Related
I have a function that returns a table of custom objects. I wish to select a certain column by name from the returned result.
create or replace type sd_Serial_Number as object (
serial_number VARCHAR2(32)
);
The table of objects
create or replace type sd_Serial_Number_Table as table of sd_Serial_Number;
The function
create function get_result
return sd_Serial_Number_Table as
v_ret sd_Serial_Number_Table;
begin
select sd_Serial_Number(selected.SERIAL_NUMBER)
bulk collect into v_ret
from (
selection here
) selected;
return v_ret;
end get_result;
When I call the function this way, I get a result with a single column called SERIAL_NUMBER
select * from table(get_result());
However, I can't do something like this
select SERIAL_NUMBER from table(get_result());
Is there a way to select the column SERIAL_NUMBER ?
"I can't" is difficult to debug. I'll show you that I can (on the same database version you use).
SQL> SELECT * FROM v$version WHERE rownum = 1;
BANNER
--------------------------------------------------------------------------------
Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 - 64bit Production
SQL> CREATE OR REPLACE TYPE sd_serial_number AS OBJECT
2 (
3 serial_number VARCHAR2 (32)
4 );
5 /
Type created.
SQL> CREATE OR REPLACE TYPE sd_serial_number_table AS TABLE OF sd_serial_number;
2 /
Type created.
SQL> CREATE OR REPLACE FUNCTION get_result
2 RETURN sd_serial_number_table
3 AS
4 v_ret sd_serial_number_table;
5 BEGIN
6 SELECT sd_serial_number (deptno)
7 BULK COLLECT INTO v_ret
8 FROM dept;
9
10 RETURN v_ret;
11 END get_result;
12 /
Function created.
Testing:
SQL> SELECT * FROM TABLE (get_result ());
SERIAL_NUMBER
--------------------------------
10
20
30
40
SQL> SELECT serial_number FROM TABLE (get_result ());
SERIAL_NUMBER
--------------------------------
10
20
30
40
SQL>
I'm hoping to dynamically reference last Fridays date in the weekly sales tables in Oracle SQL Developer i.e. SELECT * FROM Sales_DDMMYY
I can do this in SQL Server (DECLARE / SET / EXECUTE) but haven't had any joy with SQL Developer.
Even the ability to create a date variable to be referenced within the code would be a great start.
Stop!
I strongly suggest you not to do that. That's not the way to create a data model. If you have a table which contains values related to different dates, then date should be a column in that table, such as
create table sales
(id number,
datum date,
amount number
);
Insert rows as
insert into sales (id, datum, amount)
select 1, date '2020-06-01', 100 from dual union all
select 2, date '2020-05-13', 240 from dual union all
select 3, date '2020-05-13', 160 from dual;
and use it as
select sum(amount)
from sales
where datum = date '2020-05-13'
That is the way to do it. Naming columns by dates is ... well, close to a suicide.
Aha, now I see: it is a table name that contains dates. Doesn't really matter, my suggestion still stands. Do not do that. Use a date column within a single table.
If you want - and if you can afford it - partition the table on date value. Note that partitioning option exists in Oracle Enterprise Edition which is quite expensive. So - date column it is.
If there's nothing you can do about it, then dynamic SQL it is. For example:
Sample table:
SQL> create table sales_200620 as select * From dept;
Table created.
Function that accepts ddmmyy value as a parameter, composes table name and returns a refcursor:
SQL> create or replace function f_test (par_ddmmyy in varchar2)
2 return sys_refcursor
3 is
4 l_table_name varchar2(30) := 'sales_' || par_ddmmyy;
5 l_rc sys_refcursor;
6 begin
7 open l_rc for 'select * from ' || dbms_assert.sql_object_name(l_table_name);
8 return l_rc;
9 end;
10 /
Function created.
Testing:
SQL> select f_test('200620') from dual;
F_TEST('200620')
--------------------
CURSOR STATEMENT : 1
CURSOR STATEMENT : 1
DEPTNO DNAME LOC
---------- -------------- -------------
10 ACCOUNTING NEW YORK
20 RESEARCH DALLAS
30 SALES CHICAGO
40 OPERATIONS BOSTON
SQL>
Without creating a function: use substitution variable:
SQL> select * From &tn;
Enter value for tn: sales_200620
DEPTNO DNAME LOC
---------- -------------- -------------
10 ACCOUNTING NEW YORK
20 RESEARCH DALLAS
30 SALES CHICAGO
40 OPERATIONS BOSTON
SQL>
We generate tables dynamically Eg. Table T_1, T_2, T_3, etc & we can get that table names from another table by following query.
SELECT CONCAT('T_', T_ID) AS T_NAME FROM T_NAMES WHERE T_KEY = 'ABC';
Now I want to get records from this retrieved table name. What can I do ?
I'm doing like following but that's not working :
SELECT * FROM (SELECT CONCAT('T_', T_ID) AS T_NAME FROM T_NAMES WHERE T_KEY = 'ABC')
FYI : I'm hitting two individual queries as of now though I want to eliminate one and I can not follow cursor/procedure approach due to some limitations.
A procedure which utilizes refcursor seems to be the most appropriate to me. Here's an example:
SQL> -- creating test case (your T_NAMES table and T_1 which looks like Scott's DEPT)
SQL> create table t_names (t_id number, t_key varchar2(3));
Table created.
SQL> insert into t_names values (1, 'ABC');
1 row created.
SQL> create table t_1 as select * from dept;
Table created.
SQL> -- a procedure; accepts KEY and returns refcursor
SQL> create or replace procedure p_test
2 (par_key in varchar2, par_out out sys_refcursor)
3 as
4 l_t_name varchar2(30);
5 begin
6 select 'T_' || t_id
7 into l_t_name
8 from t_names
9 where t_key = par_key;
10
11 open par_out for 'select * from ' || l_t_name;
12 end;
13 /
Procedure created.
OK, let's test it:
SQL> var l_out refcursor
SQL> exec p_test('ABC', :l_out)
PL/SQL procedure successfully completed.
SQL> print l_out
DEPTNO DNAME LOC
---------- -------------- -------------
10 ACCOUNTING NEW YORK
20 RESEARCH DALLAS
30 SALES CHICAGO
40 OPERATIONS BOSTON
SQL>
I could propose to you Dynamic SQL.
First of all, you need to create a cursor. The cursor will iterate by the dynamic tables. Then you could use dynamic SQL to create a query and then execute it.
So example:
https://livesql.oracle.com/apex/livesql/file/content_C81136WLRFYZF8ION6Q57GWE1.html - detailed cursor example.
https://docs.oracle.com/cd/B28359_01/appdev.111/b28370/dynamic.htm#i13057 - dynamic SQL in Oracle
I have a workplace problem to which I am looking for an easy solution.
I am trying to replicate it in a smaller scenario.
Problem in short
I want to use nvl inside an in clause. Currently I have an input string which consists of a name. It is used in a where clause like below
and column_n = nvl(in_parameter,column_n)
Now I want to pass multiple comma separated values in same input parameter. So if I replace = with in, and transpose the input comma separated string as rows, I cannot use the nvl clause with it.
Problem in Detail
Lets consider an Employee table emp1.
Emp1
+-------+-------+
| empno | ename |
+-------+-------+
| 7839 | KING |
| 7698 | BLAKE |
| 7782 | CLARK |
+-------+-------+
Now this is a simple version of an existing stored procedure
create or replace procedure emp_poc(in_names IN varchar2)
as
cnt integer;
begin
select count(*)
into cnt
from emp1
where
ename = nvl(in_names,ename); --This is one of the condition where we will make the change.
dbms_output.put_line(cnt);
end;
So this will give the count of number of employees passed as Input Parameter. But if we pass null, it will return the whole count of employee becuase of the nvl.
So these procedure calls will render the given outputs.
Procedure Call Output
exec emp_poc('KING') 1
exec emp_poc('JOHN') 0
exec emp_poc(null) 3
Now what I want to achieve is to add another functionality. So exec emp_poc('KING,BLAKE') should give me 2. So I figured a way to split the comma separated string to rows and used that in the procedure.
So if I change the where clause as below to in
create or replace procedure emp_poc2(in_names IN varchar2)
as
cnt integer;
begin
select count(*)
into cnt
from emp1
where
ename in (select trim(regexp_substr(in_names, '[^,]+', 1, level))
from dual
connect by instr(in_names, ',', 1, level - 1) > 0
);
dbms_output.put_line(cnt);
end;
So exec emp_poc2('KING','BLAKE') gives me 2. But passing null will give result as 0. However I want to get 3 like the case with emp_proc
And I cannot use nvl with in as it expect the subquery to return a single value.
1 way I can think of is rebuilding the whole query in a variable, based in input paramteter, and then use execute immediate. But I am using some variables to collect the value and it would be difficult to achieve it with execute immediate.
I am again emphasizing that this is a simple version of a complex procedure where we are capturing many variables and it joins many tables and has multiple AND conditions in where clause.
Any ideas on how to make this work.
This may help you
CREATE OR REPLACE PROCEDURE emp_poc2(in_names IN varchar2)
AS
cnt integer;
BEGIN
SELECT COUNT(*) INTO cnt
FROM emp1
WHERE
in_names IS NULL
OR ename IN (
SELECT TRIM(REGEXP_SUBSTR(in_names, '[^,]+', 1, level))
FROM dual
CONNECT BY INSTR(in_names, ',', 1, level - 1) > 0
);
dbms_output.put_line(cnt);
END;
Other way could be use IF ELSE or UNION ALL
If your real code is much more complex then your code readability might be greatly enhanced by using a proper collection type instead.
In the example below I have created an user defined type str_list_t that is a real collection of strings.
I also use common table expression (CTE) in the sql query to enhance the readability. In this simple example the CTE benefits for readability are not obvious but for all non-trivial queries it's a valuable tool.
Test data
create table emp1(empno number, empname varchar2(10));
insert into emp1 values(5437, 'GATES');
insert into emp1 values(5438, 'JOBS');
insert into emp1 values(5439, 'BEZOS');
insert into emp1 values(5440, 'MUSK');
insert into emp1 values(5441, 'CUBAN');
insert into emp1 values(5442, 'HERJAVEC');
commit;
Supporting data type
create or replace type str_list_t is table of varchar2(4000 byte);
/
Subprogram
create or replace function emp_count(p_emps in str_list_t) return number is
v_count number;
v_is_null_container constant number :=
case
when p_emps is null then 1
else 0
end;
begin
-- you can also test for empty collection (that's different thing than a null collection)
with
-- common table expression (CTE) gives you no benefit in this simple example
emps(empname) as (
select * from table(p_emps)
)
select count(*)
into v_count
from emp1
where v_is_null_container = 1
or empname in (select empname from emps)
;
return v_count;
end;
/
show errors
Example run
SQL> select 2 as expected, emp_count(str_list_t('BALLMER', 'CUBAN', 'JOBS')) as emp_count from dual
union all
select 0, emp_count(str_list_t()) from dual
union all
select 6, emp_count(null) from dual
;
EXPECTED EMP_COUNT
---------- ----------
2 2
0 0
6 6
Here's a simplified pseudo-code version of what I'd like to be able to do in PL-SQL (Oracle):
DECLARE
mylist as ARRAY
BEGIN
mylist (1) := '1'
mylist (2) := '3'
...
SELECT *
FROM aTable
WHERE aKey IN mylist;
END;
The SELECT should return the matching records for mylist(1), mylist(2) etc. It should be similar to ORing all the values, but of course we don't know in advance how many values we get.
How can I achieve this? I know that PL/SQL has some collection datatypes, but I can't seem to get them to work properly in SQL statements.
Thanks for any ideas.
This is easy to do with the TABLE() function. The one catch is that the array variable must use a type declared in SQL. This is because SELECT uses the SQL engine, so PL/SQL declarations are out of scope.
SQL> create or replace type numbers_nt as table of number
2 /
Type created.
SQL>
SQL> declare
2 l_array numbers_nt;
3 begin
4 l_array := numbers_nt (7521,7566,7654);
5 for r in ( select ename
6 from emp
7 where empno in ( select *
8 from table (l_array)
9 )
10 )
11 loop
12 dbms_output.put_line ( 'employee name = '||r.ename);
13 end loop;
14 end;
15 /
employee name = PADFIELD
employee name = ROBERTSON
employee name = BILLINGTON
PL/SQL procedure successfully completed.
SQL>
A couple of suggestions:
1.) There's a CAST SQL keyword that you can do that might do the job... it makes your collection be treated as if it were a table.
2.) Pipelined functions. Basically a function returns data that looks like a table.
This link summarises the options and has a number of code listings that explain them.
http://www.databasejournal.com/features/oracle/article.php/3352091/CASTing-About-For-a-Solution-Using-CAST-and-Table-Functions-in-PLSQL.htm