Method Function not getting values from single row (Nested table) - sql

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

Related

Is there an oracle spatial function for finding self-intersecting linestrings?

I need to find all self-intersecting linestrings in table. SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT finds only self-intersecting polygons, because self-intersecting linestrings are allowed. Any ideas?
It takes a bit of work, but it's doable.
Since Oracle(11.2) failed to provide, the only option we have is to brake the line into segments and use RELATE on the segments' pairs.
Following is my own implementation (used in production code for over 3 years, over millions of geometries). I chose the pipelined approach to cover for overly big or complex geometries.
Prerequisit 1, database type:
CREATE OR REPLACE TYPE ElemGeom as object
(eid integer, egeom mdsys.sdo_geometry, egtype integer, eelemnum integer, evertnum integer, earea number, elength number);
CREATE OR REPLACE TYPE ElemGeomTbl as table of ElemGeom;
Prerequisit 2, splitting function:
create or replace FUNCTION LineSegments (igeom in mdsys.sdo_geometry)
RETURN ElemGeomTbl pipelined
is
seg ElemGeom := ElemGeom(null,null,null,null,null,null,null);
cursor c is select T.id, T.X ,T.Y from table(SDO_UTIL.GETVERTICES(iGEOM)) T order by 1;
type ctbl is table of c%rowtype;
carr ctbl;
seg_geom mdsys.sdo_geometry;
cnt integer:=0;
segid integer; x1 number; y1 number; x2 number; y2 number;
begin
--if igeom.sdo_gtype not in (2002,2006)
--then... if you need to catch non-linears here...
--end if;
open c;
loop
fetch c
bulk collect into carr ;
for i in carr.first .. carr.last -1
loop cnt:=cnt+1;
segid := cnt;
x1 := carr(i).X; y1 := carr(i).Y;
x2 := carr(i+1).X; y2 := carr(i+1).Y;
seg_geom:= (mdsys.sdo_geometry(2002,2100,null
,mdsys.sdo_elem_info_array(1,2,1)
,mdsys.sdo_ordinate_array(x1,y1, x2,y2)));
seg.eid:=segid;
seg.egeom:=seg_geom;
seg.egtype:=seg_geom.sdo_gtype;
pipe row(seg);
end loop;
exit when c%notfound;
end loop;
close c;
end LineSegments;
You can test its output with something like (my GEOMs are SRID 2100):
with t1 as (
select
SDO_GEOMETRY(2002,2100,NULL,
SDO_ELEM_INFO_ARRAY(1,2,1),
SDO_ORDINATE_ARRAY(290161.697,4206385.413, 290161.901,4206388.095, 290162.684,4206385.188, 290163.188,4206388.041,
290163.51,4206385.22, 290164.357,4206388.159, 290166.879,4206387.108, 290161.397,4206387.366,
290166.331,4206386.067, 290165.763,4206388.052))
as G from DUAL
)
select * from t1,table(LineSegments(g));
And the main function:
create or replace FUNCTION validate_Line
(igeom in mdsys.sdo_geometry, itol in number default null)
RETURN varchar2
is
vtol number:= nvl(itol, 1/power(10,6));
verd1 varchar2(256); verd2 varchar2(256); v varchar2(256);
begin
verd1:= sdo_geom.validate_geometry_with_context(igeom,vtol);
for r1 in ( select a.eid seg1, a.egeom geom1, b.eid seg2, b.egeom geom2
from table(LineSegments(igeom)) a, table(LineSegments(igeom)) b
where a.eid < b.eid
order by a.eid, b.eid )
loop
--I hate outputting long words, so:
v:= replace(replace(sdo_geom.relate(r1.geom1,'determine',r1.geom2, vtol)
,'OVERLAPBDYDISJOINT','OVR-BDIS'),'OVERLAPBDYINTERSECT','OVR-BINT');
if instr('EQUAL,TOUCH,DISJOINT',v) = 0 then
verd2:= verd2|| case when verd2 is not null
then ', '||r1.seg1||'-'||r1.seg2||'='||v
else r1.seg1||'-'||r1.seg2||'='||v end;
end if;
end loop;
verd1:= nvl(verd1,'NULL')
|| case when verd1 ='TRUE' and verd2 is null then null
when verd1 ='TRUE' and verd2 is not null then ' *+: '||verd2
end;
return verd1;
end validate_Line;
And its test:
with t1 as (
select
SDO_GEOMETRY(2002,2100,NULL,
SDO_ELEM_INFO_ARRAY(1,2,1),
SDO_ORDINATE_ARRAY(290161.697,4206385.413, 290161.901,4206388.095, 290162.684,4206385.188, 290163.188,4206388.041,
290163.51,4206385.22, 290164.357,4206388.159, 290166.879,4206387.108, 290161.397,4206387.366,
290166.331,4206386.067, 290165.763,4206388.052))
as G from DUAL
)
select t1.*,validate_Line(g) from t1;
This returns:
*TRUE *+: 1-7=OVR-BDIS, 1-8=OVR-BDIS, 2-7=OVR-BDIS, 2-8=OVR-BDIS, 3-7=OVR-BDIS, 3-8=OVR-BDIS, 4-7=OVR-BDIS, 4-8=OVR-BDIS, 5-7=OVR-BDIS, 5-8=OVR-BDIS, 6-9=OVR-BDIS, 7-9=OVR-BDIS*
Of course, you can modify the output to be just a flag or anything else - this is just what suited my own needs.
HTH

