Insert column with Max in sql - sql

I did not know how to insert column with max.
Select id,MAX(salary),Min(Salary)
from C
GROUP BY id;
it is give me the all id with it is maximum
and I want just the id with maximum and minimum of salary!!

Several options for you that only require a single scan of the table:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE C ( ID, SALARY ) AS
SELECT 1, 100 FROM DUAL
UNION ALL SELECT 2, 110 FROM DUAL
UNION ALL SELECT 3, 100 FROM DUAL
UNION ALL SELECT 4, 110 FROM DUAL
UNION ALL SELECT 5, 90 FROM DUAL
Query 1 - Get a single ID:
SELECT *
FROM (
SELECT ID, SALARY
FROM c
ORDER BY SALARY DESC
)
WHERE ROWNUM = 1
Results:
| ID | SALARY |
|----|--------|
| 2 | 110 |
Query 2 - Get a single ID (alternate method that will get min and max IDs):
SELECT MAX( ID ) KEEP ( DENSE_RANK LAST ORDER BY SALARY ) AS MAX_SALARY_ID,
MAX( SALARY ) AS MAX_SALARY,
MIN( ID ) KEEP ( DENSE_RANK FIRST ORDER BY SALARY ) AS MIN_SALARY_ID,
MIN( SALARY ) AS MIN_SALARY
FROM C
Results:
| MAX_SALARY_ID | MAX_SALARY | MIN_SALARY_ID | MIN_SALARY |
|---------------|------------|---------------|------------|
| 4 | 110 | 5 | 90 |
Query 3 - Get all the IDs with the maximum salary:
SELECT ID, SALARY
FROM (
SELECT ID,
SALARY,
RANK() OVER ( ORDER BY SALARY DESC ) AS RNK
FROM C
)
WHERE RNK = 1
Results:
| ID | SALARY |
|----|--------|
| 2 | 110 |
| 4 | 110 |
Query 4 - Get all IDs for min and max salary:
SELECT LISTAGG( CASE MIN_RANK WHEN 1 THEN ID END, ',' ) WITHIN GROUP ( ORDER BY ID ) AS MIN_SALARY_IDS,
MAX( CASE MIN_RANK WHEN 1 THEN SALARY END ) AS MIN_SALARY,
LISTAGG( CASE MAX_RANK WHEN 1 THEN ID END, ',' ) WITHIN GROUP ( ORDER BY ID ) AS MAX_SALARY_IDS,
MAX( CASE MAX_RANK WHEN 1 THEN SALARY END ) AS MAX_SALARY
FROM (
SELECT ID,
SALARY,
RANK() OVER ( ORDER BY SALARY ASC ) AS MIN_RANK,
RANK() OVER ( ORDER BY SALARY DESC ) AS MAX_RANK
FROM C
)
Results:
| MIN_SALARY_IDS | MIN_SALARY | MAX_SALARY_IDS | MAX_SALARY |
|----------------|------------|----------------|------------|
| 5 | 90 | 2,4 | 110 |
Query 5:
SELECT ID,
SALARY,
CASE WHEN MIN_RANK = 1 THEN 'MIN'
WHEN MAX_RANK = 1 THEN 'MAX' END AS MIN_MAX
FROM (
SELECT ID,
SALARY,
RANK() OVER ( ORDER BY SALARY ASC ) AS MIN_RANK,
RANK() OVER ( ORDER BY SALARY DESC ) AS MAX_RANK
FROM C
)
WHERE MIN_RANK = 1 OR MAX_RANK = 1
Results:
| ID | SALARY | MIN_MAX |
|----|--------|---------|
| 2 | 110 | MAX |
| 4 | 110 | MAX |
| 5 | 90 | MIN |

Select id,
salary
from C
where salary = (select MAX(salary)
from C)

