SQL - Find unique min value and associated columns - sql

I have a set of data as below, showing the history of who has done what with a record. The unique identifier for each record is shown in 'ID' and 'Rec No' is the sequential number assigned to each interaction with the record.
ID Rec No Who Type
1 1 Bob New
1 2 Bob Open
1 3 Bob Assign
1 4 Sarah Add
1 5 Bob Add
1 6 Bob Close
2 1 John New
2 2 John Open
2 3 John Assign
2 4 Bob Assign
2 5 Sarah Add
2 6 Sarah Close
3 1 Sarah New
3 2 Sarah Open
3 3 Sarah Assign
3 4 Sarah Close
I need to find all of the 'Assign' operations. However where multiple 'Assign' are in a certain ID, I want to find the first one. I then also want to find the name of the person who did that.
So ultimately from the above date I would like the output to be-
Who Count (assign)
Bob 1
John 1
Sarah 1
The code I have at the moment is-
SELECT IH.WHO, Count(IH.ID)
FROM Table.INCIDENTS_H IH
WHERE (IH.TYPE = Assign)
GROUP BY IH.WHO
But this gives the output as-
Who Count (assign)
Bob 2
John 1
Sarah 1
As it is finding that Bob did an assign on ID 2, Rec No 4.
Any help would be appreciated. I am using MS SQL.

I think something like this is what you are after:
select
who, count(id)
from (
select ID, Who, row_number() over (partition by ID order by Rec) [rownum]
from Table.INCIDENTS_H IH
WHERE (IH.TYPE = Assign)
) a
where rownum = 1
group by who
This should count only the first Assign (ordered by Rec) within each ID group.

This ought to do it:
SELECT IH.WHO, COUNT(IH.ID)
FROM INCIDENTS_H IH
JOIN (
SELECT ID, MIN([Rec No]) [Rec No]
FROM INCIDENTS_H
WHERE ([Type] = 'Assign')
GROUP BY ID
) IH2
ON IH2.ID = IH.ID AND IH2.[Rec No] = IH.[Rec No]
GROUP BY IH.WHO

You can use row_number to accomplish this
WITH INCIDENTS_H as (
SELECT
1 as ID, 1 as RecNo, 'Bob' as Who, 'New' as type
UNION ALL SELECT 1, 2, 'Bob','Open'
UNION ALL SELECT 1, 3, 'Bob','Assign'
UNION ALL SELECT 1, 4, 'Sarah','Add'
UNION ALL SELECT 1, 5, 'Bob','Add'
UNION ALL SELECT 1, 6, 'Bob','Close'
UNION ALL SELECT 2, 1, 'John','New'
UNION ALL SELECT 2, 2, 'John','Open'
UNION ALL SELECT 2, 3, 'John','Assign'
UNION ALL SELECT 2, 4, 'Bob','Assign'
UNION ALL SELECT 2, 5, 'Sarah','Add'
UNION ALL SELECT 2, 6, 'Sarah','Close'
UNION ALL SELECT 3, 1, 'Sarah','New'
UNION ALL SELECT 3, 2, 'Sarah','Open'
UNION ALL SELECT 3, 3, 'Sarah','Assign'
UNION ALL SELECT 3, 4, 'Sarah','Close')
, GetTheMin AS (
SELECT
ROW_NUMBER() over (partition by id order by recno) row,
ID,
RecNo,
Who,
type
FROM
INCIDENTS_H
WHERE
type = 'Assign'
)
SELECT Who,
COUNT(ID)
FROM GetTheMin
WHERE
row = 1
GROUP BY
who
OR you can use CROSS Apply
SELECT
who,
COUNT(id) id
FROM
(SELECT DISTINCT
MinValues.*
FROM
INCIDENTS_H h
CROSS APPLY ( SELECT TOP 1 *
FROM INCIDENTS_H h2
WHERE h.id = h2.id
ORDER BY ID, RecNo asc) MinValues) getTheMin
GROUP BY WHO
Or you can use Min which uses standard SQL John Fisher's answer demonstrates

Here's a view of everything in the table which should match your "first assign" requirement:
select a.*
from Table.INCIDENTS_H a
inner join
(select ID, min([Rec No]) [Rec No] from Table.INCIDENTS_H where Type = 'Assign' group by ID) b
on a.ID = b.ID and a.[Rec No] = b.[Rec No]
Result:
ID Rec No Who Type
1 3 Bob Assign
2 3 John Assign
3 3 Sarah Assign

select * from
(select
id, rec_no, who
from
operation_history
where
type = 'Assign'
order by rec_no asc) table_alias
group by
id
order by id asc
Tested and here are the results:
id rec_no who
1 3 Bob
2 3 John
3 3 Sarah
(Code not specific to SQL Server)

