sql using Aggregates using a combination of min(), count(), having clause - sql

Trying to find code that will allow me to count first names by group, and then return the name with the lowest count.
Sample data:-
PersonGroup FirstName
------------------------
A Bob
A Mary
A Bob
A Bob
B Michelle
B Michelle
B Greg
B Greg
B Michelle
C Cindy
C Michelle
C Michelle
D Rod
D Rod
D Rod
D Rod
D Rod
D Mary
D Mary
D Mary
D Mary
D Mary
D Mary
Output required :
PersonGroup FirstName Count
--------------------------------
A Mary 1
B Greg 2
C Cindy 1
D Rod 5
First Name columns has the name that occurs the least within the group
Count column has the count of Name that occurs the least amount of times per group
this is my code so far, but every name is being returned,
select
PersonType,
FirstName,
count (firstName) as mycount
from
[Person].[Person]
group by
FirstName,
[PersonType]
having
count(firstName) = (select min(a.cnt)
from
(select count(firstname) as cnt
from [Person].[Person]
group by [FirstName]) as a)
order by
PersonType desc

you can use row_number()
select a.*
from (select PersonType,FirstName ,count (firstName) as mycount,
row_number() over (partition by PersonType order by count(*)) as rn
from [Person].[Person]
group by FirstName,[PersonType]
) a
where rn= 1;

use window function row_number()
with cte as
(
select 'A' as PersonGroup, 'Bob' as name
union all
select 'A', 'Mary'
union all
select 'A', 'Mary'
union all
select 'B', 'Michelle'
union all
select 'B', 'Greg'
union all
select 'B', 'Greg'
union all
select 'B', 'Michelle'
union all
select 'B', 'Michelle'
union all
select 'C', 'Michelle'
union all
select 'C', 'Michelle'
union all
select 'C', 'Cindy'
union all
select 'D', 'Rod'
union all
select 'D', 'Rod'
union all
select 'D', 'Rod'
union all
select 'D', 'Rod'
union all
select 'D', 'Rod'
union all
select 'D', 'Mary'
union all
select 'D', 'Mary'
union all
select 'D', 'Mary'
union all
select 'D', 'Mary'
union all
select 'D', 'Mary'
union all
select 'D', 'Mary'
)
, cte3 as (
select personGroup, name, COUNT(*) as cnt, row_number() over(partition by PersonGroup order by COUNT(*) ) rn from cte GROUP BY personGroup, name
) select PersonGroup,name,cnt from cte3 where rn=1
demo link

Related

How to filter records based on another grouped by column in a same table

I have a sample table as below:
Target output:
I have tried with below script but only number of count produced as output:
select ID, count(COUNTRY) from test
group by COUNTRY
having count(COUNTRY)=3;
Could anyone here please help how can I get the targeted output? Thanks.
Here's one option - a subquery:
Sample data:
SQL> with test (id, name, country) as
2 (select 1, 'Mike' , 'Australia' from dual union all
3 select 2, 'Jason', 'Australia' from dual union all
4 select 3, 'Lee' , 'China' from dual union all
5 select 4, 'Simon', 'India' from dual union all
6 select 5, 'Alex' , 'Malaysia' from dual union all
7 select 6, 'John' , 'Australia' from dual
8 )
Query:
9 select id, country
10 from test
11 where country in (select country
12 from test
13 group by country
14 having count(*) = 3
15 )
16 order by id;
ID COUNTRY
---------- ---------
1 Australia
2 Australia
6 Australia
SQL>
You can use analytic functions and only scan the table once:
SELECT id, name, country
FROM (
SELECT id, name, country,
COUNT(*) OVER (PARTITION BY country) AS people_per_country
FROM table_name
)
WHERE people_per_country = 3;
Which, for the sample data:
CREATE TABLE table_name (id, name, country) AS
SELECT 1, 'Mike' , 'Australia' FROM DUAL UNION ALL
SELECT 2, 'Jason', 'Australia' FROM DUAL UNION ALL
SELECT 3, 'Lee' , 'China' FROM DUAL UNION ALL
SELECT 4, 'Simon', 'India' FROM DUAL UNION ALL
SELECT 5, 'Alex' , 'Malaysia' FROM DUAL UNION ALL
SELECT 6, 'John' , 'Australia' FROM DUAL;
Outputs:
ID
NAME
COUNTRY
1
Mike
Australia
2
Jason
Australia
6
John
Australia
db<>fiddle here

How to get the max count of an attribute with 3 tables?

