Oracle: using IN clause with text field? [duplicate] - sql

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
How to best split csv strings in oracle 9i
I have some legacy data where there's a VARCHAR2(100) field SUBID that has comma-delimited data:
empno subid
1 1, 3, 2
2 18,19, 3, 6, 9
etc.
I need to write the equivalent of
select *
from table
where id in ( select SUBID from subidtable where empno = 1 )
Is there a way to accomplish this in Oracle?
Edit:
Added some clarification. I need to do the IN clause against the values stored in a string from a single row, not all rows.

You can, but it's a little ugly. Depending on the Oracle version
You can use a variant of this askTom thread to parse the data into a collection and use the collection in your SQL statement. This should work in any version of Oracle since 8.1.5 but the syntax has gotten a bit simpler over the years.
SQL> create or replace type myTableType as table
2 of varchar2 (255);
3 /
Type created.
SQL> ed
Wrote file afiedt.buf
1 create or replace
2 function in_list( p_string in varchar2 ) return myTableType
3 as
4 l_string long default p_string || ',';
5 l_data myTableType := myTableType();
6 n number;
7 begin
8 loop
9 exit when l_string is null;
10 n := instr( l_string, ',' );
11 l_data.extend;
12 l_data(l_data.count) :=
13 ltrim( rtrim( substr( l_string, 1, n-1 ) ) );
14 l_string := substr( l_string, n+1 );
15 end loop;
16 return l_data;
17* end;
SQL> /
Function created.
SQL> select ename
2 from emp
3 where empno in (select column_value
4 from table( in_list( '7934, 7698, 7521' )));
ENAME
----------
WARD
BLAKE
MILLER
You can also use regular expressions as discussed in this StackOverflow thread
SQL> ed
Wrote file afiedt.buf
1 select ename
2 from emp
3 where empno in (select regexp_substr(str, '[^,]+',1,level)
4 from (select '7934, 7698, 7521' str from dual)
5* connect by level <= regexp_count(str,'[^,]+'))
SQL> /
ENAME
----------
WARD
MILLER
BLAKE

If you can 'fix' your table structure to have a 1:many relationship, such that each row in your subidtable contains only one id, that's your best bet.
If you can't, then you could get hold of one of the many split() functions that people have coded around the web. These take a string and return the data as a set. The problem here is that they are designed to take a single string and return a table of values, not to take a table of strings...
As this data seems to be in a bit of a hacked format, you may only need a one-time hack solution with minimal code. In such cases you can try this...
SELECT
*
FROM
table
WHERE
EXISTS (SELECT * FROM subidtable WHERE (',' || subid || ',') LIKE ('%,' || table.id || ',%'))
But be warned, it scales VERY badly. So expect slow performance if you have a large amount of data in either table.
EDIT
As your edit now shows that you're only ever processing one string from the subidtable table, the split function option becomes a lot easier to implement. See Justin's answer :)
A modification to the 'simple hack' above would be...
SELECT
*
FROM
table
WHERE
(SELECT ',' || subid || ',' FROM subidtable WHERE empno=1) LIKE ('%,' || table.id || ',%')

This is in no way elegant and an abuse of "execute immediate" but in your specific case this might work:
DECLARE
i INTEGER;
subid VARCHAR2(100) := '18,19, 3, 6, 9';
BEGIN
EXECUTE IMMEDIATE 'select 1 from dual where 6 in (' || subid || ')'
INTO i;
dbms_output.put_line('returned: ' || i);
EXCEPTION
WHEN others THEN
dbms_output.put_line('Exception: ' || SQLERRM);
END;

Related

oracle plsql: return table line by line

I am tasked to return a query result using a stored plsql procedure involving multiple tables. currently, I wrote code something like this:
create or replace function sample(userinput varchar) return varchar2 is
c_list varchar2(1000) // this line keep triggers a too small string buffer error
begin
for c in (
select name, id, count(*)
from temp join temp2 on temp.id = temp2.id
where data = userinput)
loop
c_list := c_list || c.name || c.id || '';
end loop;
end;
/
select name, sample(name)
from temp join temp2 ...
Every time I try i get either the function error or too small string buffer error, and not rly sure if this is the right approach.
Can someone help me to figure out this?
Isn't it obvious? Concatenation result is longer than 1000 characters. Use larger local variable (e.g. varchar2(30000) or even a CLOB).
On the other hand, do you really need PL/SQL? How long result do you expect? If 4000 characters is enough, use listagg:
SQL> select listagg(ename ||'-'|| empno, ', ') within group (order by ename) result
2 from emp
3 where deptno = 10;
RESULT
--------------------------------------------------------------------------------
CLARK-7782, KING-7839, MILLER-7934
SQL>
If it is longer than 4000 characters, use xmlagg:
SQL> select rtrim(xmlagg(xmlelement(e, ename ||'-'|| empno ||', ') order by ename).extract('//text()'), ', ') result
2 from emp
3 where deptno = 10;
RESULT
--------------------------------------------------------------------------------
CLARK-7782, KING-7839, MILLER-7934
SQL>
Both can be, of course, converted to a PL/SQL function, if you really need it.

