join a table into a field of a table containing multiple values - sql

I have a POST table and a TAG table, I explain the POST table contains following fields : id , title , content , tags_ids which mean that the field tags_ids can contani multiple tags , for example the POST whose ID = 1, has following tags : tag_1, tag_2 tag_5 separated with ;
POST TABLE
id title content tag_id
---------- ---------- ---------- ----------
1 title1 Text... 1; 2; 5
2 title2 Text... 3
3 title3 Text... 1; 2
4 title4 Text... 2; 3; 4
5 title4 Text... 2; 3; 4
6 title2 Text... 3
the TAG table
id name
---------- ----------
1 tag_1
2 tag_2
3 tag_3
4 tag_4
5 tag_5
so i would like to know how many posts are registered for each case.
Here is my query
select tag, COUNT(*) AS cnt
from(
select CATEGORY.name,
case
when POST.tag_id is not null then tag.name
end as tag
from POST
left join TAG ON POST.tag_id = TAG.id
)
GROUP BY tag
;
here is the result i want to display with my query
tag cnt
-------------------- --------------
tag_1, tag_2, tag_5 1
tag_3 2
tag_1, tag_2 1
tag_2, tag_3, tag_4 2
Best regards

Here's one option. Read comments within code.
Sample data:
SQL> with
2 post(id, tag_id) as
3 (select 1, '1; 2; 5' from dual union all
4 select 2, '3' from dual union all
5 select 3, '1; 2' from dual union all
6 select 4, '2; 3; 4' from dual union all
7 select 5, '2; 3; 4' from dual union all
8 select 6, '3' from dual
9 ),
10 tag (id, name) as
11 (select 1, 'tag_1' from dual union all
12 select 2, 'tag_2' from dual union all
13 select 3, 'tag_3' from dual union all
14 select 4, 'tag_4' from dual union all
15 select 5, 'tag_5' from dual
16 ),
Query begins here:
17 post_distinct as
18 -- number of rows per each distinct TAG_ID
19 (select tag_id,
20 count(*) cnt
21 from post
22 group by tag_id
23 ),
24 temp as
25 -- split TAG_ID into rows
26 (select tag_id,
27 cnt,
28 trim(regexp_substr(tag_id, '[^;]+', 1, column_value)) tag_id_split
29 from post_distinct p cross join
30 table(cast(multiset(select level from dual
31 connect by level <= regexp_count(p.tag_id, ';') + 1
32 ) as sys.odcinumberlist))
33 )
34 -- finally, join tables to get the result
35 select listagg(t.name, ', ') within group (order by t.id) tag,
36 te.cnt
37 from tag t join temp te on te.tag_id_split = t.id
38 join post_distinct p on p.tag_id = te.tag_id
39 group by p.tag_id, te.cnt
40 order by p.tag_id;
TAG CNT
-------------------- ----------
tag_1, tag_2 1
tag_1, tag_2, tag_5 1
tag_2, tag_3, tag_4 2
tag_3 2
SQL>

You do not need to split the posts, to get your expected output you can just aggregate by the tag_ids:
SELECT tag_id,
COUNT(DISTINCT id) AS num_posts
FROM post
GROUP BY tag_id;
Which, for the sample data:
CREATE TABLE POST (id, title, content, tag_id) AS
SELECT 1, 'title1', 'Text...', '1; 2; 5' FROM DUAL UNION ALL
SELECT 2, 'title2', 'Text...', '3' FROM DUAL UNION ALL
SELECT 3, 'title3', 'Text...', '1; 2' FROM DUAL UNION ALL
SELECT 4, 'title4', 'Text...', '2; 3; 4' FROM DUAL UNION ALL
SELECT 5, 'title4', 'Text...', '2; 3; 4' FROM DUAL UNION ALL
SELECT 6, 'title2', 'Text...', '3' FROM DUAL;
CREATE TABLE TAG (id, name) AS
SELECT 1, 'tag_1' FROM DUAL UNION ALL
SELECT 2, 'tag_2' FROM DUAL UNION ALL
SELECT 3, 'tag_3' FROM DUAL UNION ALL
SELECT 4, 'tag_4' FROM DUAL UNION ALL
SELECT 5, 'tag_5' FROM DUAL;
Outputs:
TAG_ID
NUM_POSTS
1; 2
1
3
2
1; 2; 5
1
2; 3; 4
2
If you want to convert the ids to names then:
SELECT ( SELECT LISTAGG(name, '; ')
WITHIN GROUP (ORDER BY INSTR('; ' || p.tag_id || '; ', '; ' || t.id || '; '))
FROM tag t
WHERE INSTR('; ' || p.tag_id || '; ', '; ' || t.id || '; ') > 0
) AS tags,
COUNT(DISTINCT id) AS num_posts
FROM post p
GROUP BY tag_id;
Which outputs:
TAGS
NUM_POSTS
tag_1; tag_2
1
tag_1; tag_2; tag_5
1
tag_2; tag_3; tag_4
2
tag_3
2
If you want to count the posts for each individual tag then:
SELECT t.id,
COUNT(DISTINCT p.id) AS num_posts
FROM post p
INNER JOIN tag t
ON ('; ' || p.tag_id || '; ' LIKE '%; ' || t.id || '; %')
GROUP BY t.id
Which outputs:
ID
NUM_POSTS
2
4
4
2
3
4
5
1
1
2
fiddle

