How to evaluate an expression in one table field into another? - oracle

I have a table, inside there is a field called x, the x field contain value of '1+2', '1+3' etc, how to get these value and calculate it and save into another field?

For simple arithmetic expressions - and depending on your Oracle version - you could use xmlquery to evaluate. Note that / has special meaning in xml, the operator for division is the keyword div - so you need a replace in case you may have forward slashes in the arithmetic expression. (If you don't have any divisions, you can simplify the query by removing the call to replace.)
Here is an example - including the test data at the top, in a with clause (not part of the solution!)
with
test_data (str) as (
select '1 + 3' from dual union all
select '3 * 5 - 2' from dual union all
select '2/4*6' from dual union all
select '3 * (1 - 3)' from dual
)
select str, xmlquery(replace(str, '/', ' div ') returning content).getNumberVal()
as evaluated_expression
from test_data;
STR EVALUATED_EXPRESSION
----------- --------------------
1 + 3 4
3 * 5 - 2 13
2/4*6 3
3 * (1 - 3) -6

If you only have valid PL/SQL arithmetic expressions in your formulas, then you can use EXECUTE IMMEDIATE to evaluate them.
For this, you'll need to create a function:
create or replace function eval_expression(p_expression in varchar2)
return number is
query varchar2(100);
result number;
begin
query := 'select ' || p_expression || ' from dual';
execute immediate query
into result;
return result;
end eval_expression;
Then you can use this function in UPDATE query:
update t
--val is another field
set t.val = eval_expression(t.x)
Naturally, with EXECUTE IMMEDIATE this query won't be extremely efficient, but it'll work. Also, with dynamic queries we're going into unsafe territory, so make sure that you don't have malicious code among your formulas.
Also, see "Evaluate Expression" on Ask TOM. Tom Kyte used a slightly more civilized approach (dbms_sql package) and created a package with a single variable support.

If the case you have mentioned needs to be considered then I will suggest you use the following query:
select
xmlquery('3+4'
returning content
).getNumberVal()
from
dual;
If More operators are involved except division then the aforementioned query will work but if the division operator is also involved then you must have to replace "/" with " div " keyword. Something like the following:
select
xmlquery(
replace( '20/5', '/', ' div ')
returning content
).getNumberVal()
from
dual;
Now, you can use it in your update statement or anywhere else.
update tab
set tab.result_column_name = xmlquery(t.your_expr_column_name
returning content
).getNumberVal()
update tab
set tab.result_column_name = xmlquery(
replace( t.your_expr_column_name, '/', ' div ')
returning content
).getNumberVal()
Demo
Cheers!!

Related

Oracle SQL Selecting weekly music rank procedure

I'm trying to make procedure to select weekly music rank based on hits and likes.
create or replace procedure select_rank(
v_title IN music.title%TYPE,
v_release_date IN music.release_date%TYPE,
v_hit IN music.hit%TYPE
) is
v_cnt number := 0;
BEGIN
select music.title, music.release_date, count(melon_user.user_idx) as likes, music.hit
into v_rank, v_title, v_cnt, v_release_date, v_hit
from melon_user, user_like_music, music
where melon_user.user_idx = user_like_music.user_idx
and user_like_music.music_idx = music.music_idx
group by music.title, music.hit, music.release_date
order by count(melon_user.user_idx) desc, music.hit desc;
DBMS_OUTPUT.PUT_LINE('v_title: ' || v_title);
DBMS_OUTPUT.PUT_LINE('v_cnt: ' || v_cnt);
DBMS_OUTPUT.PUT_LINE('v_release_date: ' || v_release_date);
DBMS_OUTPUT.PUT_LINE('v_hit: ' || v_hit);
END select_rank;
/
But I keep getting error says
PLS-00403: expression 'V_RELEASE_DATE' cannot be used as an
INTO-target of a SELECT/FETCH statement
PLS-00403: expression 'V_RELEASE_DATE' cannot be used as an
INTO-target of a SELECT/FETCH statement
PLS-00403: expression 'V_RELEASE_DATE' cannot be used as an
INTO-target of a SELECT/FETCH statement
how do I fix this?
In the procedure's signature you have:
v_release_date IN music.release_date%TYPE,
v_release_date is an IN parameter and will be read only. If you want to assign values to it wthin the procedure and return them then make it an OUT (or IN OUT) parameter.
However, you have also:
Not declared the v_rank variable.
Your query appears likely to return multiple rows - SELECT .. INTO .. will only work for queries that return a single row. If this is the case then you either want to use BULK COLLECT INTO or return a cursor.

PL/SQL Stored proc that uses a comma separated parameter to drive a dynamic LIKE clause?

Is there a fairly simple way to take an input parameter containing a comma seperated list of prefixes and return a cursor based on a select statement that uses these?
i.e. (Pseudocode)
PROCEDURE get_by_prefix(p_list_of_prefixes IN varchar2, r_csr OUT SYS_REFCURSOR)
IS
BEGIN
OPEN r_csr FOR
SELECT * FROM my_table where some_column LIKE (the_individual_fields_from p_list_of_prefixes ||'%')
END
I've tried various combinations, and now have two problems - coercing the input into a suitable table (I think it needs to go into a table type rather than a VARCHAR2_TABLE), and secondly getting the like clause to be effectively a SELECT from an internal 'pseudotable'...
EDIT: It seems that people are suggesting ways to use 'IN' with a set of potential values - whereas Im looking at using LIKE. I could use a similar technique - building up dynamic SQL, but was wondering if there isnt a more elegant way...
PL/SQL has no concept of a comma-separated list and no built-in splitter as in Perl etc, so you'll have to use one of the hand-rolled methods such as this one:
https://stewashton.wordpress.com/2016/08/01/splitting-strings-surprise
(Other methods are available.) Then it's just a matter of either populating a collection in one step and using it in the next, or else combining the two as something like this:
declare
p_list_of_prefixes varchar2(100) := 'John,Jim,Jules,Janice,Jenny';
begin
open :refcur for
with params as
( select x.firstname
from xmltable(
'ora:tokenize($X, "\,")'
passing p_list_of_prefixes as x
columns firstname varchar2(4000) path '.'
) x
)
, people as
( select 'Dave Clark' as fullname from dual union all
select 'Jim Potter' from dual union all
select 'Jenny Jones' from dual
)
select x.firstname, p.fullname
from params x
left join people p on p.fullname like x.firstname || '%';
end;
Output:
FIRSTNAME FULLNAME
-------------- -----------
John
Jim Jim Potter
Jules
Janice
Jenny Jenny Jones
Using LIKE the way you want is easy, but it is the wrong solution. (See my Comment under the original post).
Anyway - if by order of your superiors, or some other semi-legitimate reason, you must use a LIKE condition, it should look something like this:
... where ',' || p_list_of_whatever || ',' like '%,' || some_column || ',%
Concatenating commas at both ends of both sides of the comparison is needed, because you don't want Jo in the column to match John in the input list. Start from there and you will see why you need the commas on the right-hand side, and then follow from there and you will see why you need them on the left also.

Sum Listagg in a query

I am trying to sum the values from the listagg query i made.
Expected result should be like this
SELECT LEGAL_ENTITY_ID, SUM(0 + 0 + 0 + 1) as Grandtotals
FROM V_VBA_DDCR_MAIN WHERE LEGAL_ENTITY_ID=6012346 AND ROWNUM=1
GROUP BY LEGAL_ENTITY_ID
| LEGAL_ENTITY_ID | GRANDTOTALS
1| 6012346 | 1
My listagg goes like this
SELECT LISTAGG(NVL2(FLDNM,0,1) , '+ ') WITHIN GROUP (ORDER BY FLDNM) FROM LE_MERGE_DDC_MAPPING
the purpose of this query is to count the number of null fields in a row. I created a table containing the list of fields that needs to be checked for null values.
Result :
fld1 + fld2 + fld3 + fld4
=
0 + 0 + 0 + 1
So I wrote my query like this:
SELECT LEGAL_ENTITY_ID, SUM(SELECT LISTAGG(NVL2(FLDNM,0,1) , '+ ') WITHIN GROUP
(ORDER BY FLDNM) FROM LE_MERGE_DDC_MAPPING) as Grandtotals
FROM V_VBA_DDCR_MAIN WHERE LEGAL_ENTITY_ID=6000132
GROUP BY LEGAL_ENTITY_ID
However, I am getting an error ORA-00936: missing expression.
I am not sure if Oracle allows you to do a sum of the listagg function.
Any help is appreciated.
Your SELECT LISTAGG(NVL2(FLDNM,0,1) , '+ ')... query will always get string result '0+ 0+ 0+ ...', unless you have rows in LE_MERGE_DDC_MAPPING which are null. That value is nothing to do with the V_VBA_DDCR_MAIN table. You could generate a string that contains the expressions:
SELECT LISTAGG('NVL2(' || FLDNM || ' ,0,1)' , '+ ') ...
But that would just leave you with a string containing 'NVL2(fld1 ,0,1)+ NVL2(fld2 ,0,1)+ ...'. You can't sum a string, and sum() can't evaluate a string as an expression. You would need to generate a dynamic SQL statement based on that string.
There is a way to do that with XML, which isn't entirely intuitive. You can use the dbms_xmlgen package to create an XML document (as a CLOB) which contains a node with the NVL2 result from your actual target table:
select dbms_xmlgen.getxml('select nvl2(' || fldnm || ',0,1) from v_vba_ddcr_main where legal_entity_id=6012346')
from le_merge_ddc_mapping map;
I won't show the generated XML here. You can then query that to get the individual values out:
select xmlquery('/ROWSET/ROW/*/text()'
passing xmltype(
dbms_xmlgen.getxml('select nvl2(' || fldnm || ',0,1) from v_vba_ddcr_main where Legal_Entity_Id=6012346')
)
returning content)
from le_merge_ddc_mapping;
And you can them sum those:
select sum(to_number(xmlquery('/ROWSET/ROW/*/text()'
passing xmltype(
dbms_xmlgen.getxml('select nvl2(' || fldnm || ',0,1) from v_vba_ddcr_main where legal_entity_id=6012346')
)
returning content))) as grandtotals
from le_merge_ddc_mapping;
You've suggested there is a link between the tables, to determine which fields are included, so you'd need to add that. And you've said there are duplicates, which you'd need to handle - preferably by eliminating them from your view, but with a subquery if necessary. This can be a starting point for you to develop from though.
Why use LISTAGG at all?
SELECT LEGAL_ENTITY_ID,
(
SELECT COUNT(1)
FROM LE_MERGE_DDC_MAPPING
WHERE FLDNM IS NULL
) as Grandtotals
FROM V_VBA_DDCR_MAIN
WHERE LEGAL_ENTITY_ID=6000132;
(Since you are filtering on a single LEGAL_ENTITY_ID you don't need the final GROUP BY)
Are you sure there shouldn't be some sort of join condition between the two tables?

How to swap values between before and after `=` using Oracle?

I have declared a value in parameter #Data as ACCOUNT_NO|none|M=ACCOUNT_NO,ADD1|none|M=ADD1
I need to get a result as ACCOUNT_NO=ACCOUNT_NO|none|M,ADD1=ADD1|none|M.
Which means I need to swap between the values before and after =
I have the SQL Server Query for achieving this but I need Oracle query.
Declare #Data varchar(100)='ACCOUNT_NO|none|M=ACCOUNT_NO,ADD1|none|M=ADD1';
WITH
myCTE1 AS
(
SELECT CAST('<root><r>' + REPLACE(#Data,',','</r><r>') + '</r></root>' AS XML) AS parts1
)
,myCTE2 AS
(
SELECT CAST('<root><r>' + REPLACE(p1.x.value('.','varchar(max)'),'=','</r><r>') + '</r></root>' AS XML) as parts2
FROM myCTE1
CROSS APPLY parts1.nodes('/root/r') AS p1(x)
)
SELECT STUFF
(
(
SELECT ',' + parts2.value('/root[1]/r[2]','varchar(max)') + '=' + parts2.value('/root[1]/r[1]','varchar(max)')
FROM myCTE2
FOR XML PATH(''),TYPE
).value('.','varchar(max)'),1,1,'');
Expected Output if I execute the query ACCOUNT_NO=ACCOUNT_NO|none|M,ADD1=ADD1|none|M. Can anyone give an idea to do this one?
Sounds like a job for REGEXP_REPLACE:
WITH datatab as (select 'ACCOUNT_NO|none|M=ACCOUNT_NO,ADD1|none|M=ADD1' info from dual)
select info,
regexp_replace(info, '([^=]+)=([^=,]+),([^=]+)=([^=,]+)', '\2=\1,\4=\3') new_info
from datatab;
INFO NEW_INFO
--------------------------------------------- ---------------------------------------------
ACCOUNT_NO|none|M=ACCOUNT_NO,ADD1|none|M=ADD1 ACCOUNT_NO=ACCOUNT_NO|none|M,ADD1=ADD1|none|M
(as a complete aside, that's the first time I've ever written a regular expression and had it work first time. Apparently, I have gone over to the dark side... *{;-) )
ETA: If you need this in a procedure/function, you don't need to bother selecting the regular expression, you can do it in PL/SQL directly.
Here's an example of a function that returns the swapped over result:
create or replace function swap_places (p_data in varchar2)
return varchar2
is
begin
return regexp_replace(p_data, '([^=]+)=([^=,]+),([^=]+)=([^=,]+)', '\2=\1,\4=\3');
end swap_places;
/
-- example of calling the function to check the result
select swap_places('ACCOUNT_NO|none|M=ACCOUNT_NO,ADD1|none|M=ADD1') col1 from dual;
COL1
-------------------------------------------------
ACCOUNT_NO=ACCOUNT_NO|none|M,ADD1=ADD1|none|M

PL/SQL query IN comma deliminated string

I am developing an application in Oracle APEX. I have a string with user id's that is comma deliminated which looks like this,
45,4932,20,19
This string is stored as
:P5_USER_ID_LIST
I want a query that will find all users that are within this list my query looks like this
SELECT * FROM users u WHERE u.user_id IN (:P5_USER_ID_LIST);
I keep getting an Oracle error: Invalid number. If I however hard code the string into the query it works. Like this:
SELECT * FROM users u WHERE u.user_id IN (45,4932,20,19);
Anyone know why this might be an issue?
A bind variable binds a value, in this case the string '45,4932,20,19'. You could use dynamic SQL and concatenation as suggested by Randy, but you would need to be very careful that the user is not able to modify this value, otherwise you have a SQL Injection issue.
A safer route would be to put the IDs into an Apex collection in a PL/SQL process:
declare
array apex_application_global.vc_arr2;
begin
array := apex_util.string_to_table (:P5_USER_ID_LIST, ',');
apex_collection.create_or_truncate_collection ('P5_ID_COLL');
apex_collection.add_members ('P5_ID_COLL', array);
end;
Then change your query to:
SELECT * FROM users u WHERE u.user_id IN
(SELECT c001 FROM apex_collections
WHERE collection_name = 'P5_ID_COLL')
An easier solution is to use instr:
SELECT * FROM users u
WHERE instr(',' || :P5_USER_ID_LIST ||',' ,',' || u.user_id|| ',', 1) !=0;
tricks:
',' || :P5_USER_ID_LIST ||','
to make your string ,45,4932,20,19,
',' || u.user_id|| ','
to have i.e. ,32, and avoid to select the 32 being in ,4932,
I have faced this situation several times and here is what i've used:
SELECT *
FROM users u
WHERE ','||to_char(:P5_USER_ID_LIST)||',' like '%,'||to_char(u.user_id)||',%'
ive used the like operator but you must be a little carefull of one aspect here: your item P5_USER_ID_LIST must be ",45,4932,20,19," so that like will compare with an exact number "',45,'".
When using it like this, the select will not mistake lets say : 5 with 15, 155, 55.
Try it out and let me know how it goes;)
Cheers ,
Alex
Create a native query rather than using "createQuery/createNamedQuery"
The reason this is an issue is that you cannot just bind an in list the way you want, and just about everyone makes this mistake at least once as they are learning Oracle (and probably SQL!).
When you bind the string '32,64,128', it effectively becomes a query like:
select ...
from t
where t.c1 in ('32,64,128')
To Oracle this is totally different to:
select ...
from t
where t.c1 in (32,64,128)
The first example has a single string value in the in list and the second has a 3 numbers in the in list. The reason you get an invalid number error is because Oracle attempts to cast the string '32,64,128' into a number, which it cannot do due to the commas in the string.
A variation of this "how do I bind an in list" question has come up on here quite a few times recently.
Generically, and without resorting to any PLSQL, worrying about SQL Injection or not binding the query correctly, you can use this trick:
with bound_inlist
as
(
select
substr(txt,
instr (txt, ',', 1, level ) + 1,
instr (txt, ',', 1, level+1) - instr (txt, ',', 1, level) -1 )
as token
from (select ','||:txt||',' txt from dual)
connect by level <= length(:txt)-length(replace(:txt,',',''))+1
)
select *
from bound_inlist a, users u
where a.token = u.id;
If possible the best idea may be to not store your user ids in csv! Put them in a table or failing that an array etc. You cannot bind a csv field as a number.
Please dont use: WHERE ','||to_char(:P5_USER_ID_LIST)||',' like '%,'||to_char(u.user_id)||',%' because you'll force a full table scan although with the users table you may not have that many so the impact will be low but against other tables in an enterprise environment this is a problem.
EDIT: I have put together a script to demonstrate the differences between the regex method and the wildcard like method. Not only is regex faster but it's also a lot more robust.
-- Create table
create table CSV_TEST
(
NUM NUMBER not null,
STR VARCHAR2(20)
);
create sequence csv_test_seq;
begin
for j in 1..10 loop
for i in 1..500000 loop
insert into csv_test( num, str ) values ( csv_test_seq.nextval, to_char( csv_test_seq.nextval ));
end loop;
commit;
end loop;
end;
/
-- Create/Recreate primary, unique and foreign key constraints
alter table CSV_TEST
add constraint CSV_TEST_PK primary key (NUM)
using index ;
alter table CSV_TEST
add constraint CSV_TEST_FK unique (STR)
using index;
select sysdate from dual;
select *
from csv_test t
where t.num in ( Select Regexp_Substr('100001, 100002, 100003 , 100004, 100005','[^,]+', 1, Level) From Dual
Connect By Regexp_Substr('100001, 100002,100003, 100004, 100005', '[^,]+', 1, Level) Is Not Null);
select sysdate from dual;
select *
from csv_test t
where ('%,' || '100001,100002, 100003, 100004 ,100005' || ',%') like '%,' || num || ',%';
select sysdate from dual;
select *
from csv_test t
where t.num in ( Select Regexp_Substr('100001, 100002, 100003 , 100004, 100005','[^,]+', 1, Level) From Dual
Connect By Regexp_Substr('100001, 100002,100003, 100004, 100005', '[^,]+', 1, Level) Is Not Null);
select sysdate from dual;
select *
from csv_test t
where ('%,' || '100001,100002, 100003, 100004 ,100005' || ',%') like '%,' || num || ',%';
select sysdate from dual;
drop table csv_test;
drop sequence csv_test_seq;
Solution from Tony Andrews works for me. The process should be added to "Page processing" >> "After submit">> "Processes".
As you are Storing User Ids as String so You can Easily match String Using Like as Below
SELECT * FROM users u WHERE u.user_id LIKE '%'||(:P5_USER_ID_LIST)||'%'
For Example
:P5_USER_ID_LIST = 45,4932,20,19
Your Query Surely Will return Any of 1 User Id which Matches to Users table
This Will Surely Resolve Your Issue , Enjoy
you will need to run this as dynamic SQL.
create the entire string, then run it dynamically.

Resources