Используя LIMIT внутри GROUP BY, чтобы получить N результатов для каждой группы?
Следующий запрос:
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 лучших результатов для каждого id:
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?
Ответы
Ответ 1
Вы можете использовать агрегированную функцию 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 лет для каждого id:
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 rate ORDER BY rate) в столбце скорости вместо столбца года.
Максимальная длина строки, возвращаемой GROUP_CONCAT, ограничена, поэтому это хорошо работает, если вам нужно выбрать несколько записей для каждой группы.
Ответ 2
The original query used user variables and [TG40] on derived tables; the behavior of both quirks is not guaranteed. Revised answer as follows.
В 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
Ответ 3
Для меня что-то вроде
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;
Ответ 4
Нет, вы не можете LIMIT подзапросы произвольно (вы можете сделать это в ограниченной степени в новых MySQL, но не для 5 результатов для каждой группы).
Это запрос типа groupwise-maximum, который не является тривиальным для SQL. Существуют различные способы для решения того, что может быть более эффективным для некоторых случаев, но для top-n вообще вы захотите посмотреть ответьте на ответ на аналогичный предыдущий вопрос.
Как и в большинстве решений этой проблемы, он может возвращать более пяти строк, если имеется несколько строк с тем же значением rate
, поэтому вам может потребоваться некоторое количество пост-обработки, чтобы проверить это.
Ответ 5
Для этого требуется ряд подзапросов для ранжирования значений, их ограничения, затем выполнения суммы при группировке
@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;
Ответ 6
Попробуйте следующее:
SELECT h.year, h.id, h.rate
FROM (SELECT h.year, h.id, h.rate, IF(@lastid = (@lastid:=h.id), @index:[email protected]+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;
Ответ 7
Создайте виртуальные столбцы (например, 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-clause t3);
ВЫБЕРИТЕ "год" → добавить "МЕЖДУ 2000 И 2009" в разделе where (where-clause t3);
Ответ 8
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)
Ответ 9
Взял немного работы, но я решил, что мое решение будет чем-то разделяться, поскольку оно кажется элегантным, а также довольно быстрым.
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
Обратите внимание, что этот пример указан для целей вопроса и может быть легко модифицирован для других аналогичных целей.
Ответ 10
Следующая запись: sql: selcting top N записей для каждой группы описывает сложный способ достижения этого без подзапросов.
Это улучшает другие решения, предлагаемые здесь:
- Выполнение всего в одном запросе
- Возможность правильно использовать индексы
- Предотвращение подзапросов, которые, как известно, создают плохие планы выполнения в MySQL
Это, однако, не очень. Хорошим решением было бы возможно, если в MySQL были включены функции Window (aka Analytic Functions), но это не так.
В трюке, используемой в указанном сообщении, используется GROUP_CONCAT, который иногда описывается как "Функции окна для MySQL".
Ответ 11
для таких, как я, у которых были тайм-ауты запросов. Я сделал следующее, чтобы использовать ограничения и все остальное определенной группой.
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-значный предел
Ответ 12
Попробуйте следующее:
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;
Ответ 13
Пожалуйста, попробуйте выполнить хранимую процедуру. Я уже проверял. Я получаю правильный результат, но без использования 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
Ответ 14
Как получить N результатов по группе
Вы можете использовать UNION вместо GROUP и устанавливать LIMIT в каждом операторе SELECT.
Пример массива значений для сравнения:
(
SELECT * FROM tablename
WHERE column = '".$myValueArray[$n]."'
ORDER BY column DESC
LIMIT 4
)
UNION
(
SELECT * FROM tablename
WHERE column = '".$myValueArray[$n+1]."'
ORDER BY column DESC
LIMIT 4
)
UNION
(
SELECT * FROM tablename
WHERE column = '".$myValueArray[$n+2]."'
ORDER BY column DESC
LIMIT 4
);
Это немного интенсивно/дорого для большого набора. Но для небольших наборов может быть хорошим решением.
Ответ 15
Следующие группы запросов записываются на основе Grouping_COL
и поэтапно назначают row_number
каждой записи в каждой группе. Затем вы можете выполнить выбор, чтобы вернуть все записи, у которых количество строк меньше, чем счет.
SELECT
Grouping_COL ,
rownum_cnt
FROM
(
SELECT
Grouping_COL,
row_number() OVER (PARTITION BY Grouping_COL) AS rownum_cnt
FROM MyTable
)
WHERE rownum_cnt <= 100