Related

Join a table that depends on another table

I have a POST table, a CATEGORY table, a TAG table and a MIGTATION_TAG table, I explain the MIGTATION_TAG table contains the movement of the tags between the categories, for example the tag whose ID = 1 belongs to the category whose l 'ID = 10 if I change its category to 12 a line will be added to the MIGTATION_TAG table as follows:
ID 1 TAG_ID 1 CATEGOTY_ID 12
the POST table
id title content tag_id
---------- ---------- ---------- ----------
1 title1 Text... 1
2 title2 Text... 3
3 title3 Text... 1
4 title4 Text... 2
5 title5 Text... 5
6 title6 Text... 4
the CATEGORY table
id name
---------- ----------
1 category_1
2 category_2
3 category_3
the TAG table
id name fist_category_id
---------- ---------- ----------------
1 tag_1 1
2 tag_2 1
3 tag_3 3
4 tag_4 1
5 tag_5 2
the MIGTATION_TAG table
id tag_id category_id
---------- ---------- ----------------
9 1 3
8 5 1
7 1 2
5 3 1
4 2 2
3 5 3
2 3 3
1 1 3
so i would like to know how many posts are registered for each category.
in some cases if there has been no change of category for a tag then it keeps its first category,
I manage to join the TAG table to the POST table via LEFT JOIN but the problem is that the join must depend on the MIGTATION_TAG table which must check if there has been a migration, if so then it must bring me back the last MAX (tag_id ) for each tag ,
here is my query
select category, COUNT(*) AS numer_of_posts
from(
select CATEGORY.name,
case
when POST.tag_id is not null then CATEGORY.name
end as category
from POST
left join TAG ON POST.tag_id = TAG.id
left join (
select id, MAX(tag_id) tag_id
from MIGTATION_TAG
group by id, tag_id
) MIGTATION_TAG
ON TAG.id = MIGTATION_TAG.tag_id
left join CATEGORY on MIGTATION_TAG.category_id = CATEGORY.id
)
GROUP BY category
;
here is the result i want to display with my query
Important ! for the post with id = 6 the tag_id = 4 whish was not changed so it will be using the fist_category_id in TAG table
category numer_of_posts
---------- --------------
category_1 3
category_2 1
category_3 2
Best regards
You can use:
SELECT MAX(c.name) AS category,
COUNT(*)
FROM post p
INNER JOIN tag t
ON (p.tag_id = t.id)
LEFT OUTER JOIN (
SELECT tag_id,
MAX(category_id) KEEP (DENSE_RANK LAST ORDER BY id) AS category_id
FROM migration_tag
GROUP BY tag_id
) m
ON (t.id = m.tag_id)
INNER JOIN category c
ON ( COALESCE(m.category_id, t.first_category_id) = c.id )
GROUP BY c.id
ORDER BY category
Which, for the sample data:
CREATE TABLE POST ( id, title, content, tag_id ) AS
SELECT 1, 'title1', 'Text...', 1 FROM DUAL UNION ALL
SELECT 2, 'title2', 'Text...', 3 FROM DUAL UNION ALL
SELECT 3, 'title3', 'Text...', 1 FROM DUAL UNION ALL
SELECT 4, 'title4', 'Text...', 2 FROM DUAL UNION ALL
SELECT 5, 'title5', 'Text...', 5 FROM DUAL UNION ALL
SELECT 6, 'title6', 'Text...', 4 FROM DUAL;
CREATE TABLE CATEGORY ( id, name ) AS
SELECT 1, 'category_1' FROM DUAL UNION ALL
SELECT 2, 'category_2' FROM DUAL UNION ALL
SELECT 3, 'category_3' FROM DUAL;
CREATE TABLE TAG (id, name, first_category_id) AS
SELECT 1, 'tag_1', 1 FROM DUAL UNION ALL
SELECT 2, 'tag_2', 1 FROM DUAL UNION ALL
SELECT 3, 'tag_3', 3 FROM DUAL UNION ALL
SELECT 4, 'tag_4', 1 FROM DUAL UNION ALL
SELECT 5, 'tag_5', 2 FROM DUAL;
CREATE TABLE MIGRATION_TAG ( id, tag_id, category_id ) AS
SELECT 9, 1, 3 FROM DUAL UNION ALL
SELECT 8, 5, 1 FROM DUAL UNION ALL
SELECT 7, 1, 2 FROM DUAL UNION ALL
SELECT 5, 3, 1 FROM DUAL UNION ALL
SELECT 4, 2, 2 FROM DUAL UNION ALL
SELECT 3, 5, 3 FROM DUAL UNION ALL
SELECT 2, 3, 3 FROM DUAL UNION ALL
SELECT 1, 1, 3 FROM DUAL;
Outputs:
CATEGORY
COUNT(*)
category_1
3
category_2
1
category_3
2
fiddle
One option uses a left join to bring the tag table, and the a lateral join to lookup the latest migration, ifi any. We can then use conditional logic:
select coalesce(t2.category_id, t.first_category_id) category, count(*) number_of_posts
from post p
inner join tag t on t.id = p.tag_id
outer apply (
select mt.category_id
from migration_tag mt
where mt.tag_id = p.tag_id
order by mt.id desc fetch first row only
) t2
group by coalesce(t2.category_id, t.first_category_id)

