I have the following results from query and I a plsql block where I loop through the records and send_email to customers.
Anonymous block
FOR i IN (SELECT product_no, product_holder,product_catalogue FROM
product_master)
LOOP
mail_send('PRODMASTER',i.product_holder, i.product_no,i.product_catalogue);
END LOOP;
I would like to know what is the best approach if product_holder is repeating in query result then rather than sending multiple emails, would like to send one email with relevant details. E.g. In above case SMITH is repeated twice, so with above approach SMITH will get two emails, instead I would like to send one email to SMITH with product_noand product_catalogue
How can I do this?
Don't do loops within loops in PL/SQL for this - use SQL to give you the data ready for use.
First we create your table with some test data (I'm guessing datatypes - you replace with your own) :
create table product_master (
product_no varchar2(10)
, product_holder varchar2(10)
, product_catalogue varchar2(10)
)
/
insert into product_master values ('1', 'SMITH', 'TEMP')
/
insert into product_master values ('2', 'SMITH', 'TEMP')
/
insert into product_master values ('3', 'HARRY', 'ARCH')
/
insert into product_master values ('4', 'TOM' , 'DEPL')
/
commit
/
What we want to send to mail_send procedure for each product_holder is a collection (array) containing product_no and product_catalogue. So first a type that contains those two elements:
create type t_prod_cat_no as object (
product_no varchar2(10)
, product_catalogue varchar2(10)
)
/
And then a nested table type (collection type) of that type:
create type t_prod_cat_no_table as
table of t_prod_cat_no
/
The procedure mail_send then should accept the product_holder and the collection type:
create or replace procedure mail_send (
p_parameter in varchar2
, p_product_holder in varchar2
, p_product_cats_nos in t_prod_cat_no_table
)
is
begin
dbms_output.put_line('-- BEGIN '||p_parameter||' --');
dbms_output.put_line('Dear '||p_product_holder);
dbms_output.put_line('Your products are:');
for i in 1..p_product_cats_nos.count loop
dbms_output.put_line(
'Catalogue: '||p_product_cats_nos(i).product_catalogue||
' - No: '||p_product_cats_nos(i).product_no
);
end loop;
end mail_send;
/
(I just use dbms_output to simulate building a mail.)
Then you can in SQL do a group by product_holder and let SQL generate the collection containing the data:
begin
for holder in (
select pm.product_holder
, cast(
collect(
t_prod_cat_no(pm.product_no,pm.product_catalogue)
order by pm.product_catalogue
, pm.product_no
) as t_prod_cat_no_table
) product_cats_nos
from product_master pm
group by pm.product_holder
order by pm.product_holder
) loop
mail_send(
'PRODMASTER'
, holder.product_holder
, holder.product_cats_nos
);
end loop;
end;
/
The output of the above block will be:
-- BEGIN PRODMASTER --
Dear HARRY
Your products are:
Catalogue: ARCH - No: 3
-- BEGIN PRODMASTER --
Dear SMITH
Your products are:
Catalogue: TEMP - No: 1
Catalogue: TEMP - No: 2
-- BEGIN PRODMASTER --
Dear TOM
Your products are:
Catalogue: DEPL - No: 4
Doing it in SQL with a GROUP BY gives you everything in a single call from PL/SQL to SQL, which is a whole lot more efficient than first one call to get the distinct set of product_holder, loop over that, and then one call per product_holder to get the products for each holder.
UPDATE:
Added order by to the collect function in the above code to show you have control over the order that the data is populated in the collection.
You can use two loops and send mail for each product holder, something like this;
FOR i IN (SELECT distinct product_holder FROM
product_master)
LOOP
v_products := null;
v_catalogs := null;
for product in (SELECT pm.product_no, pm.product_catalogue FROM
product_master pm where pm.product_holder = i.product_holder)
loop
if v_products is null then
v_products := product.product_no;
else
v_products := v_products ||', ' ||product.product_no;
end if;
if v_catalogs is null then
v_catalogs := product.product_catalogue;
else
v_catalogs := v_catalogs ||', ' ||product.product_catalogue;
end if;
end loop;
mail_send('PRODMASTER',i.product_holder, v_products,v_catalogs);
END LOOP;
Related
Morning,
I'm trying to write a script that will convert Unload tables (UNLD to HDL files) creating a flat file using PLSQL. I keep getting syntax errors trying to run it and would appreciate some help from an expert out there!
Here are the errors:
Error(53,21): PLS-00330: invalid use of type name or subtype name
Error(57,32): PLS-00222: no function with name 'UNLDTABLE' exists in this scope
Our guess is that the unldTable variable is being treated as a String, rather than a database table object (Not really expereinced in PLSQL)
CREATE OR REPLACE PROCEDURE UNLD_TO_HDL (processComponent IN VARCHAR2)
IS
fHandle UTL_FILE.FILE_TYPE;
concatData VARCHAR2(240);
concatHDLMetaTags VARCHAR2(240);
outputFileName VARCHAR2(240);
TYPE rowArrayType IS TABLE OF VARCHAR2(240);
rowArray rowArrayType;
emptyArray rowArrayType;
valExtractArray rowArrayType;
hdlFileName VARCHAR2(240);
unldTable VARCHAR2(240);
countUNLDRows Number;
dataType VARCHAR2(240);
current_table VARCHAR2(30);
value_to_char VARCHAR2(240);
BEGIN
SELECT HDL_FILE_NAME
INTO hdlFileName
FROM GNC_HDL_CREATION_PARAMS
WHERE PROCESS_COMPONENT = processComponent;
SELECT UNLD_TABLE
INTO unldTable
FROM GNC_HDL_CREATION_PARAMS
WHERE PROCESS_COMPONENT = processComponent
FETCH NEXT 1 ROWS ONLY;
SELECT LISTAGG(HDL_META_TAG,'|')
WITHIN GROUP(ORDER BY HDL_META_TAG)
INTO concatHDLMetaTags
FROM GNC_MIG_CONTROL
WHERE HDL_COMP = processComponent;
SELECT DB_FIELD
BULK COLLECT INTO valExtractArray
FROM GNC_MIG_CONTROL
WHERE HDL_COMP = processComponent
ORDER BY HDL_META_TAG;
fHandle := UTL_FILE.FOPEN('./', hdlFileName, 'W');
UTL_FILE.PUTF(fHandle, concatHDLMetaTags + '\n');
SELECT num_rows INTO countUNLDRows FROM user_tables where table_name = unldTable;
FOR row in 1..countUNLDRows LOOP
rowArray := emptyArrayType;
FOR value in 1..valExtractArray.COUNT LOOP
rowArray.extend();
SELECT data_type INTO dataType FROM all_tab_columns where table_name = unldTable AND column_name = valExtractArray(value);
IF dataType = 'VARCHAR2' THEN (SELECT valExtractArray(value) INTO value_to_char FROM current_table WHERE ROWNUM = row);
ELSIF dataType = 'DATE' THEN (SELECT TO_CHAR(valExtractArray(value),'YYYY/MM/DD') INTO value_to_char FROM current_table WHERE ROWNUM = row);
ELSIF dataType = 'NUMBER' THEN (SELECT TO_CHAR(valExtractArray(value)) INTO value_to_char FROM current_table WHERE ROWNUM = row);
ENDIF;
rowArray(value) := value_to_char;
END LOOP;
concatData := NULL;
FOR item in 1..rowArray.COUNT LOOP
IF item = rowArray.COUNT
THEN concatData := (COALESCE(concatData,'') || rowArray(item));
ELSE concatData := (COALESCE(concatData,'') || rowArray(item) || '|');
END IF;
END LOOP;
UTL_FILE.PUTF(fHandle, concatData + '/n');
END LOOP;
UTL_FILE.FCLOSE(fHandle);
END;
Thanks,
Adam
I believe it is just an overlook in your code. You define unldTable as a varchar, which is used correctly until you try to access it as if it were a varray on line 51
rowArray(value) := unldTable(row).valExtractArray(value);
Given that you have not defined it as a varray, unldTable(row) is making the interpreter believe that you are referring to a function.
EDIT
Now that you have moved on, you should resolve the problem of invoking SELECT statements on tables that are unknown at runtime. To do so you need to make use of Dynamic SQL; you can do it in several way, the most direct being an Execute immediate statement in your case:
mystatement := 'SELECT valExtractArray(value) INTO :value_to_char FROM ' || current_table || ' WHERE ROWNUM = ' || row;
execute immediate mystatement USING OUT value_to_char;
It looks like you need to generate a cursor as
select [list of columns from GNC_MIG_CONTROL.DB_FIELD]
from [table name from GNC_HDL_CREATION_PARAMS.UNLD_TABLE]
Assuming setup like this:
create table my_table (business_date date, id integer, dummy1 varchar2(1), dummy2 varchar2(20));
create table gnc_hdl_creation_params (unld_table varchar2(30), process_component varchar2(30));
create table gnc_mig_control (db_field varchar2(30), hdl_comp varchar2(30), hdl_meta_tag integer);
insert into my_table(business_date, id, dummy1, dummy2) values (date '2018-01-01', 123, 'X','Some more text');
insert into gnc_hdl_creation_params (unld_table, process_component) values ('MY_TABLE', 'XYZ');
insert into gnc_mig_control (db_field, hdl_comp, hdl_meta_tag) values ('BUSINESS_DATE', 'XYZ', '1');
insert into gnc_mig_control (db_field, hdl_comp, hdl_meta_tag) values ('ID', 'XYZ', '2');
insert into gnc_mig_control (db_field, hdl_comp, hdl_meta_tag) values ('DUMMY1', 'XYZ', '3');
insert into gnc_mig_control (db_field, hdl_comp, hdl_meta_tag) values ('DUMMY2', 'XYZ', '4');
You could build a query like this:
select unld_table, listagg(expr, q'[||'|'||]') within group (order by hdl_meta_tag) as expr_list
from ( select t.unld_table
, case tc.data_type
when 'DATE' then 'to_char('||c.db_field||',''YYYY-MM-DD'')'
else c.db_field
end as expr
, c.hdl_meta_tag
from gnc_hdl_creation_params t
join gnc_mig_control c
on c.hdl_comp = t.process_component
left join user_tab_columns tc
on tc.table_name = t.unld_table
and tc.column_name = c.db_field
where t.process_component = 'XYZ'
)
group by unld_table;
Output:
UNLD_TABLE EXPR_LIST
----------- --------------------------------------------------------------------------------
MY_TABLE to_char(BUSINESS_DATE,'YYYY-MM-DD')||'|'||ID||'|'||DUMMY1||'|'||DUMMY2
Now if you plug that logic into a PL/SQL procedure you could have something like this:
declare
processComponent constant gnc_hdl_creation_params.process_component%type := 'XYZ';
unloadSQL long;
unloadCur sys_refcursor;
text long;
begin
select 'select ' || listagg(expr, q'[||'|'||]') within group (order by hdl_meta_tag) || ' as text from ' || unld_table
into unloadSQL
from ( select t.unld_table
, case tc.data_type
when 'DATE' then 'to_char('||c.db_field||',''YYYY/MM/DD'')'
else c.db_field
end as expr
, c.hdl_meta_tag
from gnc_hdl_creation_params t
join gnc_mig_control c
on c.hdl_comp = t.process_component
left join user_tab_columns tc
on tc.table_name = t.unld_table
and tc.column_name = c.db_field
where t.process_component = processComponent
)
group by unld_table;
open unloadCur for unloadSQL;
loop
fetch unloadCur into text;
dbms_output.put_line(text);
exit when unloadCur%notfound;
end loop;
close unloadCur;
end;
Output:
2018/01/01|123|X|Some more text
2018/01/01|123|X|Some more text
Now you just have to make that into a procedure, change dbms_output to utl_file and add your meta tags etc and you're there.
I've assumed there is only one distinct unld_table per process component. If there are more you'll need a loop to work through each one.
For a slightly more generic approach, you could build a cursor-to-csv generator which could encapsulate the datatype handling, and then you'd only need to build the SQL as select [columns] from [table]. You might then write a generic cursor to file processor, where you pass in the filename and a cursor and it does the lot.
Edit: I've updated my cursor-to-csv generator to provide file output, so you just need to pass it a cursor and the file details.
Hi I've asked a question related to this already but have a second question. I commented that I made a nested table of the teams that played rather than separate rows for each team and score.
I want to run the Method for a particular GameId rather than all the rows in the table.. I've included my Game_Type Object this time though I didn't think it was necessary last time.
CREATE TYPE Game_Type AS OBJECT
(GameId NUMBER)
/
CREATE TABLE Game_Table of Game_Type
/
INSERT INTO Game_Table
VALUES (1)
/
INSERT INTO Game_Table
VALUES (2)
/
CREATE TYPE Team_Type AS OBJECT
(TeamPlayed VARCHAR2(30),
TeamScore NUMBER(1))
/
CREATE TYPE TeamsPlayed_Table as TABLE OF Team_Type
/
CREATE OR REPLACE TYPE After_Team AS OBJECT
(GameRef REF Game_Type,
GamePlayed Teamsplayed_Table,
MAP MEMBER FUNCTION team_rating RETURN NUMBER)
/
CREATE TABLE Team_Table of After_Team NESTED TABLE GamePlayed STORE AS
GamePlayed_Nested
/
CREATE OR REPLACE TYPE BODY After_Team
AS
MAP MEMBER FUNCTION team_rating
RETURN NUMBER
IS
avg_score NUMBER;
BEGIN
SELECT AVG(c.TeamScore)
INTO avg_score
FROM Team_Table d, table(d.GamePlayed) c;
RETURN avg_score;
END;
END;
/
INSERT INTO Team_Table
VALUES((SELECT REF(a) FROM Game_Table a WHERE a.gameid = 1),
(TeamsPlayed_Table(Team_Type('Team A', 1), Team_Type('Team B', 3))))
/
INSERT INTO Team_Table
VALUES((SELECT REF(a) FROM Game_Table a WHERE a.gameid = 2),
(TeamsPlayed_Table(Team_Type('Team C', 5), Team_Type('Team D', 9))))
/
Now when I execute my method:
SELECT t.team_rating()
from Team_Table t
where t.GameRef.GameId = 1
It's returning the average for all the values as opposed to just Game 1..
Assuming you want to get the average of the TeamScore in the GamePlayed collection for that team then you can do it in pure PL/SQL (without a context-switch into the SQL scope):
CREATE OR REPLACE TYPE BODY After_Team
AS
MAP MEMBER FUNCTION team_rating
RETURN NUMBER
IS
avg_score NUMBER := 0;
j INTEGER := 0;
BEGIN
FOR i IN 1 .. self.GamePlayed.COUNT LOOP
IF self.GamePlayed(i) IS NOT NULL AND self.GamePlayed(i).TeamScore IS NOT NULL THEN
avg_score := avg_score + self.GamePlayed(i).TeamScore;
j := j + 1;
END IF;
END LOOP;
IF j > 0 THEN
RETURN avg_score / j;
ELSE
RETURN NULL;
END IF;
END;
END;
/
SQLFIDDLE
otherwise, you could use:
CREATE OR REPLACE TYPE BODY After_Team
AS
MAP MEMBER FUNCTION team_rating
RETURN NUMBER
IS
avg_score NUMBER;
BEGIN
SELECT avg( TeamScore )
INTO avg_score
FROM TABLE( self.GamePlayed );
RETURN avg_score;
END;
END;
/
SQLFIDDLE
I am new to stored procedures. Please provide me help. Later I will write be myself and i have written only few logic here. I have two tables Call_Match and Subs_Info. Columns and datatype information are shown below.
I have to write a stored procedure and the logic should be:
SP input : Number -> callNumber
SP output : varchar -> ReturnString ( comma delimited )
SP output : Number -> ErrorCode
Logic : callNumber pattern will check against Call_Match table column TextPattern. if it matches callNumber equality will check against SUBS_INFO table CALL_NO column. In return string all the values should be comma separated.
Return : DRR, NO_TIME, CFU , NRC
Table: Call_Match
Columns :
TextPattern ( varchar2 ): 0112
DRR ( Number) : 5
NO_TIME ( Number ) : 1
Table: SUBS_INFO
Columns:
CALL_NO (varchar2 ) : 01121213
CFU (Number): 1
NRC ( Number ) : 3
If there are anything missing tell me about it , it still need handeling error.
Create or replace PROCEDURE EXECUTE_DATA
( CALL_NUMBER IN VARCHAR2, RETURN_RESULT OUT VARCHAR2, ERROR_CODE OUT INTEGER )
AS
VAR_DRR NUMBER(3);
VAR_NO_TIME NUMBER(3) ;
VAR_CFU NUMBER(3);
VAR_NRC NUMBER(3);
CNT NUMBER(3);
CNT1 NUMBER(3);
BEGIN
-- check if there are values equale to call number;
SELECT COUNT(1) INTO CNT FROM CALL_MATCH WHERE TEXTPATTERN =CALL_NUMBER;
IF CNT >0 THEN
SELECT COUNT(1) INTO CNT1 FROM SUBS_INFO WHERE CALL_NO =CALL_NUMBER;
end if;
-- check if there are values in the another table;
IF CNT1 >0 THEN
SELECT CALL_MATCH.DRR ,CALL_MATCH.NO_TIME ,SUBS_INFO.CFU ,SUBS_INFO.NRC
INTO VAR_DRR ,VAR_NO_TIME , VAR_CFU,VAR_NRC
FROM CALL_MATCH,SUBS_INFO WHERE
CALL_MATCH.TEXTPATTERN =CALL_NUMBER
and SUBS_INFO.CALL_NO =CALL_NUMBER; -- its better to have a kind of checking, this is example
-- the returning result that you want
RETURN_RESULT:= VAR_DRR||','||VAR_NO_TIME||','||VAR_CFU||','||VAR_NRC;
ELSE
RETURN_RESULT :=NULL;
end if;
end;
/
I have an Oracle procedure I am working on that needs to insert every combination of Customers and Products into a specific table based on the CustomerIDs passed into the parameter.
Inputs:
Customers parameter (p_customers - comma separated):
IE: (1,2,3,4)
Order parameter (p_order - single value)
Output table that I must write to:
CustomerID, ProductID
I am hoping the following query will get me a list of the products that need be inserted:
select ProductID from orders where order_id = p_order_id
What is the most efficient way to write the insert statement?
Thank you!
Based on your "pseudo" code, try something like this. I ripped off parts of this from http://www.oratechinfo.co.uk/delimited_lists_to_collections.html
Assuming these structures:
CREATE TABLE orders (
orderId VARCHAR2(100) primary key
, customerId VARCHAR2(100)
, productId VARCHAR2(100)
);
CREATE TABLE output_table (customerId VARCHAR2(100), productId VARCHAR2(100));
Code:
CREATE TYPE varchar_table IS TABLE OF VARCHAR2(100);
/
CREATE OR REPLACE FUNCTION csv_convert(p_list IN VARCHAR2)
RETURN varchar_table
AS
l_string VARCHAR2(32767) := p_list || ',';
l_comma_index PLS_INTEGER;
l_index PLS_INTEGER := 1;
l_tab varchar_table := varchar_table();
BEGIN
LOOP
l_comma_index := INSTR(l_string, ',', l_index);
EXIT WHEN l_comma_index = 0;
l_tab.EXTEND;
l_tab(l_tab.COUNT) := SUBSTR(l_string, l_index, l_comma_index - l_index);
l_index := l_comma_index + 1;
END LOOP;
RETURN l_tab;
END csv_convert;
/
A simple unit test of the newly created csv_convert function:
SELECT * FROM TABLE(csv_convert('1,2,3,4'));
COLUMN_VALUE
------------
1
2
3
4
Now the stored proc that makes use of the csv_convert function:
CREATE OR REPLACE PROCEDURE cust_products_process (p_customers IN VARCHAR2)
IS
BEGIN
INSERT INTO output_table (customerId, productId)
SELECT customerId, productId
FROM orders o
JOIN TABLE(csv_convert(p_customers)) c
ON (o.customerId = c.column_value);
END;
/
I'm trying to return a multiple values in a %rowtype from a function using two table(employees and departments), but it not working for me.
create or replace function get_employee
(loc in number)
return mv_emp%rowtype
as
emp_record mv_emp%rowtype;
begin
select a.first_name, a.last_name, b.department_name into emp_record
from employees a, departments b
where a.department_id=b.department_id and location_id=loc;
return(emp_record);
end;
The above function compiled without any error? What is the type of MV_EMP? Ideally, it should be something like below.
create or replace type emp_type
(
first_name varchar2(20)
, last_name varchar2(20)
, depart_name varchar2(20)
)
/
create or replace function get_employee
(loc in number)
return emp_type
as
emp_record emp_type;
begin
select a.first_name, a.last_name, b.department_name into emp_record
from employees a, departments b
where a.department_id=b.department_id and location_id=loc;
return(emp_record);
end;
create type t_row as object (a varchar2(10));
create type t_row_tab as table of t_row;
We will now create a function which will split the input string.
create or replace function get_number(pv_no_list in varchar2) return t_row_tab is
lv_no_list t_row_tab := t_row_tab();
begin
for i in (SELECT distinct REGEXP_SUBSTR(pv_no_list, '[^,]+', 1, LEVEL) no_list FROM dual
CONNECT BY REGEXP_SUBSTR(pv_no_list, '[^,]+', 1, LEVEL) IS NOT NULL)
loop
lv_no_list.extend;
lv_no_list(lv_no_list.last) := t_row(i.no_list);
end loop;
return lv_no_list;
end get_number;
Once the function is in place we can use the table clause of sql statement to get the desired result. As desired we got multiple values returned from the function.
SQL> select * from table(get_number('1,2,3,4'));
A
----------
1
3
2
4
So now our function is simply behaving like a table. There can be a situation where you want these comma separated values to be a part of "IN" clause.
For example :
select * from dummy_table where dummy_column in ('1,2,3,4');
But the above query will not work as '1,2,3,4' is a string and not individual numbers. To solve this problem you can simply use following query.
select * from dummy_table where dummy_column in ( select * from table(get_number('1,2,3,4')) );
References : http://www.oraclebin.com/2012/12/returning-multiple-values-from-function.html
CREATE OR replace FUNCTION Funmultiple(deptno_in IN NUMBER)
RETURN NUMBER AS v_refcursur SYS_REFCURSOR;
BEGIN
OPEN v_refcursor FOR
SELECT *
FROM emp
WHERE deptno = deptno_in;
retun v_refcursor;
END;
To call it, use:
variable x number
exec :x := FunMultiple(10);
print x