I need to query which author sold the most books and how many books the author sold.
select a.firstname ||''|| a.lastname as fullname,
max(count(datesold))
from author a,
transaction t,
book b
where a.authorid = b.authorid
and b.bookid = t.bookid
group by
a.firstname,
a.lastname;
It gave me an error of not a single-group group function.
Any idea what is the issue here?
With some sample data
SQL> with
2 author (authorid, firstname, lastname) as
3 (select 1, 'Stephen', 'King' from dual union all
4 select 2, 'Jo' , 'Nesbo' from dual),
5 book (bookid, authorid) as
6 (select 100, 1 from dual union all
7 select 200, 1 from dual union all
8 select 300, 2 from dual
9 ),
10 transaction (trans_id, bookid) as
11 (select 1, 100 from dual union all
12 select 2, 100 from dual union all
13 select 3, 100 from dual union all
14 select 4, 300 from dual
15 ),
query uses the RANK analytic function which ranks rows by number of rows in the transaction table (it says how many books were sold). Finally, fetch row(s) that rank as highest:
16 temp as
17 (select a.firstname || ' ' || a.lastname AS fullname,
18 count(t.bookid) cnt,
19 rank() over (order by count(t.bookid) desc) rnk
20 from author a join book b on a.authorid = b.authorid
21 join transaction t on t.bookid = b.bookid
22 group by a.firstname, a.lastname
23 )
24 select fullname, cnt
25 from temp
26 where rnk = 1;
FULLNAME CNT
------------- ----------
Stephen King 3
SQL>
You can use:
select MAX(a.firstname ||' '|| a.lastname) as fullname,
COUNT(datesold)
from author a
INNER JOIN book b
ON (a.authorid = b.authorid)
INNER JOIN transaction t
ON (b.bookid = t.bookid)
GROUP BY
a.authorid
ORDER BY
COUNT(datesold) DESC
FETCH FIRST ROW ONLY;
Do not aggregate by firstname and lastname as there are many people in the world with identical names and you do not want to count everyone with the same name as a single person.
Which, for the sample data:
CREATE TABLE author (authorid, firstname, lastname, dateofbirth) AS
SELECT 1, 'Alice', 'Adams', DATE '1900-01-01' FROM DUAL UNION ALL
SELECT 2, 'Alice', 'Adams', DATE '1910-01-01' FROM DUAL UNION ALL
SELECT 3, 'Betty', 'Baron', DATE '1920-01-01' FROM DUAL UNION ALL
SELECT 4, 'Carol', 'Corrs', DATE '1930-01-01' FROM DUAL UNION ALL
SELECT 5, 'Carol', 'Corrs', DATE '1940-01-01' FROM DUAL;
CREATE TABLE book (bookid, authorid) AS
SELECT 1, 1 FROM DUAL UNION ALL
SELECT 2, 2 FROM DUAL UNION ALL
SELECT 3, 3 FROM DUAL UNION ALL
SELECT 4, 4 FROM DUAL UNION ALL
SELECT 5, 5 FROM DUAL;
CREATE TABLE transaction (bookid, datesold) AS
SELECT 1, DATE '1970-01-01' FROM DUAL UNION ALL
SELECT 1, DATE '1970-01-02' FROM DUAL UNION ALL
SELECT 1, DATE '1970-01-03' FROM DUAL UNION ALL
SELECT 1, DATE '1970-01-04' FROM DUAL UNION ALL
SELECT 3, DATE '1970-01-01' FROM DUAL UNION ALL
SELECT 4, DATE '1970-01-01' FROM DUAL UNION ALL
SELECT 4, DATE '1970-01-02' FROM DUAL UNION ALL
SELECT 5, DATE '1970-01-01' FROM DUAL UNION ALL
SELECT 5, DATE '1970-01-02' FROM DUAL UNION ALL
SELECT 5, DATE '1970-01-03' FROM DUAL;
Outputs:
FULLNAME
COUNT(DATESOLD)
Alice Adams
4
db<>fiddle here

Oracle SQL query using case when, compacting null fields