Getting ORA-00979 not a group by expression /ERROR

I'm working on a query but I'm facing a problem I am getting ORA-00979 with the following query:
Thank you in advance
SELECT POST.ID, POST.TAG_ID, TAG.ID, TAG.NAME, COUNT(*) AS TOTAL
FROM POST
LEFT JOIN TAG ON ',' || TAG.ID || ',' LIKE '%,' || POST.TAG_ID || ',%'
GROUP BY TAG.NAME
;
We're using regexp_substr to split up TAG_ID, group by the result and then join TAG TABLE.
with t as (
select regexp_substr(TAG_ID,'\d',1,level) as ID
,count(*) as NUB_of_POSTS
from post
connect by ID = prior ID and level <= regexp_count(TAG_ID,'\d') and sys_guid() <> prior sys_guid()
group by regexp_substr(TAG_ID,'\d',1,level)
)
select NAME
,NUB_of_POSTS
from t join tag using(ID)
NAME
NUB_OF_POSTS
TAG_1
3
TAG_2
3
TAG_3
4
Fiddle
Similarly ...
Sample data:
SQL> with
2 post (id, tag_id) as
3 (select 1, '1; 2; 3' from dual union all
4 select 2, '1; 2' from dual union all
5 select 3, '3' from dual union all
6 select 4, '1; 3' from dual union all
7 select 5, '2; 3' from dual
8 ),
9 tag (id, name) as
10 (select 1, 'TAG_1' from dual union all
11 select 2, 'TAG_2' from dual union all
12 select 3, 'TAG_3' from dual
13 ),
Query begins here; see comments within code
14 -- split POSTs TAG_ID into rows
15 temp as
16 (select p.id,
17 trim(regexp_substr(p.tag_id, '[^;]+', 1, column_value)) tag_id
18 from post p cross join
19 table(cast(multiset(select level from dual
20 connect by level <= regexp_count(p.tag_id, ';') + 1
21 ) as sys.odcinumberlist ))
22 )
23 -- finally, a simple join between TAG and TEMP with the COUNT aggregate function
24 select t.name as tag,
25 count(*) num_of_posts
26 from tag t join temp p on p.tag_id = t.id
27 group by t.name;
TAG NUM_OF_POSTS
----- ------------
TAG_1 3
TAG_2 3
TAG_3 4
SQL>

Get parent id from level with Oracle SQL

