Group By Expression But Get the Specific Record - sql

this is my query
SELECT TBL.PREFIX,
TBL.BASE,
TBL.SUFFIX,
TBL.CONTROL,
LISTAGG (TBL.QT_PART_USAGE, ',') AS QT_PART_USAGE,
LISTAGG (TBL.ADDORDELETE, ',') AS COMESFROM,
CASE
WHEN COUNT (*) > 1 THEN 'QT'
ELSE LISTAGG (TBL.ADDORDELETE, ',')
END AS ADDORDELETE
FROM TBL
GROUP BY TBL.CONTROL,
TBL.PREFIX,
TBL.BASE,
TBL.SUFFIX
in TBL, I use a complicated select query but it's not so important so I represent it as 'TBL'
this query returns something like that:
all I want to do is:
if 'ADDORDELETE' = 'QT'
then I want to get the QT_PART_USAGE from the record 'COMESFROM' = 'ADD' respectively
example:
QT_PART_USAGE - COMESFROM -ADDORDELETE
7,9 -ADD,DELETE -QT
I want to get
QT_PART_USAGE - COMESFROM -ADDORDELETE
7 -'WHATEVER' -QT
or
QT_PART_USAGE - COMESFROM -ADDORDELETE
1,3 -DELETE,ADD -QT
I want to get
QT_PART_USAGE -COMESFROM -ADDORDELETE
3 -'WHATEVER' -QT
depending on the sequence of the ADD in the COMESFROM column,
(DELETE,ADD) (1,3) then QT_PART_USAGE = 3
(ADD,DELETE) (7,9) then QT_PART_USAGE = 7
I want to get the QT_PART_USAGE value respect to it's index in the COMESFROM column.
May you guys help me? I appreciate a lot, thank you.
I tried getting the value with splitting the COMESFROM with ',' and get the index of the 'ADD' then respect to this index I tried to get the value from QT_PART_USAGE column but I know it's not optimal solution and so challenging. Also inner select queries make the query slow so I want to ask it here.

You could try it like here:
Select
QT_PART_USAGE "QT_PART_USAGE",
--
CASE WHEN ADDORDELETE != 'QT' THEN QT_PART_USAGE
ELSE
CASE WHEN InStr(COMES_FROM, 'ADD', 1, 1) = 1 THEN SubStr(QT_PART_USAGE, 1, InStr(QT_PART_USAGE, ',', 1, 1) -1 )
WHEN InStr(COMES_FROM, 'ADD', 1, 1) > 1 THEN SubStr(QT_PART_USAGE, InStr(QT_PART_USAGE, ',', 1, 1) + 1 )
END
END "QT_PART_USAGE_CHANGED",
COMES_FROM "COMES_FROM",
ADDORDELETE "ADDORDELETE"
From
sample_data
I left your original QT_PART_USAGE colum intact and created a new, CHANGED one, so you can see them both. With your sample data:
WITH
sample_data AS
(
Select '1' "QT_PART_USAGE", 'ADD' "COMES_FROM", 'ADD' "ADDORDELETE" From Dual Union All
Select '1' "QT_PART_USAGE", 'ADD' "COMES_FROM", 'ADD' "ADDORDELETE" From Dual Union All
Select '1' "QT_PART_USAGE", 'ADD' "COMES_FROM", 'ADD' "ADDORDELETE" From Dual Union All
Select '.00025,1' "QT_PART_USAGE", 'DELETE,ADD' "COMES_FROM", 'QT' "ADDORDELETE" From Dual Union All
Select '2' "QT_PART_USAGE", 'DELETE' "COMES_FROM", 'DELETE' "ADDORDELETE" From Dual Union All
Select '2' "QT_PART_USAGE", 'ADD' "COMES_FROM", 'ADD' "ADDORDELETE" From Dual Union All
--
Select '3' "QT_PART_USAGE", 'ADD' "COMES_FROM", 'ADD' "ADDORDELETE" From Dual Union All
Select '1' "QT_PART_USAGE", 'DELETE' "COMES_FROM", 'DELETE' "ADDORDELETE" From Dual Union All
Select '7,9' "QT_PART_USAGE", 'ADD,DELETE' "COMES_FROM", 'QT' "ADDORDELETE" From Dual Union All
Select '1' "QT_PART_USAGE", 'ADD' "COMES_FROM", 'ADD' "ADDORDELETE" From Dual Union All
Select '1' "QT_PART_USAGE", 'ADD' "COMES_FROM", 'ADD' "ADDORDELETE" From Dual Union All
Select '2' "QT_PART_USAGE", 'ADD' "COMES_FROM", 'ADD' "ADDORDELETE" From Dual
)
... it should return this ...
QT_PART_USAGE QT_PART_USAGE_CHANGED COMES_FROM ADDORDELETE
------------- --------------------- ---------- -----------
1 ADD ADD
1 ADD ADD
1 ADD ADD
.00025,1 1 DELETE,ADD QT
2 DELETE DELETE
2 ADD ADD
3 ADD ADD
1 DELETE DELETE
7,9 7 ADD,DELETE QT
1 ADD ADD
1 ADD ADD
2 ADD ADD
NOTE: This will not work if there are three or more elements. Also, if there could be spaces within the string (QT_PART_USAGE) you should get ridd of them before getting substrings...

