SQL oracle : Group and Count data - sql

I have a table like this:
+-----+------+
| acc | CASE |
+-----+------+
| 001 | a |
| 001 | b |
| 001 | c |
| 002 | a |
| 002 | b |
| 003 | b |
| 003 | c |
| 004 | a |
| 005 | b |
| 006 | b |
| 007 | a |
| 007 | b |
| 007 | c |
| 008 | a |
| 008 | b |
| n | x |
+-----+------+
I have no idea how to group and count data with
+-----------+-----------+
| case | count_acc |
+-----------+-----------+
| a | 1 |
| b | 2 |
| c | 0 |
| a+b | 2 |
| b+c | 1 |
| a+b+c | 2 |
| a+b+c+…+x | n |
+-----------+-----------+
in case a+b,b+c ... a+b+c+…+x I can't group case and count acc. Do you have any idea to group and count?

select b.case,count(distinct(a.acc)) as account from
test a , (select acc , rtrim(case,'+') case
from ( select acc , case , rn from test
model
partition by (acc)
dimension by (row_number() over (partition by acc order by case) rn )
measures (cast(case as varchar2(10)) case)
rules
(case[any] order by rn desc = case[cv()]||'+'||case[cv()+1])
)
where rn = 1) b
where a.acc = b.acc
group by b.case

You can achieve the same using LISTAGG in oracle
with test as ( select 001 acc , 'a' case FROM DUAL UNION
SELECT 001 , 'b' FROM DUAL UNION
SELECT 001 , 'c' FROM DUAL UNION
SELECT 002 , 'a' FROM DUAL UNION
SELECT 002 , 'b' FROM DUAL UNION
SELECT 003 , 'b' FROM DUAL UNION
SELECT 003 , 'c' FROM DUAL UNION
SELECT 004 , 'a' FROM DUAL UNION
SELECT 005 , 'b' FROM DUAL UNION
SELECT 006 , 'b' FROM DUAL UNION
SELECT 007 , 'a' FROM DUAL UNION
SELECT 007 , 'b' FROM DUAL UNION
SELECT 007 , 'c' FROM DUAL UNION
SELECT 008 , 'a' FROM DUAL UNION
SELECT 008 , 'b' FROM DUAL )
select case,count(1) from
(
SELECT count(1),acc, LISTAGG(case, '+') WITHIN GROUP (ORDER BY acc) AS case
FROM test
GROUP BY acc) GROUP BY case order by 1;
SQL fiddle here

First you have to organize your data, grouping the ACC and Aggregating the CASE
SELECT
LISTAGG (case,'+') WITHIN GROUP (ORDER BY case) case,
ACC
FROM TEST
GROUP BY ACC
Them you will be able to count:
SELECT case, count(*) FROM (
SELECT
LISTAGG (case,'+') WITHIN GROUP (ORDER BY case) case,
ACC
FROM TEST
GROUP BY ACC
) GROUP BY case
ORDER BY CASE;
In case you don't have function LISTAGG, below 11g for example, refer to this website:
http://oracle-base.com/articles/misc/string-aggregation-techniques

Related

Filtering a table via another table's values

