[sql] 각 그룹에서 마지막 레코드 검색-MySQL

messages아래와 같이 데이터가 포함 된 테이블 이 있습니다.

Id   Name   Other_Columns
-------------------------
1    A       A_data_1
2    A       A_data_2
3    A       A_data_3
4    B       B_data_1
5    B       B_data_2
6    C       C_data_1

query를 실행하면 다음 select * from messages group by name과 같은 결과가 나타납니다.

1    A       A_data_1
4    B       B_data_1
6    C       C_data_1

어떤 쿼리가 다음 결과를 반환합니까?

3    A       A_data_3
5    B       B_data_2
6    C       C_data_1

즉, 각 그룹의 마지막 레코드가 반환되어야합니다.

현재 이것은 내가 사용하는 쿼리입니다.

SELECT
  *
FROM (SELECT
  *
FROM messages
ORDER BY id DESC) AS x
GROUP BY name

그러나 이것은 매우 비효율적입니다. 동일한 결과를 얻는 다른 방법이 있습니까?



답변

MySQL 8.0은 이제 거의 모든 인기있는 SQL 구현과 같은 윈도우 기능을 지원합니다. 이 표준 구문을 사용하면 그룹당 최대 n 개의 쿼리를 작성할 수 있습니다.

WITH ranked_messages AS (
  SELECT m.*, ROW_NUMBER() OVER (PARTITION BY name ORDER BY id DESC) AS rn
  FROM messages AS m
)
SELECT * FROM ranked_messages WHERE rn = 1;

아래는 2009 년에이 질문에 대해 작성한 최초의 답변입니다.


나는 이런 식으로 해결책을 씁니다.

SELECT m1.*
FROM messages m1 LEFT JOIN messages m2
 ON (m1.name = m2.name AND m1.id < m2.id)
WHERE m2.id IS NULL;

성능과 관련하여 데이터의 특성에 따라 하나의 솔루션 또는 다른 솔루션이 더 나을 수 있습니다. 따라서 두 쿼리를 모두 테스트하고 데이터베이스에서 성능이 더 좋은 쿼리를 사용해야합니다.

예를 들어 StackOverflow August 데이터 덤프 복사본이 있습니다. 벤치마킹에 사용하겠습니다. Posts테이블 에는 1,114,357 개의 행이 있습니다 . Macbook Pro 2.40GHz의 MySQL 5.0.75에서 실행됩니다 .

주어진 사용자 ID (광산)에 대한 최신 게시물을 찾기 위해 쿼리를 작성합니다.

먼저 @Eric으로 표시된 기술을 GROUP BY하위 쿼리에서 사용하십시오.

SELECT p1.postid
FROM Posts p1
INNER JOIN (SELECT pi.owneruserid, MAX(pi.postid) AS maxpostid
            FROM Posts pi GROUP BY pi.owneruserid) p2
  ON (p1.postid = p2.maxpostid)
WHERE p1.owneruserid = 20860;

1 row in set (1 min 17.89 sec)

EXPLAIN분석 조차도 16 초 이상이 걸립니다.

+----+-------------+------------+--------+----------------------------+-------------+---------+--------------+---------+-------------+
| id | select_type | table      | type   | possible_keys              | key         | key_len | ref          | rows    | Extra       |
+----+-------------+------------+--------+----------------------------+-------------+---------+--------------+---------+-------------+
|  1 | PRIMARY     | <derived2> | ALL    | NULL                       | NULL        | NULL    | NULL         |   76756 |             |
|  1 | PRIMARY     | p1         | eq_ref | PRIMARY,PostId,OwnerUserId | PRIMARY     | 8       | p2.maxpostid |       1 | Using where |
|  2 | DERIVED     | pi         | index  | NULL                       | OwnerUserId | 8       | NULL         | 1151268 | Using index |
+----+-------------+------------+--------+----------------------------+-------------+---------+--------------+---------+-------------+
3 rows in set (16.09 sec)

이제 내 기술 을 사용하여 동일한 쿼리 결과를 생성하십시오 LEFT JOIN.

SELECT p1.postid
FROM Posts p1 LEFT JOIN posts p2
  ON (p1.owneruserid = p2.owneruserid AND p1.postid < p2.postid)
WHERE p2.postid IS NULL AND p1.owneruserid = 20860;

1 row in set (0.28 sec)

EXPLAIN두 테이블은 자신의 인덱스를 사용 할 수있는 분석 프로그램 :

+----+-------------+-------+------+----------------------------+-------------+---------+-------+------+--------------------------------------+
| id | select_type | table | type | possible_keys              | key         | key_len | ref   | rows | Extra                                |
+----+-------------+-------+------+----------------------------+-------------+---------+-------+------+--------------------------------------+
|  1 | SIMPLE      | p1    | ref  | OwnerUserId                | OwnerUserId | 8       | const | 1384 | Using index                          |
|  1 | SIMPLE      | p2    | ref  | PRIMARY,PostId,OwnerUserId | OwnerUserId | 8       | const | 1384 | Using where; Using index; Not exists |
+----+-------------+-------+------+----------------------------+-------------+---------+-------+------+--------------------------------------+
2 rows in set (0.00 sec)

