로그성 테이블을 조회하다 보면 같은 이름이 수십 번씩 찍혀 나와서 "종류별로 하나씩만 보고 싶다"는 순간이 옵니다. 앱 실행 이력을 쌓는 g5_app_history 테이블이 그랬는데, 앱 이름별로 하나씩만 뽑을 때 쓰는 쿼리입니다.

SELECT MIN(uid), app_name FROM g5_app_history GROUP BY app_name

어떻게 하나만 남기나

GROUP BY app_name은 app_name 값이 같은 행들을 한 묶음으로 만듭니다. 묶음 하나가 결과의 한 행이 되니까 중복이 자연스럽게 사라지죠. 그런데 묶음 안에는 uid가 여러 개라서 그중 무엇을 보여 줄지 정해야 하는데, 그 역할을 MIN(uid)가 합니다. 자동 증가 키라면 가장 작은 uid가 그 앱이 처음 기록된 행이 됩니다. 반대로 가장 최근 행을 보고 싶다면 MAX(uid)로 바꾸면 됩니다.

광고

이름만 필요하다면 SELECT DISTINCT app_name FROM g5_app_history 로도 충분합니다. 하지만 DISTINCT는 고른 컬럼 조합 전체가 같을 때만 합쳐 주기 때문에, uid처럼 행마다 다른 컬럼을 같이 넣으면 중복이 그대로 다 나옵니다. "대표 행의 키도 같이 보고 싶다"면 GROUP BY + 집계 함수 쪽이 맞습니다.

대표 행의 나머지 컬럼까지 보려면

자주 하는 실수가 SELECT uid, app_name, reg_date ... GROUP BY app_name 처럼 집계하지 않은 컬럼을 그냥 넣는 겁니다. MySQL 5.7 이후 기본값인 ONLY_FULL_GROUP_BY 모드에서는 오류가 나고, 그 모드가 꺼진 서버에서는 실행은 되지만 묶음 안의 아무 행 값이나 나와서 MIN(uid) 행과 다른 행의 값이 섞일 수 있습니다.

대표 행의 전체 내용이 필요할 때는 위 쿼리를 서브쿼리로 써서 원래 테이블과 다시 붙입니다.

SELECT h.*
FROM g5_app_history h
JOIN (SELECT MIN(uid) AS uid FROM g5_app_history GROUP BY app_name) t
  ON h.uid = t.uid

이렇게 하면 앱마다 정확히 첫 번째 행의 모든 컬럼이 나옵니다. 행이 많은 테이블이라면 app_name에 인덱스가 있어야 GROUP BY가 빨라집니다. 중복을 아예 지워 버리고 싶다면 같은 서브쿼리로 남길 uid 목록을 먼저 확인한 다음, 백업을 떠 두고 삭제하는 순서로 진행하는 게 안전합니다.

몇 번씩 중복됐는지도 같이 보기

대표 행을 고르기 전에 어떤 값이 얼마나 겹치는지 먼저 보고 싶을 때가 많습니다. 같은 GROUP BY에 COUNT(*) 를 붙이고, 두 번 이상 나온 것만 HAVING 으로 거르면 됩니다.

SELECT app_name, COUNT(*) AS cnt
FROM g5_app_history
GROUP BY app_name
HAVING COUNT(*) > 1
ORDER BY cnt DESC

WHERE 는 묶기 전의 행을 거르고 HAVING 은 묶은 뒤의 결과를 거른다는 차이가 있어서, 집계 결과로 조건을 걸 때는 HAVING 을 씁니다.

MySQL 8 이상이라면 ROW_NUMBER

MySQL 8.0, MariaDB 10.2 이상에서는 윈도 함수로 같은 결과를 낼 수 있습니다. 묶음 안에서 순번을 매긴 뒤 1번만 남기는 방식이라, "앱마다 가장 최근 3건" 처럼 개수를 늘리는 응용이 쉽습니다.

SELECT *
FROM (
  SELECT h.*, ROW_NUMBER() OVER (PARTITION BY app_name ORDER BY uid) AS rn
  FROM g5_app_history h
) t
WHERE rn = 1

ORDER BY uid DESC 로 바꾸면 가장 최근 행이, WHERE rn <= 3 으로 바꾸면 앱마다 세 건씩 나옵니다.

묶일 때 같은 값으로 보는 기준

GROUP BY는 NULL끼리를 하나의 묶음으로 봅니다. app_name 이 비어 있는 행이 여러 개 있으면 결과에 NULL 한 줄로 나옵니다. 또 기본 콜레이션이 _ci 라면 Chrome 과 chrome 이 같은 묶음이 되니, 대소문자가 다른 값을 따로 보고 싶다면 GROUP BY BINARY app_name 처럼 비교 기준을 바꿔야 합니다.