Использование LIMIT в GROUP BY, чтобы получить N результатов на группу?

388

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

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?

углубления
источник
10
Это можно сделать в MySQL, но это не так просто, как добавить LIMITпредложение. Вот статья, которая подробно объясняет проблему: Как выбрать первую / наименьшую / максимальную строку для каждой группы в SQL. Это хорошая статья - он представляет элегантное, но наивное решение проблемы «Top N на группу», а затем постепенно улучшается на этом.
Данбен
ВЫБРАТЬ * ОТ (ВЫБРАТЬ год, идентификатор, скорость ИЗ Ч. ГДЕ ГОДА МЕЖДУ 2000 И 2009
ГОДАМИ И ИДЕНТИФИКАТОР

Ответы:

115

Вы можете использовать агрегированную функцию 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, ограничена, поэтому это хорошо работает, если вам нужно выбрать несколько записей для каждой группы.

fthiella
источник
3
Это красиво , сравнительно просто и великолепное объяснение; Спасибо вам большое. К вашему последнему пункту, где разумная максимальная длина может быть вычислена, можно использовать SET SESSION group_concat_max_len = <maximum length>;В случае OP, не проблема (так как по умолчанию 1024), но в качестве примера, group_concat_max_len должно быть не менее 25: 4 (max длина строки года) + 1 (символ разделителя), умножить на 5 (первые 5 лет). Строки усечены, а не выдают ошибку, поэтому следите за предупреждениями, такими как 1054 rows in set, 789 warnings (0.31 sec).
Тимоти Джонс
Если я хочу получить точные 2 строки, а не от 1 до 5, чем я должен использовать FIND_IN_SET(). Я пытался, FIND_IN_SET() =2но не показывал результат, как ожидалось.
Amogh
FIND_IN_SET МЕЖДУ 1 и 5 будут занимать первые 5 позиций набора GROUP_CONCAT, если размер равен или больше 5. Поэтому FIND_IN_SET = 2 будет принимать только данные со 2-й позиции в вашем GROUP_CONCAT. Получив 2 строки, вы можете попробовать между 1 и 2 для 1-й и 2-й позиции, предполагая, что в наборе есть 2 строки.
jDub9
Это решение имеет гораздо лучшую производительность, чем у Salman для больших наборов данных. В любом случае, я одобрил оба таких умных решения. Спасибо!!
19
105

Исходный запрос используется пользовательские переменные и 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) < 58 строк:

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-или более поздняя версия просто использовать 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
Салман А
источник
7
Я думаю, что стоит упомянуть, что ключевой частью является ORDER BY id, так как любое изменение значения id возобновит подсчет ранга.
Руутер
Почему я должен запустить его дважды, чтобы получить ответ WHERE rank <=5? Впервые я не получаю 5 строк от каждого идентификатора, но после этого я могу получить, как вы сказали.
Бренно Лил
@BrennoLeal Я думаю, что вы забыли SETутверждение (см. Первый запрос). Это необходимо.
Салман
3
В более новых версиях ORDER BYпроизводная таблица может и часто будет игнорироваться. Это побеждает цель. Эффективные групповые найдены здесь .
Рик Джеймс
1
+1 ваш ответ переписан очень корректно, так как современные версии MySQL / MariaDB больше соответствуют стандартам ANSI / ISO SQL 1992/1999/2003, где его никогда не разрешалось использовать ORDER BYв поставляемых / подзапросах подобного типа. Именно поэтому современные версии MySQL / MariaDB игнорируют ORDER BYвходящий подзапрос без использования LIMIT, я полагаю, что стандарты ANSI / ISO SQL 2008/2011/2016 делают ORDER BYв поставленных / подзапросах законными использование его в сочетании сFETCH FIRST n ROWS ONLY
Раймондом
21

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

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

Нет, вы не можете произвольно ограничивать подзапросы (вы можете делать это в ограниченной степени в новых MySQL, но не для 5 результатов на группу).

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

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

bobince
источник
9

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

@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

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

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;
Сахарш Шах
источник
1
неизвестно a.type столбец в списке полей
ану
5
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)
Рики Морено
источник
8
Это хорошо, но MySQL не имеет оконных функций (как ROW_NUMBER()).
ypercubeᵀᴹ
3
По состоянию MySQL 8.0, row_number()это доступно .
erickg
4

