How do I conditionally group by two different columns in Oracle? - oracle

Suppose I have a table with the following data:
+----------+-----+--------+
| CLASS_ID | Day | Period |
+----------+-----+--------+
| 1 | A | CCR |
+----------+-----+--------+
| 1 | B | CCR |
+----------+-----+--------+
| 2 | A | 1 |
+----------+-----+--------+
| 2 | A | 2 |
+----------+-----+--------+
| 3 | A | 3 |
+----------+-----+--------+
| 3 | B | 4 |
+----------+-----+--------+
| 4 | A | 5 |
+----------+-----+--------+
As you could probably guess from the nature of the data, I'm working on an Oracle SQL query that pulls class schedule data from a Student Information System. I'm trying to pull a class's "period expression", a calculated value that contains the Day and Period fields into a single field. Let's get my expectation out of the way first:
If the Periods match, Period should be the GROUP BY field, and Day should be the aggregated field (via a LISTAGG function), so the calculated field would be CCR (A-B)
If the Days match, Day should be the GROUP BY field, and Period should be the aggregated field, so the calculated field would be 1-2 (A)
I'm only aware of how to do each GROUP BY individually, something like for where Days match:
SELECT
day,
LISTAGG(period, '-') WITHIN GROUP (ORDER BY period)
FROM schedule
GROUP BY day
and vice versa for matching Periods, but I'm not seeing how I could do that dynamically for Period and Day in the same query.
You'll also notice that the last row in the example data set doesn't span multiple days or periods, so I also need to account for classes that don't need a GROUP BY at all.
Edit
The end result should be:
+------------+
| Expression |
+------------+
| CCR(A-B) |
+------------+
| 1-2(A) |
+------------+
| 3-4(A-B) |
+------------+
| 5(A) |
+------------+

It is really not clear to me WHY you want output in that way. It doesn't provide any useful information (I don't think) - you can't tell, for example for class_id = 3, which combinations of day and period are actually used. There are four possible combinations (according to the output), but only two are actually in the class schedule.
Anyway - you may have your reasons. Here is how you can do it. You seem to want to LISTAGG both the day and the period (both grouped by class_id, they are not grouped by each other). The difficulty is that you want distinct values in the aggregate lists only - no duplicates. So you will need to select distinct, separately for period and for day, then to the list aggregations, and then concatenate the results in an inner join.
Something like this:
with
test_data ( class_id, day, period ) as (
select 1, 'A', 'CCR' from dual union all
select 1, 'B', 'CCR' from dual union all
select 2, 'A', '1' from dual union all
select 2, 'A', '2' from dual union all
select 3, 'A', '3' from dual union all
select 3, 'B', '4' from dual union all
select 4, 'A', '5' from dual
)
-- end of test data; the actual solution (SQL query) begins below this line
select a.class_id, a.list_per || '(' || b.list_day || ')' as expression
from ( select class_id,
listagg(period, '-') within group (order by period) as list_per
from ( select distinct class_id, period from test_data )
group by class_id
) a
inner join
( select class_id,
listagg(day, '-') within group (order by day) as list_day
from ( select distinct class_id, day from test_data )
group by class_id
) b
on a.class_id = b.class_id
;
CLASS_ID EXPRESSION
-------- ----------
1 CCR(A-B)
2 1-2(A)
3 3-4(A-B)
4 5(A)

How about union with having count(*) = 1?
select LISTAGG(period, '-') list WITHIN GROUP (ORDER BY period)
from schedule
group by CLASS_ID, day
having count(*) = 1
union all
select LISTAGG(day, '-') list WITHIN GROUP (ORDER BY day)
from schedule
group by CLASS_ID, period
having count(*) = 1

Related

Always return X number of rows

