PL SQL Query NVL with Multiple values separated by commas - sql

I have a query working that allows the user to select by date range, store (one store number) and zip code,
Regarding store I want to be able to enter multiple store numbers separated by commas.
The code below works for a single store but not for multiple store numbers
SELECT tt.id_str_rt store
,SUBSTR(tt.inf_ct,1,5) zip_code
,COUNT(tt.ai_trn) tran_count
,SUM(tr.mo_nt_tot) sales_value
FROM orco_owner.tr_trn tt
,orco_owner.tr_rtl tr
WHERE tt.id_str_rt = tr.id_str_rt
AND (tt.id_str_rt IN NVL(:PM_store_number,tt.id_str_rt) OR :PM_store_number IS NULL)
AND NVL(SUBSTR(tt.inf_ct,1,5),0) = NVL(:PM_zip_code,NVL(SUBSTR(tt.inf_ct,1,5),0))
AND tt.id_ws = tr.id_ws
AND tt.dc_dy_bsn = tr.dc_dy_bsn
AND tt.ai_trn = tr.ai_trn
AND TRUNC(TO_DATE(tt.dc_dy_bsn,'yyyy-MM-dd'))
BETWEEN NVL(:PM_date_from, TRUNC(TO_DATE(tt.dc_dy_bsn,'yyyy-MM-dd')))
AND NVL(:PM_date_to,TRUNC(TO_DATE(tt.dc_dy_bsn,'yyyy-MM-dd')))
AND LENGTH(TRIM(TRANSLATE(SUBSTR(inf_ct,1,5), '0123456789', ' '))) IS NULL
GROUP BY tt.id_str_rt,SUBSTR(tt.inf_ct,1,5)
ORDER BY zip_code, store

You could create a function like the one described in how to convert csv to table in oracle:
create or replace function splitter(p_str in varchar2) return sys.odcivarchar2list
is
v_tab sys.odcivarchar2list:=new sys.odcivarchar2list();
begin
with cte as (select level ind from dual
connect by
level <=regexp_count(p_str,',') +1
)
select regexp_substr(p_str,'[^,]+',1,ind)
bulk collect into v_tab
from cte;
return v_tab;
end;
/
Then you would use it in your query like this:
and (tt.id_str_rt in (select column_value from table(splitter(:PM_store_number)) ))
instead of this:
AND (tt.id_str_rt IN NVL(:PM_store_number,tt.id_str_rt) OR :PM_store_number IS NULL)

Related

Remove duplicate values from comma separated variable in Oracle