Построить виртуальные столбцы (например, 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, оно выглядит так:

введите описание изображения здесь

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

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

Ван Вэньань
источник
Если у вас есть ставки, которые повторяются для одного и того же идентификатора, это не будет работать, потому что ваш счетчик строк увеличится; вы не получите 3 за ряд, вы можете получить 0, 1 или 2. Можете ли вы придумать какое-либо решение для этого?
starvator
@starvator замените «t1.rate <= t2.rate» на «t1.rate <t2.rate», если лучшая ставка имеет одинаковые значения с одинаковым идентификатором, все они имеют одинаковый rownum, но не будут увеличиваться выше; например, "rate 8 in id p01", если он повторяется с использованием "t1.rate <t2.rate", оба из "rate 8 in id p01" имеют одинаковое значение rownum 0; при использовании «t1.rate <= t2.rate» значение rownum равно 2;
Ван Вэньань
3

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

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

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

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

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

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

Шломи Ноах
источник
1

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

DELIMITER $$
CREATE PROCEDURE count_limit200()
BEGIN
    DECLARE a INT Default 0;
    DECLARE stop_loop INT Default 0;
    DECLARE domain_val VARCHAR(250);
    DECLARE domain_list CURSOR FOR SELECT DISTINCT domain FROM db.one;

    OPEN domain_list;

    SELECT COUNT(DISTINCT(domain)) INTO stop_loop 
    FROM db.one;
    -- BEGIN LOOP
    loop_thru_domains: LOOP
        FETCH domain_list INTO domain_val;
        SET a=a+1;

        INSERT INTO db.two(book,artist,title,title_count,last_updated) 
        SELECT * FROM 
        (
            SELECT book,artist,title,COUNT(ObjectKey) AS titleCount, NOW() 
            FROM db.one 
            WHERE book = domain_val
            GROUP BY artist,title
            ORDER BY book,titleCount DESC
            LIMIT 200
        ) a ON DUPLICATE KEY UPDATE title_count = titleCount, last_updated = NOW();

        IF a = stop_loop THEN
            LEAVE loop_thru_domain;
        END IF;
    END LOOP loop_thru_domain;
END $$

он перебирает список доменов, а затем вставляет только ограничение 200 каждый

Dev-Ria
источник
1

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

SET @num := 0, @type := '';
SELECT `year`, `id`, `rate`,
    @num := if(@type = `id`, @num + 1, 1) AS `row_number`,
    @type := `id` AS `dummy`
FROM (
    SELECT *
    FROM `h`
    WHERE (
        `year` BETWEEN '2000' AND '2009'
        AND `id` IN (SELECT `rid` FROM `table2`) AS `temp_rid`
    )
    ORDER BY `id`
) AS `temph`
GROUP BY `year`, `id`, `rate`
HAVING `row_number`<='5'
ORDER BY `id`, `rate DESC;
MLF
источник
0

Пожалуйста, попробуйте ниже хранимую процедуру. Я уже проверил. Я получаю правильный результат, но без использования groupby.

CREATE DEFINER=`ks_root`@`%` PROCEDURE `first_five_record_per_id`()
BEGIN
DECLARE query_string text;
DECLARE datasource1 varchar(24);
DECLARE done INT DEFAULT 0;
DECLARE tenants varchar(50);
DECLARE cur1 CURSOR FOR SELECT rid FROM demo1;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;

    SET @query_string='';

      OPEN cur1;
      read_loop: LOOP

      FETCH cur1 INTO tenants ;

      IF done THEN
        LEAVE read_loop;
      END IF;

      SET @datasource1 = tenants;
      SET @query_string = concat(@query_string,'(select * from demo  where `id` = ''',@datasource1,''' order by rate desc LIMIT 5) UNION ALL ');

       END LOOP; 
      close cur1;

    SET @query_string  = TRIM(TRAILING 'UNION ALL' FROM TRIM(@query_string));  
  select @query_string;
PREPARE stmt FROM @query_string;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

END
Химаншу Патель
источник