We have a set of values which we use to populate a bar chart. For this application, we will always need 5 years of data, we will always need 5 rows of data, even if the values are NULL.
See this query. Assume that the DATE column goes from 2017, 2016, 2015.........even those we may have no data for 2014 & 2013, I will need to return a 2014 & 2013 for, with a NULL as the other column.....
SELECT period_date, actual_eps
FROM (SELECT LAST_DAY(TO_DATE(TO_CHAR(period_date),'YYYYMM')) period_date, actual_eps
FROM period_data
WHERE ticker = 'ADRO'
AND period_type = 'A'
AND actual_eps IS NOT NULL
ORDER BY period_date DESC NULLS LAST)
WHERE rownum <= 5;
So, it will return what rows it has, up to 5, and NULL for the other rows which it does not have, up to 5.......
Thanks in advance
Try using a Common Table Expression/Subquery Factoring to generate rows for each year value. Use a RIGHT JOIN to generate NULLs for any missing rows.
Normally I would use a LEFT JOIN. But in this case I think it reads better this way.
Use NVL to substitute the year for NULL period_date values.
with years as
(
select to_char(sysdate, 'YYYY') as year from dual
UNION ALL
select to_char(add_months(sysdate,-12), 'YYYY') as year from dual
UNION ALL
select to_char(add_months(sysdate,-24), 'YYYY') as year from dual
UNION ALL
select to_char(add_months(sysdate,-36), 'YYYY') as year from dual
UNION ALL
select to_char(add_months(sysdate,-48), 'YYYY') as year from dual
)
SELECT
NVL(TO_CHAR(LAST_DAY(pd.period_date),'YYYYMM'),y.year) as period_date,
pd.actual_eps
FROM period_data pd
RIGHT JOIN years y ON y.year = to_char(pd.period_date,'YYYY')
AND pd.ticker = 'ADRO'
AND pd.period_type = 'A'
AND pd.actual_eps IS NOT NULL
WHERE rownum <= 5
ORDER BY period_date desc, actual_eps nulls last;
Output:
| PERIOD_DATE | ACTUAL_EPS |
|-------------|------------|
| 201902 | foo |
| 201802 | foo |
| 201702 | foo |
| 2016 | (null) |
| 2015 | (null) |
SQL Fiddle example

How to use Oracle's LISTAGG function with a multi values?

I have an 'ITEMS' table like below:
ITEM_NO ITEM_NAME
1 Book
2 Pen
3 Sticky Notes
4 Ink
5 Corrector
6 Ruler
In another 'EMP_ITEMS' table I have the below:
EMPLOYEE ITEMS_LIST
John 1,2
Mikel 5
Sophia 2,3,6
William 3,4
Daniel null
Michael 6
The output has to be like this:
EMPLOYEE ITEMS_LIST ITEM_NAME
John 1,2 Book,Pen
Mikel 5 Corrector
Sophia 2,3,6 Pen,Sticky Notes,Ruler
William 3,4 Sticky Notes,Ink
Daniel null null
Michael 6 Ruler
I used the below query:
SELECT e.EMPLOYEE,e.ITEMS_LIST, LISTAGG(i.ITEM_NAME, ',') WITHIN GROUP (ORDER BY i.ITEM_NAME) ITEM_DESC
FROM EMP_ITEMS e
INNER JOIN ITEMS i ON i.ITEM_NO = e.ITEMS_LIST
GROUP BY e.EMPLOYEE,e.ITEMS_LIST;
But there is an error:
ORA-01722: invalid number
But there is an error: ORA-01722: invalid number
That is because your ITEMS_LIST is a string composed of numeric and comma characters and is not actually a list of numbers and you are trying to compare a single item number to a list of items.
Instead treat it as a string a look for sub-string matches. To do this you will need to surround the strings in the delimiter character and compare to see if one is the substring of the other:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE Items ( ITEM_NO, ITEM_NAME ) As
SELECT 1, 'Book' FROM DUAL UNION ALL
SELECT 2, 'Pen' FROM DUAL UNION ALL
SELECT 3, 'Sticky Notes' FROM DUAL UNION ALL
SELECT 4, 'Ink' FROM DUAL UNION ALL
SELECT 5, 'Corrector' FROM DUAL UNION ALL
SELECT 6, 'Ruler' FROM DUAL;
CREATE TABLE emp_items ( EMPLOYEE, ITEMS_LIST ) AS
SELECT 'John', '1,2' FROM DUAL UNION ALL
SELECT 'Mikel', '5' FROM DUAL UNION ALL
SELECT 'Sophia', '3,2,6' FROM DUAL UNION ALL
SELECT 'William', '3,4' FROM DUAL UNION ALL
SELECT 'Daniel', null FROM DUAL UNION ALL
SELECT 'Michael', '6' FROM DUAL;
Query 1:
SELECT e.employee,
e.items_list,
LISTAGG( i.item_name, ',' )
WITHIN GROUP (
ORDER BY INSTR( ','||e.items_list||',', ','||i.item_no||',' )
) AS item_names
FROM emp_items e
LEFT OUTER JOIN
items i
ON ( ','||e.items_list||',' LIKE '%,'||i.item_no||',%' )
GROUP BY e.employee, e.items_list
Results:
| EMPLOYEE | ITEMS_LIST | ITEM_NAMES |
|----------|------------|------------------------|
| John | 1,2 | Book,Pen |
| Mikel | 5 | Corrector |
| Daniel | (null) | (null) |
| Sophia | 3,2,6 | Sticky Notes,Pen,Ruler |
| Michael | 6 | Ruler |
| William | 3,4 | Sticky Notes,Ink |