Posts테이블 의 DDL은 다음과 같습니다 .

CREATE TABLE `posts` (
  `PostId` bigint(20) unsigned NOT NULL auto_increment,
  `PostTypeId` bigint(20) unsigned NOT NULL,
  `AcceptedAnswerId` bigint(20) unsigned default NULL,
  `ParentId` bigint(20) unsigned default NULL,
  `CreationDate` datetime NOT NULL,
  `Score` int(11) NOT NULL default '0',
  `ViewCount` int(11) NOT NULL default '0',
  `Body` text NOT NULL,
  `OwnerUserId` bigint(20) unsigned NOT NULL,
  `OwnerDisplayName` varchar(40) default NULL,
  `LastEditorUserId` bigint(20) unsigned default NULL,
  `LastEditDate` datetime default NULL,
  `LastActivityDate` datetime default NULL,
  `Title` varchar(250) NOT NULL default '',
  `Tags` varchar(150) NOT NULL default '',
  `AnswerCount` int(11) NOT NULL default '0',
  `CommentCount` int(11) NOT NULL default '0',
  `FavoriteCount` int(11) NOT NULL default '0',
  `ClosedDate` datetime default NULL,
  PRIMARY KEY  (`PostId`),
  UNIQUE KEY `PostId` (`PostId`),
  KEY `PostTypeId` (`PostTypeId`),
  KEY `AcceptedAnswerId` (`AcceptedAnswerId`),
  KEY `OwnerUserId` (`OwnerUserId`),
  KEY `LastEditorUserId` (`LastEditorUserId`),
  KEY `ParentId` (`ParentId`),
  CONSTRAINT `posts_ibfk_1` FOREIGN KEY (`PostTypeId`) REFERENCES `posttypes` (`PostTypeId`)
) ENGINE=InnoDB;


답변

UPD : 2017-03-31, MySQL 버전 5.7.5 에서는 ONLY_FULL_GROUP_BY 스위치를 기본적으로 활성화했습니다 (따라서 비 결정적 GROUP BY 쿼리는 비활성화 됨). 또한 GROUP BY 구현을 업데이트했으며 비활성화 된 스위치로도 솔루션이 더 이상 예상대로 작동하지 않을 수 있습니다. 확인해야합니다.

작품 벌금 위 빌 Karwin의 솔루션은 그룹 내에서 항목 수는 오히려 작은 경우,하지만 그룹이 오히려 큰 경우 솔루션에 대한 요구하기 때문에 쿼리의 성능이 나쁜됩니다 n*n/2 + n/2만의 IS NULL비교.

그룹이 있는 InnoDB 테이블 18684446행 에서 테스트했습니다 1182. 이 표에는 기능 테스트에 대한 테스트 결과가 포함되어 (test_id, request_id)있으며 기본 키가 있습니다. 따라서 test_id그룹이며 나는 request_id각각에 대해 마지막 을 찾고있었습니다 test_id.

Bill의 솔루션은 이미 Dell e4310에서 몇 시간 동안 실행되어 왔으며 적용 지수 (따라서 using indexEXPLAIN)에서 작동하더라도 언제 완료 될지 알 수 없습니다 .

동일한 아이디어를 기반으로하는 다른 솔루션이 몇 가지 있습니다.

  • 기본 인덱스가 BTREE 인덱스 (일반적으로 경우) 인 경우 가장 큰 (group_id, item_value)쌍은 각 내에서 마지막 값입니다 . 인덱스를 내림차순으로 걸면 group_id첫 번째 값입니다 group_id.
  • 인덱스로 커버되는 값을 읽으면 인덱스 순서대로 값을 읽습니다.
  • 각 인덱스에는 암시 적으로 추가 된 기본 키 열이 포함됩니다 (즉, 기본 키는 적용 범위 인덱스에 있음). 아래 솔루션에서 기본 키에서 직접 작동하므로 결과에 기본 키 열을 추가하면됩니다.
  • 많은 경우 하위 쿼리에서 필요한 행 ID를 필요한 순서로 수집하고 하위 쿼리의 결과를 id에 결합하는 것이 훨씬 저렴합니다. 하위 쿼리 결과의 각 행에 대해 MySQL은 기본 키를 기반으로 한 단일 가져 오기가 필요하므로 하위 쿼리가 조인에 먼저 들어가고 하위 쿼리의 ID 순서대로 행이 출력됩니다 (명시 적 ORDER BY를 생략 한 경우) 가입)

