Oracle Spatial - select objects falling within area - oracle

this is probably simple to those who know (I hope!)
I have an Oracle spatial database with a geometry column containing a list of node points, in northing/easting format (if it's relevent!)
I need to select those objects which fall within a given radius of a given point.
Northings and Eastings are 1 meter apart which makes it a bit easier.
Ideally this should include objects which cross the area even if their node points fall outside it.
Is this an easy-ish query? Maybe using SDO_WITHIN_DISTANCE?
The table looks like this:
MyTable
ID NUMBER
NAME VARCHAR2(20)
DESC VARCHAR2(50)
GEOM SDO_GEOMETRY
Thanks for any help!

You can do this one of two ways. First, as you mentioned, SDO_WITHIN_DISTANCE is a valid approach.
select
*
from center_point a
inner join target_points b
on a.id = 1
and sdo_within_distance( b.shape, a.shape, 'distance = 10' ) = 'TRUE'
;
In this case, the distance is in linear units defined by a's spatial reference. Oracle treats the coordinates as Cartesian so you will need to make sure you have a linear coordinate system before using this operator (as opposed to angular lat/lon units). Since you are working with northings/eastings, I think you'll be okay as long as the points you are comparing against are in the same spatial reference.
This approach uses an inner-loop to solve the query so not very efficient if you have a lot of points to compare against. Also, Oracle Spatial is VERY picky about the order of operands in the SDO functions so you might need to play around with parameter order to find the sweetspot. If your query runs for a long period, try switching the first and second parameter of your sdo operator. You can also play with the order of the 'from' and 'inner join' tables using the /*+ ORDERED */ hind after SELECT.
Another approach is to buffer the geometry and compare against the buffer.
select
*
from center_point a
inner join target_points b
on a.id = 1
and sdo_relate( b.shape, sdo_buffer(a.shape, 0.05 ), 'mask=anyinteract' ) = 'TRUE'
;
Keep in mind that whatever is in the second parameter of the SDO_RELATE (called the window) will not have a spatial index if you transform it like we are here with the buffer.
If you plan on doing this with several points, it is recommended to build a table where all of the source points are buffered. Then create a spatial index against the buffered areas and compare that to your target points.
For example:
create table point_bufs unrecoverable as
select sdo_buffer (a.shape, b.diminfo, 1.35)
from centerpoint a, user_sdo_geom_metadata b
where table_name='CENTERPOINT'
and column_name='SHAPE';
select
a.gif,
b.gid
from target_points a,
point_bufs b
where sdo_relate(a.shape, b.shape, 'mask=anyinteract querytype=join') = 'TRUE'
;
NOTE: When intersecting points with polygons, you always want to polygon to be in the window position of the sdo_relate (which is the second parameter). This will ensure your spatial index is used correctly.

The proper way is to use SDO_WITHIN_DISTANCE, and that is the case irrespective of the coordinate systems used, i.e. whether they are projected or geodetic:
select b.*
from my_table a, my_table b
where a.id = 1
and sdo_within_distance( b.shape, a.shape, 'distance=10 unit=meter' ) = 'TRUE';
The order of the arguments to the spatial predicates is important: the first one is the points you are searching, the second is the "query window", i.e. the point you are searching for. Notice that you should always specify the unit of your distance - here 10 meters. If you don't then it will default to the unit of the coordinate system of the table you search. For geodetic data, that will always be meters. For projected data it will be the unit of your coordinate system - generally meters too, but not always. Explicitly specifying a unit lifts all ambiguities.
You could also use the buffer approach, but that makes no difference here, and is actually slower. It does not matter that the second argument to a spatial predicate is indexed or not: that index is not used. Only the index on the first argument is required and used.
To perform the operation on a collection of geometries - i.e. for a set of points, find the points within a set distance of each of them, then consider using the SDO_JOIN() function instead, like this to find all couple of points that are within 10 meters of each other:
SELECT a.id, b.id
FROM my_table a,
my_table b,
TABLE(SDO_JOIN(
'MY_TABLE', 'SHAPE',
'MY_TABLE', 'SHAPE',
'DISTANCE=10 UNIT=METER')
) j
WHERE j.rowid1 = a.rowid
AND j.rowid2 = a.rowid
AND a.rowid < a.rowid;

Related

ORA-13249: SDO_NN cannot be evaluated without using index

I get an error when I run the following Sql Script in ORACLE
SELECT D.ID
FROM DOOR D
JOIN STREET S ON (S.ID=D.STREET_ID)
WHERE
SDO_NN(D.LOCATION,SDO_UTIL.FROM_WKTGEOMETRY('POINT (11112.0111 321314.2222)'),'sdo_num_res=6') = 'TRUE'
or
S.ID IN (17);
but when I change 'or' to 'and' or delete 'or S.ID IN (17)' I get no error.
SELECT D.ID
FROM DOOR D
JOIN STREET S ON (S.ID=D.STREET_ID)
WHERE
SDO_NN(D.LOCATION,SDO_UTIL.FROM_WKTGEOMETRY('POINT (11112.0111 321314.2222)'),'sdo_num_res=6') = 'TRUE'
and
S.ID IN (17);
Type of Location field in DOOR Table is MDSYS.SDO_GEOMETRY
and
Type of ID field in STREET Table is NUMBER
I want first SQL to work.
Can anyone help with the solution?
Would hint do any good?
SELECT /*+ LEADING(d) USE_NL(d s) INDEX s spatial_index) */
D.ID
FROM DOOR D JOIN STREET S ON (S.ID = D.STREET_ID)
WHERE SDO_NN (
D.LOCATION,
SDO_UTIL.FROM_WKTGEOMETRY ('POINT (11112.0111 321314.2222)'),
'sdo_num_res=6') =
'TRUE'
AND S.ID IN (17);
The problem here is that the SDO_NN operator (as opposed to the SDO_RELATE or SDO_WITHIN_DISTANCE operators) can only be solved via a spatial index.
With the first query, no actual filter is applied to select from the DOOR table, so the query optimizer naturally uses the spatial index.
With the second query, you restrict the DOOR table to only those rows (doors) for a given street (STREET.ID=17). That makes the optimizer choose the index that applies that filter (probably the index on STREET.STREET_ID.
The or vs and makes for a very different query and a very different result. With or, you are asking for the 6 closest doors to the selected point, from any street + all the doors on street 17 irrespective of proximity.
With and you are asking for the 6 closest doors on street 17 only. Which makes much more sense than the first syntax.
In order to make that work in your case, you need to use a hint to tell the optimizer to use the spatial index instead of the index on DOOR.STREET_ID:
SELECT /*+ INDEX (d <spatial index name>) */ D.ID
FROM DOOR D
JOIN STREET S ON (S.ID=D.STREET_ID)
WHERE SDO_NN(D.LOCATION,SDO_UTIL.FROM_WKTGEOMETRY('POINT (11112.0111 321314.2222)'),'sdo_num_res=6') = 'TRUE'
AND S.ID IN (17);
Notice the syntax of the hint: d refers to the alias that refers to the DOOR table, since this is the one you are doing the spatial filtering on. <spatial index name> is the name of the spatial index you have defined on the DOOR table (not the string "spatial_index".
Hints have a "fragile" syntax: if you make any mistake, the hint will just be ignored. Also they are not orders to the optimizer: they are just suggestions. If the optimizer finds a hint impossible to apply (like you ask for an index that does not exist), it will also ignore it.
One way to confirm that hints have been accepted and used by the optimizer is to look at the query plan it has produced. If you use sqlplus or sqlcl, you can use the EXPLAIN PLAN command that will show you the plan without actually executing the statement. If you use SQLDeveloper, that has specific buttons to show query plans.
EDIT: The INDEX hint might not be sufficient. It may be that the optimizer needs to be told also how to implement the join, as #littlefoot has indicated. So you may also need the LEADING and USE_NL hints. But try first with only the INDEX hint.

SDO_INSIDE returns zero records

So I have a table village:
CREATE TABLE village (
building_id integer PRIMARY KEY,
name VARCHAR2(30),
visitors integer,
building SDO_GEOMETRY
);
And a table visitors:
create table visitors(
id integer,
position SDO_GEOMETRY
);
Here are the inserts:
INSERT INTO village VALUES(2,'KircheV2', 4,
SDO_GEOMETRY(
2003,
NULL,
NULL,
SDO_ELEM_INFO_ARRAY(1,1003,1),
SDO_ORDINATE_ARRAY(100,100, 100,120, 120,100, 120,120)
)
);
INSERT INTO visitors VALUES (1,
SDO_GEOMETRY(
2001,
NULL,
SDO_POINT_TYPE(110, 110, NULL),
NULL,
NULL
)
);
For some reason when I try to get all visitors which are INSIDE the "KircheV2" the SQL statement always returns zero records:
SELECT * FROM visitors,village WHERE village.name like 'KircheV2' and (SDO_INSIDE(village.building,visitors.POSITION) = 'TRUE');
What can be the reason behind it? The coordinates 110;110 should actually be right in the middle of the building, so it should be inside the building.
Your data is incorrect. You can verify it like this:
SQL> select sdo_geom.validate_geometry_with_context (building,0.005) from village;
SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT(BUILDING,0.005)
-------------------------------------------------------------------------------
13348 [Element <1>] [Ring <1>]
1 row selected.
That error means:
ORA-13348: polygon boundary is not closed
In Oracle (actually all storage systems) according to OGC rules, the polygons must close, i.e. the first vertex must repeat as last vertex. So:
INSERT INTO village VALUES(2,'KircheV2', 4,
SDO_GEOMETRY(
2003,
NULL,
NULL,
SDO_ELEM_INFO_ARRAY(1,1003,1),
SDO_ORDINATE_ARRAY(100,100, 100,120, 120,100, 120,120, 100,100)
)
);
But the select still fails to return any result. Why is that ?
SQL> select sdo_geom.validate_geometry_with_context (building,0.005) from village;
SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT(BUILDING,0.005)
-------------------------------------------------------------------------------
13349 [Element <1>] [Ring <1>][Edge <2>][Edge <4>]
1 row selected.
This error means:
ORA-13349: polygon boundary crosses itself
That makes sense: the vertices clearly form a butterfly shape:
100,100, 100,120, 120,100, 120,120, 100,100
Assuming you want to form a simple rectangle, then the proper shape is:
100,100, 100,120, 120,120, 120,100, 100,100
INSERT INTO village VALUES(2,'KircheV2', 4,
SDO_GEOMETRY(
2003,
NULL,
NULL,
SDO_ELEM_INFO_ARRAY(1,1003,1),
SDO_ORDINATE_ARRAY(100,100, 100,120, 120,120, 120,100, 100,100)
)
);
Still no result. Why ?
SQL> select sdo_geom.validate_geometry_with_context (building,0.005) from village;
SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT(BUILDING,0.005)
-------------------------------------------------------------------------------
13367 [Element <1>] [Ring <1>]
1 row selected.
This means:
ORA-13367: wrong orientation for interior/exterior rings
Rings in polygons must be properly orientated. Outer rings must be counter-clockwise, inner rings (holes) must be clockwise. So you need to write it like this:
100,100, 120,100, 120,120, 100,120, 100,100
INSERT INTO village VALUES(2,'KircheV2', 4,
SDO_GEOMETRY(
2003,
NULL,
NULL,
SDO_ELEM_INFO_ARRAY(1,1003,1),
SDO_ORDINATE_ARRAY(100,100, 120,100, 120,120, 100,120, 100,100)
)
);
Still no results! But that is because your query is incorrectly formulated. SDO_INSIDE(a,b) finds all occurrences of A that are fully inside B. In your case, it is like asking for buildings that are inside a visitor. Clearly you want the reverse, so either say: SDO_INSIDE(visitor, building) or SDO_CONTAINS (building,visitor), like this:
SELECT * FROM visitors,village WHERE village.name like 'KircheV2' and SDO_INSIDE(visitors.POSITION,village.building) = 'TRUE';
SELECT * FROM visitors,village WHERE village.name like 'KircheV2' and SDO_CONTAINS(village.building,visitors.POSITION) = 'TRUE';
Some additional comments:
Your example is purely artificial, I imagine ? In real life, your polygons will come from some GIS systems - like ESRI shapefiles that you load into the database, or shapes captured from some GIS tool. Either way, both will produce correct shapes since all tools apply then OGC rules for shape closure and orientation. If not the validation function will tell you the errors, and the sdo_util.rectify_geometry function will correct the fundamental errors.
You also need to understand the spatial operators (INSIDE vs CONTAINS etc) and their effects. The documentation explains what they mean. Note that the set of SDO_xxx operators is slightly different from the ST_xxx functions defined by the OGC.
You do not specify any coordinate system (SDO_SRID is NULL). While that works in your artificial example, in real life you should always use the proper coordinate system. In particular if your shapes are geodetic (in long/lat), you must say so by using the proper SRID: 4326. This guarantees that all computations are done within the context of the ellipsoidal shape of the earth. That is especially important if you want to perform distance-based queries or measure lengths, distances or areas. Using the proper SRID for projected data is equally important. It lets you perform queries irrespective of the coordinate systems used: for example find which parcel (in a local projection) a GPS point is located.
Performance and indexing. Make sure you have spatial indexes in place. While Oracle 12.2 lets you do queries without defining an index, prior releases always require one (and fail if none exists). And performing a query on a table without a spatial index can be really slow if that table is even moderately large. For example, consider your villages and visitors example. Say you want to find out all visitors in one specific building, from a table of 10 million visitors. Without any index on the visitors table, the database will need to compare each and every visitor with the selected building. That will be expensive in CPU (I/Os are not a real issue).
Finally, it is important to write the queries properly. In an operator like F(a,b), b is used to search a so b should be the smaller set. For example finding all visitors in this building must be written as SDO_INSIDE(visitors, buildings). The reverse (in which building is this customer ?) write as SDO_CONTAINS(buildings, visitors).

Oracle Spatial - SDO_BUFFER does not work?

I have a table which has SDO_Geometries and I query all the geometries to find their start and end point, then I insert these points to another table called ORAHAN. Now my main purpose is for each point in orahan I must find if it is intersects with another point in orahan when giving 2 cm buffer to points.
So I write some pl sql using Relate and Bufer functions but when I check some records in Map Info, I saw there is points within 1 cm area from itself but no record in intersections table called ORAHANCROSSES.
Am I use these functions wrongly or what?
Note: I am using Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production and
PL/SQL Release 11.2.0.1.0 - and SDO_PACKAGE
ORAHAN has approximately 400 thousands records.(points and other columns.)
declare
BEGIN
for curs in (select * from ORAHAN t) loop
for curs2 in (select *
from ORAHAN t2
where SDO_RELATE(t2.geoloc,SDO_GEOM.SDO_BUFFER(curs.geoloc,0.02,0.5) ,
'mask=ANYINTERACT') = 'TRUE'
and t2.mi_prinx <> curs.mi_prinx) loop
Insert INTO ORAHANCROSSES
values
(curs.Mip, curs.Startmi, curs2.Mip, curs2.Startmi);
commit;
end loop;
end loop;
END;
And this is MapInfo map image that shows 3 points which are close to each other aproximately 1 centimeter. But in the orahancrosses there is no record matching these 3.
Note: 0,00001000km equals 1cm
Orahan Metadata:
select * from user_sdo_geom_metadata where table_name = 'ORAHAN';
And diminfo:
What is the coordinate system of your data ? And, most important, what tolerance have you set in your metadata ?
Some other comments:
1) Don't use a relate with buffer approach. Just use a within-distance approach.
2) You don't need a PL/SQL loop for that sort of query just use a simple CTAS:
create table orahancrosses as
select c1.mip mip_1, c1.startmi startmi_1, c2.mip mip_2, c2.startmi startmi_2
from orahan c1, orahan c2
where sdo_within_distance (c2.geoloc, c1.geoloc, 'distance=2 unit=cm') = 'TRUE'
and c2.mi_prinx <> c1.mi_prinx;
3) As written, couples of points A and B that are within 2 cm will be returned twice: once as (A,B) and once again as (B,A). To avoid that (and only return one of the cases), then write the query like this:
create table orahancrosses as
select c1.mip mip_1, c1.startmi startmi_1, c2.mip mip_2, c2.startmi startmi_2
from orahan c1, orahan c2
where sdo_within_distance (c2.geoloc, c1.geoloc, 'distance=2 unit=cm') = 'TRUE'
and c1.rowid < c2.rowid;
3) Processing the number of points you mention (400000+) should run better using the SDO_JOIN technique, like this:
create table orahancrosses as
select c1.mip mip_1, c1.startmi startmi_1, c2.mip mip_2, c2.startmi startmi_2
from table (
sdo_join (
'ORAHAN','GEOLOC',
'ORAHAN','GEOLOC',
'DISTANCE=2 UNIT=CM'
)
) j,
orahan c1,
orahan c2
where j.rowid1 < j.rowid2
and c1.rowid = j.rowid1
and c2.rowid = j.rowid2;
This will probably still take time to process - depending on the capacity of your database server. If you are licences for Oracle Enterprise Edition and your hardware has the proper capacity (# of cores) then parallelism can reduce the elapsed time.
4) You say you are using Oracle 11g. What exact version ? Version 11.2.0.4 is the terminal release for 11gR2. Anything older is no longer supported. By now you should really be on 12cR1 (12.1.0.2). The major benefit of 12.1.0.2 in your case s the Vector Performance Accelerator feature that speeds up a number of spatial functions and operators (only if you own the proper Oracle Spatial licenses - it is not available with the free Oracle Locator feature).
======================================
Using the two points in your example. Let's compute the distance:
select sdo_geom.sdo_distance(
sdo_geometry (2001,null,sdo_point_type(521554.782174622,4230983.08336913,null),null,null),
sdo_geometry (2001,null,sdo_point_type(521554.782174622,4230983.07336716,null),null,null),
0.005
) distance
from dual;
DISTANCE
----------
.01000197
1 row selected.
Notice I don't specify any SRID. Assuming the coordinates are expressed in meters, the distance between them is indeed a little more than 1 cm.
======================================
The reason why your original syntax does not work is, as you noticed, because of the tolerance you specify for the SDO_BUFFER() call. You pass it as 0.5 (=50cm) to produce a buffer with a radius of 0.02 (2cm). The effect is that the buffer produced effectively dissolves into the point itself.
For example at tolerance 0.5:
select sdo_geom.sdo_buffer(sdo_geometry (2001,null,sdo_point_type(521554.782174622,4230983.08336913,null),null,null),0.02,0.5) from dual;
Produces:
SDO_GEOMETRY(2001, NULL, SDO_POINT_TYPE(521554.782, 4230983.08, NULL), NULL, NULL)
At tolerance 0.005:
select sdo_geom.sdo_buffer(sdo_geometry (2001,null,sdo_point_type(521554.782174622,4230983.08336913,null),null,null),0.02,0.005) from dual;
You get the proper buffer:
SDO_GEOMETRY(2003, NULL, NULL, SDO_ELEM_INFO_ARRAY(1, 1003, 2), SDO_ORDINATE_ARRAY(521554.782, 4230983.06, 521554.802, 4230983.08, 521554.782, 4230983.1, 521554.762, 4230983.08, 521554.782, 4230983.06))
And the very close point now matches with that buffer:
select sdo_geom.relate(
sdo_geom.sdo_buffer(sdo_geometry (2001,null,sdo_point_type(521554.782174622,4230983.08336913,null),null,null),0.02,0.005),
'determine',
sdo_geometry (2001,null,sdo_point_type(521554.782174622,4230983.07336716,null),null,null),
0.005
) relation
from dual;
RELATION
-------------------------
CONTAINS
1 row selected.
======================================
Now the fact that your data does not have a proper explicit SRID means that the use of explicit units in measurements or distance-based searches will not work. Because the database does not know what coordinate system your data is in, it does not know how to determine that two points are less than a set number of cm or m apart. All you can do is assume the coordinates are in meters.
So in the examples I give above, replace 'DISTANCE=2 UNIT=CM' with 'DISTANCE=0.02'