I have a table like this:
Items
id group old_new object
1 A O pen
2 A N house
3 B O dog
4 B O cat
5 C N mars
6 C O sun
7 C N moon
8 C o earth
I would like the select return:
Items
group new_object old_object
A house pen
B null dog
B null cat
C mars sun
C moon earth
If I try:
select id,
case when old_new = 'N' then object end as new_object,
case when old_new = 'O' then object end as old_object
from the_table
order by id;
I have 8 row with many field as null
es: last rows:
group new_object old_object
C mars null
c null sun
C moon null
c null earth
But of group C I want only 2 rows...
is not like the other query 'Oracle sql join same table ecc...' because here don't want null result
I'm going to make the assumption that Old and New records are paired in the order they appear based on the ID value. With that assumption the following query:
WITH DTA(ID, GRP, OLD_NEW, OBJECT) AS (
select 1, 'A', 'O', 'pen' from dual union all
select 2, 'A', 'N', 'house' from dual union all
select 3, 'B', 'O', 'dog' from dual union all
select 4, 'B', 'O', 'cat' from dual union all
select 5, 'C', 'N', 'mars' from dual union all
select 6, 'C', 'O', 'sun' from dual union all
select 7, 'C', 'N', 'moon' from dual union all
select 8, 'C', 'O', 'earth' from dual
), dta2 as (
select dta.*
, row_number() over (partition by GRP, old_new order by id) rn
from dta
)
select coalesce(n.grp, o.grp) grp
, n.object new_object
, o.object old_object
from (select * from dta2 where old_new = 'N') n
full join (select * from dta2 where old_new = 'O') o
on n.grp = o.grp
and n.rn = o.rn;
Aside from the sample data section (with dta) this script first uses the analytic function ROW_NUMBER() to add a sequential number partitioned by the group and old_new columns. It then performs a full outer join on two inline views of the dta2 subfactored query, one for thr old objects and one for the new objects. The result, at least for this data set is:
GRP NEW_OBJECT OLD_OBJECT
--- ---------- ----------
A house pen
B dog
B cat
C mars sun
C moon earth
In the first step assign an index (IDX) of the chnage withing your group. I'm using order by ID, but this is upon you. The important thing is that the old and new valuea are unique connected with GRP and IDX.
In next step let PIVOT work for you (I'm using the data from #Sentinel, thx!)
WITH DTA(ID, GRP, OLD_NEW, OBJECT) AS (
select 1, 'A', 'O', 'pen' from dual union all
select 2, 'A', 'N', 'house' from dual union all
select 3, 'B', 'O', 'dog' from dual union all
select 4, 'B', 'O', 'cat' from dual union all
select 5, 'C', 'N', 'mars' from dual union all
select 6, 'C', 'O', 'sun' from dual union all
select 7, 'C', 'N', 'moon' from dual union all
select 8, 'C', 'O', 'earth' from dual
), DTA2 as (
SELECT
ROW_NUMBER() OVER (PARTITION BY GRP,OLD_NEW order by ID) as IDX,
GRP, OLD_NEW, OBJECT
from DTA
)
select * from DTA2
PIVOT (max(OBJECT) OBJECT for (OLD_NEW) in
('N' as "NEW",
'O' as "OLD"
))
order by GRP;
result
IDX, GRP, NEW_OBJECT, OLD_OBJECT
1 A house pen
1 B dog
2 B cat
2 C moon earth
1 C mars sun
Here's an alternative using PIVOT to get the results:
with items as (select 1 id, 'A' grp, 'O' old_new, 'pen' obj from dual union all
select 2 id, 'A' grp, 'N' old_new, 'house' obj from dual union all
select 3 id, 'B' grp, 'O' old_new, 'dog' obj from dual union all
select 4 id, 'B' grp, 'O' old_new, 'cat' obj from dual union all
select 5 id, 'C' grp, 'N' old_new, 'mars' obj from dual union all
select 6 id, 'C' grp, 'O' old_new, 'sun' obj from dual union all
select 7 id, 'C' grp, 'N' old_new, 'moon' obj from dual union all
select 8 id, 'C' grp, 'O' old_new, 'earth' obj from dual)
-- end of mimicking your items table with data in it. See SQL below:
select grp,
new_object,
old_object
from (select grp,
old_new,
obj,
row_number() over (partition by grp, old_new order by id) rn
from items)
pivot (max(obj)
for old_new in ('N' new_object,
'O' old_object))
order by grp,
rn;
GRP NEW_OBJECT OLD_OBJECT
--- ---------- ----------
A house pen
B dog
B cat
C mars sun
C moon earth
Provided that
there's at most one new object for each old,
there's no new object without old object, and
there's at most one old object for any group (this is not true for your sample data, but in comments you indicate you're interested in such solution as well)
a simpler query may be used than for the general case:
select
old.group as group, new.object as new_object, old.object as old_object
from
(select group, object from my_table where old_new = 'O') old
left join
(select group, object from my_table where old_new = 'N') new
on (old.group = new.group)

Binary "OR" operation on a SQL column

