Oracle SQL aggregate Geometries (Longitude, Latitude) - oracle

I have this table
I wanted to sort by POS ASC and also aggregate the two columns X, and Y (so that these POINTS become a LINESTRING, EPSG:25832) so that my ID becomes Unique.
I don't have much experience with ORACLE SDO GEOMETRIES but with PostGIS.
Is there an easy way to do it? Since I didn't find any solution which worked correctly.
The output should be:
Any help maybe..? Thanks.

You can aggregate the coordinates into a string and convert it to an SDO_GEOMETRY type:
SELECT id,
SDO_GEOMETRY(
'LINESTRING(' || LISTAGG(x || ' ' || y, ',') WITHIN GROUP (ORDER BY pos) || ')'
) AS line
FROM table_name
GROUP BY id;
However, the string is limited to 4000 characters so it may fail if you have lots of coordinates.
If you just want the string (without converting it to an SDO_GEOMETRY) then remove the call to the SDO_GEOMETRY() constructor and just use LISTAGG on its own.
Alternately, you can generate the SDO_GEOMETRY object directly using UNPIVOT and CAST/COLLECT:
SELECT id,
SDO_GEOMETRY(
2002,
NULL,
NULL,
SDO_ELEM_INFO_ARRAY(1, 2, 1),
CAST(COLLECT(value ORDER BY pos, coord) AS SDO_ORDINATE_ARRAY)
)
FROM table_name t
UNPIVOT (value FOR coord IN (x AS 1, y AS 2))
GROUP BY id
If you want it as a WKT string then:
SELECT id,
SDO_GEOMETRY(
2002,
NULL,
NULL,
SDO_ELEM_INFO_ARRAY(1, 2, 1),
CAST(
COLLECT(value ORDER BY pos, coord)
AS SDO_ORDINATE_ARRAY
)
).get_WKT() AS wkt
FROM table_name t
UNPIVOT (value FOR coord IN (x AS 1, y AS 2))
GROUP BY id
Which, for the sample data, outputs:
ID
WKT
1
LINESTRING (362019.6 5693216.74, 361967.53 5693180.03)
2
LINESTRING (361993.564 5693198.385)
3
LINESTRING (361993.564 5693198.385)
fiddle
In Oracle 11g, if you just want the WKT string then you can create the types and function:
CREATE TYPE coord IS OBJECT(x NUMBER, y NUMBER)
/
CREATE TYPE coord_table IS TABLE OF coord
/
CREATE FUNCTION coords_to_wkt(
i_coords IN coord_table
) RETURN CLOB
IS
v_clob CLOB;
BEGIN
IF i_coords IS NULL OR i_coords.COUNT = 0 THEN
RETURN NULL;
END IF;
v_clob := 'LINESTRING(' || i_coords(1).x || ' ' || i_coords(1).y;
FOR i IN 2 .. i_coords.COUNT LOOP
v_clob := v_clob || ',' || i_coords(i).x || ' ' || i_coords(i).y;
END LOOP;
RETURN v_clob || ')';
END;
/
and use:
SELECT id,
coords_to_wkt( CAST( COLLECT( coord(x, y) ORDER BY pos ) AS coord_table ) ) AS wkt
FROM table_name
GROUP BY id;
fiddle

Related

oracle sql listagg geometries lat longs [duplicate]

