Need help for dynamic calculation in oracle sql query - sql

I Have a table tab_1 with below values.
ID Calculation value
1 10
2 10
3 1+2
4 5
5 3-2
6 5+1
Need help writing the query for following logic. I have a table where the records contain either calculation strings or values to be used in calculations. I need to parse the calculation like this:
ID 3 is the sum of ID 1 and 2.
ID 5 is the minus of ID 3 and 2.
ID 6 is the sum of ID 5 and 1.
Then I need to select the records for the referenced IDs and perform the calculations. My
expected output:
ID Calculation value
3 1+2 20
5 3-2 10
6 5+1 20
Thanks -- nani

"Need help writing the query for below logic."
This is not a problem which can be solved in pure SQL, because:
executing the calculation string requires dynamic SQL
you need recursion to look up records and evaluate the results
Here is a recursive function which produces the answers you expect. It has three private procs so that the main body of the function is simple to understand. In pseudo-code:
look up record
if record is value then return it and exit
else explode calculation
recurse 1, 3, 4 for each part of exploded calculation until 2
Apologies for the need to scroll:
create or replace function dyn_calc
(p_id in number)
return number
is
result number;
n1 number;
n2 number;
l_rec t23%rowtype;
l_val number;
type split_calc_r is record (
val1 number
, operator varchar2(1)
, val2 number
);
l_calc_rec split_calc_r;
function get_rec
(p_id in number)
return t23%rowtype
is
rv t23%rowtype;
begin
select *
into rv
from t23
where id = p_id;
return rv;
end get_rec;
procedure split_calc
(p_calc in varchar2
, p_n1 out number
, p_n2 out number
, p_operator out varchar2)
is
begin
p_n1 := regexp_substr(p_calc, '[0-9]+', 1, 1);
p_n2 := regexp_substr(p_calc, '[0-9]+', 1, 2);
p_operator := translate(p_calc, '-+*%01923456789','-+*%'); --regexp_substr(p_calc, '[\-\+\*\%]', 1, 1);
end split_calc;
function exec_calc
(p_n1 in number
, p_n2 in number
, p_operator in varchar2)
return number
is
rv number;
begin
execute immediate
'select :n1 ' || p_operator || ' :n2 from dual'
into rv
using p_n1, p_n2;
return rv;
end exec_calc;
begin
l_rec := get_rec(p_id);
if l_rec.value is not null then
result := l_rec.value;
else
split_calc(l_rec.calculation
, l_calc_rec.val1
, l_calc_rec.val2
, l_calc_rec.operator);
n1 := dyn_calc (l_calc_rec.val1);
n2 := dyn_calc (l_calc_rec.val2);
result := exec_calc(n1, n2, l_calc_rec.operator);
end if;
return result;
end;
/
Run like this:
SQL> select dyn_calc(6) from dual;
DYN_CALC(6)
-----------
20
SQL>
or, to get the output exactly as you require:
select id, calculation, dyn_calc(id) as value
from t23
where calculation is not null;
Notes
There is no exception handling. If the data is invalid the function will just blow up
the split_calc() proc uses translate() to extract the operator rather than regex. This is because regexp_substr(p_calc, '[\-\+\*\%]', 1, 1) mysteriously swallows the -. This appears to be an environment-related bug. Consequently extending this function to process 1+4+2 will be awkward.
Here is a LiveSQL demo.

In SQL:
select 'ID ' +ID+ ' is the ' + case when calculation like '%-%' then ' minus '
when calculation like '%+%' then ' sum ' END +' of
ID'+replace(replace(calculation,'+',' and '),'-',' and ')
from tab_1
where calculation is not null
In Oracle:
select 'ID ' ||ID|| ' is the ' || case when calculation like '%-%' then ' minus '
when calculation like '%+%' then ' sum ' END|| ' of
ID'||replace(replace(calculation,'+',' and '),'-',' and ')
from tab_1
where calculation is not null

Related

Error "PLS-00302: component 'MIN' must be declared" trying to find min/max value

