Related
I have a oracle database where foreign key is enabled and I have tables A,B,C,D,E,F,AB,ABC,EF
Attaching the picture for hierarchy
'B' has parent 'D',
'AB' has parent 'A' as well as 'B',
'ABC' has parent 'AB' and 'C',
'EF' has parent 'E' & 'F',
Now {A,B,C,D,AB, ABC} has no link with {E,F,EF}. I have 2 requirements
I need to group them as group 1 & 2
I need to assign a hierarchy level for each group maintaining referential integrity as follows
How can I do it using sql/pl/sql in oracle database?
Assuming that you always end up at a single leaf node for each group then you can use a hierarchical query twice: from leaf-to-root to determine the groups; and then from root-to-leaf to determine the depths.
WITH groups ( child, parent, grp ) AS (
SELECT child,
parent,
DENSE_RANK() OVER ( ORDER BY CONNECT_BY_ROOT( child ) )
FROM table_name t
START WITH child NOT IN ( SELECT parent FROM table_name )
CONNECT BY PRIOR parent = child
),
depths ( child, parent, grp, depth ) AS (
SELECT child, parent, grp, LEVEL
FROM groups
START WITH parent NOT IN ( select child FROM table_name )
CONNECT BY PRIOR child = parent
)
SELECT table_name,
grp,
MAX( depth + depth_modifier ) AS depth
FROM depths
UNPIVOT ( table_name FOR depth_modifier IN ( child AS 1, parent AS 0 ) )
GROUP BY grp, table_name
ORDER BY grp, depth, table_name
Which, for your sample data:
CREATE TABLE table_name ( child, parent ) AS
SELECT 'B', 'D' FROM DUAL UNION ALL
SELECT 'AB', 'A' FROM DUAL UNION ALL
SELECT 'AB', 'B' FROM DUAL UNION ALL
SELECT 'ABC', 'AB' FROM DUAL UNION ALL
SELECT 'ABC', 'C' FROM DUAL UNION ALL
SELECT 'EF', 'E' FROM DUAL UNION ALL
SELECT 'EF', 'F' FROM DUAL;
Outputs:
TABLE_NAME | GRP | DEPTH
:--------- | --: | ----:
A | 1 | 1
C | 1 | 1
D | 1 | 1
B | 1 | 2
AB | 1 | 3
ABC | 1 | 4
E | 2 | 1
F | 2 | 1
EF | 2 | 2
If you don't always have a single leaf node for each group then you'll need a MUCH more complicated solution (probably written in PL/SQL).
db<>fiddle here
This question already has answers here:
How to convert comma separated values to rows in oracle?
(6 answers)
Closed 2 years ago.
I have over 10,000 records in class_period table. When I run the query shown below, it takes too much time to fetch the data.
Can you please help me - how can I speed up the query?
WITH DATA AS
( SELECT distinct class_time , class_id
from class_period
)
SELECT distinct class_id, trim(regexp_substr(class_time, '[^:]+', 1, LEVEL)) class_time
FROM DATA
CONNECT BY regexp_substr(class_time , '[^:]+', 1, LEVEL) IS NOT NULL
sample data attached as image
enter image description here
required data attached as image
enter image description here
i am using oracle 11g.
Fix your query so you don't need to use DISTINCT. The problem with your method is that you are using a hierarchical query with multiple rows of input and no way of correlating each level of the hierarchy to the previous level so the query will correlate it to ALL the items at the previous level of the hierarchy and you will get exponentially more and more duplicate rows generated at each depth. This is incredibly inefficient.
Change from using regular expressions to simple string functions.
Instead you can use:
WITH bounds ( class_id, class_time, start_pos, end_pos ) AS (
SELECT class_id,
class_time,
1,
INSTR( class_time, ':', 1 )
FROM data
UNION ALL
SELECT class_id,
class_time,
end_pos + 1,
INSTR( class_time, ':', end_pos + 1 )
FROM bounds
WHERE end_pos > 0
)
SELECT class_id,
CASE end_pos
WHEN 0
THEN SUBSTR( class_time, start_pos )
ELSE SUBSTR( class_time, start_pos, end_pos - start_pos )
END AS class_time
FROM bounds;
Which, for the sample data:
CREATE TABLE data ( class_id, class_time ) AS
SELECT 1, '0800AM:0830AM' FROM DUAL UNION ALL
SELECT 1, '0900AM' FROM DUAL UNION ALL
SELECT 2, '0830AM:0900AM:0930AM' FROM DUAL UNION ALL
SELECT 2, '1000AM' FROM DUAL;
Outputs:
CLASS_ID | CLASS_TIME
-------: | :---------
1 | 0800AM
1 | 0900AM
2 | 0830AM
2 | 1000AM
1 | 0830AM
2 | 0900AM
2 | 0930AM
db<>fiddle here
However, an even better method would be to change your model for storing the data and stop storing it as a delimited string and instead store it in a separate table or, maybe, as a collection in a nested table.
An example using a second table is:
CREATE TABLE data (
class_id NUMBER PRIMARY KEY
);
CREATE TABLE class_times (
class_id NUMBER REFERENCES data ( class_id ),
class_time VARCHAR2(6)
);
INSERT ALL
INTO data ( class_id ) VALUES ( 1 )
INTO data ( class_id ) VALUES ( 2 )
INTO class_times ( class_id, class_time ) VALUES ( 1, '0800AM' )
INTO class_times ( class_id, class_time ) VALUES ( 1, '0830AM' )
INTO class_times ( class_id, class_time ) VALUES ( 1, '0900AM' )
INTO class_times ( class_id, class_time ) VALUES ( 2, '0830AM' )
INTO class_times ( class_id, class_time ) VALUES ( 2, '0900AM' )
INTO class_times ( class_id, class_time ) VALUES ( 2, '0930AM' )
INTO class_times ( class_id, class_time ) VALUES ( 2, '1000AM' )
SELECT * FROM DUAL;
Then your query would be (assuming that you need other columns from data alongside the class_id):
SELECT d.class_id,
c.class_time
FROM data d
INNER JOIN class_times c
ON ( d.class_id = c.class_id );
Which outputs:
CLASS_ID | CLASS_TIME
-------: | :---------
1 | 0800AM
1 | 0830AM
1 | 0900AM
2 | 0830AM
2 | 0900AM
2 | 0930AM
2 | 1000AM
An example using a nested table is:
CREATE TYPE stringlist IS TABLE OF VARCHAR2(6);
CREATE TABLE data (
class_id NUMBER,
class_time stringlist
) NESTED TABLE class_time STORE AS data__class_time;
INSERT INTO data ( class_id, class_time )
SELECT 1, stringlist( '0800AM','0830AM' ) FROM DUAL UNION ALL
SELECT 1, stringlist( '0900AM' ) FROM DUAL UNION ALL
SELECT 2, stringlist( '0830AM','0900AM','0930AM' ) FROM DUAL UNION ALL
SELECT 2, stringlist( '1000AM' ) FROM DUAL;
Then your query would become:
SELECT d.class_id,
ct.COLUMN_VALUE AS class_time
FROM data d
CROSS APPLY TABLE ( d.class_time ) ct
Which outputs:
CLASS_ID | CLASS_TIME
-------: | :---------
1 | 0800AM
1 | 0830AM
1 | 0900AM
2 | 0830AM
2 | 0900AM
2 | 0930AM
2 | 1000AM
db<>fiddle here
MT0 spotted the big problem of your connect by filter allowing all rows to be read. You don't need to convert it to a recursive CTE, since you're already distincting all the columns you're projecting, that can be treated as your primary key (assuming it's not nullable or you don't want the null values).
You also need a special filter so that it doesn't get confused into thinking you've got an infinite loop.
WITH DATA AS
( SELECT distinct class_time , class_id
from class_period
)
SELECT distinct class_id, trim(regexp_substr(class_time, '[^:]+', 1, LEVEL)) class_time
FROM DATA
CONNECT BY regexp_substr(class_time , '[^:]+', 1, LEVEL) IS NOT NULL
and prior class_time = class_time
and prior class_id = class_id
and prior sys_guid() is not null
The prior sys_guid() is not null is the special filter to prevent it from erroring with ORA-01436: CONNECT BY loop in user data.
This should perform similarly to the recursive CTE.
I am working on to Sort the Revision column of an Oracle DB table in the asc order as per below.
At first the numeric revisions to be sorted (1,2,3,…).
Thereafter Alpha-Numeric to be sorted as following: A, B, B1, C, C1, C2,…,Y, Y2, Y3, Z, AA, AB,..,DA, …ZZ, etc.
Row_Number() in the SELECT statement to be filled with 1,2,3… for each document# (ABC, XYZ) after revision sorting out.
See the uploaded image for the required table.
I tried with SUBSTR , Order by, etc but failed to sort out as per above requirement.
Can someone help me on this ? Thanks!
As I understand your question, you want to put last revisions that contain only two characters and no digits.
You can use a conditional sort:
select
t.*,
row_number() over(
partition by doc#
order by
case when regexp_like(revision, '^\w\d?$') then 0 else 1 end,
revision
) rn
from t
order by doc#, rn
The regular expression describes a string starting with an alphanumeric character, optionally followed by a digit: these revisions should come first.
Demo on DB Fiddle:
with t as (
select 'ABC' doc#, '1' revision from dual
union all select 'ABC', '2' from dual
union all select 'ABC', '3' from dual
union all select 'ABC', 'A' from dual
union all select 'ABC', 'B' from dual
union all select 'ABC', 'B1' from dual
union all select 'ABC', 'C' from dual
union all select 'ABC', 'C1' from dual
union all select 'ABC', 'D' from dual
union all select 'ABC', 'AA' from dual
union all select 'ABC', 'AB' from dual
union all select 'ABC', 'BA' from dual
union all select 'ABC', 'DA' from dual
)
select
t.*,
row_number() over(
partition by doc#
order by
case when regexp_like(revision, '^\w\d?$') then 0 else 1 end,
revision
) rn
from t
order by doc#, rn
DOC# | REVISION | RN
:--- | :------- | -:
ABC | 1 | 1
ABC | 2 | 2
ABC | 3 | 3
ABC | A | 4
ABC | B | 5
ABC | B1 | 6
ABC | C | 7
ABC | C1 | 8
ABC | D | 9
ABC | AA | 10
ABC | AB | 11
ABC | BA | 12
ABC | DA | 13
There is well known old method: rpad(col, max-length, '0')
For example rpad(col, max(length(col)) over(), '0'
i have table like below :
|-------------|---------------------------------------------------|
|ID. | CONTENT |
|-------------|---------------------------------------------------|
|1 |<TITLE> <SUB-TITLE-1> Content <SUB-TITLE-2>Content.
|2 |<TITLE> <SUB-TITLE-1> Content <SUB-TITLE-2>Content.
|3 |<TITLE> <SUB-TITLE-1> Content <SUB-TITLE-2>Content. <SUB-TITLE-3> Content
|-------------|---------------------------------------------------|
I want to extract all text in between <>, so it will become like below :
|-------------|-------------------------------------------------|
|ID. | CONTENT |
|-------------|-------------------------------------------------|
|1 |TITLE |
|1 |SUB-TITLE-1 |
|1 |SUB-TITLE-2 |
|2 |TITLE |
|2 |SUB-TITLE-1 |
|2 |SUB-TITLE-2 |
|3 |TITLE |
|3 |SUB-TITLE-1 |
|3 |SUB-TITLE-2 |
|3 |SUB-TITLE-3 |
|-------------|-------------------------------------------------|
How to achieve this ? I'm trying to do by regex, but I think I'm lost..
My Oracle version is 18c, if that's help...
You can use the 4th argument of REGEXP_SUBSTR to specify an occurrence for matching.
To get a row for the 1st, 2nd, and 3rd occurrence, you can cross-join with a sub-query from dual.
WITH test_data AS (
SELECT 1 AS content_id, '<TITLE> <SUB-TITLE-1> Content<SUB-TITLE-2>Content.<A third sub-title>' AS content_data FROM dual UNION
SELECT 2 AS content_id, '<TITLE> <SUB-TITLE-1> Content<SUB-TITLE-2>Content.' AS content_data FROM dual
)
SELECT t.content_id,
REGEXP_SUBSTR(t.content_data, '<(.*?)>', 1, s.match_occurrence, 'i', 1) AS content_match
FROM test_data t
CROSS JOIN (
SELECT 1 AS match_occurrence FROM dual UNION
SELECT 2 AS match_occurrence FROM dual UNION
SELECT 3 AS match_occurrence FROM dual UNION
SELECT 4 AS match_occurrence FROM dual
/* ... etc, with the number of rows equal to the maximum number of matches that can appear */
) s
WHERE REGEXP_SUBSTR(t.content_data, '<.*?>', 1, s.match_occurrence) IS NOT NULL /* Only return records that have a match for the given occurrence */
ORDER BY t.content_id, s.match_occurrence
Borrowing the CONNECT_BY_LEVEL from Barbaros' excellent answer, you could do it more concisely as:
WITH test_data AS (
SELECT 1 AS content_id, '<TITLE> <SUB-TITLE-1> Content<SUB-TITLE-2>Content.<A third sub-title>' AS content_data FROM dual UNION
SELECT 2 AS content_id, '<TITLE> <SUB-TITLE-1> Content<SUB-TITLE-2>Content.' AS content_data FROM dual
)
SELECT t.content_id,
REGEXP_SUBSTR(t.content_data, '<(.*?)>', 1, LEVEL, 'i', 1) AS content_match
FROM test_data t
CONNECT BY
LEVEL <= REGEXP_COUNT(t.content_data, '<.*?>')
AND PRIOR sys_guid() IS NOT NULL
AND PRIOR content_id = content_id
ORDER BY t.content_id, LEVEL
Note that the CONNECT_BY_LEVEL method might be slower on large datasets, so I would avoid that if performance is a concern.
One option would be using instr() and substr() functions together within a
SELECT .. FROM ..CONNECT BY level style query in order to repeat through counting the numbers of > (or <) signs within each strings :
SELECT id, substr(content,
instr(content,'<',1,level)+1,
instr(content,'>',1,level)-instr(content,'<',1,level)-1) as content
FROM tab
CONNECT BY level <= regexp_count(content,'>')
AND PRIOR sys_guid() IS NOT NULL
AND PRIOR id = id
Demo
I had tried the more conventional way using SUBSTR and INSTR
With data as
Select column1,
Trim(CONTENT, '<', '>') as col2 FROM
TABLE
WITH subdata as
( Select column1,
SUBSTR(col2,0, INSTR(col2, ' '))
as s1
from
data) t1
Union
( Select t1.column1 as col1,
SUBSTR(col2, Length(t1.s1)+1
INSTR(
SUBSTR(
t1.col2, Length(t1.s1)+1,
LENGTH(col2)), ' '))) as col2
From
data) t3
Union
........ t3.... t4
From table
Perfect approach, #Josh Eller !
Only that #Gerry Gry needs it without the greater/smaller signs.
Try with grouping using parentheses?
WITH test_data(content_id,content_data) AS (
SELECT 1 , '<TITLE> <SUB-TITLE-1> Content<SUB-TITLE-2>Content.<SUB-TITLE-3>Content.' FROM dual
UNION SELECT 2 , '<TITLE> <SUB-TITLE-1> Content<SUB-TITLE-2>Content.' FROM dual
)
SELECT t.content_id
, match_occurrence
, REGEXP_SUBSTR(
t.content_data -- input string
, '[<]([^>]*)[>]' -- regex
, 1 -- starting position
, s.match_occurrence -- n-th occurrence
, '' -- regexp modifier
, 1 -- captured-subexp
) AS content_match
FROM test_data t
CROSS JOIN (
SELECT 1 FROM dual
UNION SELECT 2 FROM dual
UNION SELECT 3 FROM dual
UNION SELECT 4 FROM dual
) s(match_occurrence)
WHERE
REGEXP_SUBSTR(
t.content_data -- input string
, '[<]([^>]*)[>]' -- regex
, 1 -- starting position
, s.match_occurrence -- n-th occurrence
, '' -- regexp modifier
, 1 -- captured-subexp
)
IS NOT NULL
ORDER BY t.content_id, s.match_occurrence
;
-- out Time: First fetch (0 rows): 0.656 ms. All rows formatted: 0.667 ms
-- out content_id | match_occurrence | content_match
-- out ------------+------------------+---------------
-- out 1 | 1 | TITLE
-- out 1 | 2 | SUB-TITLE-1
-- out 1 | 3 | SUB-TITLE-2
-- out 1 | 4 | SUB-TITLE-3
-- out 2 | 1 | TITLE
-- out 2 | 2 | SUB-TITLE-1
-- out 2 | 3 | SUB-TITLE-2
-- out (7 rows)
-- out
-- out Time: First fetch (7 rows): 29.904 ms. All rows formatted: 29.947 ms
I would like to replace commas in my strings, between dynamic positions (ie. between double quotes). Note that I will not have more than 2 occurrences of double quotes in my strings if that matters.
My example:
'randomtext,123,"JEAN SEBASTIEN, GUY, DANIEL",sun'
Desired output:
'randomtext,123,"JEAN SEBASTIEN GUY DANIEL",sun'
So far I've tried things with REGEXP_REPLACE() mixed with INSTR() but could not get anything done.
Cheers
Short & clean.
with t(str) as (select 'randomtext,123,"JEAN SEBASTIEN, GUY, DANIEL",sun' from dual)
select regexp_replace(str,'(^[^"]*|[^"]*$)|,','\1') as result
from t
-
+------------------------------------------------+
| RESULT |
+------------------------------------------------+
| randomtext,123,"JEAN SEBASTIEN GUY DANIEL",sun |
+------------------------------------------------+
SQL Fiddle
In addition -
Short and clean generic version
with t(str) as
(
select 'Well,you,went,uptown,riding,in,your,limousine' from dual
union all select 'With,your,fine,"Park, Avenue, clothes"' from dual
union all select 'You,had,the,"Dom, Perignon",in,your,hand,"And, the, spoon",up,your,nose' from dual
union all select '"And, when, you",wake,"up, in, the, morning"' from dual
union all select '"With, your, head, on, fire"' from dual
union all select '"And",your,"eyes, too, bloody","to, see",Go,"on, and, cry, in",your,coffee,"But","don''t","come, bitchin''","to, me"' from dual
)
select regexp_replace(str, '((^|").*?("|$))|,', '\1') as result
from t
--
+------------------------------------------------------------------------------------------------------------+
| RESULT |
+------------------------------------------------------------------------------------------------------------+
| Well,you,went,uptown,riding,in,your,limousine |
| With,your,fine,"Park Avenue clothes" |
| You,had,the,"Dom Perignon",in,your,hand,"And the spoon",up,your,nose |
| "And when you",wake,"up in the morning" |
| "With your head on fire" |
| "And",your,"eyes too bloody","to see",Go,"on and cry in",your,coffee,"But","don't","come bitchin'","to me" |
+------------------------------------------------------------------------------------------------------------+
SQL Fiddle
Assuming you are working on CSV, then it is possible that you will also have nested double quotes as per this sample data:
CREATE TABLE test_data ( value ) AS
SELECT 'randomtext,123,"JEAN SEBASTIEN, GUY, DANIEL",sun' FROM DUAL UNION ALL
SELECT 'randomtext,123,"A, ""BC"", D",sun' FROM DUAL;
You can use the regular expression ^(.*?)("([^\"]|\\")+")(.*)$ to match the terms before, inside the quotes and after and then replace commas in only the middle parts:
SELECT value,
REGEXP_SUBSTR( value, '^(.*?)("([^\"]|"")+")(.*)$', 1, 1, NULL, 1 )
|| REPLACE(
REGEXP_SUBSTR( value, '^(.*?)("([^\"]|"")+")(.*)$', 1, 1, NULL, 2 ),
','
)
|| REGEXP_SUBSTR( value, '^(.*?)("([^\"]|"")+")(.*)$', 1, 1, NULL, 4 ) replaced_value
FROM test_data
Which outputs:
VALUE | REPLACED_VALUE
:----------------------------------------------- | :---------------------------------------------
randomtext,123,"JEAN SEBASTIEN, GUY, DANIEL",sun | randomtext,123,"JEAN SEBASTIEN GUY DANIEL",sun
randomtext,123,"A, ""BC"", D",sun | randomtext,123,"A ""BC"" D",sun
db<>fiddle here
Update
If you need to handle multiple quoted terms in a string (with nested quotes):
CREATE TABLE test_data ( value ) AS
SELECT 'randomtext,123,"JEAN SEBASTIEN, GUY, DANIEL",sun' FROM DUAL UNION ALL
SELECT 'randomtext,123,"A, ""BC"", D",sun' FROM DUAL UNION ALL
SELECT 'E,"F, G",H,"I, ""J""", K' FROM DUAL UNION ALL
SELECT 'L,M,N' FROM DUAL;
Then you can use a recursive sub-query factoring clause:
WITH replacements( value, prefix, suffix ) AS (
SELECT value,
REGEXP_SUBSTR( value, '^(.*?)("([^\"]|"")+"|$)(.*)$', 1, 1, NULL, 1 )
|| REPLACE(
REGEXP_SUBSTR( value, '^(.*?)("([^\"]|"")+"|$)(.*)$', 1, 1, NULL, 2 ),
','
),
REGEXP_SUBSTR( value, '^(.*?)("([^\"]|"")+"|$)(.*)$', 1, 1, NULL, 4 )
FROM test_data
UNION ALL
SELECT value,
prefix
|| REGEXP_SUBSTR( suffix, '^(.*?)("([^\"]|"")+"|$)(.*)$', 1, 1, NULL, 1 )
|| REPLACE(
REGEXP_SUBSTR( suffix, '^(.*?)("([^\"]|"")+"|$)(.*)$', 1, 1, NULL, 2 ),
','
),
REGEXP_SUBSTR( suffix, '^(.*?)("([^\"]|"")+"|$)(.*)$', 1, 1, NULL, 4 )
FROM replacements
WHERE suffix IS NOT NULL
)
SELECT value,
prefix AS replaced_value
FROM replacements
WHERE suffix IS NULL;
Which outputs:
VALUE | REPLACED_VALUE
:----------------------------------------------- | :---------------------------------------------
L,M,N | L,M,N
randomtext,123,"JEAN SEBASTIEN, GUY, DANIEL",sun | randomtext,123,"JEAN SEBASTIEN GUY DANIEL",sun
randomtext,123,"A, ""BC"", D",sun | randomtext,123,"A ""BC"" D",sun
E,"F, G",H,"I, ""J""", K | E,"F G",H,"I ""J""", K
db<>fiddle here