I have 2 tables:
Value
+----+-------+
| id | name |
+----+-------+
| 1 | Peter |
| 2 | Jane |
| 3 | Joe |
+----+-------+
Filter
+----+---------+------+
| id | valueid | type |
+----+---------+------+
| 1 | 1 | A |
| 2 | 1 | B |
| 3 | 1 | C |
| 4 | 1 | D |
| 5 | 2 | A |
| 6 | 2 | C |
| 7 | 2 | E |
| 8 | 3 | A |
| 9 | 3 | D |
+----+---------+------+
I need to retrieve the values from the Value table where the related Filter table does not contain the type 'B' or 'C'
So in this quick example this would be only Joe.
Please note this is a DB2 DB and i have limited permissions to run selects only.
Or also a NOT IN (<*fullselect*) predicate:
Only that my result is 'Joe', not 'Jane' - and the data constellation would point to that ...
WITH
-- your input, sans reserved words
val(id,nam) AS (
SELECT 1,'Peter' FROM sysibm.sysdummy1
UNION ALL SELECT 2,'Jane' FROM sysibm.sysdummy1
UNION ALL SELECT 3,'Joe' FROM sysibm.sysdummy1
)
,
filtr(id,valueid,typ) AS (
SELECT 1,1,'A' FROM sysibm.sysdummy1
UNION ALL SELECT 2,1,'B' FROM sysibm.sysdummy1
UNION ALL SELECT 3,1,'C' FROM sysibm.sysdummy1
UNION ALL SELECT 4,1,'D' FROM sysibm.sysdummy1
UNION ALL SELECT 5,2,'A' FROM sysibm.sysdummy1
UNION ALL SELECT 6,2,'C' FROM sysibm.sysdummy1
UNION ALL SELECT 7,2,'E' FROM sysibm.sysdummy1
UNION ALL SELECT 8,3,'A' FROM sysibm.sysdummy1
UNION ALL SELECT 9,3,'D' FROM sysibm.sysdummy1
)
-- real query starts here
SELECT
*
FROM val
WHERE id NOT IN (
SELECT valueid FROM filtr WHERE typ IN ('B','C')
)
;
-- out id | nam
-- out ----+-------
-- out 3 | Joe
Or also, a failing left join:
SELECT
val.*
FROM val
LEFT JOIN (
SELECT valueid FROM filtr WHERE typ IN ('B','C')
) filtr
ON filtr.valueid = val.id
WHERE valueid IS NULL
You can use EXISTS, as in:
select *
from value v
where not exists (
select null from filter f
where f.valueid = v.id and f.type in ('B', 'C')
);
Result:
ID NAME
--- -----
3 Joe
See running example at db<>fiddle.

Oracle: How can I pivot an EAV table with a dynamic cardinality for certain keys?