INSERT INTO TAB SELECT * FROM TABLE TAB#db2 - Too many values

I'm trying to move rows between two tables which have many columns.
The table columns are identical other than the destination table (tab#db2) has a few more columns which causes a simple INSERT to fail.
I'd like to use a simple PL/SQL statement to build a list of the columns in tab#db2 dynamically instead of typing out the names of col1, col2, etc in the INSERT and SELECT clause. Example
declare a variable as var_col_list
set col_list = output of select * from tab (omitting rows)
INSERT INTO TAB *var_col_list* SELECT *var_cols_list* FROM TABLE TAB#db2
I've researched using %rowtype but cannot find a suitable example that would take less time than simply writing out the names of the columns!
Any advice is greatly appreciated
If you use e.g. TOAD, you can right-click the table and let it Generate statement - in your case, that would be INSERT. You'd slightly modify it (remove columns you don't need) and that's all.
Otherwise, this is how you might do it semi-manually.
This is my source table:
SQL> SELECT * FROM dept;
DEPTNO DNAME LOC
---------- -------------------- --------------------
10 ACCOUNTING NEW YORK
20 RESEARCH DALLAS
30 SALES CHICAGO
40 OPERATIONS BOSTON
Target table doesn't contain all columns:
SQL> CREATE TABLE target
2 (
3 deptno NUMBER,
4 dname VARCHAR2 (20)
5 );
Table created.
Code which loops through all TARGET table columns (i.e. a table which has less columns) and composes the INSERT INTO statement:
SQL> DECLARE
2 l_str VARCHAR2 (1000);
3 BEGIN
4 FOR cur_r IN (SELECT column_name
5 FROM user_tab_columns
6 WHERE table_name = 'TARGET')
7 LOOP
8 l_str := l_str || ', ' || cur_r.column_name;
9 END LOOP;
10
11 l_str :=
12 'insert into target select ' || LTRIM (l_str, ', ') || ' from dept';
13 DBMS_OUTPUT.put_line (l_str);
14
15 EXECUTE IMMEDIATE l_str;
16 END;
17 /
insert into target select DEPTNO, DNAME from dept --> this is the L_STR contents
PL/SQL procedure successfully completed.
SQL> SELECT * FROM target;
DEPTNO DNAME
---------- --------------------
10 ACCOUNTING
20 RESEARCH
30 SALES
40 OPERATIONS
Seems to be OK.
Using the solution provided by Littefoot, I made some minor tweaks to fit my requirement perfectly:
SQL> create table taba (col1 number,col2 number);
SQL> insert into taba values (1,2);
SQL> select * from taba;
COL1 COL2
---------- ----------
1 2
SQL> create table tabb (col1 number,col2 number, col3 number);
SQL> DECLARE
l_str VARCHAR2 (32767);
BEGIN
FOR cur_r IN (SELECT column_name
FROM user_tab_columns
WHERE table_name = 'TABA'
order by column_id asc)
LOOP
l_str := l_str || ', ' || cur_r.column_name;
END LOOP;
l_str :=
'insert into tabb (' || LTRIM (l_str, ', ') || ') ' ||' select ' || LTRIM (l_str, ', ') || ' from taba';
DBMS_OUTPUT.put_line (l_str);
EXECUTE IMMEDIATE l_str;
END;
/
Output of l_str (SQL INSERT):
insert into tabb (COL1, COL2) select COL1, COL2 from taba
Result:
SQL> select * from tabb;
COL1 COL2 COL3
---------- ---------- ----------
1 2

Convert string from a table to be used as a column for selection for another table