Im getting the error "PLS-00302: component 'MIN' must be declared" when trying to find min/max value for a distance trip. Any thoughts?
create or replace procedure longandshortdist (p_distance in number)
is
cursor longshortcursor is
select source_town, destination_town, distance
from distances
where distance = p_distance;
distance_row longshortcursor%rowtype;
begin
for distance_row in longshortcursor
loop
dbms_output.put_line('source town is: ' || distance_row.source_town || 'destination
town is: ' || distance_row.destination_town || 'shortest trip is: ' ||
distance_row.min(distance) || 'longest trip is: ' || distance_row.max(distance));
end loop;
end;
The error code I'm getting:
12/1 PL/SQL: Statement ignored
12/169 PLS-00302: component 'MIN' must be declared
Errors: check compiler log
Read comments; seem to be very useful. Meanwhile, I tried to figure out what you wanted and this might be "it".
For sample data as
SQL> SELECT *
2 FROM distances
3 ORDER BY distance;
SOURCE_TOW DESTINAT DISTANCE
---------- -------- ----------
Zagreb Karlovac 40
Zadar Split 120
Koprivnica Osijek 200
procedure doesn't accept any parameters (because, you'd fetch only rows whose distance is equal to p_distance (once again, see Connor's comment)) and loops through all rows in a table.
Loop itself displays all towns, but MIN and MAX distance are displayed only once - out of the loop, once you actually calculate their values.
SQL> CREATE OR REPLACE PROCEDURE longandshortdist
2 IS
3 mindist NUMBER := 1E6;
4 maxdist NUMBER := 0;
5 BEGIN
6 FOR cur_r
7 IN (SELECT source_town, destination_town, distance FROM distances)
8 LOOP
9 mindist := LEAST (mindist, cur_r.distance);
10 maxdist := GREATEST (maxdist, cur_r.distance);
11
12 DBMS_OUTPUT.put_line (
13 'source town is: '
14 || cur_r.source_town
15 || ', destination town is: '
16 || cur_r.destination_town
17 || ', distance is: '
18 || cur_r.distance);
19 END LOOP;
20
21 DBMS_OUTPUT.put_line (
22 'shortest trip is: ' || mindist || ', longest trip is: ' || maxdist);
23 END;
24 /
Procedure created.
Testing:
SQL> SET SERVEROUTPUT ON
SQL> EXEC longandshortdist;
source town is: Zagreb, destination town is: Karlovac, distance is: 40
source town is: Zadar, destination town is: Split, distance is: 120
source town is: Koprivnica, destination town is: Osijek, distance is: 200
shortest trip is: 40, longest trip is: 200
PL/SQL procedure successfully completed.
SQL>
That is because the min and max are aggregate functions used within a query and not inside a block.
Example:
SELECT max(a.f) max, min(a.f) min
FROM (
select 1 f from dual
union all
select 2 f from dual
union all
select 3 f from dual) a
Result:
MAX
MIN
3
1
OR
I think you are trying to get the min / max of two values , if so check the links below :
max function greatest
min function least

Oracle SQL Developer Query on recommended password setup

Setting up a random password for user using
select
dbms_random.string('L',2) || dbms_random.string('X',6) || '1!' as deflvrpwd,
'${access_request_cri_acc_cas9}' as ACNTDN
from dual
New requirement
New Hire Details:
Name :John Doe
Region: America
WDID : 876214
WDID Reverse and split
Region in the middle with the letter A replaced with # symbol
Should read if we follow your formula.
= 412#meric#s678
Please suggest attribute are same as mentioned.
Thank You
Here's one option; read comments within code.
SQL> WITH
2 -- sample data
3 test (name, region, wdid)
4 AS
5 (SELECT 'John Doe', 'America', '876214' FROM DUAL),
6 temp
7 AS
8 -- reverse WDID; don't use undocumented REVERSE function
9 -- replace "A" (or "a") with "#" in REGION
10 ( SELECT name,
11 REPLACE (REPLACE (region, 'A', '#'), 'a', '#') new_region,
12 LISTAGG (letter, '') WITHIN GROUP (ORDER BY lvl DESC) new_wdid
13 FROM ( SELECT SUBSTR (wdid, LEVEL, 1) letter,
14 LEVEL lvl,
15 name,
16 region
17 FROM test
18 CONNECT BY LEVEL <= LENGTH (wdid))
19 GROUP BY name, region)
20 -- finally
21 SELECT SUBSTR (new_wdid, 1, 3) || new_region || SUBSTR (new_wdid, 4) AS result
22 FROM temp;
RESULT
--------------------------------------------------------------------------------
412#meric#678
SQL>
I don't know where s in your result comes from (this: 412#meric#s678).
There's a small cost in context switching between SQL and PL/SQL, but this doesn't sound like a high-volume or performance-critical thing, so you might find it cleaner to put the logic in a function:
create or replace function get_password (p_wdid varchar2, p_region varchar2)
return varchar2 as
l_split pls_integer;
l_password varchar2(30);
begin
-- split WDID halfway, but allow for odd lengths
l_split := floor(length(p_wdid)/2);
-- iterate over the WDID in reverse
for i in reverse 1..length(p_wdid) LOOP
-- when we reach the split point, append the modified region
if i = l_split then
l_password := l_password || translate(p_region, 'Aax', '##x');
end if;
-- append each WDID character, in reverse order
l_password := l_password || substr(p_wdid, i, 1);
end loop;
return l_password;
end get_password;
/
The WDID is reversed in a loop, and the modified region is included at the midway point, based on the length of the WDID value.
You can then do:
select get_password('876214', 'America') from dual;
GET_PASSWORD('876214','AMERICA')
--------------------------------
412#meric#678
This also doesn't have the unexplained 's' from the example in your question.
If you can't create a function but are on a recent version of Oracle then you can define an ad hoc function in a CTE:
with
function invert (p_input varchar2) return varchar2 as
l_output varchar2(30);
begin
for i in reverse 1..length(p_input) LOOP
l_output := l_output || substr(p_input, i, 1);
end loop;
return l_output;
end invert;
t (wdid, region) as (
select invert('876214'), translate('America', 'Aax', '##x')
from dual
)
select substr(wdid, 1, floor(length(wdid)/2))
|| region
|| substr(wdid, floor(length(wdid)/2) + 1)
from t;
which gets the same result. (I've called the function invert to avoid confusion with the undocumented reverse function.)
db<>fiddle showing both.

How to put comma separated values to a column in oracle

I have a JSON response and after processing the response my output looks like this :
column_variable := 'col1,col2,col3';
data_clob :=
"2017-10-14,abc,1,
2019-10-13,abc,12,
2019-10-12,abc,,
"
;
as the original response was having escape characters for new line ,data_clob also has been converted accordingly .
How do I convert this comma separated values in oracle table :
My output should look like this :
col1 col2 col3
2017-10-14 abc 1
2019-10-13 abc 12
2019-10-12 abc null
I was looking through similar questions ,but I dont want to use REGEXP_SUBSTR as I dont know the number of columns I will get in the response .
for e.g : column_variable might have 'col1,col2,col3,col4,col5,col6';
I am using oracle 12.1.0.2.0
Please help !
There is very easy way to achieve it using Polymorphic Table Functions (Oracle 18c):
Dynamic CSV to Columns Converter: Polymorphic Table Function Example:
create or replace package csv_pkg as
/* The describe function defines the new columns */
function describe (
tab in out dbms_tf.table_t,
col_names varchar2
) return dbms_tf.describe_t;
/* Fetch_rows sets the values for the new columns */
procedure fetch_rows (col_names varchar2);
end csv_pkg;
and body:
create or replace package body csv_pkg as
function describe(
tab in out dbms_tf.table_t,
col_names varchar2
)
return dbms_tf.describe_t as
new_cols dbms_tf.columns_new_t;
col_id pls_integer := 2;
begin
/* Enable the source colun for reading */
tab.column(1).pass_through := FALSE;
tab.column(1).for_read := TRUE;
new_cols(1) := tab.column(1).description;
/* Extract the column names from the header string,
creating a new column for each
*/
for j in 1 .. ( length(col_names) - length(replace(col_names,',')) ) + 1 loop
new_cols(col_id) := dbms_tf.column_metadata_t(
name=>regexp_substr(col_names, '[^,]+', 1, j),--'c'||j,
type=>dbms_tf.type_varchar2
);
col_id := col_id + 1;
end loop;
return dbms_tf.describe_t( new_columns => new_cols );
end;
procedure fetch_rows (col_names varchar2) as
rowset dbms_tf.row_set_t;
row_count pls_integer;
begin
/* read the input data set */
dbms_tf.get_row_set(rowset, row_count => row_count);
/* Loop through the input rows... */
for i in 1 .. row_count loop
/* ...and the defined columns, extracting the relevant value
start from 2 to skip the input string
*/
for j in 2 .. ( length(col_names) - length(replace(col_names,',')) ) + 2 loop
rowset(j).tab_varchar2(i) :=
regexp_substr(rowset(1).tab_varchar2(i), '[^,]+', 1, j - 1);
end loop;
end loop;
/* Output the new columns and their values */
dbms_tf.put_row_set(rowset);
end;
end csv_pkg;
--function
create or replace function csv_to_columns(
tab table, col_names varchar2
) return table pipelined row polymorphic using csv_pkg;
Then you simply pass:
select *
from csv_to_columns( data_clob, column_variable );
Here's one possible solution for Oracle versions below 18 and maybe 12, not sure... This is not perfect and will create an empty column at the end based on data you provided - extra spaces, commas, etc... This may also create a blank space between the 'SELECT' and the first column in output. All that can be removed later manually or with more coding. I hope this helps, at least in some ways:
SELECT 'SELECT '''||REPLACE(str, chr(10), ''' FROM dual'||chr(10)||'UNION ALL'||chr(10)||'SELECT ''')||''' FROM dual' str
FROM
(
SELECT TRIM(REPLACE(str, ',', ''''||', ''')) str FROM
(
SELECT TRIM(BOTH '"' FROM
'"2017-10-14,abc,1,
2019-10-13,abc,12,
2019-10-12,abc,,"') AS str FROM dual
)
)
/
This will build the select statement that can be cleaned up and executed manually or with dynamic SQL:
SELECT '2017-10-14' col, 'abc' col, '1' col, '' FROM dual
UNION ALL
SELECT '2019-10-13' col, 'abc' col, '12' col, '' FROM dual
UNION ALL
SELECT '2019-10-12' col, 'abc' col, '' col, '' FROM dual
The output of the above select statement:
COL COL_1 COL_2
2017-10-14 abc 1
2019-10-13 abc 12
2019-10-12 abc null

Oracle Function compilation error - Cannot find the reason of a PLS-00103

I am getting the errror:
Line/Col: 24/13
PLS-00103: Encountered the symbol ";" when expecting one of the following: (
When trying to compile the function:
create or replace function xml_sum (innXML XMLType, outXML XMLType) RETURN number
IS
sum NUMBER := 0;
BEGIN
FOR j IN
(SELECT y.feature, rownum
FROM XMLTABLE
('//FeatureVector/feature'
PASSING outXML
COLUMNS
feature NUMBER PATH '.') y)
LOOP
FOR i IN
(SELECT x.feature, rownum rn
FROM XMLTABLE
('//FeatureVector/feature'
PASSING innXML
COLUMNS
feature NUMBER PATH '.') x WHERE rn = j.rownum)
LOOP
sum := i.feature + j.feature;
END LOOP;
END LOOP;
RETURN sum;
END;
/
By the error it seems that I am missing a ";" but I cannot find where it is being missed.
Could someone point it out? It would certainly help!
Thanks in advance!!
You are using a Oracle reserved Key word SUM. Change it something else and your issue is resolved.
Also at this place:
WHERE rn = j.ROWNUM
you are referrring a column aliasing in where clause directly which is not allowed. Either you need a outer query or you can use it directly. You can see how i used it below:
So your code becomes:
CREATE OR REPLACE FUNCTION xml_sum (innXML XMLTYPE, outXML XMLTYPE)
RETURN NUMBER
IS
ToT_SUM NUMBER := 0;
BEGIN
FOR j
IN ( SELECT y.feature, ROWNUM
FROM XMLTABLE ('//FeatureVector/feature'
PASSING outXML
COLUMNS feature NUMBER PATH '.') y)
LOOP
FOR i
IN ( SELECT x.feature, ROWNUM rn
FROM XMLTABLE ('//FeatureVector/feature'
PASSING innXML
COLUMNS feature NUMBER PATH '.') x
WHERE ROWNUM= j.ROWNUM)
LOOP
ToT_SUM := i.feature + j.feature;
END LOOP;
END LOOP;
RETURN ToT_SUM;
END;
/
Your solution will not work as:
SELECT *
FROM any_table
WHERE ROWNUM = 2
will never return any rows.
Apart from that, #XING noted a couple of other issues and you are overwriting the value in the sum variable in each iteration so you are only getting the last value.
You should be able to rewrite the procedure to remove the need for cursor loops:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE FUNCTION xml_sum(
innXML XMLType,
outXML XMLType
) RETURN number
IS
total NUMBER := 0;
BEGIN
SELECT SUM( y.feature + x.feature )
INTO total
FROM XMLTABLE(
'//FeatureVector/feature'
PASSING outXML
COLUMNS rn FOR ORDINALITY,
feature NUMBER PATH '.'
) y
INNER JOIN
XMLTABLE(
'//FeatureVector/feature'
PASSING innXML
COLUMNS rn FOR ORDINALITY,
feature NUMBER PATH '.'
) x
ON ( x.rn = y.rn );
RETURN total;
END;
/
Query 1:
SELECT xml_sum(
XMLTYPE( '<FeatureVector><feature>1</feature><feature>2</feature></FeatureVector>' ),
XMLTYPE( '<FeatureVector><feature>1</feature><feature>2</feature></FeatureVector>' )
) AS total
FROM DUAL
Results:
| TOTAL |
|-------|
| 6 |

How do I expand a string with wildcards in PL/SQL using string functions

I have a column, which stores a 4 character long string with 4 or less wild characters (for eg. ????, ??01', 0??1 etc). For each such string like 0??1 I have to insert into another table values 0001 to 0991; for the string ??01, values will be be 0001 to 9901; for string ???? values will be 0000 to 9999 and so on.
How could I accomplish this using PL/SQL and string functions?
EDIT
The current code is:
declare
v_rule varchar2(50) := '????52132';
v_cc varchar2(50);
v_nat varchar2(50);
v_wild number;
n number;
begin
v_cc := substr(v_rule,1,4);
v_nat := substr(v_rule,5);
dbms_output.put_line (v_cc || ' '|| v_nat);
if instr(v_cc, '????') <> 0 then
v_wild := 4;
end if;
n := power(10,v_wild);
for i in 0 .. n - 1 loop
dbms_output.put_line(substr(lpad(to_char(i),v_wild,'0' ),0,4));
end loop;
end;
/
Would something like the following help?
BEGIN
FOR source_row IN (SELECT rule FROM some_table)
LOOP
INSERT INTO some_other_table (rule_match)
WITH numbers AS (SELECT LPAD(LEVEL - 1, 4, '0') AS num FROM DUAL CONNECT BY LEVEL <= 10000)
SELECT num FROM numbers WHERE num LIKE REPLACE(source_row.rule, '?', '_');
END LOOP;
END;
/
This assumes you have a table called some_table with a column rule, which contains text such as ??01, 0??1 and ????. It inserts into some_other_table all numbers from 0000 to 9999 that match these wild-carded patterns.
The subquery
SELECT LPAD(LEVEL - 1, 4, '0') AS num FROM DUAL CONNECT BY LEVEL <= 10000)
generates all numbers in the range 0000 to 9999. We then filter out from this list of numbers any that match this pattern, using LIKE. Note that _ is the single-character wildcard when using LIKE, not ?.
I set this up with the following data:
CREATE TABLE some_table (rule VARCHAR2(4));
INSERT INTO some_table (rule) VALUES ('??01');
INSERT INTO some_table (rule) VALUES ('0??1');
INSERT INTO some_table (rule) VALUES ('????');
COMMIT;
CREATE TABLE some_other_table (rule_match VARCHAR2(4));
After running the above PL/SQL block, the table some_other_table had 10200 rows in it, all the numbers that matched all three of the patterns given.
Replace * to %, ? to _ and use LIKE clause with resulting values.
To expand on #Oleg Dok's answer, which uses the little known fact that an underscore means the same as % but only for a single character and using PL\SQL I think the following is the simplest way to do it. A good description of how to use connect by is here.
declare
cursor c_min_max( Crule varchar2 ) is
select to_number(min(numb)) as min_n, to_number(max(numb)) as max_n
from ( select '0000' as numb
from dual
union
select lpad(level, 4, '0') as numb
from dual
connect by level <= 9999 )
where to_char(numb) like replace(Crule, '?', '_');
t_mm c_min_max%rowtype;
l_rule varchar2(4) := '?091';
begin
open c_min_max(l_rule);
fetch c_min_max
into t_mm;
close c_min_max;
for i in t_mm.min_n .. t_mm.max_n loop
dbms_output.put_line(lpad(i, 4, '0'));
end loop;
end;
/