Here is the query with virtual test data that were mentioned in the original post:
with T (ID, RecNo, Who, Type) as
(
select 1, 1, 'Bob', 'New' union all
select 1, 2, 'Bob', 'open' union all
select 1, 3, 'Bob', 'Assign' union all
select 1, 4, 'Sarah', 'Add' union all
select 1, 5, 'Bob', 'Add' union all
select 1, 6, 'Bob', 'Close' union all
select 2, 1, 'John', 'New' union all
select 2, 2, 'John', 'Open' union all
select 2, 3, 'John', 'Assign' union all
select 2, 4, 'Bob', 'Assign' union all
select 2, 5, 'Sarah', 'Add' union all
select 2, 6, 'Sarah', 'Close' union all
select 3, 1, 'Sarah', 'New' union all
select 3, 2, 'Sarah', 'Open' union all
select 3, 3, 'Sarah', 'Assign' union all
select 3, 4, 'Sarah', 'Close'
)
select top 1 with ties *
from T
where Type = 'Assign'
order by row_number() over(partition by ID order by RecNo)
The "select" statement that can be applied to the real situation from the question might look like:
SELECT TOP 1 WITH TIES
IH.ID, IH.[Rec No], IH.WHO, IH.TYPE
FROM Table.INCIDENTS_H IH
WHERE IH.TYPE = 'Assign'
ORDER BY ROW_NUMBER() OVER(PARTITION BY IH.ID ORDER BY IH.[Rec No]);

Related

Query don't return the right result

I have a POST table, a ACTION table and ACTION_TYPE table, I explain the ACTION table contains all the actions that were made by users, and the table ACTION_TYPE contains the actions details for example the ACTION whose ID = 4 has ACTION_TYPE_ID = 1 for POST_ID 6, which mean an action was made for post number 50, we can have many actions for one post_id
the POST table
id title content user_id
---------- ---------- ---------- ----------
1 title1 Text... 1
2 title2 Text... 1
3 title3 Text... 1
4 title4 Text... 5
5 title5 Text... 2
6 title6 Text... 1
the ACTION_TYPE table
id name
---------- ----------
1 updated
2 deleted
3 restored
4 hided
the ACTION table
id post_id action_type_id date
---------- ---------- -------------- -----
1 1 1 2017-01-01
2 1 1 2017-02-15
3 1 3 2018-06-10
4 6 1 2019-08-01
5 5 2 2019-12-09
6 2 3 2020-04-27
7 2 1 2020-07-29
8 3 2 2021-03-13
So i would like to know the last action are made for each post sometimes i would like to get teh last action made by specific action_type and user for each post.
here is my query
select actions, count(*) as cnt
from(
select ac.post_id as action_post_id, max(ac.date) as max_date,
case
when ac.action_type_id is not null then act.name
end as actions,
case
when p.user_id is not null then u.name
end as user_name
from action ac
left join post p on ac.post_id = p.id
left join user u on p.user_id = u.id
left join action_type act on ac.action_type_id = act.id
where p.user_id = 1
and act.name in ('restored','deleted','updated')
group by ac.post_id, case when ac.action_type_id is not null then act.name end , case when p.user_id is not null then u.full_name end
)
group by actions
;
so here is one probleme i want to groupe by post_id but it ask me to add cases too so i get the following error:
/ORA-00979. 00000 - "not a group by expression/
when i delete the cases from group by
but when i use this query i get false result
here the result i get
actions user_name cnt
---------- ---------- -----------
updated ERIC 2
deleted ERIC 2
restored ERIC 2
so here the result expected to be
actions user_name cnt
---------- ---------- -----------
updated ERIC 2
deleted ERIC 1
restored ERIC 1
the sum must equal 4 but i'm geting 6 it's like if it brings more than one action per post
Important ! when i use simple query to check manualy actions the sum equal 4
Best regards
You can use:
SELECT MAX(t.name) AS action_name,
MAX(u.name) AS user_name,
COUNT(*) AS number_posts
FROM users u
INNER JOIN post p
ON (u.id = p.user_id)
INNER JOIN (
SELECT post_id,
MAX(action_type_id) KEEP (DENSE_RANK LAST ORDER BY "DATE", id)
AS action_type_id
FROM action
GROUP BY post_id
) a
ON (p.id = a.post_id)
INNER JOIN action_type t
ON (t.id = a.action_type_id)
GROUP BY
u.id,
t.id
Which, for the sample data:
CREATE TABLE users (id, name) AS
SELECT 1, 'Eric' FROM DUAL;
CREATE TABLE POST ( id, title, content, user_id) AS
SELECT 1, 'title1', 'Text...', 1 FROM DUAL UNION ALL
SELECT 2, 'title2', 'Text...', 1 FROM DUAL UNION ALL
SELECT 3, 'title3', 'Text...', 1 FROM DUAL UNION ALL
SELECT 4, 'title4', 'Text...', 5 FROM DUAL UNION ALL
SELECT 5, 'title5', 'Text...', 2 FROM DUAL UNION ALL
SELECT 6, 'title6', 'Text...', 1 FROM DUAL;
CREATE TABLE ACTION_TYPE ( id, name ) AS
SELECT 1, 'updated' FROM DUAL UNION ALL
SELECT 2, 'deleted' FROM DUAL UNION ALL
SELECT 3, 'restored' FROM DUAL UNION ALL
SELECT 4, 'hided' FROM DUAL;
CREATE TABLE ACTION ( id, post_id, action_type_id, "DATE") AS
SELECT 1, 1, 1, DATE '2017-01-01' FROM DUAL UNION ALL
SELECT 2, 1, 1, DATE '2017-02-15' FROM DUAL UNION ALL
SELECT 3, 1, 3, DATE '2018-06-10' FROM DUAL UNION ALL
SELECT 4, 6, 1, DATE '2019-08-01' FROM DUAL UNION ALL
SELECT 5, 5, 2, DATE '2019-12-09' FROM DUAL UNION ALL
SELECT 6, 2, 3, DATE '2020-04-27' FROM DUAL UNION ALL
SELECT 7, 2, 1, DATE '2020-07-29' FROM DUAL UNION ALL
SELECT 8, 3, 2, DATE '2021-03-13' FROM DUAL;
Outputs:
ACTION_NAME
USER_NAME
NUMBER_POSTS
restored
Eric
1
updated
Eric
2
deleted
Eric
1
fiddle

