Oracle SQL - Returning the count from a delimited field - oracle

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 |

Related

Building an Oracle view with a sequence

I'm trying to build a new Oracle view off of a table. The only difference between the two is that I want to add a new column with a unique ID. The IDs have to be unique, but does not need to be ordered.
I tried to run a script like this:
CREATE VIEW <VIEW_NAME>
(
ID, VALUE1, VALUE2,...
)
AS
SELECT SEQ1.NEXTVAL, VAL1, VAL2,... FROM <TABLE>
However, I'm running into errors. A previous post mentions it's not really possible but didn't elaborate so I was hoping to get some clarity. Doing an INSERT doesn't seem useful because I'd have to populate all the other values too, at least from what I've been reading.
Edit: IDs should be consistent every time I look at the view.
Picture of error:
If you just want a unique value for each row then you can use ROWNUM or the ROW_NUMBER analytic function:
CREATE VIEW view_name ( ID, VALUE1, VALUE2,... ) AS
SELECT ROWNUM,
VAL1,
VAL2,
...
FROM table_name
or
CREATE VIEW view_name ( ID, VALUE1, VALUE2,... ) AS
SELECT ROW_NUMBER() OVER ( ORDER BY val1, val2 ),
VAL1,
VAL2,
...
FROM table_name
IDs should be consistent every time I look at the view.
I do not think this is possible; you would need to store the IDs somewhere and that requires a table rather than a view/sequence.
For example:
Oracle Setup:
CREATE TABLE table_name ( val1, val2 ) AS
SELECT 1, 'a' FROM DUAL UNION ALL
SELECT 2, 'b' FROM DUAL
CREATE SEQUENCE view_name__seq;
CREATE FUNCTION seq_value RETURN NUMBER
IS
BEGIN
RETURN view_name__seq.NEXTVAL;
END;
/
CREATE VIEW view_name ( id, value1, value2 ) AS
SELECT seq_value, val1, val2 FROM table_name;
If you select from the view then first time:
SELECT * FROM view_name;
you get:
ID | VALUE1 | VALUE2
-: | -----: | :-----
1 | 1 | a
2 | 2 | b
and second time you get:
ID | VALUE1 | VALUE2
-: | -----: | :-----
3 | 1 | a
4 | 2 | b
db<>fiddle here
and the IDs are not consistent.
I would suggest you to use INVISIBLE column in your base table.
ALTER TABLE MY_TABLE ADD MY_UNIQUE_ID NUMBER INVISIBLE;
Now, assign sequential number to it or use as GENERATED AS IDENTITY (try this)
This will not break you application and you can use it in your view to achieve the desired result and return consistent values in your view.
Cheers!!

Return first row in each group from Oracle SQL

Hi I need to come up with a query that efficiently returns just one record per group(I might be thinking about it wrong) and stop searching for more records in that group as soon as it has found one record.
This is my table:
|col1 | col2|
|-----|-----|
| A | 1 |
| A | 2 |
| B | 3 |
| B | 4 |
I want to return.
|col1 | col2|
|-----|-----|
| A | 1 |
| B | 3 |
Note that I don't actually care if in row one I have A,1 or A,2(same applies to second row).
What I want is to get one record that has A in first column could be any record that matches that criteria, and similarly I want one record that has B in col1.
The closes that I know to getting this are two queries
SELECT col1, MIN(col2)
FROM tablename
GROUP BY col1
and the other:
SELECT *
FROM tablename
WHERE col1 = 'A'
AND ROWNUM = 1
First query is not good enough because it will try to find all records that have A in col1(in the actual table I'm looking at this means searching though millions of rows, and my indicies won't be of much help here). Second query will return just one value of col1 at a time, so I'd have to run it thousands of times to get all the records I need.
NOTE:
I did see similar question in here but the answers were focused on just getting the right query results, I my case issue is how long do I need to wait for these results.
Sounds like this is the query you are looking for:
select col1
, min(col2) keep (dense_rank first order by rownum) col2
from tablename
group by col1;
I feel the below query is returning the result quick.
SELECT col1,col2 FROM (
SELECT col1,col2, ROW_NUMBER () over (partition by col1 order by col2 asc)
minseq FROM tablename
--where rownum < 1000000000
)
where minseq = 1;
Normal query
SELECT col1, MIN(col2)
FROM tablename
GROUP BY col1
took 1 min to fetch 500 records out of 100000000 records and this query took 0.03 seconds

Understanding MultiSet in Oracle & advantages,importance of it

