How to write and call an Oracle function in SQL - oracle

I have two Oracle tables with a similar structure:
I'd like to write a function in Oracle which sum all values for each ID and returns a pair (ID,Text) where Text='ALERT' if the sum is greater than 100, 'OK' otherwise:
Then, I'd like to execute a query for each table, for example something like that:
SELECT MY_FUN() FROM TABLE_1
SELECT MY_FUN() FROM TABLE_2
Is this a right approach? How could I write this function?
Thanks

Generally it is considered bad practice to call a function in SQL which executes SQL. It creates all kinds of problems.
Here is one solution:
create or replace function my_fun
( p_sum in number) return varchar2 is
begin
if p_sum > 100 then return 'ALERT';
else return 'OK';
end if;
end;
/
Run it like this:
select id, my_fun(sum(val)) as state
from your_table
group by id;

You can do this in sql like the following. If you want to create a function to to this for different tables, you can wrap this in a function that takes the table name and run the query via dynamic SQL, but you are limited to the same columns.
-- begin test data
with test_data(id, value) as
(select 1, 100 from dual union all
select 1, 50 from dual union all
select 2, 75 from dual union all
select 3, 50 from dual union all
select 3, 51 from dual)
-- end test data
select id, sum(value),
case when sum(value) > 100 then 'ALERT'
else 'OK' end AS alert
from test_data
group by id;

Related

How to convert string value returned from oracle apex 20.1 multiselect item into comma separated numbers array