Related

How do I select rows from table that have one or more than one specific value in a column?

I have a table containing data such as:
BP_NUMBER,CONTRACT_TYPE
0000123, 1
0000123, 2
0000123, 3
0000123, 4
0000124, 4
0000124, 4
0000124, 4
0000125, 4
0000126, 1
0000126, 5
I want to select rows containing one or more occurrences of CONTRACT_TYPE = 4. In other words, I want to know who are the clients with one or more contracts of the same type and type 4.
I tried this query:
SELECT * FROM (
SELECT BP_NUMBER, CONTRACT_TYPE, COUNT(*) OVER (PARTITION BY BP_NUMBER) CT FROM CONTRACTS
WHERE (1=1)
AND DATE = '18/10/2022'
AND CONTRACT_TYPE = 4)
WHERE CT= 1;
But it returns rows with only one occurrence of CONTRACT_TYPE = 4.
Also tried something like:
SELECT BP_NUMBER FROM CONTRACTS
WHERE (1=1)
AND CONTRACT_TYPE = 4
AND CONTRACT_TYPE NOT IN (SELECT CONTRACT_TYPE FROM CONTRACTS WHERE CONTRACT_TYPE != 4 GROUP BY CONTRACT_TYPE);
Trying to avoid any other contract types than 4. I really don't understand why it doesn't work.
The expected result would be:
0000124 --(4 occurrences of type 4)
0000125 --(1 occurrence of type 4)
Any help? Thanks
You can try something like this:
SELECT
BP_NUMBER
FROM CONTRACTS c1
WHERE CONTRACT_TYPE = 4
AND NOT EXISTS
(SELECT 1 FROM CONTRACTS c2 WHERE c2.BP_NUMBER = c1.BP_NUMBER
AND c2.CONTRACT_TYPE <> c1.CONTRACT_TYPE)
Depending on how you actually want to see it (and what other values you might want to include), you could either do a DISTINCT on the BP_NUMBER, or group on that column (and potentially others)
A similar result could also be achieved using an outer join between two instances of the CONTRACTS table. Essentially, you need the second instance of the same table so that you can exclude output rows when there are records with the "unwanted" contract types
You can just do the aggregation like here:
WITH
tbl AS
(
Select '0000123' "BP_NUMBER", '1' "CONTRACT_TYPE" From Dual Union All
Select '0000123', '2' From Dual Union All
Select '0000123', '3' From Dual Union All
Select '0000123', '4' From Dual Union All
Select '0000124', '4' From Dual Union All
Select '0000124', '4' From Dual Union All
Select '0000124', '4' From Dual Union All
Select '0000125', '4' From Dual Union All
Select '0000126', '1' From Dual Union All
Select '0000126', '5' From Dual
)
Select
BP_NUMBER "BP_NUMBER",
Count(*) "OCCURENCIES"
From
tbl
WHERE CONTRACT_TYPE = '4'
GROUP BY BP_NUMBER
ORDER BY BP_NUMBER
--
-- R e s u l t :
--
-- BP_NUMBER OCCURENCIES
-- --------- -----------
-- 0000123 1
-- 0000124 3
-- 0000125 1

Extract a desired character from an alphanumeric column using concise REGEX in Oracle SQL