How to select the minimum value in a table or the next one in oracle sql

I have a table L1_CI_PER_ADDRESS with these columns
PER_ID,
SEQ_NUM,
ADDRESS_ID,
ADDRESS_TYPE_XFLG,
START_DT,
END_DT,
SEASON_START_MMDD,
SEASON_END_MMDD,
ADDRESS_PRIO_FLG,
DELIVERABLE_FLG,
VERSION,
LOAD_DATE
I want to select ADDRESS_TYPE_XFLG where the value is MAIN-AE if it exists or the MAIN-EN if the first one does not exists. Else I want to select CORRESPOND-AE or CORRESPOND-AE if MAIN-AE and MAIN-EN do not exists.
How can I do this? I am new to Oracle SQL. I want to remove the duplicates returned when I do my select.
One of the issues is that some person ID's have all four (MAIN-AE, MAIN-EN, CORRESPOND-AE, CORRESPOND-EN), so in this case I just want MAIN-AE to be returned.
I hope my question is clear.
enter image description here
It's top-n query. Use row_number():
select *
from (
select PER_ID, address_id, ADDRESS_TYPE_XFLG,
row_number() over (partition by per_id
order by case ADDRESS_TYPE_XFLG
when 'MAIN-AE' then 1
when 'MAIN-EN' then 2
when 'CORRESPOND-AE' then 3
when 'CORRESPOND-EN' then 4
end) as rn
from L1_CI_PER_ADDRESS)
where rn = 1
If person can own two addresses with the same flag then you need to add proper order after case when section, probably something like , seq_num desc.
Test:
with L1_CI_PER_ADDRESS(PER_ID, address_id, ADDRESS_TYPE_XFLG ) as (
select 1, 1, 'CORRESPOND-AE' from dual union all
select 1, 2, 'MAIN-AE' from dual union all
select 1, 3, 'CORRESPOND-EN' from dual union all
select 1, 4, 'MAIN-EN' from dual union all
select 2, 5, 'CORRESPOND-AE' from dual union all
select 3, 6, 'MAIN-AE' from dual union all
select 4, 7, 'CORRESPOND-EN' from dual union all
select 4, 8, 'MAIN-AE' from dual
)
select PER_ID, address_id
from (
select PER_ID, address_id, ADDRESS_TYPE_XFLG,
row_number() over (partition by per_id
order by case ADDRESS_TYPE_XFLG
when 'MAIN-AE' then 1
when 'MAIN-EN' then 2
when 'CORRESPOND-AE' then 3
when 'CORRESPOND-EN' then 4
end) as rn
from L1_CI_PER_ADDRESS)
where rn = 1
Output:
PER_ID ADDRESS_ID ADDRESS_TYPE_XFLG
---------- ---------- -----------------
1 2 MAIN-AE
2 5 CORRESPOND-AE
3 6 MAIN-AE
4 8 MAIN-AE