I have the following Entity–attribute–value (EAV) table in Oracle:
| ID | Key | Value |
|----|-------------|--------------|
| 1 | phone_num_1 | 111-111-1111 |
| 1 | phone_num_2 | 222-222-2222 |
| 1 | contact_1 | friend |
| 1 | contact_2 | family |
| 1 | first_name | mike |
| 1 | last_name | smith |
| 2 | phone_num_1 | 333-333-3333 |
| 2 | phone_num_2 | 444-444-4444 |
| 2 | contact_1 | family |
| 2 | contact_2 | friend |
| 2 | first_name | john |
| 2 | last_name | adams |
| 3 | phone_num_1 | 555-555-5555 |
| 3 | phone_num_2 | 666-666-6666 |
| 3 | phone_num_3 | 777-777-7777 |
| 3 | contact_1 | work |
| 3 | contact_2 | family |
| 3 | contact_3 | friend |
| 3 | first_name | mona |
| 3 | last_name | lisa |
Notice that some keys are indexed and therefore have an association with other indexed keys. For example, phone_num_1 is to be associated with contact_1.
Note: There is no hard limit to the number of indexes. There can be 10, 20, or even 50 phone_num_*, but it's guaranteed that for each phone_num_N, there is a corresponding contact_N
This is my desired result:
| ID | Phone_Num | Contact | First_Name | Last_Name |
|----|--------------|---------|------------|-----------|
| 1 | 111-111-1111 | friend | mike | smith |
| 1 | 222-222-2222 | family | mike | smith |
| 2 | 333-333-3333 | family | john | adams |
| 2 | 444-444-4444 | friend | john | adams |
| 3 | 555-555-5555 | work | mona | lisa |
| 3 | 666-666-6666 | family | mona | lisa |
| 3 | 777-777-7777 | friend | mona | lisa |
What have I tried/looked at:
I have looked into the pivot function of Oracle; however, I don't believe that can solve my problem since I don't have a fixed number of attributes that I want to pivot on.
I've looked at these posts:
SQL Query to return multiple key value pairs from a single table in one row
Pivot rows to columns without aggregate
Question:
Is what I'm tying to accomplish at all possible purely with SQL? If so, how can it be done? If not, please explain why.
Any help is much appreciated and here's the with table to help you get started:
with
table_1 ( id, key, value ) as (
select 1,'phone_num_1','111-111-1111' from dual union all
select 1,'phone_num_2','222-222-2222' from dual union all
select 1,'contact_1','friend' from dual union all
select 1,'contact_2','family' from dual union all
select 1,'first_name','mike' from dual union all
select 1,'last_name','smith' from dual union all
select 2,'phone_num_1','333-333-3333' from dual union all
select 2,'phone_num_2','444-444-4444' from dual union all
select 2,'contact_1','family' from dual union all
select 2,'contact_2','friend' from dual union all
select 2,'first_name','john' from dual union all
select 2,'last_name','adams' from dual union all
select 3,'phone_num_1','555-555-5555' from dual union all
select 3,'phone_num_2','666-666-6666' from dual union all
select 3,'phone_num_3','777-777-7777' from dual union all
select 3,'contact_1','work' from dual union all
select 3,'contact_2','family' from dual union all
select 3,'contact_3','friend' from dual union all
select 3,'first_name','mona' from dual union all
select 3,'last_name','lisa' from dual
)
select * from table_1;
This is not a dynamic pivot as you have a fixed set of keys - you just need to separate the enumeration of the keys from the keys themselves first.
You need to:
Separate the phone_num and contact key prefixes from the enumerated item; then
Pivot the common keys that have no enumeration so that they are associated with each enumerated key; and finally,
Pivot a second time to get the enumerated keys in a row together.
Oracle Setup:
CREATE TABLE table_1 ( id, key, value ) as
select 1,'phone_num_1','111-111-1111' from dual union all
select 1,'phone_num_2','222-222-2222' from dual union all
select 1,'contact_1','friend' from dual union all
select 1,'contact_2','family' from dual union all
select 1,'first_name','mike' from dual union all
select 1,'last_name','smith' from dual union all
select 2,'phone_num_1','333-333-3333' from dual union all
select 2,'phone_num_2','444-444-4444' from dual union all
select 2,'contact_1','family' from dual union all
select 2,'contact_2','friend' from dual union all
select 2,'first_name','john' from dual union all
select 2,'last_name','adams' from dual union all
select 3,'phone_num_1','555-555-5555' from dual union all
select 3,'phone_num_2','666-666-6666' from dual union all
select 3,'phone_num_3','777-777-7777' from dual union all
select 3,'contact_1','work' from dual union all
select 3,'contact_2','family' from dual union all
select 3,'contact_3','friend' from dual union all
select 3,'first_name','mona' from dual union all
select 3,'last_name','lisa' from dual
Query:
SELECT *
FROM (
SELECT id,
CASE
WHEN key LIKE 'phone_num_%' THEN 'phone_num'
WHEN key LIKE 'contact_%' THEN 'contact'
ELSE key
END AS key,
CASE
WHEN key LIKE 'phone_num_%'
OR key LIKE 'contact_%'
THEN TO_NUMBER( SUBSTR( key, INSTR( key, '_', -1 ) + 1 ) )
ELSE NULL
END AS item,
value,
MAX( CASE key WHEN 'first_name' THEN value END )
OVER ( PARTITION BY id ) AS first_name,
MAX( CASE key WHEN 'last_name' THEN value END )
OVER ( PARTITION BY id ) AS last_name
FROM table_1
)
PIVOT( MAX( value ) FOR key IN ( 'contact' AS contact, 'phone_num' AS phone_num ) )
WHERE item IS NOT NULL
ORDER BY id, item
Output:
ID | ITEM | FIRST_NAME | LAST_NAME | CONTACT | PHONE_NUM
-: | ---: | :--------- | :-------- | :------ | :-----------
1 | 1 | mike | smith | friend | 111-111-1111
1 | 2 | mike | smith | family | 222-222-2222
2 | 1 | john | adams | family | 333-333-3333
2 | 2 | john | adams | friend | 444-444-4444
3 | 1 | mona | lisa | work | 555-555-5555
3 | 2 | mona | lisa | family | 666-666-6666
3 | 3 | mona | lisa | friend | 777-777-7777
db<>fiddle here
If you can refactor the table then a simple improvement would be to add an extra column to hold the enumeration of the keys and use NULL when it is a value common to every enumeration:
CREATE TABLE table_1 ( id, key, line, value ) as
select 1, 'phone_num', 1, '111-111-1111' from dual union all
select 1, 'phone_num', 2, '222-222-2222' from dual union all
select 1, 'contact', 1, 'friend' from dual union all
select 1, 'contact', 2, 'family' from dual union all
select 1, 'first_name', NULL, 'mike' from dual union all
select 1, 'last_name', NULL, 'smith' from dual
Then your set of keys is always fixed and you do not need to extract the enumeration value from the key.
This is ugly, but I think does what you need
select t1.* , t2.value, t3.n, t3.f
from table_1 t1
inner join table_1 t2 on t1.id = t2.id and REPLACE(t1.key, 'phone_num_', '') = REPLACE(t2.key, 'contact_', '')
inner join (
select ID, min(case when Key = 'first_name' then Value end) as n, min(case when Key = 'last_name' then Value end) as f
from table_1
group by ID
) t3 on t1.id = t3.id
where
t1.Key not in('first_name','last_name')
SELECT id,
phone,
contact,
first_value(last) IGNORE NULLS over (partition BY id order by id DESC range BETWEEN CURRENT row AND unbounded following ) last_name,
first_value(FIRST) IGNORE NULLS over (partition BY id order by id DESC range BETWEEN CURRENT row AND unbounded following ) first_name
FROM
(SELECT id,
value,
row_number() over ( partition BY id,SUBSTR(KEY,1 ,instr(KEY,'',1)-1) order by KEY) rn,
SUBSTR(KEY,1 ,instr(KEY,'',1) -1) KEY
FROM table_1
) pivot ( MAX(value) FOR KEY IN ( 'phone' AS phone,'last' AS last,'first' AS FIRST,'contact' AS contact))
ORDER BY id;