you can use first_value or last_value
The FIRST_VALUE analytic function is similar to the FIRST analytic
function, allowing you to return the first result from an ordered set.
https://oracle-base.com/articles/misc/first-value-and-last-value-analytic-functions
create table C (id int, salary int);
insert into c values(1, 1);
insert into c values(2, 2);
insert into c values(3, 3);
insert into c values(4, 4);
insert into c values(5, 5);
Select distinct first_value(id) over ( order by salary desc)
from C
FIRST_VALUE(ID)OVER(ORDERBYSAL
1 5

Related

Is it possible to update rows randomly with a group of set values?

I have a work assignment table that I would like help with. What I would like to do is randomly assign peoples names to the rows in the table. For example, the table currently looks like:
TASK |NAME
1 Get Chicken |
2 Clean Chicken|
3 Cook Chicken |
4 Eat Chicken |
5 Eat Corn |
6 Takeout Trash|
I have 4 employees that I want to assign these tasks to, but do not want to show any favoritism. Here is what that table looks like:
NAME
John
Lucy
Fred
Jasmine
How can I randomly update the NAME field based on the above names?
edit based on comments. I changed the number of tasks to something not divisible by 4. In this case the number of tasks is now 6. I want to make it to where no one can get 2 or more tasks more then the rest of their colleagues. But in this case, it's ok for someone to have 1 more task then their colleagues. he result should be something like (but random):
TASK |NAME
1 Get Chicken |John
2 Clean Chicken|Jasmine
3 Cook Chicken |Lucy
4 Eat Chicken |Fred
5 Eat Corn |Fred
6 Takeout Trash|Jasmine
Here is a pure SQL way to do it.
MERGE INTO so_tasks t USING (
WITH numbered_tasks AS ( SELECT t.*,
row_number() OVER (ORDER BY dbms_random.value) task_number,
count(*) OVER () total_tasks FROM so_tasks t ),
numbered_employees AS ( SELECT e.*,
row_number() OVER (ORDER BY dbms_random.value) employee_number,
count(*) OVER () total_employees FROM so_employees e)
SELECT nt.task,
ne.name
FROM numbered_tasks nt
INNER JOIN numbered_employees ne
ON ne.employee_number-1 = mod(nt.task_number-1, ne.total_employees) ) u
ON ( t.task = u.task )
WHEN MATCHED THEN UPDATE SET t.name = u.name;
It sorts each list randomly and assigned a number to each row in each list. It then gets the row from the employee list whose number matched the task number MOD the total number of employees.
Here is a fully example:
Create tables
CREATE TABLE so_tasks
( task VARCHAR2(30) NOT NULL PRIMARY KEY,
name VARCHAR2(30) );
INSERT INTO so_tasks ( task ) VALUES ('Get Chicken');
INSERT INTO so_tasks ( task ) VALUES ('Clean Chicken');
INSERT INTO so_tasks ( task ) VALUES ('Cook Chicken');
INSERT INTO so_tasks ( task ) VALUES ('Eat Chicken');
INSERT INTO so_tasks ( task ) VALUES ('Eat Corn');
INSERT INTO so_tasks ( task ) VALUES ('Takeout Trash');
CREATE TABLE so_employees
( name VARCHAR2(30) NOT NULL PRIMARY KEY );
INSERT INTO so_employees ( name ) VALUES ('John');
INSERT INTO so_employees ( name ) VALUES ('Lucy');
INSERT INTO so_employees ( name ) VALUES ('Fred');
INSERT INTO so_employees ( name ) VALUES ('Jasmine');
COMMIT;
Merge
MERGE INTO so_tasks t USING (
WITH numbered_tasks AS ( SELECT t.*,
row_number() OVER (ORDER BY dbms_random.value) task_number,
count(*) OVER () total_tasks FROM so_tasks t ),
numbered_employees AS ( SELECT e.*,
row_number() OVER (ORDER BY dbms_random.value) employee_number,
count(*) OVER () total_employees FROM so_employees e)
SELECT nt.task,
ne.name
FROM numbered_tasks nt
INNER JOIN numbered_employees ne
ON ne.employee_number-1 = mod(nt.task_number-1, ne.total_employees) ) u
ON ( t.task = u.task )
WHEN MATCHED THEN UPDATE SET t.name = u.name;
Results
SELECT * FROM so_tasks;
+---------------+---------+
| TASK | NAME |
+---------------+---------+
| Get Chicken | John |
| Clean Chicken | Jasmine |
| Cook Chicken | Lucy |
| Eat Chicken | Fred |
| Eat Corn | Jasmine |
| Takeout Trash | Fred |
+---------------+---------+
Your exact assignments for each task will be different, but there will never be more than a one task difference between any two employees.
You can give the tasks random sequential numbers and the employees another random sequential number and then join the two tables using those numbers and then use a MERGE statement to update the table correlating on the ROWID pseudo-column to uniquely identify each task.
Oracle Setup:
CREATE TABLE table_name ( task VARCHAR2(20), name VARCHAR2(20) );
INSERT INTO table_name ( TASK )
SELECT 'Get Chicken' FROM DUAL UNION ALL
SELECT 'Clean Chicken' FROM DUAL UNION ALL
SELECT 'Cook Chicken' FROM DUAL UNION ALL
SELECT 'Eat Chicken' FROM DUAL UNION ALL
SELECT 'Eat Corn' FROM DUAL UNION ALL
SELECT 'Takeout Trash' FROM DUAL;
CREATE TABLE employees ( NAME ) AS
SELECT 'John' FROM DUAL UNION ALL
SELECT 'Lucy' FROM DUAL UNION ALL
SELECT 'Fred' FROM DUAL UNION ALL
SELECT 'Jasmine' FROM DUAL;
Merge:
MERGE INTO table_name dst
USING (
WITH random_tasks ( rid, rn ) AS (
SELECT ROWID,
ROW_NUMBER() OVER ( ORDER BY DBMS_RANDOM.VALUE )
FROM table_name
),
random_names ( name, rn, num_employees ) AS (
SELECT name,
ROW_NUMBER() OVER ( ORDER BY DBMS_RANDOM.VALUE ),
COUNT(*) OVER ()
FROM employees
)
SELECT rid,
name
FROM random_tasks t
INNER JOIN
random_names n
ON ( MOD( t.rn, n.num_employees ) + 1 = n.rn )
) src
ON ( src.rid = dst.ROWID )
WHEN MATCHED THEN
UPDATE SET name = src.name;
Result:
SELECT * FROM table_name;
TASK | NAME
:------------ | :------
Get Chicken | John
Clean Chicken | Jasmine
Cook Chicken | Fred
Eat Chicken | Lucy
Eat Corn | Fred
Takeout Trash | Lucy
db<>fiddle here
Assuming you're fine with a PL/SQL solution (you could do it in a single update statement but unless it's performance critical, I'd find the loop easier to follow)
begin
for src in (select t.task_id, e.name
from (select t.*,
row_number() over (order by dbms_random.value) rnk
from task t) t
join
(select e.*,
row_number() over (order by dbms_random.value) rnk,
count(*) over () num_emps
from employee e) e
on( mod( t.rnk, e.num_emps ) = e.rnk - 1 ) )
loop
update task
set name = src.name
where task_id = src.task_id;
end loop;
end;
/
Basically, this is randomly sorting both lists and then going down the list of tasks and assigning the next employee to that task. If the number of tasks isn't a multiple of the number of employees, some employees will get an extra task but no employee will have more than 1 more task than another.

Returning a value from a column associated with the maximum value of another column

Sorry for the confusing title, but I didn't know how else to phrase my problem.
I have a query that returns a table of test scores, as well as some descriptions of the test itself:
+----------------+------------+-----------+
| student_id | test_score | test_term |
+----------------+------------+-----------+
| 1 123 | 614 | Spring |
| 2 123 | 547 | Summer |
| 3 123 | 628 | Fall |
+----------------+------------+-----------+
As you can see, Student 123 took the Math test 3 times. I'm trying to write a query that will return both the highest score that Student 123 achieved, as well as the Test Term associated with that score. Here's what I have so far:
SELECT
MAX (test_score) as "highest_math_score",
CASE WHEN test_score = MAX(test_score) THEN test_term
ELSE null
END as "highest_test_term"
FROM Table1
GROUP BY
student_id
However, I am getting the error: not a GROUP BY expression.
Any thoughts on how to do this?
You can use MAX..KEEP:
SELECT student_id,
max(test_score),
max(test_term) KEEP ( DENSE_RANK FIRST ORDER BY test_score desc )
FROM test_data
GROUP BY student_id;
Full example:
WITH test_data ( student_id, test_score, test_term ) AS
( SELECT 123, 614, 'Spring' FROM DUAL UNION ALL
SELECT 123, 547, 'Summer' FROM DUAL UNION ALL
SELECT 123, 628, 'Fall' FROM DUAL UNION ALL
SELECT 456, 999, 'Spring' FROM DUAL UNION ALL
SELECT 456, 1111, 'Summer' FROM DUAL UNION ALL
SELECT 456, 888, 'Fall' FROM DUAL )
SELECT student_id, max(test_score), max(test_term) KEEP ( DENSE_RANK FIRST ORDER BY test_score desc )
FROM test_data
GROUP BY student_id;
+------------+-----------------+--------+
| STUDENT_ID | MAX(TEST_SCORE) | TERM |
+------------+-----------------+--------+
| 123 | 628 | Fall |
| 456 | 1111 | Summer |
+------------+-----------------+--------+
Without any window functions:
select t1.student_id, t."highest_math_score"
from t1
join
(SELECT
MAX (test_score) as "highest_math_score", student_id
FROM t1
GROUP BY
student_id)t on t.student_id = t1.student_id and t.highest_math_score = t1.test_score
You appear to want :
SELECT MAX(test_score) OVER (PARTITION BY student_id) AS "highest_math_score",
(CASE WHEN test_score = MAX(test_score) OVER (PARTITION BY student_id)
THEN test_term
END) AS "highest_test_term"
FROM Table1;
However, you can also use dense_rank() :
SELECT t1.*
FROM (SELECT t.*, DENSE_RANK() OVER (PARTITION BY student_id ORDER BY test_score DESC) AS seq
FROM Table1 t
) t1
WHERE seq = 1;
Another option would be subquery with correlation approach :
SELECT t.*
FROM Table1 t
WHERE test_score = (SELECT MAX(t1.test_score) FROM Table1 t1 WHERE t1.student_id = t.student_id);

How to find the difference of sum between maximum cumulative sum and minimum cumulative sum?

Thank you in advance.
I have a table :
id|| Subject || Marks
1 || English || 10
2 || Maths || 30
3 || History || 50
4 || English || 70
5 || Maths || 80
6 || History || 90
7 || English || 100
8 || Maths || 130
9 || History || 150
I want to find the difference of sum in terms of max cumulative sum and minimum cumulative sum for each subject:
Example
Subject || Marks || Cumulative sum || Cumulative diff(max-min)
history || 50 || 50 ||
History || 90 || 140 ||
History || 150 || 290 || (290-50)=240
I am able to get the cumulative sum but unable to find the rest of the values:
select t1.id,t1.marks,sum(t2.marks) as sum1
from [NorthWind].[dbo].[Table_1] t1
inner join [NorthWind].[dbo].[Table_1] t2 on t1.id>=t2.id
group by t1.id,t1.marks
order by t1.id
Thank you.
Try this
Rextester sample
with tbl1 as (
select t.*
,sum(marks) over (partition by subject order by id) as cum_sum
,sum(marks) over (partition by subject order by id)
- min(marks) over (partition by subject order by id)
as diff
,row_number() over (partition by subject order by id desc) as rnk
from your_table t
)
select
id,Subject,Marks,cum_sum
,case when rnk=1 then diff else null end as cum_diff
from tbl1 t
order by subject,id;
Output:
+----+---------+-------+---------+----------+
| id | Subject | Marks | cum_sum | cum_diff |
+----+---------+-------+---------+----------+
| 1 | English | 10 | 10 | NULL |
| 4 | English | 70 | 80 | NULL |
| 7 | English | 100 | 180 | 170 |
| 3 | History | 50 | 50 | NULL |
| 6 | History | 90 | 140 | NULL |
| 9 | History | 150 | 290 | 240 |
| 2 | Maths | 30 | 30 | NULL |
| 5 | Maths | 80 | 110 | NULL |
| 8 | Maths | 130 | 240 | 210 |
+----+---------+-------+---------+----------+
Try this using window functions.
You can use window sum to find cuml sum and then use max and min on that.
select subject,
cuml_marks,
max(cuml_marks) over (partition by subject)
- min(cuml_marks) over (partition by subject) as cuml_diff
from (
select subject,
sum(marks) over (
partition by subject order by id
) as cuml_marks
from t
) t;
You could use SUM() OVER() like this
DECLARE #SampleData AS TABLE
(
Id int,
Subject varchar(20),
Marks int
)
INSERT INTO #SampleData
VALUES
( 1, 'English', 10),
( 2, 'Maths', 30),
( 3, 'History', 50),
( 4, 'English', 70),
( 5, 'Maths', 80),
( 6, 'History', 90),
( 7, 'English', 100),
( 8, 'Maths', 130),
( 9, 'History', 150 )
SELECT *,
sum(sd.Marks) OVER(PARTITION BY sd.Subject ORDER BY sd.Id) AS [Cumulative sum],
sum(sd.Marks) OVER(PARTITION BY sd.Subject) - FIRST_VALUE(sd.Marks) OVER(PARTITION BY sd.Subject ORDER BY sd.Id) AS [Cumulative diff(max-min)]
-- or try MIN(sd.Marks).....
FROM #SampleData sd
ORDER BY sd.Subject, sd.Id
Demo link: http://rextester.com/GAE89325
DECLARE #TempData AS TABLE
(
Id int,
Subject varchar(20),
Marks int
)
INSERT INTO #TempData
VALUES
( 1, 'English', 10),
( 2, 'Maths', 30),
( 3, 'History', 50),
( 4, 'English', 70),
( 5, 'Maths', 80),
( 6, 'History', 90),
( 7, 'English', 100),
( 8, 'Maths', 130),
( 9, 'History', 150 )
Declare #SelectSubject Varchar(20)='History' --SELECT your subject here like English,Maths,History
SELECT Subject
,Marks
,RunningTotal AS CumulativeSum
,CASE
WHEN Rno = 3
THEN Cumulativediff
ELSE ''
END AS Cumulativediff
FROM (
SELECT *
, (
MAX(RunningTotal) OVER (ORDER BY Subject)) - (MIN(RunningTotal) OVER (ORDER BY Subject)
) AS Cumulativediff
,ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS Rno
FROM (
SELECT *
FROM (
SELECT ISNULL(Subject, 'GrandTotal') AS Subject
,ISNULL(CASt(Marks AS VARCHAR(10)), 'SubTotal') AS Marks
,SUM(Marks) OVER (
PARTITION BY Subject ORDER BY Id
) AS RunningTotal
FROM #TempData
) Dt
WHERE dt.Subject = #SelectSubject
) Dt2
) Final
Result:
Subject Marks CumulativeSum Cumulativediff
-----------------------------------------------
History 50 50 0
History 90 140 0
History 150 290 240

In Oracle, how to select specific row while aggregating all rows

I have a requirement that I need to both aggregate all rows by id, and find 1 specific row among the rows of the same id. It's like 2 SQL queries, but I want to make it in 1 SQL query. I'm using Oracle database.
for example,table t1 whose data looks like:
id | name | num
----- -------- -------
1 | 'a' | 1
2 | 'b' | 3
2 | 'c' | 6
2 | 'd' | 6
I want to aggregate the data by the id, find the 'name' with the highest 'count', and sum all count of the id to 'total_count'.
There are 2 rows with same num, pick up the first one.
id | highest_num | name_of_highest_num | total_num | avg_num
----- ------------- --------------------- ------------ -------------------
1 | 1 | 'a' | 1 | 1
2 | 6 | 'c' | 15 | 5
Can I get this result by 1 Oracle SQL query?
Thanks in advance for any replies.
Oracle Setup:
CREATE TABLE table_name ( id, name, num ) AS
SELECT 1, 'a', 1 FROM DUAL UNION ALL
SELECT 2, 'b', 3 FROM DUAL UNION ALL
SELECT 2, 'c', 6 FROM DUAL UNION ALL
SELECT 2, 'd', 6 FROM DUAL;
Query:
SELECT id,
MAX( num ) AS highest_num,
MAX( name ) KEEP ( DENSE_RANK LAST ORDER BY num ) AS name_of_highest_num,
SUM( num ) AS total_num,
AVG( num ) AS avg_num
FROM table_name
GROUP BY id
Output:
ID HIGHEST_NUM NAME_OF_HIGHEST_NUM TOTAL_NUM AVG_NUM
-- ----------- ------------------- --------- -------
1 1 a 1 1
2 6 d 15 5
Here's one option using row_number in a subquery with conditional aggregation:
select id,
max(num) as highest_num,
max(case when rn = 1 then name end) as name_of_highest_num,
sum(num) as total_num,
avg(num) as avg_num
from (
select id, name, num,
row_number() over (partition by id order by num desc) rn
from a
) t
group by id
SQL Fiddle Demo
Sounds like you want to use some analytic functions. Something like this should work
select id,
num highest_num,
name name_of_highest_num,
total total_num,
average avg_num
from (select id,
num,
name,
rank() over (partition by id
order by num desc, name asc) rnk,
sum(num) over (partition by id) total,
avg(num) over (partition by id) average
from table t1)
where rnk = 1

Count Top 5 Elements spread over rows and columns

Using T-SQL for this table:
+-----+------+------+------+-----+
| No. | Col1 | Col2 | Col3 | Age |
+-----+------+------+------+-----+
| 1 | e | a | o | 5 |
| 2 | f | b | a | 34 |
| 3 | a | NULL | b | 22 |
| 4 | b | c | a | 55 |
| 5 | b | a | b | 19 |
+-----+------+------+------+-----+
I need to count the TOP 3 names (Ordered by TotalCount DESC) across all rows and columns, for 3 Age groups: 0-17, 18-49, 50-100. Also, how do I ignore the NULLS from my results?
If it's possible, how I can also UNION the results for all 3 age groups into one output table to get 9 results (TOP 3 x 3 Age groups)?
Output for only 1 Age Group: 18-49 would look like this:
+------+------------+
| Name | TotalCount |
+------+------------+
| b | 4 |
| a | 3 |
| f | 1 |
+------+------------+
You need to unpivot first your table and then exclude the NULLs. Then do a simple COUNT(*):
WITH CteUnpivot(Name, Age) AS(
SELECT x.*
FROM tbl t
CROSS APPLY ( VALUES
(col1, Age),
(col2, Age),
(col3, Age)
) x(Name, Age)
WHERE x.Name IS NOT NULL
)
SELECT TOP 3
Name, COUNT(*) AS TotalCount
FROM CteUnpivot
WHERE Age BETWEEN 18 AND 49
GROUP BY Name
ORDER BY COUNT(*) DESC
ONLINE DEMO
If you want to get the TOP 3 for each age group:
WITH CteUnpivot(Name, Age) AS(
SELECT x.*
FROM tbl t
CROSS APPLY ( VALUES
(col1, Age),
(col2, Age),
(col3, Age)
) x(Name, Age)
WHERE x.Name IS NOT NULL
),
CteRn AS (
SELECT
AgeGroup =
CASE
WHEN Age BETWEEN 0 AND 17 THEN '0-17'
WHEN Age BETWEEN 18 AND 49 THEN '18-49'
WHEN Age BETWEEN 50 AND 100 THEN '50-100'
END,
Name,
COUNT(*) AS TotalCount
FROM CteUnpivot
GROUP BY
CASE
WHEN Age BETWEEN 0 AND 17 THEN '0-17'
WHEN Age BETWEEN 18 AND 49 THEN '18-49'
WHEN Age BETWEEN 50 AND 100 THEN '50-100'
END,
Name
)
SELECT
AgeGroup, Name, TotalCount
FROM(
SELECT *,
rn = ROW_NUMBER() OVER(PARTITION BY AgeGroup, Name ORDER BY TotalCount DESC)
FROM CteRn
) t
WHERE rn <= 3;
ONLINE DEMO
The unpivot technique using CROSS APPLY and VALUES:
An Alternative (Better?) Method to UNPIVOT (SQL Spackle) by Dwain Camps
You can check below multiple-CTE SQL select statement
Row_Number() with Partition By clause is used ordering records within each group categorized by ages
/*
CREATE TABLE tblAges(
[No] Int,
Col1 VarChar(10),
Col2 VarChar(10),
Col3 VarChar(10),
Age SmallInt
)
INSERT INTO tblAges VALUES
(1, 'e', 'a', 'o', 5),
(2, 'f', 'b', 'a', 34),
(3, 'a', NULL, 'b', 22),
(4, 'b', 'c', 'a', 55),
(5, 'b', 'a', 'b', 19);
*/
;with cte as (
select
col1 as col, Age
from tblAges
union all
select
col2, Age
from tblAges
union all
select
col3, Age
from tblAges
), cte2 as (
select
col,
case
when age < 18 then '0-17'
when age < 50 then '18-49'
else '50-100'
end as grup
from cte
where col is not null
), cte3 as (
select
grup,
col,
count(grup) cnt
from cte2
group by
grup,
col
)
select * from (
select
grup, col, cnt, ROW_NUMBER() over (partition by grup order by cnt desc) cnt_grp
from cte3
) t
where cnt_grp <= 3
order by grup, cnt