MySQL이 인덱스를 사용하는 3 가지 방법 은 몇 가지 세부 사항을 이해하기위한 훌륭한 기사입니다.

해결책 1

이것은 엄청나게 빠르며 18M + 행에서 약 0.8 초가 걸립니다.

SELECT test_id, MAX(request_id) AS request_id
FROM testresults
GROUP BY test_id DESC;

순서를 ASC로 변경하려면 하위 쿼리에 넣고 ID 만 반환하고 하위 쿼리로 사용하여 나머지 열에 조인하십시오.

SELECT test_id, request_id
FROM (
    SELECT test_id, MAX(request_id) AS request_id
    FROM testresults
    GROUP BY test_id DESC) as ids
ORDER BY test_id;

이것은 내 데이터에 약 1.2 초가 걸립니다.

해결책 2

내 테이블에 약 19 초가 걸리는 다른 솔루션이 있습니다.

SELECT test_id, request_id
FROM testresults, (SELECT @group:=NULL) as init
WHERE IF(IFNULL(@group, -1)=@group:=test_id, 0, 1)
ORDER BY test_id DESC, request_id DESC

내림차순으로 테스트를 반환합니다. 전체 인덱스 스캔을 수행하기 때문에 속도가 훨씬 느리지 만 각 그룹에 대해 N 개의 최대 행을 출력하는 방법을 알려줍니다.

쿼리의 단점은 쿼리 캐시로 결과를 캐시 할 수 없다는 것입니다.


답변

중간에 있으므로 하위 쿼리 를 사용하여 올바른 그룹화를 반환하십시오.

이 시도:

select
    a.*
from
    messages a
    inner join
        (select name, max(id) as maxid from messages group by name) as b on
        a.id = b.maxid

그렇지 않으면 id최대 값을 원합니다.

select
    a.*
from
    messages a
    inner join
        (select name, max(other_col) as other_col
         from messages group by name) as b on
        a.name = b.name
        and a.other_col = b.other_col

이렇게하면 매우 느리거나 비효율적 인 하위 쿼리의 상관 된 하위 쿼리 및 / 또는 순서를 피할 수 있습니다.


답변

다른 솔루션에 도달했습니다. 각 그룹의 마지막 게시물에 대한 ID를 얻은 다음 첫 번째 쿼리의 결과를 WHERE x IN구문 의 인수로 사용하여 메시지 테이블에서 선택합니다 .

SELECT id, name, other_columns
FROM messages
WHERE id IN (
    SELECT MAX(id)
    FROM messages
    GROUP BY name
);

다른 솔루션과 비교하여 이것이 어떻게 수행되는지는 모르지만 3 백만 행 이상의 테이블에서 훌륭하게 작동했습니다. (1200+ 결과로 4 초 실행)

이것은 MySQL과 SQL Server 모두에서 작동합니다.


답변

하위 쿼리 바이올린 링크에 의한 솔루션

select * from messages where id in
(select max(id) from messages group by Name)

솔루션 결합 조건 바이올린 링크

select m1.* from messages m1
left outer join messages m2
on ( m1.id<m2.id and m1.name=m2.name )
where m2.id is null

이 게시물의 이유는 바이올린 링크 만 제공하는 것입니다. 다른 답변에 동일한 SQL이 이미 제공되어 있습니다.


답변

상당한 속도의 접근 방식은 다음과 같습니다.

SELECT *
FROM messages a
WHERE Id = (SELECT MAX(Id) FROM messages WHERE a.Name = Name)

결과

Id  Name    Other_Columns
3   A   A_data_3
5   B   B_data_2
6   C   C_data_1


답변

다음은 두 가지 제안입니다. 먼저, mysql이 ROW_NUMBER ()를 지원하면 매우 간단합니다.

WITH Ranked AS (
  SELECT Id, Name, OtherColumns,
    ROW_NUMBER() OVER (
      PARTITION BY Name
      ORDER BY Id DESC
    ) AS rk
  FROM messages
)
  SELECT Id, Name, OtherColumns
  FROM messages
  WHERE rk = 1;

나는 “마지막”이라고 가정하고 당신은 마지막으로 ID 순서를 의미합니다. 그렇지 않은 경우 ROW_NUMBER () 창의 ORDER BY 절을 적절히 변경하십시오. ROW_NUMBER ()를 사용할 수없는 경우 이것은 또 다른 해결책입니다.

둘째, 그렇지 않은 경우 다음과 같이 진행하는 것이 좋습니다.

SELECT
  Id, Name, OtherColumns
FROM messages
WHERE NOT EXISTS (
  SELECT * FROM messages as M2
  WHERE M2.Name = messages.Name
  AND M2.Id > messages.Id
)

즉, 같은 이름을 가진 나중의 ID 메시지가없는 메시지를 선택하십시오.