I have a query that returns weekdays
Select PERSON_NAME, PERSON_DAY from PERSON_DAYS WHERE PERSON_ID = #myId
say I obtain
John 1 (mo)
John 3 (mo tu)
John 8 (th)
I need to obtain for John all the days when is busy. How do I a logical OR on the PERSON_DAY column in this query?
the result should be 11 (mo tu th)
well here my best so far
;with PowersOf2
as
(
select 1 as Number
union all
select A.Number * 2 from PowersOf2 as A where A.Number < 64
)
select P.PERSON_NAME, sum(distinct P.PERSON_DAY & PowersOf2.Number)
from PERSON_DAYS as P
left outer join PowersOf2 on PowersOf2.Number <= P.PERSON_DAY
where P.PERSON_ID = #myId
group by P.PERSON_NAME
SQL FIDDLE EXAMPLE
If I understand you correctly, you can use a combination of bitwise operator and and aggregate function sum to do what you want.
Example:
with person_days as (
select 'John' as person_name, 1 as weekday --mo
union select 'John', 3 -- mo, tu
union select 'John', 8 -- th
union select 'Jane', 1 -- mo
union select 'Jane', 9 -- mo, th
union select 'Jane', 40 -- th, sa
),
Bits AS (
SELECT 1 AS BitMask --mo
UNION ALL SELECT 2 --tu
UNION ALL SELECT 4 --we
UNION ALL SELECT 8 --th
UNION ALL SELECT 16 --fr
UNION ALL SELECT 32 --sa
UNION ALL SELECT 64 --su
UNION ALL SELECT 128
)
, person_single_days as (
select distinct person_name, weekday & bits.BitMask single_weekday
from person_days
inner join bits on person_days.weekday & bits.BitMask > 0
)
select person_name, sum(single_weekday) weekdays
from person_single_days
group by person_name;
result:
person_name weekdays
----------- -----------
Jane 41
John 11
"inspired" by Roman's CTE: (note that the first CTE just generates demo data)
with p as
(
select 'John' as PERSON_NAME, 1 as PERSON_DAY
union
select 'John', 3
union
select 'John', 8
union
select 'Jane', 2
union
select 'Jane', 4
),
cte as
(
select PERSON_NAME, PERSON_DAY from p
union all
select cte2.PERSON_NAME, p.PERSON_DAY | cte2.PERSON_DAY
from p
inner join cte as cte2 on p.PERSON_NAME = cte2.PERSON_NAME
where p.PERSON_DAY & cte2.PERSON_DAY = 0
)
select PERSON_NAME, MAX(PERSON_DAY) from cte
group by PERSON_NAME
I think what you are looking for is a custom aggregate that does an OR. You can write that using SQL CLR in .NET. This is probably the cleanest solution. It will be reusable, too.
Alternatively, you could use cursor-based loops to calculate the result.
You could also (mis)use CTE's for this purpose.

Is it possible to compare tuples in oracle-compatible sql?

I'm not 100% if tuples is the term for what I'm talking about but I'm looking at something like this:
Table grades
user grade
------------
Jim B
Bill C
Tim A
Jim B+
I know I can do:
SELECT COUNT(*)
FROM grades
WHERE (
(user = 'Jim' AND grade = 'B')
OR (user = 'Tim' AND grade = 'C')
);
But is there a way to do something more like this?
SELECT COUNT(*)
FROM grades
WHERE (user, grade) IN (('Jim','B'), ('Tim','C'));
EDIT: As a side note, I'd only tested with:
(user, grade) = ('Tim','C')
And that fails, so I assumed IN would fail as well, but I was wrong (thankfully!).
The query you posted should be valid syntax
SQL> ed
Wrote file afiedt.buf
1 with grades as (
2 select 'Jim' usr, 'B' grade from dual
3 union all
4 select 'Bill', 'C' from dual
5 union all
6 select 'Tim', 'A' from dual
7 union all
8 select 'Jim', 'B+' from dual
9 )
10 select *
11 from grades
12 where (usr,grade) in (('Jim','B'),
13 ('Tim','C'),
14* ('Tim','A'))
SQL> /
USR GR
---- --
Jim B
Tim A
You could use a subquery to treat a list of tuples like a table:
SELECT COUNT(*)
FROM grades
JOIN (
SELECT 'Jim' as user, 'B' as grade from dual
UNION ALL
SELECT 'Tim', 'C' from dual
UNION ALL
SELECT 'Pim', 'D' from dual
) as SearchTarget
ON SearchTarget.user = grades.user
and SearchTarget.grade = grades.grade