Как выбрать верхние 3 значения из каждой группы в таблице с SQL, которые имеют дубликаты

Предположим, что у нас есть таблица, которая имеет два столбца, один столбец содержит имена некоторых людей, а другой столбец содержит некоторые значения, относящиеся к каждому человеку. Один человек может иметь более одного значения. Каждое значение имеет числовой тип. Вопрос в том, что мы хотим выбрать верхние 3 значения для каждого человека из таблицы. Если один человек имеет менее 3 значений, мы выбираем все значения для этого человека.

Проблема может быть решена, если в таблице нет дубликатов в запросе, представленном в этой статье Выбрать верхние 3 значения из каждой группы в таблице с помощью SQL. Но если есть дубликаты, каково это решение?

Например, если для одного имени Джон у него есть 5 значений, связанных с ним. Они составляют 20,7,7,7,4. Мне нужно вернуть пары имя/значение, как указано ниже, по порядку по убыванию для каждого имени:

-----------+-------+
| name     | value |
-----------+-------+
| John     |    20 |
| John     |     7 |
| John     |     7 |
-----------+-------+

Для Джона нужно вернуть только три строки, хотя для Джона есть три 7.

Ответ 1

Во многих современных СУБД (например, Postgres, Oracle, SQL-Server, DB2 и многих других) следующее будет работать нормально. Он использует CTE и функцию ранжирования ROW_NUMBER(), которая является частью последнего стандарта SQL:

 WITH cte AS
  ( SELECT name, value,
           ROW_NUMBER() OVER (PARTITION BY name
                              ORDER BY value DESC
                             )
             AS rn
    FROM t
  )
SELECT name, value, rn
FROM cte
WHERE rn <= 3
ORDER BY name, rn ;

Без CTE, только ROW_NUMBER():

SELECT name, value, rn
FROM 
  ( SELECT name, value,
           ROW_NUMBER() OVER (PARTITION BY name
                              ORDER BY value DESC
                             )
             AS rn
    FROM t
  ) tmp 
WHERE rn <= 3
ORDER BY name, rn ; 

Протестировано:


В MySQL и других СУБД, которые не имеют ранжирующих функций, нужно использовать либо производные таблицы, коррелированные подзапросы, либо самосоединиться с GROUP BY.

Предполагается, что (tid) является первичным ключом таблицы:

SELECT t.tid, t.name, t.value,              -- self join and GROUP BY
       COUNT(*) AS rn
FROM t
  JOIN t AS t2
    ON  t2.name = t.name
    AND ( t2.value > t.value
        OR  t2.value = t.value
        AND t2.tid <= t.tid
        )
GROUP BY t.tid, t.name, t.value
HAVING COUNT(*) <= 3
ORDER BY name, rn ;


SELECT t.tid, t.name, t.value, rn
FROM
  ( SELECT t.tid, t.name, t.value,
           ( SELECT COUNT(*)                -- inline, correlated subquery
             FROM t AS t2
             WHERE t2.name = t.name
              AND ( t2.value > t.value
                 OR  t2.value = t.value
                 AND t2.tid <= t.tid
                  )
           ) AS rn
    FROM t
  ) AS t
WHERE rn <= 3
ORDER BY name, rn ;

Протестировано в MySQL

Ответ 2

Я собирался уменьшить вопрос. Однако я понял, что действительно может потребоваться решение для кросс-баз данных.

Предполагая, что вы ищете независимый от базы данных способ сделать это, единственный способ, которым я могу думать, - использовать коррелированные подзапросы (или не equijoins). Вот пример:

select distinct t.personid, val, rank
from (select t.*,
             (select COUNT(distinct val) from t t2 where t2.personid = t.personid and t2.val >= t.val
             ) as rank
      from t
     ) t
where rank in (1, 2, 3)

Однако каждая база данных, которую вы упоминаете (и я отмечаю, Hadoop не является базой данных), имеет лучший способ сделать это. К сожалению, ни один из них не является стандартным SQL.

Вот пример его работы в SQL Server:

with t as (
      select 1 as personid, 5 as val union all
      select 1 as personid, 6 as val union all
      select 1 as personid, 6 as val union all
      select 1 as personid, 7 as val union all
      select 1 as personid, 8 as val
     )
select distinct t.personid, val, rank
from (select t.*,
             (select COUNT(distinct val) from t t2 where t2.personid = t.personid and t2.val >= t.val
             ) as rank
      from t
     ) t
where rank in (1, 2, 3);

Ответ 3

Используя GROUP_CONCAT и FIND_IN_SET, вы можете это сделать. Check SQLFIDDLE.

SELECT *
FROM tbl t
WHERE FIND_IN_SET(t.value,(SELECT
                             SUBSTRING_INDEX(GROUP_CONCAT(t1.value ORDER BY VALUE DESC),',',3)
                           FROM tbl t1
                           WHERE t1.name = t.name
                           GROUP BY t1.name)) > 0
ORDER BY t.name,t.value desc

Ответ 4

Если ваш результирующий набор не настолько тяжелый, вы можете написать хранимую процедуру (или анонимный PL/SQL-блок) для этой проблемы, которая выполняет итерацию результирующего набора и находит большие три символа простым алгоритмом сравнения.

Ответ 5

Попробуйте это -

CREATE TABLE #list ([name] [varchar](100) NOT NULL, [value] [int] NOT NULL)
INSERT INTO #list VALUES ('John', 20), ('John', 7), ('John', 7), ('John', 7), ('John', 4);

WITH cte
AS (
SELECT NAME
    ,value
    ,ROW_NUMBER() OVER (
        PARTITION BY NAME ORDER BY (value) DESC
        ) RN
FROM #list
)
SELECT NAME
,value
FROM cte
WHERE RN < 4
ORDER BY value DESC

Ответ 6

Это работает для MS SQL. Должен быть работоспособным на любом другом диалекте SQL, который имеет возможность назначать номера строк в группе по или над предложением (или эквивалентным)

if object_id('tempdb..#Data') is not null drop table #Data;
GO

create table #data (name varchar(25), value integer);
GO
set nocount on;
insert into #data values ('John', 20);
insert into #data values ('John', 7);
insert into #data values ('John', 7);
insert into #data values ('John', 7);
insert into #data values ('John', 5);
insert into #data values ('Jack', 5);
insert into #data values ('Jane', 30);
insert into #data values ('Jane', 21);
insert into #data values ('John', 5);
insert into #data values ('John', -1);
insert into #data values ('John', -1);
insert into #data values ('Jane', 18);
set nocount off;
GO

with D as (
SELECT
     name
    ,Value
    ,row_number() over (partition by name order by value desc) rn
From
    #Data
)
SELECT Name, Value
FROM D
WHERE RN <= 3
order by Name, Value Desc

Name    Value
Jack    5
Jane    30
Jane    21
Jane    18
John    20
John    7
John    7