Pass subquery value to IN statement - sql

In one table named prefs, I have a column named "Value" of type clob that holds this value: 'T', 'L'
I need to query table attendance_code to retrieve the records where column att_code values are either T or L. The att_code column is of type varchar2.
As a manual model query, this works fine:
SELECT id FROM attendance_code ac WHERE ac.att_code IN ('T', 'L')
Result:
ID
1 4903
2 4901
Attempt 1
SELECT id FROM attendance_code ac WHERE ac.att_code IN (SELECT value from prefs WHERE name = 'AEADS|SAR|tdyCodes')
ORA-00932: inconsistent datatypes: expected - got CLOB
00932. 00000 - "inconsistent datatypes: expected %s got %s"
Attempt 2
SELECT id FROM attendance_code ac WHERE ac.att_code IN (SELECT dbms_lob.substr(value, 4000, 1) from prefs WHERE name = 'AEADS|SAR|tdyCodes')
This yields no error, but returns no rows.
Attempt 3
(Based on https://blogs.oracle.com/aramamoo/how-to-split-comma-separated-string-and-pass-to-in-clause-of-select-statement)
select id from attendance_code ac where ac.att_code IN (
select regexp_substr(value,'[^,]+', 1, level) from prefs WHERE name = 'AEADS|SAR|tdyCodes'
connect by regexp_substr(value, '[^,]+', 1, level) is not null );
ORA-00932: inconsistent datatypes: expected - got CLOB
00932. 00000 - "inconsistent datatypes: expected %s got %s"
Attempt 4
select id from attendance_code ac where ac.att_code IN (
select regexp_substr(dbms_lob.substr(value, 4000, 1),'[^,]+', 1, level) from prefs WHERE name = 'AEADS|SAR|tdyCodes'
connect by regexp_substr(dbms_lob.substr(value, 4000, 1), '[^,]+', 1, level) is not null );
ORA-06502: PL/SQL: numeric or value error: character string buffer too small
ORA-06512: at line 1
06502. 00000 - "PL/SQL: numeric or value error%s"
*Cause: An arithmetic, numeric, string, conversion, or constraint error
occurred. For example, this error occurs if an attempt is made to
assign the value NULL to a variable declared NOT NULL, or if an
attempt is made to assign an integer larger than 99 to a variable
declared NUMBER(2).
*Action: Change the data, how it is manipulated, or how it is declared so
that values do not violate constraints.
I have also tried changing the value in the prefs table to just T,L (removing the single quotes) and running all of the above against that, to no avail.
What is the proper way to do this, please?

You can use hierarchical query with removed single quotes as follows:
SELECT ID
FROM ATTENDANCE_CODE AC
WHERE AC.ATT_CODE IN (
SELECT
REPLACE(TRIM(REGEXP_SUBSTR(
(SELECT value from prefs WHERE name = 'AEADS|SAR|tdyCodes'),
'[^,]+', 1, LEVEL)), '''', '') FROM DUAL
CONNECT BY REGEXP_SUBSTR(
(SELECT value from prefs WHERE name = 'AEADS|SAR|tdyCodes'),
'[^,]+', 1, LEVEL) IS NOT NULL
);
or simplify the above query with the CTE as following:
WITH DATAA ( VALS ) AS (
SELECT VALUE
FROM PREFS
WHERE NAME = 'AEADS|SAR|tdyCodes'
)
SELECT ID
FROM ATTENDANCE_CODE AC,
DATAA D
WHERE AC.ATT_CODE IN (
SELECT
REPLACE(TRIM(REGEXP_SUBSTR(D.VALS, '[^,]+', 1, LEVEL)), '''', '')
FROM DUAL
CONNECT BY
REGEXP_SUBSTR(D.VALS, '[^,]+', 1, LEVEL) IS NOT NULL
);
Example with Dual as following:
SQL> SELECT DUMMY FROM DUAL AC
2 WHERE
3 'T' IN (
4 SELECT
5 REPLACE(TRIM(REGEXP_SUBSTR(
6 (SELECT q'#'T', 'L'#' FROM DUAL), '[^,]+', 1, LEVEL)), '''', '')
7 FROM DUAL
8 CONNECT BY
9 REGEXP_SUBSTR((
10 SELECT q'#'T', 'L'#' FROM DUAL), '[^,]+', 1, LEVEL) IS NOT NULL
11 );
DUMMY
-------
X
SQL>
Cheers!!

Don't store delimited values in a string; put them in another table (where you can have one row per item) with a foreign key referencing to the row in the parent table (for example: db<>fiddle) or use a collection and a nested table.
An example of this second option is:
CREATE TYPE char_list IS TABLE OF CHAR(1);
Then
CREATE TABLE prefs (
id NUMBER(10,0) PRIMARY KEY,
name VARCHAR2(50) NOT NULL UNIQUE,
SchoolId NUMBER(10,0),
UserId NUMBER(10,0),
Value char_list,
YearID NUMBER(4,0)
) NESTED TABLE value STORE AS prefs__value;
So for your sample data:
INSERT INTO prefs ( id, name, schoolId, UserId, value, YearID )
VALUES ( 262806, 'AEADS|SAR|tdyCodes', 109, 0, char_list( 'T', 'L' ), 29 );
CREATE TABLE attendance_code ( id, att_code ) AS
SELECT 4903, 'L' FROM DUAL UNION ALL
SELECT 4902, 'X' FROM DUAL UNION ALL
SELECT 4901, 'T' FROM DUAL;
Your query would be:
SELECT *
FROM attendance_code
WHERE att_code MEMBER OF ( SELECT value FROM prefs WHERE name = 'AEADS|SAR|tdyCodes' );
Which outputs:
| ID |
| ---: |
| 4903 |
| 4901 |
db<>fiddle here

Related

Is there a way to explicitly give a type in a common table expression?

This is another sequel of this question:
I've to applied a polymorphic table function replacing all the null value of varchar2 by something.
In this example:
WITH
a(aa1,aa2,aa3)
AS
(SELECT 1, '2', SYSDATE FROM DUAL
UNION ALL
SELECT NULL, NULL, NULL FROM DUAL)
SELECT *
FROM TABLE (f_replace_nulls(a))
the type of aa2 is varchar2(1) because '2' is a varchar2(1).
if f_replace_nulls replace the null value by 'n/a' only 'n' will be given in dbfiddle and on my computer the following error occurs.
[Error] Execution (2: 1): ORA-62576: target size(1) is smaller than source size(3) for column(AA2) in Put Columns
code
To delete this problem I would like to explicitly mention that aa2 is a varchar2(5).
Or if there is another way to solve this problem, I would be glad to know.
You can cast a literal to a data type:
cast('2' as varchar2(5))
so:
WITH
a(aa1,aa2,aa3)
AS
(SELECT 1, cast('2' as varchar2(5)), SYSDATE FROM DUAL
UNION ALL
SELECT NULL, NULL, NULL FROM DUAL)
SELECT *
FROM TABLE (f_replace_nulls(a))
AA3
AA1
AA2
27-JUN-22
1
2
null
-1
tn/a
db<>fiddle

Multiple Sequence in SQL Oracle / Varchar2 ID with letters and digits

I am trying to 'generate' a Varchar ID that contains letters and digits too with a sequence, but I think there will be more sequences than one.
What I am looking forward to is something like this 'DJ_Digit_Digit_Letter_Letter_Letter'. An example would be DJ00WVX/DJ01HYZ/DJ99ZZZ. This is also my primary key so it would be good if I could not encounter any primary key errors.
I thought about working in ascii so I will generate numbers between 65 and 90 (A-Z) and Insert them into the column. Same for the numbers in ascii 45 to 57 or something like that. I don't know how I could use more sequences on the same column like 'letter_seq.nextvalue,digit_seq.nextvalue', i know that this example is wrong, but it was easier for me to explain it. Maybe all my thinking is wrong
In conclusion I need to get something that starts with DJ followed by 2 digits and 3 letters, all wrapped up in a primary key. It's ok to get like 20-30 unique entries, I think.
Thank you for your help!
You can create an INVISIBLE IDENTITY column and then create your letter-digit-letter sequence as a function generated from the invisible column:
CREATE TABLE table_name (
id_seq_value INT
INVISIBLE
GENERATED ALWAYS AS IDENTITY( START WITH 0 INCREMENT BY 1 MINVALUE 0 MAXVALUE 175799 )
NOT NULL
CONSTRAINT table_name__id_seq_value__u UNIQUE,
id VARCHAR2(7)
GENERATED ALWAYS AS (
CAST(
'DJ'
|| TO_CHAR( FLOOR( id_seq_value / POWER( 26, 3 ) ), 'FM00' )
|| CHR( 65 + MOD( FLOOR( id_seq_value / POWER(26, 2) ), 26 ) )
|| CHR( 65 + MOD( FLOOR( id_seq_value / POWER(26, 1) ), 26 ) )
|| CHR( 65 + MOD( id_seq_value, 26 ) )
AS VARCHAR2(7)
)
)
CONSTRAINT table_name__id__pk PRIMARY KEY,
name VARCHAR2(50)
);
Then you can do:
INSERT INTO table_name ( name ) VALUES ( 'Item1' );
INSERT INTO table_name ( name ) VALUES ( 'Item2' );
Then:
SELECT * FROM table_name;
Outputs:
ID | NAME
:------ | :----
DJ00AAA | Item1
DJ00AAB | Item2
(If you want to increment the digits and the letters together then change the IDENTITY to INCREMENT BY 17577 (263+1).)
db<>fiddle here
To me, it looks like this:
SQL> create table test (id varchar2(10));
Table created.
SQL> create sequence seq1;
Sequence created.
SQL> insert into test (id)
2 with
3 a as (select chr(64 + level) la from dual
4 connect by level <= 26),
5 b as (select chr(64 + level) lb from dual
6 connect by level <= 26),
7 c as (select chr(64 + level) lc from dual
8 connect by level <= 26)
9 select 'DJ' || lpad(seq1.nextval, 2, '0')
10 || la || lb || lc id
11 from a cross join b cross join c;
17576 rows created.
SQL>
Several sample values:
SQL> with temp as
2 (select id,
3 row_number() Over (order by id) rna,
4 row_Number() over (order by id desc) rnd
5 from test
6 )
7 select id
8 from temp
9 where rna <= 5
10 or rnd <= 5
11 order by id;
ID
----------
DJ01AAA
DJ02AAB
DJ03AAC
DJ04AAD
DJ05AAE
DJ99OUK
DJ99OUL
DJ99OUM
DJ99OUN
DJ99OUO
10 rows selected.
SQL>
However, if
It's ok to get like 20-30 unique entries, I think.
means that you need to generate at most 30 values, well, you'd easily create them manually; why would you develop any software solution for that? If you started typing, you'd be over by now.

value substitution/replacement in a string

I have a string x-y+z. The values for x, y and z will be stored in a table. Say
x 10
y 15
z 20
This string needs to be changed like 10-15+20.
Anyway I can achieve this using plsql or sql?
using simple Pivot we can do
DECLARE #Table1 TABLE
( name varchar(1), amount int )
;
INSERT INTO #Table1
( name , amount )
VALUES
('x', 10),
('y', 15),
('Z', 25);
Script
Select CAST([X] AS VARCHAR) +'-'+CAST([Y] AS VARCHAR)+'+'+CAST([Z] AS VARCHAR) from (
select * from #Table1)T
PIVOT (MAX(amount) FOR name in ([X],[y],[z]))p
An approach could be the following, assuming a table like this:
create table stringToNumbers(str varchar2(16), num number);
insert into stringToNumbers values ('x', 10);
insert into stringToNumbers values ('y', 20);
insert into stringToNumbers values ('zz', 30);
insert into stringToNumbers values ('w', 40);
First tokenize your input string with something like this:
SQL> with test as (select 'x+y-zz+w' as string from dual)
2 SELECT 'operand' as typ, level as lev, regexp_substr(string, '[+-]+', 1, level) as token
3 FROM test
4 CONNECT BY regexp_instr(string, '[a-z]+', 1, level+1) > 0
5 UNION ALL
6 SELECT 'value', level, regexp_substr(string, '[^+-]+', 1, level) as token
7 FROM test
8 CONNECT BY regexp_instr(string, '[+-]', 1, level - 1) > 0
9 order by lev asc, typ desc;
TYP LEV TOKEN
------- ---------- --------------------------------
value 1 x
operand 1 +
value 2 y
operand 2 -
value 3 zz
operand 3 +
value 4 w
In the example I used lowercase literals and only +/- signs; you can easily edit it to handle something more complex; also, I assume the input string is well-formed.
Then you can join your decoding table to the tokenized string, building the concatenation:
SQL> select listagg(nvl(to_char(num), token)) within group (order by lev asc, typ desc)
2 from (
3 with test as (select 'x+y-zz+w' as string from dual)
4 SELECT 'operand' as typ, level as lev, regexp_substr(string, '[+-]+', 1, level) as token
5 FROM test
6 CONNECT BY regexp_instr(string, '[a-z]+', 1, level+1) > 0
7 UNION ALL
8 SELECT 'value', level, regexp_substr(string, '[^+-]+', 1, level) as token
9 FROM test
10 CONNECT BY regexp_instr(string, '[+-]', 1, level - 1) > 0
11 order by lev asc, typ desc
12 ) tokens
13 LEFT OUTER JOIN stringToNumbers on (str = token);
LISTAGG(NVL(TO_CHAR(NUM),TOKEN))WITHINGROUP(ORDERBYLEVASC,TYPDESC)
--------------------------------------------------------------------------------
10+20-30+40
This assumes that every literal in you input string has a corrensponding value in table. You can even handle the case of strings with no corrensponding number, for example assigning 0:
SQL> select listagg(
2 case
3 when typ = 'operand' then token
4 else to_char(nvl(num, 0))
5 end
6 ) within group (order by lev asc, typ desc)
7 from (
8 with test as (select 'x+y-zz+w-UNKNOWN' as string from dual)
9 SELECT
.. ...
16 ) tokens
17 LEFT OUTER JOIN stringToNumbers on (str = token);
LISTAGG(CASEWHENTYP='OPERAND'THENTOKENELSETO_CHAR(NVL(NUM,0))END)WITHINGROUP(ORD
--------------------------------------------------------------------------------
10+20-30+40-0
Create a function like this:
create table ttt1
( name varchar(1), amount int )
;
INSERT INTO ttt1 VALUES ('x', 10);
INSERT INTO ttt1 VALUES ('y', 15);
INSERT INTO ttt1 VALUES ('z', 25);
CREATE OR REPLACE FUNCTION replace_vars (in_formula VARCHAR2)
RETURN VARCHAR2
IS
f VARCHAR2 (2000) := UPPER (in_formula);
BEGIN
FOR c1 IN ( SELECT UPPER (name) name, amount
FROM ttt1
ORDER BY name DESC)
LOOP
f := REPLACE (f, c1.name, c1.amount);
END LOOP;
return f;
END;
select replace_vars('x-y+z') from dual
Here's another way to approach the problem that attempts to do it all in SQL. While not necessarily the most flexible or fastest, maybe you can get some ideas from another way to approach the problem. It also shows a way to execute the final formula to get the answer. See the comments below.
Assumes all variables are present in the variable table.
-- First build the table that holds the values. You won't need to do
-- this if you already have them in a table.
with val_tbl(x, y, z) as (
select '10', '15', '20' from dual
),
-- Table to hold the formula.
formula_tbl(formula) as (
select 'x-y+z' from dual
),
-- This table is built from a query that reads the formula a character at a time.
-- When a variable is found using the case statement, it is queried in the value
-- table and it's value is returned. Otherwise the operator is returned. This
-- results in a row for each character in the formula.
new_formula_tbl(id, new_formula) as (
select level, case regexp_substr(formula, '(.|$)', 1, level, NULL, 1)
when 'x' then
(select x from val_tbl)
when 'y' then
(select y from val_tbl)
when 'z' then
(select z from val_tbl)
else regexp_substr(formula, '(.|$)', 1, level, NULL, 1)
end
from formula_tbl
connect by level <= regexp_count(formula, '.')
)
-- select id, new_formula from new_formula_tbl;
-- This puts the rows back into a single string. Order by id (level) to keep operands
-- and operators in the right order.
select listagg(new_formula) within group (order by id) formula
from new_formula_tbl;
FORMULA
----------
10-15+20
Additionally you can get the result of the formula by passing the listagg() call to the following xmlquery() function:
select xmlquery(replace( listagg(new_formula) within group (order by id), '/', ' div ')
returning content).getNumberVal() as result
from new_formula_tbl;
RESULT
----------
15

Oracle Query to select separate alphabet+numbers and numbers after special character

I am writing a query in oracle where I have requirement to separate alphabet+numbers and numbers after special character as 2 different columns
Eg.
Colum Value is
ABC 123#78800,XYZ#4666,PQR 444#9900
Output Required
Column 1 : ABC 123,XYZ,PQR 444
Column 2 : 78800,4666, 9900
I tried following query:
select TRANSLATE('ABC 123#78800,XYZ#4666,PQR 444#9900 ','ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#','ABCDEFGHIJKLMNOPQRSTUVWXYZ') from dual.
Output is "ABC ,XYZ,PQR " numbers are missing
What you need is regex
with table_name as
(
select 'ABC 123#78800,XYZ#4666,PQR 444#9900' col_name from dual
)
select
REGEXP_REPLACE(col_name,'#([^#,])*', null) alphabet_num
,REGEXP_REPLACE(col_name,'([^#,])*#', null) num_value
from
table_name;
Editted: Remove some redundant character as suggested by MTO
Oracle Setup:
CREATE TABLE table_name ( col VARCHAR2(100) );
INSERT INTO table_name VALUES ( 'ABC 123#78800,XYZ#4666,PQR 444#9900' );
CREATE TYPE key_value_pair AS OBJECT(
key VARCHAR2(20),
value NUMBER(10)
);
/
CREATE TYPE key_value_pair_table AS TABLE of key_value_pair;
/
Query:
SELECT *
FROM table_name t,
TABLE(
CAST(
MULTISET(
SELECT REGEXP_SUBSTR( t.col, '(^|,)([^,#]+)#(\d+)', 1, LEVEL, NULL, 2 ),
TO_NUMBER(
REGEXP_SUBSTR( t.col, '(^|,)([^,#]+)#(\d+)', 1, LEVEL, NULL, 3 )
)
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT( t.col, '(^|,)([^,#]+)#(\d+)' )
)
AS key_value_pair_table
)
);
Output:
COL KEY VALUE
----------------------------------- ------- -----
ABC 123#78800,XYZ#4666,PQR 444#9900 ABC 123 78800
ABC 123#78800,XYZ#4666,PQR 444#9900 XYZ 4666
ABC 123#78800,XYZ#4666,PQR 444#9900 PQR 444 9900

I have two tables with different dimension on column

I have two table with different dimension on one column like this:
name varchar2(256 char)
and in table where I want to insert is
lastname varchar2(50 byte)
When I compile the next code:
INSERT INTO TCPD_PERSONEL_COPY
( FIRSTNAME,LASTNAME, EMAIL1)
SELECT NAME,GIVEN_NAME, MAIL
FROM glob
WHERE ( NOT EXISTS (SELECT EMAIL1
FROM tlp
WHERE GLOB.mail = TLP.email1 )
AND GLOB.mail IS NOT NULL )
OR ( NOT EXISTS (SELECT userid
FROM TLP
WHERE GLOB.LOGIN = TLP.userid )
AND GLOB.mail IS NULL )
AND GLOB.COUNTRY='France';
I have the next error:
SQL Error: ORA-12899: value too large for column "TEST"."TLP"."LASTNAME" (actual: 53, maximum: 50)
12899. 00000 - "value too large for column %s (actual: %s, maximum: %s)"
My question is: How I can do this insert possible, having in mind the fact as I don't have the permission to modify the structure of column with ALTER.
Well, you cannot insert 256 characters into a 50 character field, obviously. If you can't alter the table and are willing to live with the data loss, you could truncate the column's value using substr:
INSERT INTO TCPD_PERSONEL_COPY
( FIRSTNAME,LASTNAME, DEPARTMENT, TELEPHONE1, EMAIL1, USERID, DATECREATED)
SELECT SUBSTR(NAME, 1, 50), GIVEN_NAME, DEPARTMENT, PHONE, MAIL, LOGIN, SYSDATE
FROM gal
WHERE ( NOT EXISTS (SELECT EMAIL1
FROM TCPD_PERSONEL_COPY
WHERE GAL.mail = TCPD_PERSONEL_COPY.email1 )
AND GAL.mail IS NOT NULL )
OR ( NOT EXISTS (SELECT userid
FROM TCPD_PERSONEL_COPY
WHERE gAL.LOGIN = TCPD_PERSONEL_COPY.userid )
AND GAL.mail IS NULL )
AND GAL.COUNTRY='France';