Использование LIMIT в GROUP BY, чтобы получить N результатов на группу? - PullRequest
343 голосов
/ 25 января 2010

Следующий запрос:

SELECT
year, id, rate
FROM h
WHERE year BETWEEN 2000 AND 2009
AND id IN (SELECT rid FROM table2)
GROUP BY id, year
ORDER BY id, rate DESC

Выходы:

year    id  rate
2006    p01 8
2003    p01 7.4
2008    p01 6.8
2001    p01 5.9
2007    p01 5.3
2009    p01 4.4
2002    p01 3.9
2004    p01 3.5
2005    p01 2.1
2000    p01 0.8
2001    p02 12.5
2004    p02 12.4
2002    p02 12.2
2003    p02 10.3
2000    p02 8.7
2006    p02 4.6
2007    p02 3.3

Мне нужны только 5 лучших результатов для каждого идентификатора:

2006    p01 8
2003    p01 7.4
2008    p01 6.8
2001    p01 5.9
2007    p01 5.3
2001    p02 12.5
2004    p02 12.4
2002    p02 12.2
2003    p02 10.3
2000    p02 8.7

Есть ли способ сделать это, используя какой-нибудь LIMIT-подобный модификатор, который работает в GROUP BY?

Ответы [ 13 ]

100 голосов
/ 23 марта 2013

Вы можете использовать агрегированную функцию GROUP_CONCAT , чтобы собрать все годы в один столбец, сгруппированный по id и упорядоченный по rate:

SELECT   id, GROUP_CONCAT(year ORDER BY rate DESC) grouped_year
FROM     yourtable
GROUP BY id

Результат:

-----------------------------------------------------------
|  ID | GROUPED_YEAR                                      |
-----------------------------------------------------------
| p01 | 2006,2003,2008,2001,2007,2009,2002,2004,2005,2000 |
| p02 | 2001,2004,2002,2003,2000,2006,2007                |
-----------------------------------------------------------

И тогда вы можете использовать FIND_IN_SET , который возвращает позицию первого аргумента внутри второго, например.

SELECT FIND_IN_SET('2006', '2006,2003,2008,2001,2007,2009,2002,2004,2005,2000');
1

SELECT FIND_IN_SET('2009', '2006,2003,2008,2001,2007,2009,2002,2004,2005,2000');
6

Используя комбинацию GROUP_CONCAT и FIND_IN_SET и фильтрацию по позиции, возвращаемой find_in_set, вы можете использовать этот запрос, который возвращает только первые 5 лет для каждого идентификатора:

SELECT
  yourtable.*
FROM
  yourtable INNER JOIN (
    SELECT
      id,
      GROUP_CONCAT(year ORDER BY rate DESC) grouped_year
    FROM
      yourtable
    GROUP BY id) group_max
  ON yourtable.id = group_max.id
     AND FIND_IN_SET(year, grouped_year) BETWEEN 1 AND 5
ORDER BY
  yourtable.id, yourtable.year DESC;

Пожалуйста, смотрите скрипку здесь .

Обратите внимание, что если несколько строк могут иметь одинаковую ставку, вам следует рассмотреть использование GROUP_CONCAT (ставка DISTINCT ORDER BY) в столбце ставки вместо столбца года.

Максимальная длина строки, возвращаемой GROUP_CONCAT, ограничена, поэтому это хорошо работает, если вам нужно выбрать несколько записей для каждой группы.

84 голосов
/ 16 мая 2015

В исходном запросе использовались пользовательские переменные и ORDER BY в производных таблицах; поведение обеих причуд не гарантировано. Пересмотренный ответ следующим образом.

В MySQL 5.x вы можете использовать звание бедного человека над разделом для достижения желаемого результата. Просто внешнее объедините таблицу с самим собой и для каждой строки посчитайте количество строк меньшее , чем оно. В приведенном выше случае меньшая строка - это строка с более высокой скоростью:

SELECT t.id, t.rate, t.year, COUNT(l.rate) AS rank
FROM t
LEFT JOIN t AS l ON t.id = l.id AND t.rate < l.rate
GROUP BY t.id, t.rate, t.year
HAVING COUNT(l.rate) < 5
ORDER BY t.id, t.rate DESC, t.year

Демонстрация и результат :

| id  | rate | year | rank |
|-----|------|------|------|
| p01 |  8.0 | 2006 | 0    |
| p01 |  7.4 | 2003 | 1    |
| p01 |  6.8 | 2008 | 2    |
| p01 |  5.9 | 2001 | 3    |
| p01 |  5.3 | 2007 | 4    |
| p02 | 12.5 | 2001 | 0    |
| p02 | 12.4 | 2004 | 1    |
| p02 | 12.2 | 2002 | 2    |
| p02 | 10.3 | 2003 | 3    |
| p02 |  8.7 | 2000 | 4    |

Обратите внимание, что если ставки имеют связи, например:

100, 90, 90, 80, 80, 80, 70, 60, 50, 40, ...

Приведенный выше запрос вернет 6 строк:

100, 90, 90, 80, 80, 80

Измените на HAVING COUNT(DISTINCT l.rate) < 5, чтобы получить 8 строк:

100, 90, 90, 80, 80, 80, 70, 60

Или измените на ON t.id = l.id AND (t.rate < l.rate OR (t.rate = l.rate AND t.pri_key > l.pri_key)), чтобы получить 5 строк:

 100, 90, 90, 80, 80

В MySQL 8 или более поздней версии просто используйте функции RANK, DENSE_RANK или ROW_NUMBER:

SELECT *
FROM (
    SELECT *, RANK() OVER (PARTITION BY id ORDER BY rate DESC) AS rnk
    FROM t
) AS x
WHERE rnk <= 5
17 голосов
/ 04 октября 2013

Для меня что-то вроде

SUBSTRING_INDEX(group_concat(col_name order by desired_col_order_name), ',', N) 

работает отлично. Нет сложного запроса.


например: получить топ 1 для каждой группы

SELECT 
    *
FROM
    yourtable
WHERE
    id IN (SELECT 
            SUBSTRING_INDEX(GROUP_CONCAT(id
                            ORDER BY rate DESC),
                        ',',
                        1) id
        FROM
            yourtable
        GROUP BY year)
ORDER BY rate DESC;
9 голосов
/ 05 января 2013

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

SELECT h.year, h.id, h.rate 
FROM (SELECT h.year, h.id, h.rate, IF(@lastid = (@lastid:=h.id), @index:=@index+1, @index:=0) indx 
      FROM (SELECT h.year, h.id, h.rate 
            FROM h
            WHERE h.year BETWEEN 2000 AND 2009 AND id IN (SELECT rid FROM table2)
            GROUP BY id, h.year
            ORDER BY id, rate DESC
            ) h, (SELECT @lastid:='', @index:=0) AS a
    ) h 
WHERE h.indx <= 5;
9 голосов
/ 02 ноября 2012

Для этого требуется ряд подзапросов для ранжирования значений, их ограничения, а затем для суммирования при группировании

@Rnk:=0;
@N:=2;
select
  c.id,
  sum(c.val)
from (
select
  b.id,
  b.bal
from (
select   
  if(@last_id=id,@Rnk+1,1) as Rnk,
  a.id,
  a.val,
  @last_id=id,
from (   
select 
  id,
  val 
from list
order by id,val desc) as a) as b
where b.rnk < @N) as c
group by c.id;
9 голосов
/ 25 января 2010

Нет, вы не можете произвольно ОГРАНИЧИТЬ подзапросы (вы можете сделать это в ограниченной степени в более новых MySQL, но не для 5 результатов на группу).

Это запрос с групповым максимальным типом, который нетривиально выполнять в SQL. Существует различных способов для решения проблемы, которая в некоторых случаях может быть более эффективной, но для top-n в целом вы захотите посмотреть ответ Билла на аналогичный предыдущий вопрос.