I have a variable (called: all_email_list) which contains 3 email address lists altogether. (I found some similar question but not the same with a proper solution)
Example: test#asd.com, test2#asd.com,test#asd.com,test3#asd.com, test4#asd.com,test2#asd.com (it can contain spaces between comas but not all the time)
The desired output: test#asd.com, test2#asd.com,test3#asd.com,test4#asd.com
declare
first_email_list varchar2(4000);
second_email_list varchar2(4000);
third_email_list varchar2(4000);
all_email_list varchar2(4000);
begin
select listagg(EMAIL,',') into first_email_list from UM_USER a left join UM_USERROLLE b on (a.mynetuser=b.NT_NAME) left join UM_RULES c on (c.id=b.RULEID) where RULEID = 902;
select listagg(EMAIL,',') into second_email_list from table2 where CFT_ID =:P25_CFT_TEAM;
select EMAIL into third_email_list from table3 WHERE :P25_ID = ID;
all_email_list:= first_email_list || ',' || second_email_list || ',' || third_email_list;
dbms_output.put_line(all_email_list);
end;
Any solution to solve this in a simple way? By regex maybe.
Solution description. Use CTE to first split up the list of emails into rows with 1 email address per row (testd_rows). Then select distinct rows (testd_rows_unique) from testd_rows and finally put them back together with listagg. From 19c onwards you can use LISTAGG with the DISTINCT keyword.
set serveroutput on size 999999
clear screen
declare
all_email_list varchar2(4000);
l_unique_email_list varchar2(4000);
begin
all_email_list := 'test#asd.com, test2#asd.com,test#asd.com,test3#asd.com, test4#asd.com,test2#asd.com';
WITH testd_rows(email) AS
(
select regexp_substr (all_email_list, '[^, ]+', 1, rownum) split
from dual
connect by level <= length (regexp_replace (all_email_list, '[^, ]+')) + 1
), testd_rows_unique(email) AS
(
SELECT distinct email FROM testd_rows
)
SELECT listagg(email, ',') WITHIN GROUP (ORDER BY email)
INTO l_unique_email_list
FROM testd_rows_unique;
dbms_output.put_line(l_unique_email_list);
end;
/
test2#asd.com,test3#asd.com,test4#asd.com,test#asd.com
But ... why are you converting rows to a comma separated string and then de-duping it ? Use UNION to take out the duplicate values in a single SELECT statement and do LISTAGG on the values. No regexp needed then. UNION will skip duplicates as opposed to UNION ALL which returns all the rows.
DECLARE
all_email_list varchar2(4000);
BEGIN
WITH all_email (email) AS
(
select email from UM_USER a left join UM_USERROLLE b on (a.mynetuser=b.NT_NAME) left join UM_RULES c on (c.id=b.RULEID) where RULEID = 902
UNION
select email from table2 where CFT_ID =:P25_CFT_TEAM
UNION
select email from table3 WHERE :P25_ID = ID
)
SELECT listagg(email, ',') WITHIN GROUP (ORDER BY email)
INTO all_email_list
FROM all_email;
dbms_output.put_line(all_email_list);
END;
/
You could leverage the apex_string.split table function to simplify the code.
12c+ makes it real clean
select listagg(distinct column_value,',') within group (order by null)
from apex_String.split(replace('test#asd.com, test2#asd.com,test#asd.com,test3#asd.com, test4#asd.com,test2#asd.com'
,' ')
,',')
11g needs a wrapping table() and listagg doesn't support distinct.
select listagg(email,',') within group (order by null)
from
(select distinct column_value email
from table(apex_String.split(replace('test#asd.com, test2#asd.com,test#asd.com,test3#asd.com, test4#asd.com,test2#asd.com',' '),','))
);

How to FETCH perticular records from table1 and INSERT into table2?

Image_1 is "table_1:-Product"
Image 2 is "table_2:-User_detail"
Now,
i want to select data from table_1 (p_name,p_amount) with use of particular "p_id" with "prompt" and insert into table_2 (purchased_item,amount_total,due_date) at particular select "u_id"
p_name-> purchased_item
p_amount->amount_total
due_date is sysdate
Expected output:-
If i am select "p_id=101" from table_1 and "u_id=3676" from table_2 then
i get this update in table_2 ---
"row-1"
u_id:-3676
u_name:-Rv
email:-rv#gmail.com
purchased_item:-LED
amount_total:-5000
due_date:-sysdate
please help me out,how this is possible? with procedure or with trigger?
thanx in advanced.
I am new pl/sql learner.
how this is possible? with procedure or with trigger?
Neither, I guess. UPDATE will do.
update user_detail d set
(d.purchased_item, d.amount_Total, d.due_Date) =
(select p.p_name, p.p_amount, sysdate
from product p
where p.p_id = :par_p_id
)
where d.u_id = :par_u_id;
If it has to be a procedure, then
create or replace procedure p_iud (par_p_id in product.p_id%type,
par_u_id in user_detail.u_id%type
)
as
begin
update user_detail d set
(d.purchased_item, d.amount_Total, d.due_Date) =
(select p.p_name, p.p_amount, sysdate
from product p
where p.p_id = par_p_id
)
where d.u_id = par_u_id;
end;
/
Call it as
begin
p_iud(101, 3676);
end;
/
If you'd want to pass several products at the same time, one option is to pass them as a string with comma-separated values. The procedure would then be
create or replace procedure p_iud (par_p_id in varchar2,
par_u_id in user_detail.u_id%type
)
as
begin
update user_detail d set
(d.purchased_item, d.amount_total, d.due_Date) =
(select listagg(p.p_name, ',') within group (order by p.p_name),
sum(p.p_amount),
sysdate
from product p
where p.p_id in (select to_number(regexp_substr(par_p_id, '[^,]+', 1, level))
from dual
connect by level <= regexp_count(par_p_id, ',') + 1
)
)
where d.u_id = par_u_id;
end;
/
and you'd call it as
begin
p_iud('101,301', 3676);
end;
/
Possible problem: as LISTAGG concatenates all those product names, it'll fail if such a "long" string is too long for PURCHASED_ITEM column (you could SUBSTR it, if that's an option)

