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.


















There is a Lambert Conformal Conic projection used by Computer Aided Dispatch (CAD) in Victoria, Australia that is called BEST VicGrid.
It is not available as part of the standard Oracle install (most Australian projections in are provided to Oracle by Michael Stoke’s Navigate company in Sydney), and so has to be created. Here is how to create and test it.
I have given the new projection the SRID 82476.
DELETE FROM SDO_COORD_REF_SYSTEM
WHERE srid = 82476;
COMMIT;
Now let’s conduct some tests. For this I will compare Oracle against the only GIS I own, Manifold GIS.
Prompt Conduct tests....
-- What projections are used in the test?
I hope this article is of use to the odd Victorian Oracle user!


















As a part of my last article on tracking down and correcting ORA-13011 errors when using SDO_GEOM.VALIDATE_LAYER_WITH_CONTEXT() , I showed how to update the diminfo structure associated with a table’s geometry column in its USER_SDO_GEOM_METADATA entry.
Here was the code in that article.
UPDATE user_sdo_geom_metadata
SET diminfo = ( SELECT MDSYS.SDO_DIM_ARRAY(
MDSYS.SDO_DIM_ELEMENT('X', minx, maxx, 0.05),
MDSYS.SDO_DIM_ELEMENT('Y', miny, maxy, 0.05)) as diminfo
FROM ( SELECT TRUNC( MIN( v.x ) - 1,0) as minx,
ROUND( MAX( v.x ) + 1,0) as maxx,
TRUNC( MIN( v.y ) - 1,0) as miny,
ROUND( MAX( v.y ) + 1,0) as maxy
FROM (SELECT SDO_AGGR_MBR(a.geometry) as mbr
FROM rectangles a) b,
TABLE(mdsys.sdo_util.getvertices(b.mbr)) v
)
)
WHERE table_name = 'RECTANGLES'
AND column_name = 'GEOMETRY';
Now, that code is pretty neat, but it has in it a few things that need changing every time you want to use it for another table’s geometry column, or you want to change the MBR shrinkage/expansion factor (ie – 1/+ 1), or you can’t remember the sdo_tolerance values (in the above, 0.05).
What we need to do is encapsulate this in a PL/SQL Procedure that you can call anytime you need to. Here is one such implementation that is, I believe, generic enough to do all that is required.
/**
function UpdateSdoMetadata
* description Updates 2D spatial extent of DIMINFO associated with table/column in all_sdo_geom_metadata
* param p_table_name The object containing the spatal data.
* param p_column_name The sdo_geometry column to be analyzed.
* param p_mbr_factor Expansion/Shrinkage amount for MBR of current data.
* param p_commit Whether to commit the update.
*/
Create Or Replace
Procedure UpdateSdoMetadata( p_table_name in varchar2,
p_column_name in varchar2,
p_mbr_factor in number,
p_commit in boolean := false )
As
v_mbr_factor number := case when p_mbr_factor is null then 0 else p_mbr_factor end;
v_diminfo mdsys.sdo_dim_array;
Begin
-- Check if something to process
If ( p_table_name is null or p_column_name is null ) Then
Return;
End If;
I’ve placed this in the TOOLS package in my free PL/SQL packages.
I hope the code is useful to someone.


















I was sent a request for help the other day from some I know overseas:
Do you know the true meaning and impact of ‘ORA-13011 value is out of range’? I have a dataset with a lot of very (almost impossible) precise data coming from Bentley microstation. And we have a ‘businessrule’ that the default tolerance in all the tables is 0.005. Any suggestion how to react on a ORA-13011 error when using sdo_geom.validate_layer_with_context
What was not said till a later email was that (my italics highlight what I suspected):
We use FME to transform … DGN-files to Oracle Spatial. An existing table is truncated and refilled with geometries. Because the tables already exist, they are already registered in de user_metadata and FME is not updating it.
Answer
Let’s construct an example that you can all try that shows this problem.
First, let’s create a 10,000 rectangular polygons inside this area.
+------------------------+ 608222 5497524
| |
| |
| |
| |
| |
| |
294829 5162028 +------------------------+
DROP TABLE rectangles;
DROP TABLE rectangles succeeded.
Let’s summary what was inserted into the table…
SELECT count(*) || ' rectangles were stored' As Result
FROM rectangles;
Now, let’s create an Oracle Metadata entry that is DELIBERATELY too small.
DELETE FROM user_sdo_geom_metadata WHERE table_name = 'RECTANGLES';
1 rows deleted
Now, let’s run SDO_GEOM.VALIDATE_LAYER_WITH_CONTEXT;
DROP TABLE rectangles_v;
DROP TABLE rectangles_v succeeded.
This indicates that of the 10,000 rectangles processed all are invalid (see following) as “The result table contains one row for each invalid geometry”.
As a check, let’s run SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT() on a 10% sample of all the rectangles in the table:
SELECT DISTINCT SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT(a.geometry,b.diminfo)
FROM RECTANGLES SAMPLE (10) a,
user_sdo_geom_metadata b
WHERE b.table_name = 'RECTANGLES'
AND b.column_name = 'GEOMETRY';
Notice how, in both cases, we get the Oracle error 13011 which is:
ORA-13011: value is out of range
Cause: A specified dimension value is outside the range defined for that dimension.
Action: Make sure that all values to be encoded are within the defined dimension range.
Now, let’s change the user_sdo_geom_metadata to be correct by calculating it from the actual data….
UPDATE user_sdo_geom_metadata
SET diminfo = ( SELECT MDSYS.SDO_DIM_ARRAY(
MDSYS.SDO_DIM_ELEMENT('X', minx, maxx, 0.05),
MDSYS.SDO_DIM_ELEMENT('Y', miny, maxy, 0.05)) as diminfo
FROM ( SELECT TRUNC( MIN( v.x ) - 1,0) as minx,
ROUND( MAX( v.x ) + 1,0) as maxx,
TRUNC( MIN( v.y ) - 1,0) as miny,
ROUND( MAX( v.y ) + 1,0) as maxy
FROM (SELECT SDO_AGGR_MBR(a.geometry) as mbr
FROM rectangles a) b,
TABLE(mdsys.sdo_util.getvertices(b.mbr)) v
)
)
WHERE table_name = 'RECTANGLES'
AND column_name = 'GEOMETRY';
Now, let’s re-run the validate_layer_with_context….
DROP TABLE rectangles_v;
DROP TABLE rectangles_v succeeded.
SDO_GEOM.VALIDATE_LEYER_WITH_CONTEXT does not write a row if a geometry is valid. There are no rows in the table other than the row that is written indicating how many rows have been processed.
This can be checked by running SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT() against the whole layer in an ordinary SQL statement:
SELECT DISTINCT SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT(a.geometry,b.diminfo)
FROM RECTANGLES a,
user_sdo_geom_metadata b
WHERE b.table_name = 'RECTANGLES'
AND b.column_name = 'GEOMETRY';
I hope this is of help to someone.

