I have this table
I wanted to sort by POS ASC and also aggregate the two columns X, and Y (so that these POINTS become a LINESTRING, EPSG:25832) so that my ID becomes Unique.
I don't have much experience with ORACLE SDO GEOMETRIES but with PostGIS.
Is there an easy way to do it? Since I didn't find any solution which worked correctly.
The output should be:
Any help maybe..? Thanks.
You can aggregate the coordinates into a string and convert it to an SDO_GEOMETRY type:
SELECT id,
SDO_GEOMETRY(
'LINESTRING(' || LISTAGG(x || ' ' || y, ',') WITHIN GROUP (ORDER BY pos) || ')'
) AS line
FROM table_name
GROUP BY id;
However, the string is limited to 4000 characters so it may fail if you have lots of coordinates.
If you just want the string (without converting it to an SDO_GEOMETRY) then remove the call to the SDO_GEOMETRY() constructor and just use LISTAGG on its own.
Alternately, you can generate the SDO_GEOMETRY object directly using UNPIVOT and CAST/COLLECT:
SELECT id,
SDO_GEOMETRY(
2002,
NULL,
NULL,
SDO_ELEM_INFO_ARRAY(1, 2, 1),
CAST(COLLECT(value ORDER BY pos, coord) AS SDO_ORDINATE_ARRAY)
)
FROM table_name t
UNPIVOT (value FOR coord IN (x AS 1, y AS 2))
GROUP BY id
If you want it as a WKT string then:
SELECT id,
SDO_GEOMETRY(
2002,
NULL,
NULL,
SDO_ELEM_INFO_ARRAY(1, 2, 1),
CAST(
COLLECT(value ORDER BY pos, coord)
AS SDO_ORDINATE_ARRAY
)
).get_WKT() AS wkt
FROM table_name t
UNPIVOT (value FOR coord IN (x AS 1, y AS 2))
GROUP BY id
Which, for the sample data, outputs:
ID
WKT
1
LINESTRING (362019.6 5693216.74, 361967.53 5693180.03)
2
LINESTRING (361993.564 5693198.385)
3
LINESTRING (361993.564 5693198.385)
fiddle
In Oracle 11g, if you just want the WKT string then you can create the types and function:
CREATE TYPE coord IS OBJECT(x NUMBER, y NUMBER)
/
CREATE TYPE coord_table IS TABLE OF coord
/
CREATE FUNCTION coords_to_wkt(
i_coords IN coord_table
) RETURN CLOB
IS
v_clob CLOB;
BEGIN
IF i_coords IS NULL OR i_coords.COUNT = 0 THEN
RETURN NULL;
END IF;
v_clob := 'LINESTRING(' || i_coords(1).x || ' ' || i_coords(1).y;
FOR i IN 2 .. i_coords.COUNT LOOP
v_clob := v_clob || ',' || i_coords(i).x || ' ' || i_coords(i).y;
END LOOP;
RETURN v_clob || ')';
END;
/
and use:
SELECT id,
coords_to_wkt( CAST( COLLECT( coord(x, y) ORDER BY pos ) AS coord_table ) ) AS wkt
FROM table_name
GROUP BY id;
fiddle

oracle function for: select distinct values count from comma separated string

I need an Oracle (11) function to handle this question.
I need to counting distinct values count from comma separated string.
For example the comma separated string:
'Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text'
The result have to be = 6
Beacuse of
Lorem
Ipsum
is
simply
dummy
text
I want to use like this
select fn_dist_count_values_in_list_arr('Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text') from dual;
Can anyone help to write this ("fn_dist_count_values_in_list_arr") oracle function?
You don't need a context switch from SQL to a PL/SQL function and can do it all in SQL:
SELECT ( SELECT COUNT( DISTINCT CAST(column_value AS VARCHAR2(20)) )
FROM XMLTABLE( ('"'||REPLACE(value,',','","')||'"') ) )
AS num_distinct_values
FROM table_name
Which, for the sample data:
CREATE TABLE table_name ( value ) AS
SELECT 'Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text' FROM DUAL;
Outputs:
| NUM_DISTINCT_VALUES |
| ------------------: |
| 6 |
If you want a pure PL/SQL function (so that you do not have multiple context-switches) then:
CREATE FUNCTION fn_dist_count_values_in_list_arr (
list_value IN VARCHAR2
) RETURN NUMBER DETERMINISTIC
IS
TYPE t_words IS TABLE OF NUMBER(1,0) INDEX BY VARCHAR2(200);
v_words t_words;
v_start PLS_INTEGER := 1;
v_end PLS_INTEGER;
BEGIN
IF list_value IS NULL THEN
RETURN 0;
END IF;
LOOP
v_end := INSTR( list_value, ',', v_start );
EXIT WHEN v_end = 0;
v_words(SUBSTR(list_value, v_start, v_end - v_start ) ) := 1;
v_start := v_end + 1;
END LOOP;
v_words(SUBSTR(list_value,v_start)) := 1;
RETURN v_words.COUNT;
END;
/
and then:
SELECT fn_dist_count_values_in_list_arr( value )
FROM table_name
outputs:
| FN_DIST_COUNT_VALUES_IN_LIST_ARR(VALUE) |
| --------------------------------------: |
| 6 |
db<>fiddle here
CREATE OR REPLACE FUNCTION DIST_COUNT_VALUES_IN_STR_ARR
(STR_ARR IN VARCHAR2)
RETURN NUMBER
AS
DIST_COUNT NUMBER(38);
BEGIN
SELECT COUNT(DISTINCT COL1)
INTO DIST_COUNT FROM (
SELECT REGEXP_SUBSTR(STR_ARR,'[^,]+', 1, LEVEL) COL1
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT(STR_ARR, ',') + 1
);
RETURN DIST_COUNT;
END;
This worked for me. The inner query separates the elements into rows by using regex on the comma character. I had to rename your function as i hit the limit of the max length of an object name for my version of Oracle.
And another approach would be to use hierarchical query to split comma separated values to a set of rows (with clause) and run a simple sql query against it
with str_parsed as (SELECT REGEXP_SUBSTR('Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text', '[^,]+', 1, LEVEL) val
FROM dual
CONNECT BY REGEXP_SUBSTR('Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text', '[^,]+', 1, LEVEL) IS NOT NULL)
select count(distinct val) from str_parsed