I have seen a lot of answer's using Multiset & AS SYS.ODCIVARCHAR2LIST etc.. and I am not able to understand the logic behind/ how it works out. Please find below example (actually taken from another question). Request you to please explain the workflow/working for understanding multiset. And yes I tried to read the documents related to it.
CREATE TABLE table_name ( Id, Column1, Column2 ) AS
SELECT 1, 'A,B,C', 'H' FROM DUAL UNION ALL
SELECT 2, 'D,E', 'J,K' FROM DUAL UNION ALL
SELECT 3, 'F', 'L,M,N' FROM DUAL;
Query:
SELECT t.id,
c1.COLUMN_VALUE AS c1,
c2.COLUMN_VALUE AS c2
FROM table_name t
CROSS JOIN
TABLE(
CAST(
MULTISET(
SELECT REGEXP_SUBSTR( t.Column1, '[^,]+', 1, LEVEL )
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT( t.Column1, '[^,]+' )
) AS SYS.ODCIVARCHAR2LIST
)
) c1
CROSS JOIN
TABLE(
CAST(
MULTISET(
SELECT REGEXP_SUBSTR( t.Column2, '[^,]+', 1, LEVEL )
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT( t.Column2, '[^,]+' )
) AS SYS.ODCIVARCHAR2LIST
)
) c2
Result::
| ID | C1 | C2 |
|----|----|----|
| 1 | A | H |
| 1 | B | H |
| 1 | C | H |
| 2 | D | J |
| 2 | D | K |
| 2 | E | J |
| 2 | E | K |
| 3 | F | L |
| 3 | F | M |
| 3 | F | N |
Thanks in advance.
Oracle offers three different types of collection, on other languages term "array" is more common.
Associative array (or index-by table)
VARRAY (variable-size array)
Nested table
Have a look at Collections Types in order to see the differences and where which one can be used.
Start with something simple, e.g.
create type number_table_type as table of number;
You can use them in several ways, the three selects are equivalent. They all select table emp and stores emp_id values into Nested table emp_T.
create table emp (
emp_id number,
emp_name varchar2(100));
insert into emp values (10, 'Scott');
insert into emp values (20, 'King');
insert into emp values (30, 'Tiger');
declare
emp_T number_table_type:
begin
select cast(collect(emp_id) as number_table_type)
into emp_T
from emp;
select emp_id
bulk collect into emp_T
from emp;
SELECT CAST(MULTISET(SELECT emd_id FROM emp) AS number_table_type)
into emp_T
FROM dual;
emp_T := number_table_type(10,20,30);
for i in 1..3 loop
emp_T.EXTEND;
emp_T(i) := 10*i;
end loop;
end;
For the opposite way, i.e. transform a Nested table into "normal" table use TABLE function:
select *
from TABLE(emp_T);
Oracle provides Multiset Operators and Multiset Conditions. They offer function like join two arrays, make values distinct, etc. Many times I see developers writing a LOOP in their code where a Mulitset Operator/Condition would do the same stuff with a single command.
Once you are familiar with such basic array you could also try more complex structures. However, according to my feeling such complex structures are quite common in training material and documentation but hardly used in "real life". Oracle strength is to store and manipulate relational data, rather than object oriented data.
SYS.ODCIVARCHAR2LIST, et al. are just some predefined types. Actually it does not matter wether you use them or create you own types.
Well, you should review such a code from the deepest level and navigate up, in order to figure out what's going on.
This is based on the first row of your table: as you can see, that "regexp" magic converts your comma separated values (i.e. column) into rows.
SQL> with test as
2 (select 1, 'A,B,C' column1 from dual)
3 select regexp_substr(t.column1, '[^,]+', 1, level)
4 from test t
5 connect by level <= regexp_count(t.column1, '[^,]+');
REGEXP_SUBSTR(T.COLU
--------------------
A
B
C
SQL>
MULTISET creates a "collection" of those values, while CAST "converts" it into a SYS.ODCIVARCHAR2LIST type. It is, as you can see, owned by SYS and acts as if you created your own type (using the CREATE TYPE command) which contains VARCHAR2 values. When types are "simple" as this one, or the one that contains numbers (so you'd use SYS.ODCINUMBERLIST), you can use predefined one instead of creating your own. Therefore, that collection contains A, B and C.
Finally, TABLE function produces a collection of rows that can be queried, just as if it was an "ordinary" table.

Oracle SQL Merge Multiple rows With Same ID But Out of Order Identifiers

I am trying to create a distinct list of parts to do analysis on in a table. The table contains a column of Part IDs and a column of Identifiers. The Identifiers are separated within the same entry by pipes but unfortunately the Identifiers are out of order. I'm not sure if this is possible but any help would be greatly appreciated!
For example (currently Both ID and Identifiers are VARCHAR2)
ID Identifiers
1 |1|2|
1 |2|1|
2 |3|A|1|B|
2 |B|1|3|A|
3 |1|3|2|
3 |1|5|
3 |2|1|3|
4 |AA|BB|1|3A|
4 |1|3A|AA|BB|
and I need the query to return
ID Identifiers
1 |1|2|
2 |3|A|1|B|
3 |1|5|
3 |1|3|2|
4 |1|AA|BB|3A|
It does not matter what specific order the identifiers are ordered in as long as all contents within that identifier are the same. For example, |1|5| or |5|1| doesn't matter but I need to see both entries |1|5| and |1|3|2. My original thought was to create separate out the identifiers into separate columns and then alphabetically concatenate back into one column but i'm not sure...thanks in advance!
Something like this (assuming there are no duplicate rows in the input table - if there are, the solution needs to be modified a bit).
In the solution I build the test_table for testing (it is not part of the solution), and I build another factored subquery in the WITH clause. This works in Oracle 11 and above. For earlier versions of Oracle, the subquery defined as prep needs to be moved as a subquery within the final query instead.
with
test_table ( id, identifiers ) as (
select '1', '|1|2|' from dual union all
select '1', '|2|1|' from dual union all
select '2', '|3|A|1|B|' from dual union all
select '2', '|B|1|3|A|' from dual union all
select '3', '|1|3|2|' from dual union all
select '3', '|1|5|' from dual union all
select '3', '|2|1|3|' from dual union all
select '4', '|AA|BB|1|3A|' from dual union all
select '4', '|1|3A|AA|BB|' from dual
),
prep ( id, identifiers, token ) as (
select id, identifiers, regexp_substr(identifiers, '[^|]+', 1, level)
from test_table
connect by level <= regexp_count(identifiers, '\|') - 1
and prior identifiers = identifiers
and prior sys_guid() is not null
)
select distinct id,
'|' || listagg(token, '|') within group (order by token) || '|'
as identifiers
from prep
group by id, identifiers
order by id, identifiers -- ORDER BY is optional
;
Output:
ID IDENTIFIERS
--- --------------------
1 |1|2|
2 |1|3|A|B|
3 |1|2|3|
3 |1|5|
4 |1|3A|AA|BB|
5 rows selected.

How do I conditionally group by two different columns in 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

Resources