SpatialDB Advisor
I received a comment the other day from a user of my PL/SQL packages about how he had extracted the GEOM.isCompound() function from the GEOM package, and re-written it in pure PL/SQL getting rid of the SQL pivot that that I use.
That’s the sort of behaviour I encourage.
The user’s comment was this:
This [i.e., his rewrite without the SQL] runs about 20-30 times faster and answers the same question.
And this is about what I would have expected!
But this comment reminded me about that article I have been meaning to write (for the past few years) on how to examine the performance differences between two implementations of a function in PL/SQL. I used the technique in my article on a fast method for creating multi-point geometries but I never got around to the sort of article that follows. At the end of the article I will discuss the SQL vs pure programmatic approach: there is no right answer, just some observations from someone who has written the odd PL/SQL function over the years.
The original GEOM.isCompound() function was extracted for the purpose of this article. With a slight rename, here it is:
create or replace
Function is_Compound(p_elem_info in mdsys.sdo_elem_info_array)
return boolean
Is
v_compound_element_count number := 0;
Begin
SELECT count(*) as c_element_count
INTO v_compound_element_count
FROM (SELECT e.id,
e.etype,
e.offset,
e.interpretation
FROM (SELECT trunc((rownum - 1) / 3,0) as id,
sum(case when mod(rownum,3) = 1 then sei.column_value else null end) as offset,
sum(case when mod(rownum,3) = 2 then sei.column_value else null end) as etype,
sum(case when mod(rownum,3) = 0 then sei.column_value else null end) as interpretation
FROM TABLE(p_elem_info) sei
GROUP BY trunc((rownum - 1) / 3,0)
) e
) i
WHERE ( i.etype = 2 AND i.interpretation = 2 )
OR ( i.etype in (1003,2003) AND i.interpretation IN (2,4) );
Return v_compound_element_count > 0;
End is_Compound;
The commentator’s function is as follows:
create or replace
function has_compound_curves ( geom_in in sdo_geometry)
return integer
as
etype number;
interpret number;
begin
for i in geom_in.sdo_elem_info.first .. geom_in.sdo_elem_info.last loop
case
when mod(i,3) = 1 then null; -- continue;
when mod(i,3) = 2 then etype := geom_in.sdo_elem_info(i);
when mod(i,3) = 0
then
interpret := geom_in.sdo_elem_info(i);
if((etype = 2 and interpret = 2) or (etype in (1003,2003) and interpret in (2,4))) then
return 1;
end if;
end case;
end loop;
return 0;
end has_compound_curves;
Now, let’s compare the two using Tom Kyte’s runstats_pkg package.
set serveroutput on size unlimited
declare
v_OK integer;
v_geom mdsys.sdo_geometry := MDSYS.SDO_GEOMETRY(2006,null,null,MDSYS.SDO_ELEM_INFO_ARRAY(1,2,1,5,2,1,9,2,1,17,4,2,17,2,1,23,2,2),MDSYS.SDO_ORDINATE_ARRAY(50,15,55,15,60,15,65,15,10,25,20,30,25,25,30,30,110,178,110,175,120,175,120,178,115,180,110,178));
begin
runstats_pkg.rs_start;
v_OK := has_compound_curves(v_geom);
runstats_pkg.rs_middle;
v_OK := case when is_compound(v_geom.sdo_elem_info) = TRUE then 1 else 0 end;
runstats_pkg.rs_stop;
end;
/
There appears to be little difference. So, I’ve short-circuited any more testing as a single execution of each function occurs in less than 1/100th of a second and so cannot be reasonably compared. The only way to compare the algorithms is to write a procedure that executes each 1000 times as follows:
create or replace
procedure test_compound(p_geom in mdsys.sdo_geometry,
p_is in integer)
As
v_OK integer;
v_is_compound boolean := case when p_is > 0 then true else false end;
Begin
For i IN 1..1000 LOOP
If ( v_is_compound ) Then
v_OK := case when is_compound(p_geom.sdo_elem_info) then 1 else 0 end;
Else
v_OK := has_compound_curves(p_geom);
End If;
End Loop;
end test_compound;
Which we would execute using the runstats_pkg as follows:
set serveroutput on size unlimited;
declare
v_geom mdsys.sdo_geometry := MDSYS.SDO_GEOMETRY(2006,null,null,MDSYS.SDO_ELEM_INFO_ARRAY(1,2,1,5,2,1,9,2,1,17,4,2,17,2,1,23,2,2),MDSYS.SDO_ORDINATE_ARRAY(50,15,55,15,60,15,65,15,10,25,20,30,25,25,30,30,110,178,110,175,120,175,120,178,115,180,110,178));
begin
runstats_pkg.rs_start;
test_compound(v_geom,0);
runstats_pkg.rs_middle;
test_compound(v_geom,1);
runstats_pkg.rs_stop(10);
end;
This tells me that the pure PL/SQL implementation runs in 1.1% of the time of the original implementation based on SQL. That is a huge difference between the two which is expected and appears to be in agreement with my commentator.
Can we modify is_Compound and close the performance gap?
Let’s modify is_Compound by:
create or replace
Function is_Compound(p_elem_info in mdsys.sdo_elem_info_array)
return integer
Is
v_compound_element_count number := 0;
Begin
SELECT count(*) as c_element_count
INTO v_compound_element_count
FROM (SELECT trunc((rownum - 1) / 3,0) as id,
sum(case when mod(rownum,3) = 1 then sei.column_value else null end) as offset,
sum(case when mod(rownum,3) = 2 then sei.column_value else null end) as etype,
sum(case when mod(rownum,3) = 0 then sei.column_value else null end) as interpretation
FROM TABLE(p_elem_info) sei
GROUP BY trunc((rownum - 1) / 3,0)
) e
WHERE ( e.etype = 2 AND e.interpretation = 2 )
OR ( e.etype in (1003,2003) AND e.interpretation IN (2,4) );
Return v_compound_element_count;
End is_Compound;
/
And then run it again against has_compound_curves() function. Note that first we need to modify the test_compound procedure to handle the changed return type by removing the “case when is_compound….end” construct and replacing it by a simple call to is_compound.
The new results are:
A very, very slight improvement.
Let’s try modifying the function by adding in an “and rownum < 2” predicate to “short circuit” the query once we have found a compound element. Also, let’s add in a test to short-circuit when we find a compound element (4,1005,2005) and not just a potential sub-element of one.
create or replace
Function is_Compound(p_elem_info in mdsys.sdo_elem_info_array)
return integer
Is
v_compound_element_count number := 0;
Begin
SELECT count(*) as c_element_count
INTO v_compound_element_count
FROM (SELECT trunc((rownum - 1) / 3,0) as id,
sum(case when mod(rownum,3) = 1 then sei.column_value else null end) as offset,
sum(case when mod(rownum,3) = 2 then sei.column_value else null end) as etype,
sum(case when mod(rownum,3) = 0 then sei.column_value else null end) as interpretation
FROM TABLE(p_elem_info) sei
GROUP BY trunc((rownum - 1) / 3,0)
) e
WHERE ( e.etype = 2 AND e.interpretation = 2 )
OR ( e.etype in (1003,2003) AND e.interpretation IN (2,4) )
AND rownum < 2;
Return v_compound_element_count;
End is_Compound;
Now, one last final run against the new has_compound_curves PL/SQL function….
anonymous block completed
Run1 ran in 3 hsecs
Run2 ran in 90 hsecs
Run 1 ran in 3.33 % of the time
Again, not much change.
Let’s try reordering the geometry so the compound element is first…
select sdo_aggr_union(sdoaggrtype(geom,0.05)) as geom from (
select rin,geom from (
select rownum as rin,e.geometry as geom
from table(geom.extractelementspiped(MDSYS.SDO_GEOMETRY(2006,null,null,MDSYS.SDO_ELEM_INFO_ARRAY(1,2,1,5,2,1,9,2,1,17,4,2,17,2,1,23,2,2),MDSYS.SDO_ORDINATE_ARRAY(50,15,55,15,60,15,65,15,10,25,20,30,25,25,30,30,110,178,110,175,120,175,120,178,115,180,110,178)),0) ) e
) order by rin desc );
And run the new geometry against the new has_compound_curves PL/SQL function….
set serveroutput on size unlimited;
declare
v_geom mdsys.sdo_geometry := MDSYS.SDO_GEOMETRY(2006,null,null,MDSYS.SDO_ELEM_INFO_ARRAY(1,4,2,1,2,2,5,2,1,13,2,1,17,2,1,21,2,1),MDSYS.SDO_ORDINATE_ARRAY(120,178,115,180,110,178,110,175,120,175,120,178,60,15,65,15,50,15,55,15,10,25,20,30,25,25,30,30));
begin
runstats_pkg.rs_start;
test_compound(v_geom,0);
runstats_pkg.rs_middle;
test_compound(v_geom,1);
runstats_pkg.rs_stop(10);
end;
/
Reorganising the SDO_GEOMETRY had little effect on the SQL algorithm.
Even though I always suspected the issue was and is the overhead of setting up multiple SQL calls, I still want to complete the last of the modifications to the SQL function.
create or replace
Function is_Compound(p_elem_info in mdsys.sdo_elem_info_array)
return integer
Is
v_compound_element_count integer := 0;
Begin
SELECT 1 as c_element_count
INTO v_compound_element_count
FROM (SELECT trunc((rownum - 1) / 3,0) as id,
sum(case when mod(rownum,3) = 1 then sei.column_value else null end) as offset,
sum(case when mod(rownum,3) = 2 then sei.column_value else null end) as etype,
sum(case when mod(rownum,3) = 0 then sei.column_value else null end) as interpretation
FROM TABLE(p_elem_info) sei
GROUP BY trunc((rownum - 1) / 3,0)
) e
WHERE ( ( e.etype = 2 AND e.interpretation = 2 /* circular arc*/ )
OR ( e.etype in (4,1005,2005) )
OR ( e.etype in (1003,2003) AND e.interpretation IN (2/*circular arc*/,4/*circle*/) )
)
AND rownum < 2;
Return 1;
Exception
When NO_DATA_FOUND Then
Return 0;
End is_Compound;
Since I have decided to handle etype 4,1005 and 2005 elements, I should change the has_compound_curves() function. Since I am changing it I will change the function to have it accept and mdsys.sdo_elem_info_array as does the is_compound
() function.
create or replace
function has_compound_curves ( elem_in in mdsys.sdo_elem_info_array)
return integer
as
etype number;
interpret number;
begin
for i in elem_in.first .. elem_in.last loop
case
when mod(i,3) = 1 then null; -- continue;
when mod(i,3) = 2 then etype := elem_in(i);
when mod(i,3) = 0
then
interpret := elem_in(i);
if( (etype = 2 and interpret = 2)
or
( etype in (4,1005,2005) )
or
(etype in (1003,2003) and interpret in (2,4))
) then
return 1;
end if;
end case;
end loop;
return 0;
end has_compound_curves;
Testing reveals:
set serveroutput on size unlimited;
declare
v_geom mdsys.sdo_geometry := MDSYS.SDO_GEOMETRY(2006,null,null,MDSYS.SDO_ELEM_INFO_ARRAY(1,4,2,1,2,2,5,2,1,13,2,1,17,2,1,21,2,1),MDSYS.SDO_ORDINATE_ARRAY(120,178,115,180,110,178,110,175,120,175,120,178,60,15,65,15,50,15,55,15,10,25,20,30,25,25,30,30));
begin
runstats_pkg.rs_start;
test_compound(v_geom.sdo_elem_info,0);
runstats_pkg.rs_middle;
test_compound(v_geom.sdo_elem_info,1);
runstats_pkg.rs_stop(10);
end;
/
anonymous block completed
Run1 ran in 0 hsecs
Run2 ran in 90 hsecs
Run 1 ran in 0 % of the time
Conclusion
It does not appear to matter how much effort we put in to making the SQL version of is_compound() more efficient, the non-SQL version will beat us every time. The performance difference is enormous with the gap, after having been close a small amount, immediately widened by removing of the need to “dereference” the sdo_elem_info_array (this reminds me of the performance difference I extracted from my VB6 library when I removed as much “dereferencing” of OraObjects in runtime).
Discussion
First off, even when you use SQL in a function, it doesn’t hurt to do some
profiling and improvement of its performance. In this case what I thought
was a pretty simple, fast piece of SQL, turned out to be capable of being
improved.
Secondly, why do I use SQL when I could use pure programmatic PL/SQL? I started out using programmatic SQL (as Pro Oracle Spatial uses) but I generally found:
The thing I liked about using SQL is that it is:
However easier an algorithm is to write in SQL (and not all are), when one has it running correctly (the goal of all programming), if the algorithm is, as is said in project management, “on the critical path”, then it has to be converted to straight PL/SQL (as my commentator has done).
Of course I always meant to test that my SQL approach is NOT orders of magnitude slower than non-SQL approaches. Sadly, my simple testing above shows that that is not the case (well, at least in this case). Obviously the efficiency of one approach versus another depends on how it is used and the sensitivity of the process to the processing time of the function. But since a user has discovered this then, perhaps, now is the time to swap out the SQL algorithm for a much faster PL/SQL based on i.e., his!
Thanks very much to him for bringing my attention to the speed issues of the original isCompound().
I hope others find this article useful.


















Fast is often overrated anyway, I’ll take readability before short execution times (within reason :)) most of the time.
The “has_compound_curves” function will also perform poorly for geometries with huge sdo_elem_arrays and no compound geometries, i.e. it favors sets producing hits and short arrays. (Don’t reorder an already ordered set, rather maintain the order you’ve more or less got.)
Singling out functions like that isn’t always a good idea anyway and can give a false impression. A faster function might simply be shifting the load onto the user. That being said you can almost halve the execution time of the “has_compound_curves” function by having it accept mdsys.sdo_elem_info_array instead of an sdo_geomtry but it also changes how it is used a little.
Thanks for the article, I like pondering like this :)
— Sveinn R. Joelsson Dec 17, 11:32 pm #