I am going through a "clean-up" process of a character column that holds employee rankings that should have a single character: 0-5 or M or U but instead it has up to 3 characters that are either numeric, alpha or alphanumeric.
I spent the better part of two days researching online (Regex, Stack overflow and Oracle resources) and testing and trying various options and the below "RESULT" (refer to code) is what I came up with. It does the job but I can't help but think that there is a more concise way of doing this. For example, at one point I thought I was close to accomplishing the task with a single instance of REGEXP_SUBSTR which used "|" (refer to PREV_TRY in code below). But then I couldn't figure out how to take this result and extract from it the first character for cases [1-4]{1}[ABCUL]{1} or the last character for all other cases.
Here is a reproducible example along with the solution I have so far:
WITH T AS (
SELECT 'M' EX FROM DUAL UNION ALL
SELECT 'U' FROM DUAL UNION ALL
SELECT '1A' FROM DUAL UNION ALL --some two character values are [1-4]{1}[ABCUL]{1}
SELECT TO_CHAR(ROWNUM) FROM DUAL CONNECT BY LEVEL <= 7 UNION ALL
SELECT '0' FROM DUAL UNION ALL
SELECT '113' FROM DUAL UNION ALL--if its numeric it can be 0-999
SELECT '03' FROM DUAL UNION ALL--some two character values are 0[1-4]{1}
SELECT '99' FROM DUAL UNION ALL
SELECT 'RG1' FROM DUAL UNION ALL--some three character values are RG[1-4]{1}
SELECT 'NA' FROM DUAL UNION ALL--some values are 'NA' or 'N/A'
SELECT null FROM DUAL --there are null values
)
SELECT EX
,NVL(SUBSTR(
NVL(
SUBSTR(REGEXP_SUBSTR(EX,'(^(0|RG)?[0-5MU]?$)'),-1,1)
,REGEXP_SUBSTR(EX,'^[1-4]{1}[ABCUL]{1}'))
,1,1),0) RESULT --what I came up with so far
,NVL(REGEXP_SUBSTR(EX,'(^(0|RG)?[0-5MU]?$)|(^[1-4]{1}[ABCUL]{1})'),0) PREV_TRY
FROM T
I summarize what I need to accomplish with these rules:
if its a single character then return any character that matches
[0-5MU].
if its a single digit followed by a single alpha character then
return the digit [1-4]{1}[ABCUL]{1}. E.g., '2A' returns '2'
if its RG[1-4] then return the digit. E.g., 'RG3' returns '3'
if its 0[1-4] then return the second digit. E.g., '03' returns '3'
all else return 0
You can use:
WITH T (ex)AS (
SELECT 'M' FROM DUAL UNION ALL
SELECT 'U' FROM DUAL UNION ALL
SELECT '1A' FROM DUAL UNION ALL --some two character values are [1-4]{1}[ABCUL]{1}
SELECT TO_CHAR(ROWNUM) FROM DUAL CONNECT BY LEVEL <= 7 UNION ALL
SELECT '0' FROM DUAL UNION ALL
SELECT '113' FROM DUAL UNION ALL--if its numeric it can be 0-999
SELECT '03' FROM DUAL UNION ALL--some two character values are 0[1-4]{1}
SELECT '99' FROM DUAL UNION ALL
SELECT 'RG1' FROM DUAL UNION ALL--some three character values are RG[1-4]{1}
SELECT 'NA' FROM DUAL UNION ALL--some values are 'NA' or 'N/A'
SELECT null FROM DUAL --there are null values
)
SELECT ex,
COALESCE(
REGEXP_REPLACE(
ex,
'^([0-5MU])$|^(\d)[A-Z]$|^(RG|0)([1-4])$|^.*$',
'\1\2\4'
),
'0'
) AS replacement
FROM t
Which outputs:
EX
REPLACEMENT
M
M
U
U
1A
1
1
1
2
2
3
3
4
4
5
5
6
0
7
0
0
0
113
0
3
3
99
0
RG1
1
NA
0
<null>
0
db<>fiddle here

Comparing 2 lists in Oracle