match words in all rows in same column

There is a column in table 'mytable' named 'Description'.
+----+-------------------------------+
| ID | Description |
+----+-------------------------------+
| 1 | My NAME is Sajid KHAN |
| 2 | My Name is Ahmed Khan |
| 3 | MY friend name is Salman Khan |
+----+-------------------------------+
I need to write an Oracle SQL query/procedure/function to list the distinct words in the column.
The output should be:
+------------------+-------+
| Word | Count |
+------------------+-------+
| MY | 3 |
| NAME | 3 |
| IS | 3 |
| SAJID | 1 |
| KHAN | 3 |
| AHMED | 1 |
| FRIEND | 1 |
| SALMAN | 1 |
+------------------+-------+
Word matching should be case-insensitive.
I am using Oracle 12.1.
Let's suppose we would somehow manage to split every description in words.
So, instead of single row with Id = 1 and Description = 'My NAME is Sajid KHAN' we'd have 5 rows like this
ID | Description
--- | ------------
1 | My
1 | NAME
1 | is
1 | Sajid
1 | KHAN
in this form it'd be trivial, something like
select Description, count(*) from data_in_new_form group by Description
So, let's do this using recursive query.
create table mytable
as
select 1 as ID, 'My NAME is Sajid KHAN' as Description from dual
union all
select 2, 'My Name is Ahmed Khan' from dual
union all
select 3, 'MY friend name is Salman Khan' from dual
union all
select 4, 'test, punctuation! it is' from dual
;
with
rec (id, str, depth, element_value) as
(
-- Anchor member.
select id, upper(Description) as str, 1 as depth, REGEXP_SUBSTR( upper(Description), '(.*?)( |$)', 1, 1, NULL, 1 ) AS element_value
from mytable
UNION ALL
-- Recursive member.
select id, str, depth + 1, REGEXP_SUBSTR( str ,'(.*?)( |$)', 1, depth+1, NULL, 1 ) AS element_value
from rec
where depth < regexp_count(str, ' ')+1
)
, data as (
select * from rec
--order by id, depth
)
select element_value, count(*) from data
group by element_value
order by element_value
;
Please notice this version doesn't do anything about punctuation assuming words are separated with spaces.
UPDATE alternative way using hierarchic query
with rec as
(
SELECT id, LEVEL AS depth,
REGEXP_SUBSTR( upper(description) ,'(.*?)( |$)', 1, LEVEL, NULL, 1 ) AS element_value
FROM mytable
CONNECT BY LEVEL <= regexp_count(description, ' ')+1
and prior id = id
and prior SYS_GUID() is not null
)
, data as (
select * from rec
--order by id, depth
)
select element_value, count(*) from data
group by element_value
order by 2 desc
;
This query will work. The ordering of the words may be different. However, frequent words come at the beginning as you have listed.
SELECT word,
COUNT(*)
FROM
(SELECT TRIM (REGEXP_SUBSTR (Description, '[^ ]+', 1, ROWNUM) ) AS Word
FROM
(SELECT LISTAGG(UPPER(Description),' ') within GROUP(
ORDER BY ROWNUM ) AS Description
FROM mytable
)
CONNECT BY LEVEL <= REGEXP_COUNT ( Description, '[^ ]+')
)
GROUP BY WORD
ORDER BY 2 DESC;