I have a hierarchical structure defined by level and order of elements. Is it possible to create "parent_id" column with Oracle SQL without using procedures?
I need to generate red values:
test data:
with t as
(
select 1 id, 'element1' name, 1 level_ from dual union all
select 2 id, 'element2' name, 2 level_ from dual union all
select 3 id, 'element3' name, 3 level_ from dual union all
select 4 id, 'element4' name, 3 level_ from dual union all
select 5 id, 'element5' name, 3 level_ from dual union all
select 6 id, 'element6' name, 3 level_ from dual union all
select 7 id, 'element7' name, 2 level_ from dual union all
select 8 id, 'element8' name, 3 level_ from dual union all
select 9 id, 'element9' name, 4 level_ from dual union all
select 10 id, 'element10' name, 4 level_ from dual union all
select 11 id, 'element11' name, 1 level_ from dual union all
select 12 id, 'element12' name, 2 level_ from dual union all
select 13 id, 'element13' name, 3 level_ from dual union all
select 14 id, 'element14' name, 4 level_ from dual union all
select 15 id, 'element15' name, 4 level_ from dual union all
select 16 id, 'element16' name, 3 level_ from dual union all
select 17 id, 'element17' name, 4 level_ from dual union all
select 18 id, 'element18' name, 4 level_ from dual union all
select 19 id, 'element19' name, 1 level_ from dual
)
select * from t
From Oracle 12, you can use MATCH_RECOGNIZE:
select *
from t
MATCH_RECOGNIZE (
ORDER BY id DESC
MEASURES
child.id AS id,
child.name AS name,
child.lvl AS lvl,
parent.id AS parent_id
ONE ROW PER MATCH
AFTER MATCH SKIP TO NEXT ROW
PATTERN (child ancestors*? (parent | $))
DEFINE
parent AS lvl = child.lvl - 1
)
ORDER BY id
Or, again from Oracle 12, a LATERAL join:
select *
from t c
LEFT OUTER JOIN LATERAL(
SELECT p.id AS parent_id
FROM t p
WHERE c.id > p.id
AND c.lvl = p.lvl + 1
ORDER BY id DESC
FETCH FIRST ROW ONLY
)
ON (1 = 1)
ORDER BY id
Or, in earlier versions:
SELECT id, name, lvl, parent_id
FROM (
SELECT c.*,
p.id AS parent_id,
ROW_NUMBER() OVER (PARTITION BY c.id ORDER BY p.id DESC) AS rn
FROM t c
LEFT OUTER JOIN t p
ON (c.id > p.id AND c.lvl = p.lvl + 1)
)
WHERE rn = 1
ORDER BY id
Which, for the sample data:
CREATE TABLE t (id, name, lvl ) as
select 1, 'element1', 1 from dual union all
select 2, 'element2', 2 from dual union all
select 3, 'element3', 3 from dual union all
select 4, 'element4', 3 from dual union all
select 5, 'element5', 3 from dual union all
select 6, 'element6', 3 from dual union all
select 7, 'element7', 2 from dual union all
select 8, 'element8', 3 from dual union all
select 9, 'element9', 4 from dual union all
select 10, 'element10', 4 from dual union all
select 11, 'element11', 1 from dual union all
select 12, 'element12', 2 from dual union all
select 13, 'element13', 3 from dual union all
select 14, 'element14', 4 from dual union all
select 15, 'element15', 4 from dual union all
select 16, 'element16', 3 from dual union all
select 17, 'element17', 4 from dual union all
select 18, 'element18', 4 from dual union all
select 19, 'element19', 1 from dual;
All output:
ID
NAME
LVL
PARENT_ID
1
element1
1
null
2
element2
2
1
3
element3
3
2
4
element4
3
2
5
element5
3
2
6
element6
3
2
7
element7
2
1
8
element8
3
7
9
element9
4
8
10
element10
4
8
11
element11
1
null
12
element12
2
11
13
element13
3
12
14
element14
4
13
15
element15
4
13
16
element16
3
12
17
element17
4
16
18
element18
4
16
19
element19
1
null
db<>fiddle here

ORACLE SQL | If a column contains a value, then it will exclude a different value from the same column