How collpase all subgroups into one line and keep the same order

This is a simplified version of my table
+----+----------+------------+------------+
| ID | Category | Start Date | End Date |
+----+----------+------------+------------+
| 1 | 'Alpha' | 2018/04/12 | 2018/04/15 |
| 2 | null | 2018/04/17 | 2018/04/21 |
| 3 | 'Gamma' | 2018/05/02 | 2018/05/07 |
| 4 | 'Gamma' | 2018/05/09 | 2018/05/11 |
| 5 | 'Gamma' | 2018/05/11 | 2018/05/17 |
| 6 | 'Alpha' | 2018/05/17 | 2018/05/23 |
| 7 | 'Alpha' | 2018/05/23 | 2018/05/24 |
| 8 | null | 2018/05/24 | 2018/06/02 |
| 9 | 'Beta' | 2018/06/12 | 2018/06/16 |
| 10 | 'Beta' | 2018/06/16 | 2018/06/20 |
+----+----------+------------+------------+
All Start Date are unique, not nullable and they have the same order as the IDs (if a and b are IDs and a < b then StartDate[a] < StartDate[b]). The Start Date is not always equal to the End Date of the previous row for the same Category (look at id 3 and 4).
I'm looking for a query that will give me the following result
+----------+------------+------------+
| Category | Start Date | End Date |
+----------+------------+------------+
| 'Alpha' | 2018/04/12 | 2018/04/15 |
| null | 2018/04/17 | 2018/04/21 |
| 'Gamma' | 2018/05/02 | 2018/05/17 |
| 'Alpha' | 2018/05/17 | 2018/05/24 |
| null | 2018/05/24 | 2018/06/02 |
| 'Beta' | 2018/06/12 | 2018/06/20 |
+----------+------------+------------+
Note: The End Date will be equal to End Date of the last row in the subgroup (same continuous Category).
This is a gaps-and-islands problem. I think you can use the difference of row numbers:
select category, min(startdate), max(enddate)
from (select t.*,
row_number() over (order by id) as seqnum,
row_number() over (partition by category order by id) as seqnum_c
from t
) t
group by category, (seqnum - seqnum_c)
order by min(startdate);
This is a gaps and islands question, you can use such a logic below
select category, min(start_date) as start_date, max(end_date) as end_date
from
(
select tt.*, sum(grp) over (order by id, start_date) sm
from
(
with t( ID, Category, Start_Date, End_Date) as
(
select 1 , 'Alpha' , date'2018-04-12',date'2018-04-15' from dual union all
select 2 , null , date'2018-04-17',date'2018-04-21' from dual union all
select 3 , 'Gamma' , date'2018-05-02',date'2018-05-07' from dual union all
select 4 , 'Gamma' , date'2018-05-09',date'2018-05-11' from dual union all
select 5 , 'Gamma' , date'2018-05-11',date'2018-05-17' from dual union all
select 6 , 'Alpha' , date'2018-05-17',date'2018-05-23' from dual union all
select 7 , 'Alpha' , date'2018-05-23',date'2018-05-24' from dual union all
select 8 , null , date'2018-05-24',date'2018-06-02' from dual union all
select 9 , 'Beta' , date'2018-06-12',date'2018-06-16' from dual union all
select 10 , 'Beta' , date'2018-06-16',date'2018-06-20' from dual
)
select id, Category,
decode(nvl(lag(end_date) over
(order by end_date),start_date),start_date,0,1)
as grp, --> means prev. value equals or not
row_number() over (order by id, end_date) as rn, start_date, end_date
from t
) tt
order by rn
)
group by Category, sm
order by end_date;
CATEGORY START_DATE END_DATE
Alpha 12.04.2018 15.04.2018
NULL 17.04.2018 21.04.2018
Gamma 02.05.2018 07.05.2018
Gamma 09.05.2018 17.05.2018
Alpha 17.05.2018 24.05.2018
NULL 24.05.2018 02.06.2018
Beta 12.06.2018 20.06.2018

