Getting ORA-00979 not a group by expression /ERROR - sql

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>

Related

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

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

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 substitute with Comma Separate values in Oracle SQL

I have a scenario like below (Oracle's SQL).
Table:Employee
S.No
Name
Role
1
a
ELE,PLU,OTH
2
b
MAN,DIR
3
c
DIR,FND
4
d
Table: Role_master
Role
Role name
ELE
Electrician
PLU
Plumber
MAN
Manager
DIR
Director
FND
Founder
OTH
Other
With the above tables, i would like to join both and expecting the output like below. Please help on the better way.
S.No
Name
Role
1
a
Electrician,Plumber,Other
2
b
Manager,Director
3
c
Director,Founder
4
d
Here's one option:
sample data in lines #1 - 12; query begins at line #14
split employee.role into rows (i.e. separate values) so that you could join them to role_master.role
aggregate them back (using listagg)
SQL> with
2 employee (sno, name, role) as
3 (select 1, 'a', 'ELE,PLU,OTH' from dual union all
4 select 2, 'b', 'MAN,DIR' from dual
5 ),
6 role_master (role, role_name) as
7 (select 'ELE', 'Electrician' from dual union all
8 select 'PLU', 'Plumber' from dual union all
9 select 'OTH', 'Other' from dual union all
10 select 'MAN', 'Manager' from dual union all
11 select 'DIR', 'Director' from dual
12 )
13 --
14 select e.sno,
15 e.name,
16 listagg(m.role_name, ',') within group (order by column_value) role
17 from employee e cross join
18 table(cast(multiset(select level from dual
19 connect by level <= regexp_count(e.role, ',') + 1
20 ) as sys.odcinumberlist))
21 join role_master m on m.role = regexp_substr(e.role, '[^,]+', 1, column_value)
22 group by e.sno, e.name;
SNO NAME ROLE
---------- ---- ----------------------------------------
1 a Electrician,Plumber,Other
2 b Manager,Director
SQL>
Another solution could use this logic :
First, generate the maximum number of rows needed for the split step (required_rows_v)
Then, make left join between the three data sources like below
Then, use listagg function to re-aggregate rows
With required_rows_v (lvl) as (
select level lvl
from dual
connect by level <= ( select max( regexp_count( e.Role, '[^,]+' ) ) from employee e )
)
select e.SNO,
e.NAME,
listagg(rm.Role_name, ',')within group (order by e.SNo, v.lvl) Role
from employee e
left join required_rows_v v on v.lvl <= regexp_count( e.Role, '[^,]+' )
left join Role_master rm on rm.Role = regexp_substr( e.Role, '[^,]+', 1, v.lvl )
group by e.SNO, e.NAME
demo

Oracle - How use string data in (in operator)

In tbl_1 I have:
id
1
2
3
4
5
6
7
8
9
in tbl_2:
id value
1 1,2,3
2 5
Select * from tbl_1 where id in (Select value from tbl_2 where id = 2); --is OK
Select * from tbl_1 where id in (Select value from tbl_2 where id = 1);
--Need this resault: 3 rows: 1, 2 and 3
Fix your data model! You should not be storing numbers as strings. You should have properly declared foreign key relationships. Strings should not be used to store multiple values.
Sometimes, we are stuck with other people's really, really, really bad decisions. You can do what you want with `like:
Select t1.*
from tbl_1 t1
where exists (select 1
from tbl_2 t2
where t2.id = 1 and
',' || t2.value || ',' like '%,' || t1.id ',%'
);
However, your effort should be going into fixing the data, rather than trying to deal with it. The correct data would be a junction/association table with one row per id and value for table 2:
id value
1 1
1 2
1 3
2 5
Yet another option:
SQL> with
2 -- sample data
3 tbl_1 (id) as
4 (select 1 from dual union all
5 select 2 from dual union all
6 select 3 from dual union all
7 select 4 from dual union all
8 select 5 from dual union all
9 select 6 from dual union all
10 select 7 from dual union all
11 select 8 from dual union all
12 select 9 from dual
13 ),
14 tbl_2 (id, value) as
15 (select 1, '1,2,3' from dual union all
16 select 2, '5,6,7' from dual
17 )
18 -- query which returns what you want
19 select a.id
20 from tbl_1 a join
21 (select regexp_substr(b.value, '[^,]+', 1, column_value) id
22 from tbl_2 b cross join
23 table(cast(multiset(select level from dual
24 connect by level <= regexp_count(b.value, ',') + 1
25 ) as sys.odcinumberlist))
26 where b.id = 1
27 ) c on c.id = a.id;
ID
----------
1
2
3
SQL>
One option uses string functions:
select t1.*
from t1
inner join t2 on ',' || t2.value || ',' like '%,' || t1.id || ',%'
where t2.id = 1

Oracle SQL query to fetch manyToMany record in a single column

I have the following three tables, my requirement is to fetch manytoMany joined tabled as a column. Can someone pls help me in writing the query.
IPtype can only be of two types public and private, so the result should have two more columns as mentioned below. If there is multiple public or private ip mapped to single asset then is should be displayed as comma separated.
Thanks
Looks like outer join (to return assets that don't have any IP addresses) with listagg (to aggregate IP addresses per asset) problem.
SQL> with
2 -- Sample data
3 asset (assetid) as
4 (select 1 from dual union all
5 select 2 from dual union all
6 select 3 from dual union all
7 select 4 from dual
8 ),
9 ip (ipid, ipnumber, iptype) as
10 (select 1, '1.2.3.4' , 'Public' from dual union all
11 select 2, '99.22.3.4', 'Private' from dual union all
12 select 3, '11.22.3.4', 'Public' from dual union all
13 select 4, '55.22.3.4', 'Public' from dual union all
14 select 5, '66.22.3.4', 'Private' from dual union all
15 select 6, '77.22.3.4', 'Private' from dual
16 ),
17 asset_ip_map (assetid, ipid) as
18 (select 1, 1 from dual union all
19 select 1, 2 from dual union all
20 select 2, 3 from dual union all
21 select 2, 4 from dual union all
22 select 3, 5 from dual union all
23 select 3, 6 from dual
24 )
25 -- Query you need
26 select a.assetid,
27 listagg(case when iptype = 'Public' then i.ipnumber end, ', ') within group (order by null) public_ip,
28 listagg(case when iptype = 'Private' then i.ipnumber end, ', ') within group (order by null) private_ip
29 from asset a left join asset_ip_map m on m.assetid = a.assetid
30 left join ip i on i.ipid = m.ipid
31 group by a.assetid
32 order by a.assetid;
ASSETID PUBLIC_IP PRIVATE_IP
---------- ------------------------------ ------------------------------
1 1.2.3.4 99.22.3.4
2 11.22.3.4, 55.22.3.4
3 66.22.3.4, 77.22.3.4
4
SQL>