Dynamically spilt rows in to mulitple comma-separated lists

I have a table that contains a list of users.
USER_TABLE
USER_ID DEPT
------- ----
USER1 HR
USER2 FINANCE
USER3 IT`
Using a SQL statement, I need to get the list of users as a delimited string returned as a varchar2 - this is the only datatype I can use as dictated by the application I'm using, e.g.
USER1, USER2, USER3
The issue I have is the list will exceed 4000 characters. I have the following which will manually chunk up the users in to lists of 150 users at a time (based on user_id max size being 20 characters plus delimiters safely fitting in to 4000 characters).
SELECT LISTAGG(USER_ID, ',') WITHIN GROUP (ORDER BY USER_ID)
FROM (SELECT DISTINCT USER_ID AS USER_ID, ROW_NUMBER() OVER (ORDER BY USER_ID) RN FROM TABLE_NAME)
WHERE RN <= 150
START WITH RN = 1
CONNECT BY PRIOR RN = RN - 1
UNION
SELECT LISTAGG(USER_ID, ',') WITHIN GROUP (ORDER BY USER_ID)
FROM (SELECT DISTINCT USER_ID AS USER_ID, ROW_NUMBER() OVER (ORDER BY USER_ID) RN FROM TABLE_NAME)
WHERE RN > 150 AND RN <= 300
START WITH RN = 1
CONNECT BY PRIOR RN = RN - 1
This is manual and would require an additional UNION for each chunk of 150 users and the total number of users could increase at a later date.
Is it possible to do this so the delimited strings of user_ids are generated dynamically so they fit in to multiple chunks of 4000 characters and no user_ids are split over multiple strings?
Ideally, I'd want the output to look like this:
USER1, USER2, USER3 (to) USER149
USER150, USER151, USER152 (to) USER300
USER301, USER302, USER303 (to) USER450`
The solution needs to be a SELECT statement as the schema is read-only and we aren't able to create any objects on the database. We're using Oracle 11g.
You can do this with a pipelined function:
create or replace function get_user_ids
return sys.dbms_debug_vc2coll pipelined
is
rv varchar2(4000) := null;
begin
for r in ( select user_id, length(user_id) as lng
from user_table
order by user_id )
loop
if length(rv) + r.lng + 1 > 4000
then
rv := rtrim(rv, ','); -- remove trailing comma
pipe row (rv);
rv := null;
end if;
rv := rv || r.user_id || ',';
end loop;
return;
end;
/
You would call it like this:
select column_value as user_id_csv
from table(get_user_ids);
Alternate way using below function :
create or replace FUNCTION my_agg_user
RETURN CLOB IS
l_string CLOB;
TYPE t_bulk_collect_test_tab IS TABLE OF VARCHAR2(4000);
l_tab t_bulk_collect_test_tab;
CURSOR user_list IS
SELECT USER_ID
FROM USER_TABLE ;
BEGIN
OPEN user_list;
LOOP
FETCH user_list
BULK COLLECT INTO l_tab LIMIT 1000;
FOR indx IN 1 .. l_tab.COUNT
LOOP
l_string := l_string || l_tab(indx);
l_string := l_string || ',';
END LOOP;
EXIT WHEN user_list%NOTFOUND;
END LOOP;
CLOSE user_list;
RETURN l_string;
END my_agg_user;
After function created ,
select my_agg_user from dual;
I believe the SQL I have below should work in most cases. I've hard-coded the SQL to break the strings up in to 150 entries of user id, but the rest is dynamic.
The middle part produces duplicates, which requires an additional distinct to eliminate, but I'm not sure if there is a better way to do this.
WITH POSITION AS ( SELECT ((LEVEL-1) * 150 + 1) FROM_POS, LEVEL * 150 TO_POS
FROM DUAL
CONNECT BY LEVEL <= (SELECT COUNT(DISTINCT( USER_ID)) / 150 FROM TABLE_NAME)
)
SELECT DISTINCT
LISTAGG(USER_ID, ',') WITHIN GROUP (ORDER BY USER_ID) OVER (PARTITION BY FROM_POS, TO_POS)
FROM
(SELECT DISTINCT USER_ID AS USER_ID, ROW_NUMBER() OVER (ORDER BY USER_ID) RN FROM TABLE_NAME) V0 ,
POSITION
WHERE V0.RN >= POSITION.FROM_POS
AND V0.RN <= POSITION.TO_POS