I have this query that returns the data below it
select LISTAGG(d.DOCUMENT_TYPE_CD, ',') WITHIN GROUP (ORDER BY D.DOCUMENT_TYPE_CD) as value
from test_table d;
VALUE
---------
CI,ECI,POA
now I'm trying to add a condition whenever 'ECI' value is present, it should exclude 'CI' in the result like this one below
VALUE
---------
ECI,POA
I tried using case statement in where condition it prompted an error
select LISTAGG(d.DOCUMENT_TYPE_CD, ',')
WITHIN GROUP (ORDER BY D.DOCUMENT_TYPE_CD) as value
from test_table d
where CASE d.DOCUMENT_TYPE_CD
WHEN 'ECI' THEN d.DOCUMENT_TYPE_CD <> 'CI'
END;
ORA-00905: missing keyword
00905. 00000 - "missing keyword"
*Cause:
*Action:
Error at Line: 7 Column: 36
is there any other way I could resolve this?
See if this helps; read comments within code.
SQL> with
2 test (id, document_type_cd) as
3 -- sample data
4 (select 1, 'ECI' from dual union all
5 select 1, 'CI' from dual union all
6 select 1, 'POA' from dual union all
7 --
8 select 2, 'CI' from dual union all
9 select 2, 'POA' from dual union all
10 --
11 select 3, 'XYZ' from dual union all
12 select 3, 'ABC' from dual
13 ),
14 temp as
15 -- see whether CI and ECI exist per each ID
16 (select id,
17 sum(case when document_type_cd = 'CI' then 1 else 0 end) sum_ci,
18 sum(case when document_type_cd = 'ECI' then 1 else 0 end) sum_eci
19 from test
20 group by id
21 ),
22 excl as
23 -- exclude CI rows if ECI exist for that ID
24 (select a.id,
25 a.document_type_cd
26 from test a join temp b on a.id = b.id
27 where a.document_type_cd <> case when b.sum_ci > 0 and b.sum_eci > 0 then 'CI'
28 else '-1'
29 end
30 )
31 -- finally:
32 select e.id,
33 listagg(e.document_type_cd, ',') within group (order by e.document_type_cd) result
34 from excl e
35 group by e.id;
ID RESULT
---------- --------------------
1 ECI,POA
2 CI,POA
3 ABC,XYZ
SQL>
Something like this:
select LISTAGG(d.DOCUMENT_TYPE_CD, ',')
WITHIN GROUP (ORDER BY D.DOCUMENT_TYPE_CD) as value
from test_table d,
(select sum (case when DOCUMENT_TYPE_CD = 'CI' then 1 else 0 end) C
from test_table) A
where d.DOCUMENT_TYPE_CD <> case when A.c > 0 then 'CI' when A.c = 0 then ' ' end;
DEMO
You may identify the presence of both the values with two conditional aggregations in the same group by and then replace CI inside the result of listagg in one pass.
with a(id, cd) as (
select 1, 'ABC' from dual union all
select 1, 'ECI' from dual union all
select 1, 'CI' from dual union all
select 1, 'POA' from dual union all
select 2, 'XYZ' from dual union all
select 2, 'ECI' from dual union all
select 2, 'CI' from dual union all
select 2, 'POA' from dual union all
select 3, 'CI' from dual union all
select 3, 'POA' from dual union all
select 4, 'ABC' from dual union all
select 4, 'DEF' from dual
)
select
id,
ltrim(
/*Added comma in case CI will be at the beginning*/
replace(
',' || listagg(cd, ',') within group (order by cd asc),
decode(
/*If both are present, then replace CI. If not, then do not replace anything*/
max(decode(cd, 'CI', 1))*max(decode(cd, 'ECI', 1)),
1,
',CI,'
),
','
),
','
) as res
from a
group by id
ID | RES
-: | :----------
1 | ABC,ECI,POA
2 | ECI,POA,XYZ
3 | CI,POA
4 | ABC,DEF
db<>fiddle here
Instead of using GROUP BY, you can also use windowing (aka analytic) functions to check the presence of ECI per group (test data shamelessly stolen from #littlefoot):
with
test (id, document_type_cd) as
-- sample data
(select 1, 'ECI' from dual union all
select 1, 'CI' from dual union all
select 1, 'POA' from dual union all
--
select 2, 'CI' from dual union all
select 2, 'POA' from dual union all
--
select 3, 'XYZ' from dual union all
select 3, 'ABC' from dual
),
temp as
(select id,
document_type_cd,
sum(case when document_type_cd = 'ECI' then 1 else 0 end) over (partition by id) as sum_eci
from test
)
select a.id,
listagg(a.document_type_cd, ',') within group (order by a.document_type_cd) result
from temp a
where a.document_type_cd != 'CI' or sum_eci = 0
group by a.id;

how to extract unique words from cell and count them

I have an column "DESCRIPTION" (VARCHAR2 (500 Byte))
I want as result two columns. First extract from each cell unique words and display them in one column, and in second count their frequency.
Additionaly I have limiting parametre "ENTRYDATE" (i.e. "WHERE ENTRYDATE BETWEEN 20180101 and 20190101"). Because table is quite big.
I have some solution in Excel, but it's messy and painful to do.
Is it even possible to do in Oracle with SELECT?
Example:
NUMBER OF COLUMN | EXPLANATION
1 | roses are red violets are blue
2 | red violets
3 | red
4 | roses
5 | blue
RESULT:
WORDS | COUNTING
roses | 2
are | 2
red | 3
violets | 2
blue | 2
Variation of query:
with test as
(select 1 as nor, 'roses are red violets are blue' as explanation from dual union all
select 2 as nor, 'red violets' as explanation from dual union all
select 3 as nor, 'red' as explanation from dual union all
select 4 as nor, 'roses' as explanation from dual union all
select 5 as nor, 'blue' as explanation from dual
),
temp as
(select nor,
trim(column_value) word
from test join xmltable(('"' || replace(explanation, ' ', '","') ||'"')) on 1 = 1
)
select word,
count(*)
from temp
group by word
order by word;
returns ORA-00905: missing keyword
Split explanation into rows (so that you'd get words), then apply COUNT function to those words.
SQL> with test (nor, explanation) as
2 (select 1, 'roses are red violets are blue' from dual union all
3 select 2, 'red violets' from dual union all
4 select 3, 'red' from dual union all
5 select 4, 'roses' from dual union all
6 select 5, 'blue' from dual
7 ),
8 temp as
9 (select nor,
10 regexp_substr(explanation, '[^ ]+', 1, column_value) word
11 from test join table(cast(multiset(select level from dual
12 connect by level <= regexp_count(explanation, ' ') + 1
13 ) as sys.odcinumberlist)) on 1 = 1
14 )
15 select word,
16 count(*)
17 from temp
18 group by word
19 order by word;
WORD COUNT(*)
------------------------------ ----------
are 2
blue 2
red 3
roses 2
violets 2
SQL>
You mentioned entrydate column but there's none in your sample data so - if necessary, include it into the TEMP CTE.
Edit
Huh, Oracle 9i ... back to the Dark Ages:
SQL> with test (nor, explanation) as
2 (select 1, 'roses are red violets are blue' from dual union all
3 select 2, 'red violets' from dual union all
4 select 3, 'red' from dual union all
5 select 4, 'roses' from dual union all
6 select 5, 'blue' from dual
7 ),
8 temp as
9 (select nor,
10 trim(column_value) word
11 from test join xmltable(('"' || replace(explanation, ' ', '","') ||'"')) on 1 = 1
12 )
13 select word,
14 count(*)
15 from temp
16 group by word
17 order by word;
WORD COUNT(*)
-------------------- ----------
are 2
blue 2
red 3
roses 2
violets 2
SQL>
The problem is in your old Oracle version. This query should work, it has only basic connect by, instr and dbms_random:
select word, count(1) counting
from (
select id, trim(case pos2 when 0 then substr(description, pos1)
else substr(description, pos1, pos2 - pos1)
end) word
from (
select id, description,
case level when 1 then 1 else instr(description, ' ', 1, level - 1) end pos1,
instr(description, ' ', 1, level) pos2
from t
connect by prior dbms_random.value is not null
and prior id = id
and level <= length(description) - length(replace(description, ' ', '')) + 1))
group by word
demo
-- Oracle 12c+
with test (nor, explanation) as (
select 1, 'roses are red violets are blue' from dual union all
select 2, 'red violets' from dual union all
select 3, 'red' from dual union all
select 4, 'roses' from dual union all
select 5, 'blue' from dual)
select regexp_substr(explanation, '\S+', 1, lvl) word, count(*) cnt
from test,
lateral(
select rownum lvl
from dual
connect by level <= regexp_count(explanation, '\S+')
)
group by regexp_substr(explanation, '\S+', 1, lvl);
WORD CNT
------------------------------ ----------
roses 2
are 2
violets 2
red 3
blue 2