Oracle Spatial : Find the neighboring buildings

I'm creating an application which finds the neighboring buildings/ hydrants of a building on fire. I've created the tables:
CREATE TABLE building (
buildingno VARCHAR(40) PRIMARY KEY
, buildingname VARCHAR2(32),noofvertices INT
, shape MDSYS.SDO_GEOMETRY)
and
CREATE TABLE hydrant (hydrantno VARCHAR(40) PRIMARY KEY
, point MDSYS.SDO_GEOMETRY)
and
CREATE TABLE firebuilding(hydrantno VARCHAR(40) PRIMARY KEY)
I want to find the nearest neighbors of a particular building (both hydrants and buildings). Can I do this without creating a spatial index on the column name?
I am learning spatial querying and the dataset I'm working on is small (about 20 entries in each table and won't grow).
Do you have a good reason not to create a spatial index?
If you do, and if the number of shapes is small, you might get acceptable results and performance with a "brute force" approach that uses SDO_GEOM.SDO_DISTANCE to calculate the distance between the given point and each of the other points points and then picks the smallest distance. For example, if firebuilding identifies the given building, the following query identifies the closest building(s) using a tolerance of 1 metre (if the coordinates are geodetic) or 1 coordinate unit (if the coordinates are non-geodetic):
SELECT
B.*
FROM
(
SELECT
A.*,
DENSE_RANK () OVER (ORDER BY A.DISTANCE) AS RANKING
FROM
(
SELECT
OTHER_BUILDINGS.*,
SDO_GEOM.SDO_DISTANCE(BUILDING.SHAPE, OTHER_BUILDINGS.SHAPE, 1) DISTANCE
FROM
FIREBUILDING,
BUILDING,
BUILDING OTHER_BUILDINGS
WHERE
BUILDING.BUILDINGNO = FIREBUILDING.BUILDINGNO
AND
OTHER_BUILDINGS.BUILDINGNO <> BUILDING.BUILDINGNO
) A
) B
WHERE
B.RANKING = 1;

Using spatial locator for Oracle

I am able to run the below query but when i swap the parameter for sdo_nn, I get an error of SDO_NN cannot be evaluated without using index
Works:
SELECT
c.customer_id,
c.first_name,
c.last_name,
sdo_nn_distance (1) distance
FROM stores s,
customers c
AND sdo_nn
(c.cust_geo_location, s.store_geo_location, 'sdo_num_res=1', 1)= 'TRUE'
ORDER BY distance;
Does not work:
SELECT
c.customer_id,
c.first_name,
c.last_name,
sdo_nn_distance (1) distance
FROM stores s,
customers c
AND sdo_nn
(s.store_geo_location,c.cust_geo_location, 'sdo_num_res=1', 1)= 'TRUE'
ORDER BY distance;
Anyone can explain to me why the sequence matter?
From Oracle's online docs sdo_nn needs the first parameter to be spatially indexed. the second parameter does not have that necessity/constraint.
So when swapping parameters you need to make sure the "now first" parameter (i.e. s.store_geo_location) is spatially indexed. See this on how to create a spatial index in Oracle.
Add the compiler hint to tell the query parser what index to use e.g.:
select
/*+ index(tableName,sdoIndexName) */
...
WARNING Compiler hints fail SILENTLY

Resources