I have a multi select enabled select list. I want to use all the selected ids inside an IN () operator in pl/sql query. Selected values are returned as below,
"1","5","4"
I want to use em as numbers as below,
1,5,4
My query is like,
UPDATE EMPLOYEE SET EMPSTAT = 'Active' WHERE EMPID IN (:P500_EMPIDS);
This is the employee table:
SQL> select * from employee;
EMPID EMPSTAT
---------- --------
1 Inactive
2 Inactive
4 Inactive
5 Inactive
SQL>
This is a way to split comma-separated values into rows (not into a list of values you'd use in IN!). Note that:
line #3: REPLACE function replaces double quotes with an empty string
line #3: then it is split into rows using REGEXP_SUBSTR with help of hierarchical query
SQL> with test (col) as
2 (select '"1","5","4"' from dual)
3 select regexp_substr(replace(col, '"', ''), '[^,]+', 1, level) val
4 from test
5 connect by level <= regexp_count(col, ',') + 1;
VAL
--------------------
1
5
4
SQL>
Usually multiselect items have colon-separated values, e.g. 1:5:4. If that's really the case, regular expression would look like this:
regexp_substr(col, '[^:]+', 1, level) val
Use it in Apex as:
update employee e set
e.empstat = 'Active'
where e.empid in
(select regexp_substr(replace(:P1_ITEM, '"', ''), '[^,]+', 1, level)
from dual
connect by level <= regexp_count(:P1_ITEM, ',') + 1
);
Result is:
3 rows updated.
SQL> select * from employee order by empid;
EMPID EMPSTAT
---------- --------
1 Active
2 Inactive
4 Active
5 Active
SQL>
Try it.
Thanks for helping everyone.Please check this and tell me if anything is wrong. I found a solution as below,
DECLARE
l_selected APEX_APPLICATION_GLOBAL.VC_ARR2;
BEGIN
l_selected := APEX_UTIL.STRING_TO_TABLE(:P500_EMPIDS);
FOR i in 1 .. l_selected.count LOOP
UPDATE EMPLYEE SET EMPSTATUS = 'ACTIVE' WHERE EMPID = to_number(l_selected(i));
END LOOP;
END;
You can use the API apex_string for this. If you want to use the IN operator you'll have to use EXECUTE IMMEDIATE because you cannot use a concatenated string in an IN operator.
Instead what you could do is the following:
DECLARE
l_array apex_t_varchar2;
BEGIN
l_array := apex_string.split(p_str => :P500_EMPIDS, p_sep => ':');
FOR i IN 1..l_array.count LOOP
UPDATE EMPLOYEE SET EMPSTAT = 'Active' WHERE EMPID = l_array(i);
END LOOP;
END;
Explanation: convert the colon separated list of ids to a table of varchar2, then loop through the elements of that table.
Note that I'm using ":" as a separator, that is what apex uses for multi selects. If you need "," then change code above accordingly.
Note that you can use apex_string directly within an update statement, so the answer of Koen Lostrie could be modified to not need a loop:
UPDATE EMPLOYEE
SET EMPSTAT = 'Active'
WHERE EMPID IN (
select to_number(trim('"' from column_value))
from table(apex_string.split(:P500_EMPIDS,','))
);
Testcase:
with cte1 as (
select '"1","2","3"' as x from dual
)
select to_number(trim('"' from column_value))
from table(apex_string.split((select x from cte1),','))

PL/SQL FOR Loop Error when Populating Dimension Table

I am populating a dimension table named TIMES with data from an OLTP Table called SALES with the following code:
CREATE TABLE TIMES
(saleDay DATE PRIMARY KEY,
dayType VARCHAR(50) NOT NULL);
BEGIN
FOR rec IN
(SELECT saleDate, CASE WHEN h.hd IS NOT NULL THEN 'Holiday'
WHEN to_char(saleDate, 'd') IN (1,7) THEN 'Weekend'
ELSE 'Weekday' END dayType
FROM SALES s LEFT JOIN
(SELECT '01.01' hd FROM DUAL UNION ALL
SELECT '15.01' FROM DUAL UNION ALL
SELECT '19.01' FROM DUAL UNION ALL
SELECT '28.05' FROM DUAL UNION ALL
SELECT '04.07' FROM DUAL UNION ALL
SELECT '08.10' FROM DUAL UNION ALL
SELECT '11.11' FROM DUAL UNION ALL
SELECT '22.11' FROM DUAL UNION ALL
SELECT '25.12' FROM DUAL) h
ON h.hd = TO_CHAR(s.saleDate, 'dd.mm'))
LOOP
INSERT INTO TIMES VALUES rec;
END LOOP;
END;
/
When I run this, I'm getting the errors ORA-00001 (Unique Constraint Violation) and ORA-06512. I believe this is happening because the code is trying to input multiple dates (some of which are the same) into PK for my TIMES Dimension Table (saleDay). How would I implement a clause into this loop so it will only populate one instance of each saleDate into the saleDay PK so there isn't a violation?
For instance, If there are three rows in the SALES table where the saleDate is 2015-10-10, the code should only populate ONE instance of 2015-10-10 into the saleDay PK. I'm thinking the direction I should head is to implement a WHILE clause, however I'm not 100% sure on how that would work since this code is also using CASE to determine whether the saleDay was a Holiday, Weekday, or Weekend and populating the result into the dayType column.
Adding DISTINCT as suggested in a Comment below your question is one way to solve the problem.
The following approach may be more efficient:
for rec in (select distinct saledate from sales)
loop
insert into times (saleday, daytype) values
(rec.saledate, CASE .......);
end loop;
That is: put the CASE expression in the INSERT statement, not in the definition of the (implicit) cursor. There is no reason to compute the CASE expression multiple times for the same date, which may appear many times in the SALES table. There is no reason for the CASE expression to be part of the cursor, either. The CASE expression can use an IN condition (case when to_char(rec.saledate, 'dd.mm') in ('01.01', '15.01', ....) then 'Holiday' when .......)
Unless, of course, the homework problem specifically instructs you to use a left outer join....... :-(
Adding DISTINCT resolved this. Originally thought DISTINCT would negatively impact the CASE but it doesn't. Thanks to I3rutt for pointing this out.
BEGIN
FOR rec IN
(SELECT DISTINCT saleDate, CASE WHEN h.hd IS NOT NULL THEN 'Holiday'
WHEN to_char(saleDate, 'd') IN (1,7) THEN 'Weekend'
ELSE 'Weekday' END dayType
FROM SALES s LEFT JOIN
(SELECT '01.01' hd FROM DUAL UNION ALL
SELECT '15.01' FROM DUAL UNION ALL
SELECT '19.01' FROM DUAL UNION ALL
SELECT '28.05' FROM DUAL UNION ALL
SELECT '04.07' FROM DUAL UNION ALL
SELECT '08.10' FROM DUAL UNION ALL
SELECT '11.11' FROM DUAL UNION ALL
SELECT '22.11' FROM DUAL UNION ALL
SELECT '25.12' FROM DUAL) h
ON h.hd = TO_CHAR(s.saleDate, 'dd.mm'))
LOOP
INSERT INTO TIMES VALUES rec;
END LOOP;
END;
/

Need to write a procedure to fetch given rownums

I need to write one procedure to pick the record for given rows
for example
procedure test1
(
start_ind number,
end_ind number,
p_out ref cursor
)
begin
opecn p_out for
select * from test where rownum between start_ind and end_ind;
end;
when we pass start_ind 1 and end_ind 10 its working.But when we change start_ind to 5
then query looks like
select * from test where rownum between 5 and 10;
and its fails and not shows the output.
Please assist how to fix this issue.Thanks!
The rownum is assigned and then the where condition evaluated. Since you'll never have a rownum 1-4 in your result set, you never get to rownum 5. You need something like this:
SELECT * FROM (
SELECT rownum AS rn, t.*
FROM (
SELECT t.*
FROM test t
ORDER BY t.whatever
)
WHERE ROWNUM <= 10
)
WHERE rn >= 5
You'll also want an order by clause in the inner select, or which rows you get will be undefined.
This article by Tom Kyte pretty much tells you everything you need to know: http://www.oracle.com/technetwork/issue-archive/2006/06-sep/o56asktom-086197.html
SELECT *
from (SELECT rownum AS rn, t.*
FROM MyTable t
WHERE ROWNUM <= 10
ORDER BY t.NOT-Whatever
-- (its highly important to use primary or unique key of MyTable)
WHERE rn > 5
As a hint, :
Typically we use store-procedures for data validation, access control, extensive or complex processing that requires execution of several SQL statements. Stored procedures may return result sets, i.e. the results of a SELECT statement. Such result sets can be processed using cursors, by other stored procedures, by associating a result set locator, or by applications
I think you are going to use the ruw-number to fetch paged queries.
Try to create a generic select query based on the idea mentioned above.
Two possibilities:
1) Your table is an index-organized table. So its data is sorted. You would select those first rows you want to avoid and based on that get the next rows you are looking for:
create or replace procedure get_records
(
vi_start_ind integer,
vi_end_ind integer,
vo_cursor out sys_refcursor
) as
begin
open vo_cursor for
select *
from test
where rownum <= vi_end_ind - vi_start_ind + 1
and rowid not in
(
select rowid
from test
where rownum < vi_start_ind
)
;
end;
2) Your table is not index-organized, which is normally the case. Then its records are not sorted. To get records m to n, you would have to tell the system what order you have in mind:
create or replace procedure get_records
(
vi_start_ind number,
vi_end_ind number,
vo_cursor out sys_refcursor
) as
begin
open vo_cursor for
select *
from test
where rownum <= vi_end_ind - vi_start_ind + 1
and rowid not in
(
select rowid from
(
select rowid
from test
order by somthing
)
where rownum < vi_start_ind
)
order by something
;
end;
All this said, think it over what you want to achieve. If you want to use this procedure to read your table block for block, keep in mind that it will read the same data again and again. To know what rows 1,000,001 to 1,000,100 are, the dbms must read through one million rows first.

How to compare items in an array to those in a database column using regular expressions?

I'm trying to take a list of elements in an array like this:
['GRADE', 'GRATE', 'GRAPE', /*About 1000 other entries here ...*/ ]
and match them to their occurrences in a column in an Oracle database full of entries like this:
1|'ANTERIOR'
2|'ANTEROGRADE'
3|'INGRATE'
4|'RETROGRADE'
5|'REIGN'
...|...
/*About 1,000,000 other entries here*/
For each entry in that array of G words, I'd like to loop through the word column of the Oracle database and try to find the right-sided matches for each entry in the array. In this example, entries 2, 3, and 4 in the database would all match.
In any other programming language, it would look something like this:
for entry in array:
for each in column:
if entry.right_match(each):
print entry
How do I do this in PL/SQL?
In PL/SQL it can be done in this way:
declare
SUBTYPE my_varchar2_t IS varchar2( 100 );
TYPE Roster IS TABLE OF my_varchar2_t;
names Roster := Roster( 'GRADE', 'GRATE', 'GRAPE');
begin
FOR c IN ( SELECT id, name FROM my_table )
LOOP
FOR i IN names.FIRST .. names.LAST LOOP
IF regexp_like( c.name, names( i ) ) THEN
DBMS_OUTPUT.PUT_LINE( c.id || ' ' || c.name );
END IF;
END LOOP;
END LOOP;
end;
/
but this is row by row processing, for large table it would be very slow.
I think it might be better to do it in a way shown below:
create table test123 as
select 1 id ,'ANTERIOR' name from dual union all
select 2,'ANTEROGRADE' from dual union all
select 3,'INGRATE' from dual union all
select 4,'RETROGRADE' from dual union all
select 5,'REIGN' from dual ;
create type my_table_typ is table of varchar2( 100 );
/
select *
from table( my_table_typ( 'GRADE', 'GRATE', 'GRAPE' )) x
join test123 y on regexp_like( y.name, x.column_value )
;
COLUMN_VALUE ID NAME
------------- ---------- -----------
GRADE 2 ANTEROGRADE
GRATE 3 INGRATE
GRADE 4 RETROGRADE

BULK COLLECT into a table of objects

When attempting to use a BULK COLLECT statement I got error ORA-00947: not enough values.
An example script:
CREATE OR REPLACE
TYPE company_t AS OBJECT (
Company VARCHAR2(30),
ClientCnt INTEGER );
/
CREATE OR REPLACE
TYPE company_set AS TABLE OF company_t;
/
CREATE OR REPLACE
FUNCTION piped_set (
v_DateBegin IN DATE,
v_DateEnd IN DATE
)
return NUMBER /*company_set pipelined*/ as
v_buf company_t := company_t( NULL, NULL);
atReport company_set;
sql_stmt VARCHAR2(500) := '';
begin
select * BULK COLLECT INTO atReport
from (
SELECT 'Descr1', 1 from dual
UNION
SELECT 'Descr2', 2 from dual ) ;
return 1;
end;
The error occurs at the line select * BULK COLLECT INTO atReport.
Straight PL/SQL works fine by the way (so no need to mention it as a solution). Usage of BULK COLLECT into a user table type is the question.
Your company_set is a table of objects, and you're selecting values, not objects comprised of those values. This will compile:
select * BULK COLLECT INTO atReport
from (
SELECT company_t('Descr1', 1) from dual
UNION
SELECT company_t('Descr2', 2) from dual ) ;
... but when run will throw ORA-22950: cannot ORDER objects without MAP or ORDER method because the union does implicit ordering to identify and remove duplicates, so use union all instead:
select * BULK COLLECT INTO atReport
from (
SELECT company_t('Descr1', 1) from dual
UNION ALL
SELECT company_t('Descr2', 2) from dual ) ;

Resources