I have 2 lists which I need to compare. I need to find if at least one element from List A is found in List B. I know IN doesn't work with 2 lists. What are my other options?
Basically something like this :
SELECT
CASE WHEN ('A','B','C') IN ('A','Z','H') THEN 1 ELSE 0 END "FOUND"
FROM DUAL
Would appreciate any help!
You are probably looking for something like this. The WITH clause is there just to simulate your "lists" (whatever you mean by that); they are not really part of the solution. The query you need is just the last three lines (plus the semicolon at the end).
with
first_list (str) as (
select 'A' from dual union all
select 'B' from dual union all
select 'C' from dual
),
second_list(str) as (
select 'A' from dual union all
select 'Z' from dual union all
select 'H' from dual
)
select case when exists (select * from first_list f join second_list s
on f.str = s.str) then 1 else 0 end as found
from dual
;
FOUND
----------
1
In Oracle you can do:
select
count(*) as total_matches
from table(sys.ODCIVarchar2List('A', 'B', 'C')) x,
table(sys.ODCIVarchar2List('A', 'Z', 'H')) y
where x.column_value = y.column_value;
You need to repeat the conditions:
SELECT (CASE WHEN 'A' IN ('A', 'Z', 'H') OR
'B' IN ('A', 'Z', 'H') OR
'C' IN ('A', 'Z', 'H')
THEN 1 ELSE 0
END) as "FOUND"
FROM DUAL
If you are working with collection of String you can try Multiset Operators.
create type coll_of_varchar2 is table of varchar2(4000);
and:
-- check if exits
select * from dual where cardinality (coll_of_varchar2('A','B','C') multiset intersect coll_of_varchar2('A','Z','H')) > 0;
-- list of maching elments
select * from table(coll_of_varchar2('A','B','C') multiset intersect coll_of_varchar2('A','Z','H'));
Additionally:
-- union of elemtns
select * from table(coll_of_varchar2('A','B','C') multiset union distinct coll_of_varchar2('A','Z','H'));
select * from table(coll_of_varchar2('A','B','C') multiset union all coll_of_varchar2('A','Z','H'));
-- eelemnt from col1 not in col2
select * from table(coll_of_varchar2('A','A','B','C') multiset except all coll_of_varchar2('A','Z','H'));
select * from table(coll_of_varchar2('A','A','B','C') multiset except distinct coll_of_varchar2('A','Z','H'));
-- check if col1 is subset col2
select * from dual where coll_of_varchar2('B','A') submultiset coll_of_varchar2('A','Z','H','B');
I am trying to do something very similar but the first list is another field on the same query created with listagg and containing integer numbers like:
LISTAGG(my_first_list,', ') WITHIN GROUP(
ORDER BY
my_id
) my_first_list
and return this with all the other fields that I am already returning
SELECT
CASE WHEN my_first_list IN ('1,2,3') THEN 1 ELSE 0 END "FOUND"
FROM DUAL

Oracle traverse tree upto specific node

How to traverse tree from some child nodes up to specific node in Oracle (not to root node)?
We have input: a specific node A, a list child nodes.
Output expected: subtree from node A to all the child node of A in the input list.
We have write some query, but wonder if we could have a better way to do that.
Thank for any help!
WITH table_name AS
(
SELECT '1' AS code, ' ' AS code_ct FROM dual UNION ALL
SELECT '2', ' ' FROM dual UNION ALL
SELECT '11', '1' FROM dual UNION ALL
SELECT '12', '1' FROM dual UNION ALL
SELECT '111', '11' FROM dual UNION ALL
SELECT '112', '11' FROM dual UNION ALL
SELECT '1111', '111' FROM dual UNION ALL
SELECT '1112', '111' FROM dual UNION ALL
SELECT '1113', '111' FROM dual UNION ALL
SELECT '1114', '111' FROM dual UNION ALL
SELECT '1115', '111' FROM dual UNION ALL
SELECT '21', '2' FROM dual UNION ALL
SELECT '211', '21' FROM dual UNION ALL
SELECT '22', '2' FROM dual
)
--Current query that give expected output
SELECT *
FROM table_name
START WITH code IN ('1112', '1114', '1115', '211')
CONNECT BY PRIOR code_ct = code
INTERSECT
SELECT *
FROM table_name
START WITH code = '11'
CONNECT BY PRIOR code = code_ct;
/*
--test improving query
SELECT SUBSTR(SYS_CONNECT_BY_PATH(code , '_'), 2) AS path
FROM table_name
WHERE code = '11'
START WITH code IN ('1112', '1114', '1115', '221')
CONNECT BY PRIOR code_ct = code;
*/
Use:
SELECT distinct *
FROM table_name
START WITH code IN ('1112', '1114', '1115', '221')
CONNECT BY PRIOR code_ct = code and prior code <> '11'
order by 1;
Since INTERSECT operator removes duplicates from the final resultset, then DISTINCT must be used in order to get the same result as from the query with intersect.