How can I omit groups of records where the attribute is based on a 'like' clause across 2 different fields

I need a query to omit groups where the number 16 is present in both records and are present across different attributes within the group . Basically, if we have a 16 somewhere in attributes on different records, then we know what accounts for these groups, and no further analysis is needed on them. We would like to keep results where 16 only occurs in one record in either attribute, 16 occurs in neither, and records that have nulls in them but do not have the 16 in 2 records in different attributes.
Here is an example:
---------------------------------------------
| groupid | category | test_results |
---------------------------------------------
| 001 | red13tall | |
| 001 | | blue16small |
| 002 | green16small| |
| 002 | | blue16small |
| 003 | yellow3tall | |
| 003 | | green2giant |
| 004 | orange16tall | |
| 004 | | blue16tall |
| 005 | red16short | |
| 005 | green12bald | orange14tall |
| 006 | blue3short | red16big |
| 006 | green16flat | |
---------------------------------------------
This is the result we are looking for:
---------------------------------------------
| groupid | category | test_results |
---------------------------------------------
| 001 | red13tall | |
| 001 | | blue16small |
| 003 | yellow3tall | |
| 003 | | green2giant |
| 005 | red16short | |
| 005 | green12bald | orange14tall |
------------------------------------------
Assuming your table is called your_table and has a primary key of id, then
SELECT t3.groupid, t3.category, t3.test_results
FROM your_table t3
WHERE t3.groupid NOT IN (
SELECT t1.groupid
FROM your_table t1, your_table t2
WHERE t1.id <> t2.id
AND t1.groupid = t2.groupid
AND t1.category LIKE '%16%'
AND t2.test_results LIKE '%16%'
)
Note, this assumes you're looking for 16 to appear in two different rows in the 2 different columns. If you don't care if they appear in the same row then you can remove the t1.id <> t2.id condition.
One way or another you need conditional counting. If you use analytic functions you can avoid joins, which are often a performance drag.
For the solution below I interpreted your words literally: each group has exactly two rows, and a group is excluded if all three conditions are met: BOTH rows have 16 at least once (in category or in test_results); 16 appears in category at least once; and 16 appears in test_results at least once.
You can modify the query very easily if you don't need the condition on each row of the group having 16 at least once (remove all references to r_ct).
with
test_data ( groupid, category, test_results ) as (
select '001', 'red13tall' , null from dual union all
select '001', null , 'blue16small' from dual union all
select '002', 'green16small', null from dual union all
select '002', null , 'blue16small' from dual union all
select '003', 'yellow3tall' , null from dual union all
select '003', null , 'green2giant' from dual union all
select '004', 'orange16tall', null from dual union all
select '004', null , 'blue16tall' from dual union all
select '005', 'red16short' , null from dual union all
select '005', 'green12bald' , 'orange14tall' from dual union all
select '006', 'blue3short' , 'red16big' from dual union all
select '006', 'green16flat' , null from dual
)
-- end of test data (not part of solution); SQL query begins below this line
select groupid, category, test_results
from (
select groupid, category, test_results,
count(case when category like '%16%' then 1
when test_results like '%16%' then 1 end)
over (partition by groupid) as r_ct,
count(case when category like '%16%' then 1 end)
over (partition by groupid) as c_ct,
count(case when test_results like '%16%' then 1 end)
over (partition by groupid) as t_ct
from test_data
)
where r_ct < 2 or c_ct = 0 or t_ct = 0
order by groupid -- if needed
;
Output:
GROUPID CATEGORY TEST_RESULTS
------- ------------ ------------
001 red13tall
001 blue16small
003 yellow3tall
003 green2giant
005 red16short
005 green12bald orange14tall
6 rows selected.