Как и в большинстве решений этой проблемы, он может возвращать более пяти строк, если имеется несколько строк с одинаковым значением rate, поэтому вам может потребоваться некоторое количество постобработки, чтобы проверить это.

4 голосов
/ 09 мая 2016

Создание виртуальных столбцов (например, RowID в Oracle)

таблица:

`
CREATE TABLE `stack` 
(`year` int(11) DEFAULT NULL,
`id` varchar(10) DEFAULT NULL,
`rate` float DEFAULT NULL) 
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
`

данные:

insert into stack values(2006,'p01',8);
insert into stack values(2001,'p01',5.9);
insert into stack values(2007,'p01',5.3);
insert into stack values(2009,'p01',4.4);
insert into stack values(2001,'p02',12.5);
insert into stack values(2004,'p02',12.4);
insert into stack values(2005,'p01',2.1);
insert into stack values(2000,'p01',0.8);
insert into stack values(2002,'p02',12.2);
insert into stack values(2002,'p01',3.9);
insert into stack values(2004,'p01',3.5);
insert into stack values(2003,'p02',10.3);
insert into stack values(2000,'p02',8.7);
insert into stack values(2006,'p02',4.6);
insert into stack values(2007,'p02',3.3);
insert into stack values(2003,'p01',7.4);
insert into stack values(2008,'p01',6.8);

SQL вроде этого:

select t3.year,t3.id,t3.rate 
from (select t1.*, (select count(*) from stack t2 where t1.rate<=t2.rate and t1.id=t2.id) as rownum from stack t1) t3 
where rownum <=3 order by id,rate DESC;

если удалить предложение where в t3, оно будет выглядеть так:

enter image description here

GET "TOP N Record" -> добавить "rownum <= 3" в предложении where (предложение where в t3); </p>

ВЫБЕРИТЕ «год» -> добавьте «между 2000 и 2009» в предложении where (предложение where в t3);

3 голосов
/ 25 октября 2016

Потребовалось немного поработать, но я думаю, что мое решение будет чем-то, чем можно поделиться, поскольку оно кажется элегантным и довольно быстрым.

SELECT h.year, h.id, h.rate 
  FROM (
    SELECT id, 
      SUBSTRING_INDEX(GROUP_CONCAT(CONCAT(id, '-', year) ORDER BY rate DESC), ',' , 5) AS l
      FROM h
      WHERE year BETWEEN 2000 AND 2009
      GROUP BY id
      ORDER BY id
  ) AS h_temp
    LEFT JOIN h ON h.id = h_temp.id 
      AND SUBSTRING_INDEX(h_temp.l, CONCAT(h.id, '-', h.year), 1) != h_temp.l

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

2 голосов
/ 29 ноября 2012
SELECT year, id, rate
FROM (SELECT
  year, id, rate, row_number() over (partition by id order by rate DESC)
  FROM h
  WHERE year BETWEEN 2000 AND 2009
  AND id IN (SELECT rid FROM table2)
  GROUP BY id, year
  ORDER BY id, rate DESC) as subquery
WHERE row_number <= 5

Подзапрос практически идентичен вашему запросу. Единственное изменение добавляет

row_number() over (partition by id order by rate DESC)
2 голосов
/ 06 июля 2012

В следующем посте: sql: выбор первой записи N для каждой группы описывает сложный способ достижения этого без подзапросов.

Это улучшает другие решения, предлагаемые здесь:

  • Выполнение всего за один запрос
  • Умение правильно использовать индексы
  • Избегание подзапросов, которые, как известно, создают плохие планы выполнения в MySQL

Это, однако, не красиво. Хорошее решение было бы достижимо, если бы в MySQL были включены оконные функции (или аналитические функции), но это не так. Уловка, использованная в упомянутом посте, использует GROUP_CONCAT, который иногда описывается как «оконные функции бедняков для MySQL».

...