How to add a space to an existing string in Oracle character functions without using regular expressions

I have a field as name in a table with names inserted without spaces. Eg: "MarkJones".
Now I want to create a space between the first and lastname of a person within the same column to be displayed as "Mark Jones" using Oracle functions.
I have tried this query
SELECT instr('MarkJones', '%||Upper(*)||%') AS substr1,
SUBSTR('MarkJones', instr('MarkJones', '%lower(*)upper(*)%')) AS substr2,
substr1||' '||substr2
FROM dual
;
However, this query is not working. I want to try it using oracle functions including translate, substr and instr, but no regular expressions.
This approach works for the simple example given, but fails if the name has more than 2 uppercase letters in it. If this is coursework as expected, maybe the requirements are not too difficult for the names to parse as we all know that is fraught with heartache and you can never account for 100% of names from all nationalities.
Anyway my approach was to move through the string looking for uppercase letters and if found replace them with a space followed by the letter. I used the ASCII function to test their ascii value to see if they were an uppercase character. The CONNECT BY construct (needed to loop through each character of the string) returns each character in its own row so LISTAGG() was employed to reassemble back into a string and ltrim to remove the leading space.
I suspect if this is coursework it may be using some features you should not be using yet. At least you should get out of this the importance of receiving and/or giving complete specifications!
SQL> with tbl(name) as (
select 'MarkJones' from dual
)
select ltrim(listagg(case
when ascii(substr(name, level, 1)) >= 65 AND
ascii(substr(name, level, 1)) <= 90 THEN
' ' || substr(name, level, 1)
else substr(name, level, 1)
end, '')
within group (order by level)) fixed
from tbl
connect by level <= length(name);
FIXED
------------------------------------
Mark Jones
When you are ready, here's the regexp_replace version anyway :-)
Find and "remember" the 2nd occurrence of an uppercase character then replace it with a space and the "remembered" uppercase character.
SQL> with tbl(name) as (
select 'MarkJones' from dual
)
select regexp_replace(name, '([A-Z])', ' \1', 1, 2) fixed
from tbl;
FIXED
----------
Mark Jones
Not sure we should go against #Alex Poole advice, but it looks like an homework assignment.
So my idea is to point the second Upper Case. Its doable if you create a set of the upper cases, on which you valuate the position in input string iStr. Then if you're allowed to use length, you can use this position to build firstName too:
SELECT substr(iStr, 1, length(iStr)-length(substr(iStr, instr(iStr, u)))) firstName
, substr(iStr, instr(iStr, u)) lastName
, substr(iStr, 1, length(iStr)-length(substr(iStr, instr(iStr, u)))) ||' '||
substr(iStr, instr(iStr, u)) BINGO
FROM ( select 'MarkJones' iStr from dual
union all select 'SomeOtherNames' from dual -- 2 u-cases gives 2 different results
union all select 'SomeOtherOols' from dual -- only one result
union all select 'AndJim' from dual
union all select 'JohnLenon' from dual
union all select 'LemingWay' from dual
),
( select 'A' U from dual
union all select 'B' from dual
union all select 'C' from dual
union all select 'D' from dual
union all select 'E' from dual
union all select 'F' from dual
union all select 'G' from dual
union all select 'H' from dual
union all select 'I' from dual
union all select 'J' from dual
union all select 'K' from dual
union all select 'L' from dual
union all select 'M' from dual
union all select 'N' from dual
union all select 'O' from dual
union all select 'P' from dual
union all select 'Q' from dual
union all select 'R' from dual
union all select 'S' from dual
union all select 'T' from dual
union all select 'U' from dual
union all select 'V' from dual
union all select 'W' from dual
union all select 'X' from dual
union all select 'Y' from dual
union all select 'Z' from dual
) upper_cases
where instr(iStr, U) > 1
;