GROUP BY 내에서 LIMIT를 사용하여 그룹당 N 개의 결과를 얻습니까?


385

다음 쿼리 :

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

내가 원하는 것은 각 ID의 상위 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

GROUP BY 내에서 작동하는 수정 자와 같은 일종의 LIMIT를 사용하여 이것을 수행하는 방법이 있습니까?


10
이 작업은 MySQL에서 수행 할 수 있지만 LIMIT절 을 추가하는 것만 큼 간단하지는 않습니다 . 다음은 문제를 자세히 설명하는 기사입니다. SQL에서 그룹당 첫 번째 / 최소 / 최대 행을 선택하는 방법 좋은 기사입니다. "그룹당 상위 N 개"문제에 대해 우아하지만 순진한 솔루션을 소개합니다. 그것에 향상됩니다.
danben

SELECT * FROM (선택 연도, id, 2000 년과 2009 년 사이의 h 년부터, 그리고 IN IN (표 2에서 선택하여 제거) GROUP BY id, 연도 ORDER BY id, 비율 DESC) LIMIT 5
Mixcoatl

답변:


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_CONCATand 의 조합을 사용하고 FIND_IN_SETfind_in_set에서 반환 한 위치로 필터링하면 모든 id에 대해 처음 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 rate ORDER BY rate)를 사용하는 것을 고려해야합니다.

GROUP_CONCAT에 의해 반환되는 문자열의 최대 길이는 제한되어 있으므로 모든 그룹에 대해 몇 개의 레코드를 선택해야하는 경우에 효과적입니다.


3
그것은 아름답고 성능이 뛰어나고 비교적 간단하며 훌륭한 설명입니다. 정말 고맙습니다. 마지막으로 합리적 인 최대 길이를 계산할 수있는 SET SESSION group_concat_max_len = <maximum length>;경우 OP를 사용할 수 있습니다 (기본값은 1024이므로 문제가 아님).하지만 예를 들어 group_concat_max_len은 25 : 4 이상이어야합니다 (최대 연도 문자열 길이) + 1 (구분 문자), 5 배 (처음 5 년) 오류가 발생하지 않고 문자열이 잘 리므로와 같은 경고를 확인하십시오 1054 rows in set, 789 warnings (0.31 sec).
Timothy Johns

내가 사용해야하는 것보다 1에서 5가 아닌 정확히 2 개의 행을 가져 오려면 FIND_IN_SET(). 시도 FIND_IN_SET() =2했지만 예상대로 결과를 표시하지 않았습니다.
Amogh

FIND_IN_SET BETWEEN 1과 5는 크기가 5 이상인 경우 GROUP_CONCAT 세트의 처음 5 개 위치를 가져옵니다. 따라서 FIND_IN_SET = 2는 GROUP_CONCAT에서 두 번째 위치의 데이터 만 가져옵니다. 2 행을 얻으면 세트에 줄이 2 개 있다고 가정하면 1 위와 2 위를 1과 2 사이에서 시도 할 수 있습니다.
jDub9

이 솔루션은 대용량 데이터 세트에서 Salman보다 훨씬 우수한 성능을 제공합니다. 어쨌든 그런 영리한 솔루션에 대해 모두 엄지 손가락을 썼습니다. 감사!!
tiomno

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 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

7
id의 값을 변경하면 순위 계산이 다시 시작되므로 핵심 부분은 id by ORDER BY라고 언급 할 가치가 있다고 생각합니다.
ruuter

응답을 얻으려면 왜 두 번 실행해야 WHERE rank <=5합니까? 처음으로 각 ID에서 5 행을 얻지 못하지만 그 후에는 당신이 말한대로 얻을 수 있습니다.
Brenno Leal

@BrennoLeal 나는 당신이 SET진술을 잊고 있다고 생각합니다 (첫 번째 쿼리 참조). 필요합니다.
Salman A

3
최신 버전에서는 ORDER BY파생 테이블 의 in을 무시할 수 있으며 종종 무시됩니다. 이것은 목표를 이깁니다. 효율적인 그룹 별 정보는 여기 에서 찾을 수 있습니다 .
Rick James

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법적 deliverd / 하위 쿼리와 함께 사용FETCH FIRST n ROWS ONLY
레이몬드 Nijland

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;

귀하의 솔루션은 완벽하게 작동했지만 하위 쿼리에서 연도 및 기타 열을 검색하고 싶습니다. 어떻게 할 수 있습니까?
MaNn

9

아니요, 하위 쿼리는 임의로 제한 할 수 없습니다 (최신 MySQL에서는 제한적으로 수행 할 수 있지만 그룹당 5 개의 결과에는 해당되지 않음).

이것은 그룹 단위의 최대 유형 쿼리이며 SQL에서는 쉽지 않습니다. 가 있습니다 여러 가지 경우에보다 효율적으로 할 수있는 것을 해결하기는하지만, 일반적으로 상위 N 당신이보고 싶을거야 빌의 대답은 유사 이전의 질문에.

이 문제에 대한 대부분의 솔루션과 마찬가지로 동일한 rate값을 가진 여러 행이있는 경우 5 개 이상의 행을 반환 할 수 있으므로이를 확인하기 위해 많은 사후 처리가 필요할 수 있습니다.


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
anu

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

가상 열 구축 (Oracle의 RowID)

표:

`
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;

t3에서 where 절을 삭제하면 다음과 같이 표시됩니다.

여기에 이미지 설명을 입력하십시오

GET "TOP N Record"-> where 절 (t3의 where-clause)에 "rownum <= 3"을 추가하십시오.

"연도"를 선택하십시오-> where 절 (t3의 where-clause)에 "BETWEEN 2000 AND 2009"를 추가하십시오.


동일한 ID에 대해 반복되는 요율이있는 경우 rowNum 수가 증가하기 때문에 작동하지 않습니다. 행 당 3을 얻지 못하고 0, 1 또는 2를 얻을 수 있습니다. 이것에 대한 해결책을 생각할 수 있습니까?
starvator 2016 년

@starvator "t1.rate <= t2.rate"를 "t1.rate <t2.rate"로 변경하십시오. 최상의 비율이 동일한 ID에서 동일한 값을 갖는 경우 모두 동일한 행 번호를 갖지만 더 이상 증가하지는 않습니다. "id p01의 rate 8"과 같이 "t1.rate <t2.rate"를 사용하여 반복하면 "id p01의 rate 8"은 모두 같은 행 번호 0을 가지며; "t1.rate <= t2.rate"를 사용하는 경우 행 번호는 2입니다.
Wang Wen'an

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에서 Window Functions (일명 Analytic Functions)를 활성화하면 좋은 솔루션을 얻을 수 있지만 그렇지 않습니다. 상기 포스트에서 사용 된 트릭은 GROUP_CONCAT를 사용하는데, 때때로 "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 개로 제한을 삽입합니다.


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;

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
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.