Create view from multiple tables, combine values from multiple rows into one row

I have 3 tables as below:
Area table:
UserID | Area
---------------
1 | 10001
2 | 10002
3 | 10003
Info table:
UserID | Info
-----------------
1 | U1_Info1
1 | U1_Info2
1 | U1_Info3
2 | U2_Info1
3 | U3_Info1
Company table:
UserID | Company
-----------------
1 | ComA
2 | ComB
3 | ComC
After that, I want group by UserID. My expected result as below:
UserID | Area | Info1 | Info2 | Info3 | Company
----------------------------------------------------------
1 | 10001 | U1_Info1 | U1_Info2 | U1_Info3 | ComA
2 | 10002 | U2_Info1 | | | ComB
3 | 10003 | U3_Info1 | | | ComC
User 3 doesn't have Info2 and Info3 so I set them = ' '.
Can I make a View like that?
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE Area ( UserID, Area ) AS
SELECT 1, 10001 FROM DUAL
UNION ALL SELECT 2, 10002 FROM DUAL
UNION ALL SELECT 3, 10003 FROM DUAL;
CREATE TABLE Info ( UserID, Info ) AS
SELECT 1, 'U1_Info1' FROM DUAL
UNION ALL SELECT 1, 'U1_Info2' FROM DUAL
UNION ALL SELECT 1, 'U1_Info3' FROM DUAL
UNION ALL SELECT 2, 'U2_Info1' FROM DUAL
UNION ALL SELECT 3, 'U3_Info1' FROM DUAL;
CREATE TABLE Company (UserID, Company ) AS
SELECT 1, 'ComA' FROM DUAL
UNION ALL SELECT 2, 'ComB' FROM DUAL
UNION ALL SELECT 3, 'ComC' FROM DUAL;
CREATE VIEW TEST AS
SELECT A.UserID,
MAX( A.Area ) AS Area,
MAX( CASE WHEN I.Info LIKE '%_Info1' THEN I.Info END ) AS Info1,
MAX( CASE WHEN I.Info LIKE '%_Info2' THEN I.Info END ) AS Info2,
MAX( CASE WHEN I.Info LIKE '%_Info3' THEN I.Info END ) AS Info3,
MAX( C.Company ) AS Company
FROM Area A
INNER JOIN
Company C
ON ( A.UserID = C.UserID )
LEFT OUTER JOIN
Info I
ON ( A.UserID = I.UserID )
GROUP BY
A.UserID
Query 1:
SELECT * FROM test
Results:
| USERID | AREA | INFO1 | INFO2 | INFO3 | COMPANY |
|--------|-------|----------|----------|----------|---------|
| 1 | 10001 | U1_Info1 | U1_Info2 | U1_Info3 | ComA |
| 2 | 10002 | U2_Info1 | (null) | (null) | ComB |
| 3 | 10003 | U3_Info1 | (null) | (null) | ComC |