Search comma separated value in oracle 12

I have a Table - Product In Oracle, wherein p_spc_cat_id is stored as comma separated values.
p_id p_name p_desc p_spc_cat_id
1 AA AAAA 26,119,27,15,18
2 BB BBBB 0,0,27,56,57,4
3 BB CCCC 26,0,0,15,3,8
4 CC DDDD 26,0,27,7,14,10
5 CC EEEE 26,119,0,48,75
Now I want to search p_name which have p_spc_cat_id in '26,119,7' And this search value are not fixed it will some time '7,27,8'. The search text combination change every time
my query is:
select p_id,p_name from product where p_spc_cat_id in('26,119,7');
when i execute this query that time i can't find any result
I am little late in answering however i hope that i understood the question correctly.
Read further if: you have a table storing records like
1. 10,20,30,40
2. 50,40,20,70
3. 80,60,30,40
And a search string like '10,60', in which cases it should return rows 1 & 3.
Please try below, it worked for my small table & data.
create table Temp_Table_Name (some_id number(6), Ab varchar2(100))
insert into Temp_Table_Name values (1,'112,120')
insert into Temp_Table_Name values (2,'7,8,100,26')
Firstly lets breakdown the logic:
The table contains comma separated data in one of the columns[Column AB].
We have a comma separated string which we need to search individually in that string column. ['26,119,7,18'-X_STRING]
ID column is primary key in the table.
1.) Lets multiple each record in the table x times where x is the count of comma separated values in the search string [X_STRING]. We can use below query to create the cartesian join sub-query table.
Select Rownum Sequencer,'26,119,7,18' X_STRING
from dual
CONNECT BY ROWNUM <= (LENGTH( '26,119,7,18') - LENGTH(REPLACE( '26,119,7,18',',',''))) + 1
Small note: Calculating count of comma separated values =
Length of string - length of string without ',' + 1 [add one for last value]
2.) Create a function PARSING_STRING such that PARSING_STRING(string,position). So If i pass:
PARSING_STRING('26,119,7,18',3) it should return 7.
CREATE OR REPLACE Function PARSING_STRING
(String_Inside IN Varchar2, Position_No IN Number)
Return Varchar2 Is
OurEnd Number; Beginn Number;
Begin
If Position_No < 1 Then
Return Null;
End If;
OurEnd := Instr(String_Inside, ',', 1, Position_No);
If OurEnd = 0 Then
OurEnd := Length(String_Inside) + 1;
End If;
If Position_No = 1 Then
Beginn := 1;
Else
Beginn := Instr(String_Inside, ',', 1, Position_No-1) + 1;
End If;
Return Substr(String_Inside, Beginn, OurEnd-Beginn);
End;
/
3.) Main query, with the join to multiply records.:
select t1.*,PARSING_STRING(X_STRING,Sequencer)
from Temp_Table_Name t1,
(Select Rownum Sequencer,'26,119,7,18' X_STRING from dual
CONNECT BY ROWNUM <= (Select (LENGTH( '26,119,7,18') - LENGTH(REPLACE(
'26,119,7,18',',',''))) + 1 from dual)) t2
Please note that with each multiplied record we are getting 1 particular position value from the comma separated string.
4.) Finalizing the where condition:
Where
/* For when the value is in the middle of the strint [,value,] */
AB like '%,'||PARSING_STRING(X_STRING,Sequencer)||',%'
OR
/* For when the value is in the start of the string [value,]
parsing the first position comma separated value to match*/
PARSING_STRING(AB,1) = PARSING_STRING(X_STRING,Sequencer)
OR
/* For when the value is in the end of the string [,value]
parsing the last position comma separated value to match*/
PARSING_STRING(AB,(LENGTH(AB) - LENGTH(REPLACE(AB,',',''))) + 1) =
PARSING_STRING(X_STRING,Sequencer)
5.) Using distinct in the query to get unique ID's
[Final Query:Combination of all logic stated above: 1 Query to find them all]
select distinct Some_ID
from Temp_Table_Name t1,
(Select Rownum Sequencer,'26,119,7,18' X_STRING from dual
CONNECT BY ROWNUM <= (Select (LENGTH( '26,119,7,18') - LENGTH(REPLACE( '26,119,7,18',',',''))) + 1 from dual)) t2
Where
AB like '%,'||PARSING_STRING(X_STRING,Sequencer)||',%'
OR
PARSING_STRING(AB,1) = PARSING_STRING(X_STRING,Sequencer)
OR
PARSING_STRING(AB,(LENGTH(AB) - LENGTH(REPLACE(AB,',',''))) + 1) = PARSING_STRING(X_STRING,Sequencer)
You can use like to find it:
select p_id,p_name from product where p_spc_cat_id like '%26,119%'
or p_spc_cat_id like '%119,26%' or p_spc_cat_id like '%119,%,26%' or p_spc_cat_id like '%26,%,119%';
Use the Oracle function instr() to achieve what you want. In your case that would be:
SELECT p_name
FROM product
WHERE instr(p_spc_cat_id, '26,119') <> 0;
Oracle Doc for INSTR
If the string which you are searching will always have 3 values (i.e. 2 commas present) then you can use below approach.
where p_spc_cat_id like regexp_substr('your_search_string, '[^,]+', 1, 1)
or p_spc_cat_id like regexp_substr('your_search_string', '[^,]+', 1, 2)
or p_spc_cat_id like regexp_substr('your_search_string', '[^,]+', 1, 3)
If you cant predict how many values will be there in your search string
(rather how many commas) in that case you may need to generate dynamic query.
Unfortunately sql fiddle is not working currently so could not test this code.
SELECT p_id,p_name
FROM product
WHERE p_spc_cat_id
LIKE '%'||'&i_str'||'%'`
where i_str is 26,119,7 or 7,27,8
This solution uses CTE's. "product" builds the main table. "product_split" turns products into rows so each element in p_spc_cat_id is in it's own row. Lastly, product_split is searched for each value in the string '26,119,7' which is turned into rows by the connect by.
with product(p_id, p_name, p_desc, p_spc_cat_id) as (
select 1, 'AA', 'AAAA', '26,119,27,15,18' from dual union all
select 2, 'BB', 'BBBB', '0,0,27,56,57,4' from dual union all
select 3, 'BB', 'CCCC', '26,0,0,15,3,8' from dual union all
select 4, 'CC', 'DDDD', '26,0,27,7,14,10' from dual union all
select 5, 'CC', 'EEEE', '26,119,0,48,75' from dual
),
product_split(p_id, p_name, p_spc_cat_id) as (
select p_id, p_name,
regexp_substr(p_spc_cat_id, '(.*?)(,|$)', 1, level, NULL, 1)
from product
connect by level <= regexp_count(p_spc_cat_id, ',')+1
and prior p_id = p_id
and prior sys_guid() is not null
)
-- select * from product_split;
select distinct p_id, p_name
from product_split
where p_spc_cat_id in(
select regexp_substr('26,119,7', '(.*?)(,|$)', 1, level, NULL, 1) from dual
connect by level <= regexp_count('26,119,7', ',') + 1
)
order by p_id;
P_ID P_
---------- --
1 AA
3 BB
4 CC
5 CC
SQL>

Error with SELECT statement in a FOR loop

When I try to execute the code below I receive this error here:
Error(9,4): PLS-00103: Encountered the symbol "SELECT" when expecting
one of the following: ( ) - + case mod new not null table continue avg count current exists max min prior sql
stddev sum variance execute multiset the both leading trailing
forall merge year month day hour minute second timezone_hour
timezone_minute timezone_region timezone_abbr time timestamp
interval date
CREATE OR REPLACE PROCEDURE PROC_LIST_SIMILAR_TVSERIES
(seriesName IN SERIES.NAME%TYPE)
AS
CURSOR series IS (SELECT IDS FROM SERIES WHERE NAME = seriesName);
allSeries SERIES%ROWTYPE;
BEGIN
FOR series IN allSeries
(SELECT 2* ( SELECT COUNT(*)
FROM DICT d
WHERE d.idt IN ( SELECT DISTINCT IDT
FROM POSTING
WHERE IDS = series
INTERSECT
SELECT DISTINCT IDT
FROM POSTING
WHERE IDS = allSeries.IDS
)
)
/ ( ( SELECT DISTINCT COUNT(IDT)
FROM POSTING
WHERE IDS = series
) +
( SELECT DISTINCT COUNT(IDT)
FROM POSTING
WHERE IDS = allSeries.IDS )
)
INTO similarity
FROM SERIES s1
SERIES s2
WHERE s1.IDS = series
AND s2.IDS != series
);
IF similarity > 0.7 THEN
DBMS_OUTPUT.PUT_LINE('ok');
END LOOP;
END;
/
What the code does is take in a name, find it's ID, and compare it to other id's (and avoid comparing it to the same ID). I'm trying to print out "ok" whenever the similarity calculation is over 0.7 . No idea why this doesn't work.
First, if you have a cursor named the same as the table, what is the series%rowtype going to look like? The cursor or the table? Bad idea.
Second, you never execute the cursor to get the ID, so your subsequent cursor loop is looking for records that match allSeries.IDS which is null because you haven't populated it.
Try this as a starting point, although I'm guessing that you still will have work to do on your cursor query. Still, at least it points you to the right code structures...
CREATE OR REPLACE PROCEDURE PROC_LIST_SIMILAR_TVSERIES
(seriesName IN SERIES.NAME%TYPE)
AS
CURSOR seriesCur IS (SELECT IDS FROM SERIES WHERE NAME = seriesName);
allSeries seriesCur%ROWTYPE;
BEGIN
OPEN seriesCur;
FETCH seriesCur INTO allSeries;
IF seriesCur%NOTFOUND
THEN
CLOSE seriesCur;
raise_application_error(-20001,'Your SeriesName does not exist');
END IF;
CLOSE seriesCur;
FOR seriesRec IN
-- this query is a mess! Tried to fix up some aspects of it according to what I THINK you're trying to do.
(SELECT 2*
(SELECT COUNT(*) FROM DICT d WHERE d.idt IN (
SELECT DISTINCT IDT FROM POSTING WHERE IDS = allSeries.IDS
INTERSECT
SELECT DISTINCT IDT FROM POSTING WHERE IDS = allSeries.IDS))
/ ((SELECT DISTINCT COUNT(IDT) FROM POSTING WHERE IDS = allSeries.IDS) +
(SELECT DISTINCT COUNT(IDT) FROM POSTING WHERE IDS = allSeries.IDS) ) similarity
FROM SERIES s1, SERIES s2
WHERE s1.IDS = allSeries.IDS
AND s2.IDS != allSeries.IDS)
LOOP
IF seriesRec.similarity > 0.7 THEN
DBMS_OUTPUT.PUT_LINE('ok');
END IF;
END LOOP;
END;
/
I am still trying to understand the logic in the SQL statement. But i
have hopefully tried to remove the syntactical error. Hope it helps.
CREATE OR REPLACE PROCEDURE PROC_LIST_SIMILAR_TVSERIES(
seriesName IN SERIES.NAME%TYPE)
AS
similarity PLS_INTEGER;
BEGIN
FOR i IN
(SELECT IDS FROM SERIES WHERE NAME = seriesName
)
LOOP
--The logic i am still not able to understand
SELECT *,
(SELECT COUNT(*)
FROM DICT d
WHERE d.idt IN
( SELECT DISTINCT IDT FROM POSTING WHERE IDS = I.IDS
INTERSECT
SELECT DISTINCT IDT
FROM POSTING
WHERE IDS = allSeries.IDS
) / (
(SELECT DISTINCT COUNT(IDT) FROM POSTING WHERE IDS = i.IDS
) +
(SELECT DISTINCT COUNT(IDT) FROM POSTING WHERE IDS = I.IDS
) )
)
INTO similarity
FROM SERIES s1,
SERIES s2
WHERE s1.IDS = s2.IDS
AND s2.IDS != I.IDS;
IF similarity > 0.7 THEN
DBMS_OUTPUT.PUT_LINE('ok');
END IF;
END LOOP;
END;
/