Efficient way to pull counts for all permutations of a field

I have an oracle DB w/ a table that contains records associated to a person (based on an ID). The records are categorized as category = 1, 2, or 3.
I would like to pull as follows:
- # of people with only a category 1 record (no category=2 or 3)
- # of people with only a category 2 record (no category=1 or 3)
- # of people with only a category 3 record (no category=1 or 2)
- # of people with both category 1 & 2 records (no category=3)
- # of people with both category 1 & 3 records (no category=2)
- # of people with all category records 1,2, & 3
- # of people with both a category 2 & 3 records (no category=1)
I could only think of the following solution (modified for each case):
select count(*) from table1
where id in (select id from table1 where category=1)
and id not in (select id from table1 where category=2)
and id not in (select id from table1 where category=3)
But, I believe this is a highly inefficient way of doing this, was wondering if anyone had quicker/better way of getting this info.
Thanks!
One way to do this is to bring the categories together, using listagg() and then reaggregate:
select categories, count(*)
from (select listagg(t1.category, ',') within group (order by t1.category) as categories, personid
from table1 t1
group by personid
) x
group by categories;
EDIT:
If you need distinct values:
select categories, count(*)
from (select listagg(t1.category, ',') within group (order by t1.category) as categories, personid
from (select distinct t1.category, t1.personid from table1 t1) t1
group by personid
) x
group by categories;
Here is a query that, for each ID, shows the count of distinct categories and the MIN and MAX category. This query can be used as a sub-query in further processing (you didn't explain exactly HOW you want the results to be presented). When the COUNT is 1, then the single category is that in the MIN_CAT column; when the COUNT is 3, then all three categories are present for that ID; and when the COUNT is 2, then the two categories that are present are in the MIN and the MAX columns. Whatever else you need to do from here should be very simple; for example you can now GROUP BY CT, MIN_CAT, MAX_CT and count ID's.
I do a count(distinct category) to allow the possibility of non-unique (id, category) - as illustrated in the sample data I include in a WITH clause (which is NOT part of the SQL query!)
with
test_data ( id, category ) as (
select 101, 3 from dual union all
select 101, 1 from dual union all
select 101, 3 from dual union all
select 104, 2 from dual union all
select 105, 2 from dual union all
select 105, 2 from dual union all
select 105, 1 from dual union all
select 106, 1 from dual union all
select 106, 2 from dual union all
select 106, 3 from dual union all
select 106, 3 from dual
)
select id,
count(distinct category) as ct,
min(category) as min_cat,
max(category) as max_cat
from test_data
group by id
;
ID CT MIN_CAT MAX_CAT
--- -- ------- -------
101 2 1 3
105 2 1 2
104 1 2 2
106 3 1 3
Oracle Setup:
CREATE TABLE test_data ( id, category ) as
select 101, 3 from dual union all
select 101, 1 from dual union all
select 101, 3 from dual union all
select 104, 2 from dual union all
select 105, 2 from dual union all
select 105, 2 from dual union all
select 105, 1 from dual union all
select 106, 1 from dual union all
select 106, 2 from dual union all
select 106, 3 from dual union all
select 106, 3 from dual union all
select 107, 1 from dual union all
select 107, 3 from dual;
Query:
SELECT c1,
c2,
c3,
LTRIM(
DECODE( c1, 1, ',1' ) || DECODE( c2, 1, ',2' ) || DECODE( c3, 1, ',3' ),
','
) AS categories,
COUNT(1) AS num_people,
LISTAGG( id, ',' ) WITHIN GROUP ( ORDER BY id ) AS people
FROM ( SELECT DISTINCT * FROM test_data )
PIVOT ( COUNT(1) FOR category IN ( 1 AS c1, 2 AS c2, 3 AS c3 ) )
GROUP BY c1, c2, c3;
Output:
C1 C2 C3 CATEGORIES NUM_PEOPLE PEOPLE
-- -- -- ---------- ---------- ----------
0 1 0 2 1 104
1 0 1 1,3 2 101,107
1 1 0 1,2 1 105
1 1 1 1,2,3 1 106

How to do select count(*) group by and select * at same time?

For example, I have table:
ID | Value
1 hi
1 yo
2 foo
2 bar
2 hehe
3 ha
6 gaga
I want my query to get ID, Value; meanwhile the returned set should be in the order of frequency count of each ID.
I tried the query below but don't know how to get the ID and Value column at the same time:
SELECT COUNT(*) FROM TABLE group by ID order by COUNT(*) desc;
The count number doesn't matter to me, I just need the data to be in such order.
Desire Result:
ID | Value
2 foo
2 bar
2 hehe
1 hi
1 yo
3 ha
6 gaga
As you can see because ID:2 appears most times(3 times), it's first on the list,
then ID:1(2 times) etc.
you can try this -
select id, value, count(*) over (partition by id) freq_count
from
(
select 2 as ID, 'foo' as value
from dual
union all
select 2, 'bar'
from dual
union all
select 2, 'hehe'
from dual
union all
select 1 , 'hi'
from dual
union all
select 1 , 'yo'
from dual
union all
select 3 , 'ha'
from dual
union all
select 6 , 'gaga'
from dual
)
order by 3 desc;
select t.id, t.value
from TABLE t
inner join
(
SELECT id, count(*) as cnt
FROM TABLE
group by ID
)
x on x.id = t.id
order by x.cnt desc
How about something like
SELECT t.ID,
t.Value,
c.Cnt
FROM TABLE t INNER JOIN
(
SELECT ID,
COUNT(*) Cnt
FROM TABLE
GROUP BY ID
) c ON t.ID = c.ID
ORDER BY c.Cnt DESC
SQL Fiddle DEMO
I see the question is already answered, but since the most obvious and most simple solution is missing, I'm posting it anyway. It doesn't use self joins nor subqueries:
SQL> create table t (id,value)
2 as
3 select 1, 'hi' from dual union all
4 select 1, 'yo' from dual union all
5 select 2, 'foo' from dual union all
6 select 2, 'bar' from dual union all
7 select 2, 'hehe' from dual union all
8 select 3, 'ha' from dual union all
9 select 6, 'gaga' from dual
10 /
Table created.
SQL> select id
2 , value
3 from t
4 order by count(*) over (partition by id) desc
5 /
ID VALU
---------- ----
2 bar
2 hehe
2 foo
1 yo
1 hi
6 gaga
3 ha
7 rows selected.

Query with priority

I have a query that I cannot get to work right. I have 3 tables; Person, PersonProgram and Category.
Person: ID, ....
PersonProgram: ID, PersonID, Category, Code ...
Category: ID, ...
The Person table has 1 record for each person and the PersonProgram has multiple programs per person. There are 4 categories and I need to pull into a single row, of each person, with specific Program of each category.
Person Table:
1
2
3
PersonProgram Table
1, 1, 1, 1
2, 1, 2, 1
3, 1, 1, 3
4, 2, 1, 1
5, 2, 3, 3
What the desired outcome should be:
PersonID, ProgramIDforCat1, ProgramIDforCat2, ProgramIDforCat3, ProgramIDforCat4
1, 1, 2, NULL, NULL
2, 1, NULL, 3, NULL
The problem is that there is multiple Program records for each person and category with a code of 1, 2 or 3. I need to put priority on Code 1 then Code 3 and ignore the rest, while still only pulling 1 record, or NULL if it does not exist.
I am losing it trying to get this to work.
FYI, it has to be in a view.
Thanks for any help.
WITH Person AS
(
SELECT 1 AS ID UNION ALL
SELECT 2 AS ID UNION ALL
SELECT 3 AS ID
),
PersonProgram AS
(
SELECT 1 AS ID, 1 AS PersonID, 1 AS Category, 1 AS Code UNION ALL
SELECT 2, 1, 2, 1 UNION ALL
SELECT 3, 1, 1, 3 UNION ALL
SELECT 4, 2, 1, 1 UNION ALL
SELECT 5, 2, 3, 3
),
pp2 AS
(
SELECT *
,ROW_NUMBER() OVER
(PARTITION BY PersonID, Category
ORDER BY CASE WHEN Code = 1 THEN 0 ELSE 1 END,
CASE WHEN Code = 3 THEN 0 ELSE 1 END) AS RN
FROM PersonProgram
)
select PersonID ,
max(case when Category =1 then pp2.ID end) ProgramIDforCat1,
max(case when Category =2 then pp2.ID end) ProgramIDforCat2,
max(case when Category =3 then pp2.ID end) ProgramIDforCat3,
max(case when Category =4 then pp2.ID end) ProgramIDforCat4
from Person p join pp2
on pp2.PersonID = p.ID
WHERE RN=1
group by PersonID
Returns
PersonID ProgramIDforCat1 ProgramIDforCat2 ProgramIDforCat3 ProgramIDforCat4
----------- ---------------- ---------------- ---------------- ----------------
1 1 2 NULL NULL
2 4 NULL 5 NULL
This is different from your expected results. (though I can make it the same by using pp2.Category rather than pp2.ID) Can you clarify?