I have a table where I store records to be used as a column name for my queries where the record is an actual column on another table.
TBL_1
COL_1
==========
SAMPLE_COL
TBL_2
SAMPLE_COL_1 SAMPLE_COL2
============ ===========
ABC DEF
I'm having a problem using the record that I fetched to use as an actual column. I already tried a bunch of things like casting and using case (using case works but it's a bit of a brute force and I'm looking for a more elegant way of doing this).
This is a sample query that I have tried:
SELECT (SELECT column_1 FROM tbl_1)
FROM tbl_2
Expected output
SAMPLE_COL_1
============
ABC
Actual output
(SELECT column_1 FROM tbl_1)
============================
SAMPLE_COL_1
This is what I've tried that worked so far but a brute force technique
SELECT (
CASE
WHEN (SELECT column_1 FROM tbl_2) = 'SAMPLE_COL_1' THEN SAMPLE_COL_1
ELSE SAMPLE_COL_2
END
)
FROM tbl_2
Appreciate the help! Keep safe from COVID-19 everyone :)
It's not that easy as you'd want it to be - you'll have to use dynamic SQL. Here's an example, based on Scott's table(s).
Create a function that accepts table and column names and returns ref cursor.
SQL> create or replace function f_test
2 (par_table_name in varchar2, par_column_name in varchar2)
3 return sys_refcursor
4 is
5 rc sys_refcursor;
6 begin
7 open rc for 'select ' || dbms_assert.simple_sql_name(par_column_name) ||
8 ' from ' || dbms_assert.sql_object_name(par_table_name);
9 return rc;
10 end;
11 /
Function created.
Testing:
SQL> select f_test('dept', 'dname') from dual;
F_TEST('DEPT','DNAME
--------------------
CURSOR STATEMENT : 1
CURSOR STATEMENT : 1
DNAME
--------------
ACCOUNTING
RESEARCH
SALES
OPERATIONS
SQL> select f_test('dual', 'dummy') from dual;
F_TEST('DUAL','DUMMY
--------------------
CURSOR STATEMENT : 1
CURSOR STATEMENT : 1
D
-
X
SQL>
Another example, with column (and table) names stored in a table (something like you posted).
Table that contains those info and the function:
SQL> select * from tbl_1;
TNAM CNAME
---- -----
dept dname
dual dummy
SQL> create or replace function f_test
2 (par_table_name in varchar2)
3 return sys_refcursor
4 is
5 l_str varchar2(1000);
6 rc sys_refcursor;
7 begin
8 select 'select ' || dbms_assert.simple_sql_name(cname) ||
9 ' from ' || dbms_assert.sql_object_name(tname)
10 into l_str
11 from tbl_1
12 where tname = dbms_assert.sql_object_name(par_table_name);
13 open rc for l_str;
14 return rc;
15 end;
16 /
Function created.
Testing:
SQL> select f_test('dept') from dual;
F_TEST('DEPT')
--------------------
CURSOR STATEMENT : 1
CURSOR STATEMENT : 1
DNAME
--------------
ACCOUNTING
RESEARCH
SALES
OPERATIONS
SQL>

How to use 'sysdate' if its a string constant

I extract data from a table, the field is mostly null, but sometimes it's sysdate. However since its from a table, after getting the field its 'sysdate', between single quotes. How can I use it?
I have tried to_date, to_date(to_char()).
I need something that works within
select to_date('sysdate') from dual;
You can use a case expression:
select case
when the_column = 'sysdate' then sysdate
else to_date(the_column)
end as date_value
from the_table;
The only way I know is dynamic SQL. Here's an example:
SQL> create table test (id number, col varchar2(20));
Table created.
SQL> insert into test
2 select 1, '''sysdate''' from dual union all
3 select 2, null from dual;
2 rows created.
SQL> declare
2 l_res test%rowtype;
3 l_str varchar2(200);
4 begin
5 for cur_r in (select id, col from test) loop
6 l_str := 'select ' || cur_r.id ||', '||
7 nvl(replace(cur_r.col, chr(39), null), 'null') || ' from dual';
8 execute immediate l_str into l_res;
9 dbms_output.put_line(l_res.id ||': '|| l_res.col);
10 end loop;
11 end;
12 /
1: 24.06.2019 12:18:39
2:
PL/SQL procedure successfully completed.
SQL>

How can I combine multiple rows into a comma-delimited list in Oracle? [duplicate]

This question already has answers here:
SQL Query to concatenate column values from multiple rows in Oracle
(10 answers)
Closed 8 years ago.
I have a simple query:
select * from countries
with the following results:
country_name
------------
Albania
Andorra
Antigua
.....
I would like to return the results in one row, so like this:
Albania, Andorra, Antigua, ...
Of course, I can write a PL/SQL function to do the job (I already did in Oracle 10g), but is there a nicer, preferably non-Oracle-specific solution (or may be a built-in function) for this task?
I would generally use it to avoid multiple rows in a sub-query, so if a person has more then one citizenship, I do not want her/him to be a duplicate in the list.
My question is based on the similar question on SQL server 2005.
UPDATE:
My function looks like this:
CREATE OR REPLACE FUNCTION APPEND_FIELD (sqlstr in varchar2, sep in varchar2 ) return varchar2 is
ret varchar2(4000) := '';
TYPE cur_typ IS REF CURSOR;
rec cur_typ;
field varchar2(4000);
begin
OPEN rec FOR sqlstr;
LOOP
FETCH rec INTO field;
EXIT WHEN rec%NOTFOUND;
ret := ret || field || sep;
END LOOP;
if length(ret) = 0 then
RETURN '';
else
RETURN substr(ret,1,length(ret)-length(sep));
end if;
end;
The WM_CONCAT function (if included in your database, pre Oracle 11.2) or LISTAGG (starting Oracle 11.2) should do the trick nicely. For example, this gets a comma-delimited list of the table names in your schema:
select listagg(table_name, ', ') within group (order by table_name)
from user_tables;
or
select wm_concat(table_name)
from user_tables;
More details/options
Link to documentation
Here is a simple way without stragg or creating a function.
create table countries ( country_name varchar2 (100));
insert into countries values ('Albania');
insert into countries values ('Andorra');
insert into countries values ('Antigua');
SELECT SUBSTR (SYS_CONNECT_BY_PATH (country_name , ','), 2) csv
FROM (SELECT country_name , ROW_NUMBER () OVER (ORDER BY country_name ) rn,
COUNT (*) OVER () cnt
FROM countries)
WHERE rn = cnt
START WITH rn = 1
CONNECT BY rn = PRIOR rn + 1;
CSV
--------------------------
Albania,Andorra,Antigua
1 row selected.
As others have mentioned, if you are on 11g R2 or greater, you can now use listagg which is much simpler.
select listagg(country_name,', ') within group(order by country_name) csv
from countries;
CSV
--------------------------
Albania, Andorra, Antigua
1 row selected.
For Oracle you can use LISTAGG
You can use this as well:
SELECT RTRIM (
XMLAGG (XMLELEMENT (e, country_name || ',')).EXTRACT ('//text()'),
',')
country_name
FROM countries;
you can try this query.
select listagg(country_name,',') within group (order by country_name) cnt
from countries;
The fastest way it is to use the Oracle collect function.
You can also do this:
select *
2 from (
3 select deptno,
4 case when row_number() over (partition by deptno order by ename)=1
5 then stragg(ename) over
6 (partition by deptno
7 order by ename
8 rows between unbounded preceding
9 and unbounded following)
10 end enames
11 from emp
12 )
13 where enames is not null
Visit the site ask tom and search on 'stragg' or 'string concatenation' . Lots of
examples. There is also a not-documented oracle function to achieve your needs.
I needed a similar thing and found the following solution.
select RTRIM(XMLAGG(XMLELEMENT(e,country_name || ',')).EXTRACT('//text()'),',') country_name from
In this example we are creating a function to bring a comma delineated list of distinct line level AP invoice hold reasons into one field for header level query:
FUNCTION getHoldReasonsByInvoiceId (p_InvoiceId IN NUMBER) RETURN VARCHAR2
IS
v_HoldReasons VARCHAR2 (1000);
v_Count NUMBER := 0;
CURSOR v_HoldsCusror (p2_InvoiceId IN NUMBER)
IS
SELECT DISTINCT hold_reason
FROM ap.AP_HOLDS_ALL APH
WHERE status_flag NOT IN ('R') AND invoice_id = p2_InvoiceId;
BEGIN
v_HoldReasons := ' ';
FOR rHR IN v_HoldsCusror (p_InvoiceId)
LOOP
v_Count := v_COunt + 1;
IF (v_Count = 1)
THEN
v_HoldReasons := rHR.hold_reason;
ELSE
v_HoldReasons := v_HoldReasons || ', ' || rHR.hold_reason;
END IF;
END LOOP;
RETURN v_HoldReasons;
END;
I have always had to write some PL/SQL for this or I just concatenate a ',' to the field and copy into an editor and remove the CR from the list giving me the single line.
That is,
select country_name||', ' country from countries
A little bit long winded both ways.
If you look at Ask Tom you will see loads of possible solutions but they all revert to type declarations and/or PL/SQL
Ask Tom
SELECT REPLACE(REPLACE
((SELECT TOP (100) PERCENT country_name + ', ' AS CountryName
FROM country_name
ORDER BY country_name FOR XML PATH('')),
'&<CountryName>', ''), '&<CountryName>', '') AS CountryNames
you can use this query to do the above task
DECLARE #test NVARCHAR(max)
SELECT #test = COALESCE(#test + ',', '') + field2 FROM #test SELECT field2= #test
for detail and step by step explanation visit the following link
http://oops-solution.blogspot.com/2011/11/sql-server-convert-table-column-data.html