Oracle SQL - Returning the count from a delimited field

I'm fairly inexperienced with SQL so hopefully this question is not too silly. Here is the scenario:
I have a VARCHAR2 column that stores a series of values delimited by product. Depending on on the account, they can have one or multiple products. I'm trying to write a query that will return the values but also provide a count for each type or product.
For example:
ProductColumn: P1, P2, P3, P4
Table: TableAccount
Sample Value 1: P1:P2:P3
Sample Value 2: P1
Sample Value 3: P2:P3
My current query only returns a count of all different value types including the delimited values:
select
ProductColumn,
count(8) cnt
from TableAccount
group by ProductColumn
Any suggestions would be appreciated!
If the product codes are reliable separated by colons, you can use substring to pull the code values, separate from the separator colons. That allows you to return then to the caller, each in separate fields, so summing, grouping, etc. However, that will get messy if any of the values are longer than two
bytes. This is why data normalization rules specifically spell out not putting more than one piece of data into a single table column. If it were me, I'd write a PL SQL that splits them out and writes it all cleanly to a NORMALIZED table, then queries from that table. And I would be all over my boss about getting this design flaw FIXED.
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE TableAccount ( value ) AS
SELECT 'P1:P2:P3' FROM DUAL
UNION ALL SELECT 'P1' FROM DUAL
UNION ALL SELECT 'P2:P3' FROM DUAL
UNION ALL SELECT 'P1:P3' FROM DUAL
UNION ALL SELECT 'P1:P4' FROM DUAL
UNION ALL SELECT 'P5' FROM DUAL;
Query 1:
SELECT item,
COUNT(1) AS frequency
FROM (
SELECT REGEXP_SUBSTR( value, '[^:]+', 1, COLUMN_VALUE ) AS item
FROM TableAccount t,
TABLE(
CAST(
MULTISET(
SELECT LEVEL
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT( t.value, '[^:]+')
) AS sys.OdciNumberList
)
)
)
GROUP BY item
ORDER BY item
Results:
| ITEM | FREQUENCY |
|------|-----------|
| P1 | 4 |
| P2 | 2 |
| P3 | 3 |
| P4 | 1 |
| P5 | 1 |

Merging Query Result into single row - Oracle

Can I do like this in oracle,.? I have some data like this:
No | Data |
===========
1 | A |
1 | B |
1 | C |
1 | D |
Is there any query that can produce a result like this,.?
No | Data |
=================
1 | A, B, C, D |
Many thanks :D
Maybe this page shows what you are looking for.
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE TEST ( ID, DATA ) AS
SELECT 1, 'A' FROM DUAL
UNION ALL SELECT 1, 'B' FROM DUAL
UNION ALL SELECT 1, 'C' FROM DUAL
UNION ALL SELECT 1, 'D' FROM DUAL
UNION ALL SELECT 2, 'E' FROM DUAL
UNION ALL SELECT 2, 'F' FROM DUAL;
Query 1:
SELECT ID,
LISTAGG( DATA, ',' ) WITHIN GROUP ( ORDER BY DATA ) AS AGGREGATED_DATA
FROM TEST
GROUP BY ID
Results:
| ID | AGGREGATED_DATA |
|----|-----------------|
| 1 | A,B,C,D |
| 2 | E,F |
In Oracle we can use wm_concat function. Here is the query for example above:
SELECT no, wm_concat(data) from table group by no
reference: wm_concat
select
no,
rtrim (xmlagg (xmlelement (d, data|| ',')).extract ('//text()'), ',') data
from
table_name
group by
no
;

Resources