Trying to pass cursor value in function and return multiple value in string

I am trying to pass a parameter in a function and use also use cursor value value in that function to return all multiple values concatenated in string
i have a function name 'func_multi_val' whose return type is string inside that i am creating a cursor 'stage_val' .now getting value from these cursor i want to use in function loop wise and concatenating those values which return from function in strings.
CREATE OR REPLACE (cur_date in date)
FUNCTION func_multi_val
RETURN string
is
var_value string;
BEGIN
cursor stage_val is
SELECT AGE_CD,
decode(AGE_CD,'04','am,'05','bm','7u',NULL,AGE_DESC) AGE_DESC ,
AGE_SEQ
FROM PROD_AGE_MST
WHERE AGE_SEQ < 15
AND AGE_CD NOT IN ('6A','05')
ORDER BY 3
FOR i IN stage_val
LOOP
SELECT Round(NVL(SUM(NVL(PROD_WT,0)),0),0)
INTO X
FROM Prod_age_p_s_cur
WHERE PSWF_DATE BETWEEN :PROD_DATE AND :PROD_DATE + 1
AND PSWF_AGE_CD=:AGE_CD;
---how to concatenate value return from function in string?
End Loop
return var_value
Expected: value return from function should be concatenate in a string
Actual:I am tying as shown above no idea about how to achieve it.
i am new to oracle. any idea would be appreciated
If I understood you correctly you should add this line at the end of your loop:
var_value := var_value || var_value;
like so:
CREATE OR REPLACE FUNCTION func_multi_val(cur_date in date) RETURN string is
var_value string;
cursor stage_val is
SELECT AGE_CD,
decode(AGE_CD, '04', 'am', '05', 'bm', '7u', NULL, AGE_DESC) AGE_DESC,
AGE_SEQ
FROM PROD_AGE_MST
WHERE AGE_SEQ < 15
AND AGE_CD NOT IN ('6A', '05')
ORDER BY 3;
BEGIN
FOR i IN stage_val LOOP
SELECT Round(NVL(SUM(NVL(PROD_WT, 0)), 0), 0)
INTO X
FROM Prod_age_p_s_cur
WHERE PSWF_DATE BETWEEN :PROD_DATE AND :PROD_DATE + 1
AND PSWF_AGE_CD = :AGE_CD;
--concatinating return string:
var_value := var_value || var_value;
END LOOP;
return var_value;
END;
Please note that I made some syntax adjustments to make your code executable.
You have multiple issues including:
decode(AGE_CD,'04','am,'05','bm','7u',NULL,AGE_DESC) is missing a closing single quote.
You need a length on the var_value string; declaration.
The CURSOR should be declared before the BEGIN.
There is no :PROD_DATE input or variable and you should not be using a bind variable; did you mean cur_date?
:age_cd should not be a bind variable and probably should reference the cursor value using i.age_cd.
You need a semi-colon on END LOOP;
You need an END; statement to terminate the function.
You have not declared the X variable.
Which would give you something like:
CREATE OR REPLACE FUNCTION func_multi_val (cur_date in date)
RETURN string
IS
var_value string(4000);
x string(40);
CURSOR stage_val is
SELECT AGE_CD
FROM PROD_AGE_MST
WHERE AGE_SEQ < 15
AND AGE_CD NOT IN ('6A', '05')
ORDER BY AGE_SEQ;
BEGIN
FOR i IN stage_val LOOP
SELECT Round(NVL(SUM(NVL(PROD_WT,0)),0),0)
INTO X
FROM Prod_age_p_s_cur
WHERE PSWF_DATE BETWEEN cur_date AND cur_date + 1
AND PSWF_AGE_CD= i.AGE_CD;
var_value := var_value || ',' || x;
END LOOP;
return var_value;
END;
/
However, you do not need to use cursors and can just solve it in a single SQL statement that is not going to need to context-switch between SQL and PL/SQL:
CREATE OR REPLACE FUNCTION func_multi_val (cur_date in date)
RETURN string
IS
var_value string(4000);
BEGIN
SELECT LISTAGG( total, ',' ) WITHIN GROUP ( ORDER BY rn )
INTO var_value
FROM (
SELECT Round(NVL(SUM(NVL(c.PROD_WT,0)),0),0) AS total,
rn
FROM Prod_age_p_s_cur c
INNER JOIN (
SELECT ROW_NUMBER() OVER ( ORDER BY AGE_SEQ ) rn,
AGE_CD
FROM PROD_AGE_MST
WHERE AGE_SEQ < 15
AND AGE_CD NOT IN ('6A','05')
ORDER BY AGE_SEQ
) m
ON ( c.PSWF_AGE_CD= m.AGE_CD )
WHERE c.PSWF_DATE BETWEEN cur_date AND cur_date + 1
GROUP BY rn
);
return var_value;
END;
/
Which for the inputs:
CREATE TABLE prod_age_mst ( age_cd, age_seq, age_desc ) AS
SELECT '04', 1, 'aa' FROM DUAL UNION ALL
SELECT '05', 2, 'bb' FROM DUAL UNION ALL
SELECT '7u', 3, 'cc' FROM DUAL UNION ALL
SELECT '6A', 4, 'dd' FROM DUAL;
CREATE TABLE prod_age_p_s_cur ( prod_wt, pswf_date, pswf_age_cd ) AS
SELECT 1, DATE '2019-07-22', '04' FROM DUAL UNION ALL
SELECT 2, DATE '2019-07-22', '04' FROM DUAL UNION ALL
SELECT 3, DATE '2019-07-22', '05' FROM DUAL UNION ALL
SELECT 4, DATE '2019-07-22', '7u' FROM DUAL;
Then:
SELECT func_multi_val( DATE '2019-07-22' ) FROM DUAL;
Outputs:
| FUNC_MULTI_VAL(DATE'2019-07-22') |
| :------------------------------- |
| 3,4 |
db<>fiddle here

Evaluating dynamic PLSQL statment

The purpose of this proc is to combine columns of a record type into a line of text.
Example: SALARY= 80000, POSITION=ENG, ID=8
How can I evaluate l_stmt dynamically.
Please note I do not want to do it the traditional way by concatenation.
Open to other suggestions.
Thank you
CREATE TABLE Employee
(
ID INTEGER,
POSITION VARCHAR2 (32),
SALARY NUMBER
);
INSERT INTO EMPLOYEE (ID, POSITION, SALARY)
VALUES (1, 'ENG', '100000');
INSERT INTO EMPLOYEE (ID, POSITION, SALARY)
VALUES (2, 'PROGRAMMER', '80000');
COMMIT;
CREATE OR REPLACE PROCEDURE column_to_text (rec employee%ROWTYPE)
AS
TYPE tb IS TABLE OF VARCHAR2 (30)
INDEX BY BINARY_INTEGER;
l_colnames tb;
l_stmt VARCHAR2 (2500);
BEGIN
SELECT column_name
BULK COLLECT INTO l_colnames
FROM sys.all_tab_cols
WHERE table_name = 'EMPLOYEE';
FOR i IN 1 .. l_colnames.COUNT
LOOP
l_stmt :=
l_stmt || l_colnames (i) || '=' || 'REC.' || l_colnames (i) || ', ';
END LOOP;
DBMS_OUTPUT.PUT_LINE ( l_stmt );
--SALARY=REC.SALARY, POSITION=REC.POSITION, ID=REC.ID,
END;

How to find out the number of digits of an oracle number

I have a table in oracle that looks like this:
name | type | nullable
------------------------------------------
person_name | varchar(20) | yes
weight_coeficient | number | yes
...
How can I figure out how many digits a value of weight_coeficient has ? For example:
3.0123456789 has 11 digits (precision = 11) and 10 digits after the decimal (scale = 10)
Is there any sql command/function that does that, something like GetPrecision( select.. ) that returns 11 ?
Note also that the definition of the table does not specify scale and precision. So as far as I know the maximum precision is applied for all the numbers. So I'm not interested in finding out the precision (= 48) of the definition, but the precision of a specific value in the table. Is that possible just using oracle commands ?
Thank you in advance, JP
How about....
SELECT LENGTH(TRANSLATE(TO_CHAR(3.0123456789),'1234567890.-','1234567890'))
FROM dual
The translate simply removes the non numeric characters .-
Slight improvement is to use:
length(to_char(:number)) - coalesce(length(translate(to_char(:number), 'x1234567890', 'x')), 0)
When you to_char inserts an 'E' for exponential or a different grouping character or decimal separator, it will still work.
Use this script to generate a the appropriate casts for your data. I wrote this to help move NUMBER data with unspecified precision & scale in Oracle to Postgres, via Kafka-Connect. Kafka-Connect lets one select data to copy over to another database via query, but since we did not have our number precision set on the Oracle side, Kafka-Connect was inserting everything into Postgres as a big decimal. I.e, an Oracle 1 would be inserted as 1.000000000<30 decimals>.
SET SERVEROUTPUT ON;
DECLARE
Q1 VARCHAR2 (4000 CHAR);
str VARCHAR2 (300 CHAR);
BEGIN
FOR rec
IN (SELECT column_name AS column_name
FROM all_tab_cols
WHERE owner = 'YOUR_SCHEMA'
AND TABLE_NAME = 'YOUR_TABLE'
AND DATA_TYPE = 'NUMBER')
LOOP
q1 :=
'SELECT REPLACE(''cast( ''
|| :1
|| '' as NUMBER(''
|| TO_CHAR (MAX (LENGTH_OF_DECIMAL) + MAX (length_of_integer))
|| '',''
|| TO_CHAR (MAX (length_of_decimal))
|| ''))'',''NUMBER(0,0)'',''NUMBER'') as result
FROM (SELECT charnum,
CASE
WHEN INSTR (charnum, ''.'') > 0
THEN
SUBSTR (charnum, INSTR (charnum, ''.'') + 1)
ELSE
NULL
END
AS decimal_part,
CASE
WHEN INSTR (charnum, ''.'') > 0
THEN
REPLACE (
REPLACE (
SUBSTR (charnum,
1,
INSTR (charnum, ''.'') - 1),
''-'',
''''),
''+'',
'''')
ELSE
REPLACE (REPLACE (charnum, ''-'', ''''), ''+'', '''')
END
AS integer_part,
CASE
WHEN INSTR (charnum, ''.'') > 0
THEN
LENGTH (
SUBSTR (charnum, INSTR (charnum, ''.'') + 1))
ELSE
0
END
AS length_of_decimal,
CASE
WHEN INSTR (charnum, ''.'') > 0
THEN
NVL (
LENGTH (
REPLACE (
REPLACE (
SUBSTR (charnum,
1,
INSTR (charnum, ''.'') - 1),
''-'',
''''),
''+'',
'''')),
0)
ELSE
NVL (
LENGTH (
REPLACE (REPLACE (charnum, ''-'', ''''),
''+'',
'''')),
0)
END
AS length_of_integer
FROM (SELECT cast(col_name2 AS VARCHAR2 (50))
AS charnum
FROM YOUR_TABLE)) T1';
q1 := REPLACE (q1, 'col_name2', rec.column_name);
EXECUTE IMMEDIATE REPLACE (q1, 'col_name2', rec.column_name)
INTO str
USING rec.column_name;
DBMS_OUTPUT.PUT_LINE (str);
END LOOP;
END;
/
Test cases:
create table precision_tester(test_val number);
--change YOUR_TABLE to PRECISION_TESTER
(run script, verify output, and delete from precision_tester after each test)
insert into precision_tester(test_val) values (null);
insert into precision_tester(test_val) values (+1);
insert into precision_tester(test_val) values (-1);
insert into precision_tester(test_val) values (-1.00);
insert into precision_tester(test_val) values (+1.001);
insert into precision_tester(test_val) values (+12.001);
Yields the below dbms output:
cast( TEST_VAL as NUMBER)
cast( TEST_VAL as NUMBER(1,0))
cast( TEST_VAL as NUMBER(1,0))
cast( TEST_VAL as NUMBER(1,0))
cast( TEST_VAL as NUMBER(4,3))
cast( TEST_VAL as NUMBER(5,3))

Resources