Traverse through loop to find repeated names

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;

Oracle: Return multiple values in a function

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

Wants numeric data from varchar2 datatype column

I have a table Product with a varchar2 datatype of column name Value, in this column values are stored as
All,10:23,0.84522,1.245,10:54:68,
All,1:22:00,0.245,45:12:00
etc.
We have to extract all the floating values like (0.84522,1.245,0.245) and ones that ends with ":00" like (1:22:00,45:12:00).
I have following query, but it doesn't seems to work; it gives me all the values except characters.
select * from Product where Values BETWEEN to_char (0) and to_char (2);
I think this would work
select *
FROM Product
WHERE
(Value LIKE '%:00' AND Value<> 'ALL') AND (Value BETWEEN to_NUMBER (0) and to_NUMBER (2))
Try this query:
select *
from (select distinct regexp_substr(t.value, '[^,]+', 1, level) phrase
from Product t
connect by regexp_substr(t.value, '[^,]+', 1, level) is not null) ph
where regexp_like(ph.phrase, '(\d+\.\d+)|(.+:00)')
The regular expression in the where clause may need some tunning
What it does is-
seperates all phrases (the inner query)
selects only those that matches your criteria
UPDATE
If you suffer from performance you can try a different approach:
create or replace type phrase_typ is object
(
phrase varchar2(100)
)
;
/
create or replace type phrase_tab as table of phrase_typ;
/
create or replace function split_string(del in varchar2) return phrase_tab
pipelined is
phrase varchar2(1000);
str_t varchar2(1000);
v_del_i number;
cursor c is with t as
select value from product;
begin
for r in c loop
str_t := r.value;
while str_t is not null loop
v_del_i := instr(str_t, del, 1, 1);
if v_del_i = 0 then
phrase := str_t;
str_t := '';
else
phrase := substr(str_t, 1, v_del_i - 1);
str_t := substr(str_t, v_del_i + 1);
end if;
if regexp_like(phrase, '(\d+\.\d+)|(.+:00)') then
pipe row(phrase_typ(phrase));
end if;
end loop;
end loop;
return;
end split_string;
/
Now your query should look like this:
select * from table(split_string(','))

SQL Query to get count of words in table

I have a table which have schema like this
id name
1 jack
2 jack of eden
3 eden of uk
4 m of s
I want to execute a query which gives me count of words like this
count word
2 jack
2 eden
3 of
this means jack has been here 2 times, eden 2 times and of has been 3 times.
Hope you got the question, m trying too but not getting the right query or approach to it
thnx
Assuming your table is named temp (probably not - change it to the right name of your table)
I used a subquery for finding all the words in your table:
select distinct regexp_substr(t.name, '[^ ]+',1,level) word , t.name, t.id
from temp t
connect by level <= regexp_count(t.name, ' ') + 1
this query splits all the words from all records. I aliased it words.
Then I joined it with your table (in the query it's called temp) and counted the number of occurences in every record.
select words.word, count(regexp_count(tt.name, words.word))
from(
select distinct regexp_substr(t.name, '[^ ]+',1,level) word , t.name, t.id
from temp t
connect by level <= regexp_count(t.name, ' ') + 1) words, temp tt
where words.id= tt.id
group by words.word
You can also add:
having count(regexp_count(tt.name, words.word)) > 1
update: for better performance we can replace the inner subquery with the results of a pipelined function:
first, create a schema type and a table of it:
create or replace type t is object(word varchar2(100), pk number);
/
create or replace type t_tab as table of t;
/
then create the function:
create or replace function split_string(del in varchar2) return t_tab
pipelined is
word varchar2(4000);
str_t varchar2(4000) ;
v_del_i number;
iid number;
cursor c is
select * from temp; -- change to your table
begin
for r in c loop
str_t := r.name;
iid := r.id;
while str_t is not null loop
v_del_i := instr(str_t, del, 1, 1);
if v_del_i = 0 then
word := str_t;
str_t := '';
else
word := substr(str_t, 1, v_del_i - 1);
str_t := substr(str_t, v_del_i + 1);
end if;
pipe row(t(word, iid));
end loop;
end loop;
return;
end split_string;
now the query should look like:
select words.word, count(regexp_count(tt.name, words.word))
from(
select word, pk as id from table(split_string(' '))) words, temp tt
where words.id= tt.id
group by words.word